Fatality/StageHook.cs
using System;
using System.Collections.Generic;
using SquishyTerry.Sim;

namespace SquishyTerry;

/// <summary>
/// The end of a fatality: the vaudeville "get the hook" gag. A long wooden shepherd's crook swings in
/// from the right, catches Terry under the jaw and yanks him off stage; then a fresh Terry slides in
/// from the left and the next run starts.
/// <para>
/// All physics: the hook is a mass on a spring chasing where the stagehand wants it (so it
/// overshoots, and tilts as it accelerates); once it has him, the hooked skin is pulled towards it
/// like a mouse grab (squashing his side, up to a limit), and the head as a whole is a mass on a
/// spring behind the hook, so it lags, then gets carried off. The head's acceleration is fed to the
/// soft body as an inertial force, so he sloshes on the yank and when the next Terry lands.
/// </para>
/// </summary>
public sealed class StageHook : Component
{
	/// <summary>Seconds of X eyes before the hook comes in.</summary>
	[Property] public float Beat { get; set; } = 0.9f;

	/// <summary>Playback speed of the whole gag (lower it to watch in slow motion).</summary>
	[Property, Range( 0.02f, 3f )] public float Speed { get; set; } = 2.4f;

	/// <summary>Editor testing: tick to run the gag (X eyes, hook, next Terry) as a fatality would.</summary>
	[Property] public bool Test { get; set; }

	enum Phase { Idle, Wait, Behind, Around, Enter, Tug, Yank, Return }

	const float Thickness = 0.8f;     // tube radius
	const float Snug = 1.6f;          // gap between the crook and his head (world units)
	const float PoleLength = 110f;    // off to the right, well past the edge of the screen
	const int Sides = 10;

	// Pace (the classic gag: a steady slide in, a beat, then a yank that picks up speed).
	const float EnterStiffness = 22f;
	const float TugTime = 0.45f;
	const float YankAcceleration = 480f; // world units / s²: a hard, sudden yank
	const float YankTopSpeed = 230f;

	const float HeadStiffness = 55f;  // how hard the crook drags his head along (lower = he lags and stretches more)
	const float Grip = 16f;           // how far he stretches (world units) before the rest of him is dragged along
	const float Wobble = 0.35f;       // how much of the head's acceleration sloshes the soft body

	Phase _phase;
	float _t;

	SquishyFace _face;
	KnifeDrop _knife;

	// The hook: its contact point (inside of the crook) and its tilt, both springs.
	Vector3 _hook, _hookVel;
	float _tilt, _tiltVel;
	Vector3 _goal;

	// The head: rigid offset and velocity.
	Vector3 _head, _headVel;
	int _triangle;
	Vector3 _catch;    // where on the jaw it hooks, with the head at rest
	Vector3 _right, _up, _forward;
	float _offstage;   // head offset that's past the edge of the screen

	GameObject _object;
	ModelRenderer _renderer;
	Vector2 _builtFor, _builtCentre;
	Vector3 _clear;
	float _behind;
	Vector3 _eye;

	public bool Busy => _phase != Phase.Idle;

	/// <summary>Starts the gag (after the X-eyes beat).</summary>
	public void Begin()
	{
		if ( Busy ) return;
		_phase = Phase.Wait;
		_t = 0f;
	}

	protected override void OnEnabled()
	{
		_object = new GameObject( true, "Stage Hook" );
		_object.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
		_object.NetworkMode = NetworkMode.Never;
		_renderer = _object.Components.Create<ModelRenderer>();
		_renderer.Tint = new Color( 0.56f, 0.35f, 0.17f );
		_renderer.RenderType = ModelRenderer.ShadowRenderType.Off;
		_object.Enabled = false;
	}

	protected override void OnDisabled()
	{
		_object?.Destroy();
		_object = null;
	}

	protected override void OnUpdate()
	{
		_face ??= Components.Get<SquishyFace>();
		_knife ??= Components.Get<KnifeDrop>();
		if ( Test && _face is not null )
		{
			Test = false;
			_face.XEyes = true;
			_face.InputLocked = true;
			Begin();
		}
		if ( _phase == Phase.Idle ) return;
		var camera = Scene.Camera;
		if ( _face?.Sim is null || camera is null ) return;

		float dt = MathF.Min( RealTime.Delta, 1f / 30f ) * SlowMo.Scale * Speed;
		_t += dt;

		switch ( _phase )
		{
			case Phase.Wait:
				if ( _t >= Beat ) StartEnter( camera );
				break;

			case Phase.Behind:
				// In from the wings, passing behind his head to his left...
				_goal = _clear + _forward * _behind;
				StepHook( dt, EnterStiffness, 0.7f );
				if ( (_hook - _goal).Length < 2f || _t > 2.5f ) { _phase = Phase.Around; _t = 0f; }
				break;

			case Phase.Around:
				// ...comes forward beside him...
				_goal = _clear;
				StepHook( dt, EnterStiffness * 1.5f, 0.7f );
				if ( (_hook - _goal).Length < 1f || _t > 1.5f ) { _phase = Phase.Enter; _t = 0f; }
				break;

			case Phase.Enter:
				// ...and slides across so his head goes into the crook, settling snug against his jaw.
				_goal = _catch - _right * 0.3f;
				StepHook( dt, EnterStiffness, 0.6f );
				if ( _t > 0.4f && (_hook - _goal).Length < 0.6f && _hookVel.Length < 4f || _t > 2f )
				{
					_phase = Phase.Tug;
					_t = 0f;
				}
				break;

			case Phase.Tug:
				// A little wind-up: pulls back snug against him, then...
				_goal = _catch + _right * 1.2f;
				StepHook( dt, 60f, 0.6f );
				StepHead( dt );
				if ( _t > TugTime ) { _phase = Phase.Yank; _t = 0f; }
				break;

			case Phase.Yank:
				// ...GET OFF THE STAGE. Out to the right, lifting a little.
				// The stagehand hauls: the goal speeds up to a top speed; the hook chases it on its spring.
				float speedUpTime = YankTopSpeed / YankAcceleration;
				float pulled = _t < speedUpTime
					? 0.5f * YankAcceleration * _t * _t
					: 0.5f * YankAcceleration * speedUpTime * speedUpTime + YankTopSpeed * (_t - speedUpTime);
				_goal = _catch + _right * (1.2f + pulled) + _up * MathF.Min( pulled * 0.08f, 5f );
				StepHook( dt, 300f, 0.7f );
				StepHead( dt );
				if ( Vector3.Dot( _head, _right ) > _offstage ) NextAct();
				break;

			case Phase.Return:
				// The next Terry bounces in from the left and settles.
				var accel = -_head * 55f - _headVel * (2f * 0.32f * MathF.Sqrt( 55f ));
				_headVel += accel * dt;
				_head += _headVel * dt;
				_face.Accelerate( accel * Wobble, dt );
				_face.HeadOffset = _head;
				if ( _t > 0.5f && _head.Length < 0.05f && _headVel.Length < 0.5f || _t > 4f )
				{
					_face.HeadOffset = Vector3.Zero;
					_face.InputLocked = false;
					_phase = Phase.Idle;
				}
				break;
		}

		UpdateVisual();
	}

	void StartEnter( CameraComponent camera )
	{
		var rot = camera.WorldRotation;
		_right = rot.Right;
		_up = rot.Up;
		_forward = rot.Forward;

		// Past the edge of the screen at the head's distance, plus the head's size.
		float distance = Vector3.Dot( _face.WorldPosition - camera.WorldPosition, _forward );
		float halfHeight = distance * MathF.Tan( camera.FieldOfView.DegreeToRadian() * 0.5f );
		_offstage = halfHeight * Screen.Aspect + _face.WorldScale.x * 1.1f;

		// Hook him round the far (left) side of his head, so pulling right drags him off.
		_triangle = LeftSideTriangle();
		_catch = _face.SurfacePoint( _triangle, new Vector3( 1f, 0f, 0f ) );
		_eye = camera.WorldPosition;

		// A crook big enough for his whole head to sit in.
		// A crook that goes round his head like a collar, sized to him at that height.
		var (radii, centre) = CrossSection();
		var crook = new Vector2( radii.x + Snug, radii.y + Snug );
		if ( (crook - _builtFor).Length > 0.5f || (centre - _builtCentre).Length > 0.5f ) BuildMesh( crook, centre );

		// Where the crook waits clear of him on his left (its open side facing him), and how far
		// behind him it passes on the way there.
		_clear = _catch - _right * (2f * (crook.x + Thickness) + 2f);
		_behind = centre.y + crook.y + Thickness + 6f;
		_hook = _catch + _right * (_offstage + 20f) + _forward * _behind;
		_hookVel = Vector3.Zero;
		_tilt = 0.25f;
		_tiltVel = 0f;
		_head = Vector3.Zero;
		_headVel = Vector3.Zero;

		_object.Enabled = true;
		_phase = Phase.Behind;
		_t = 0f;
	}

	/// <summary>
	/// His head's horizontal cross-section at the height of the hooked point (at rest, world units):
	/// half width and half depth, and the centre relative to the hooked point (x right, y away from the camera).
	/// </summary>
	(Vector2 Radii, Vector2 Centre) CrossSection()
	{
		var sim = _face.Sim;
		var rest = _face.InitialPositions;
		var hooked = rest[sim.Indices[_triangle * 3]];
		float minX = float.MaxValue, maxX = float.MinValue, minZ = float.MaxValue, maxZ = float.MinValue;
		void Include( Vec3[] points )
		{
			if ( points is null ) return;
			foreach ( var p in points )
			{
				if ( MathF.Abs( p.Y - hooked.Y ) > 0.12f ) continue;
				minX = MathF.Min( minX, p.X ); maxX = MathF.Max( maxX, p.X );
				minZ = MathF.Min( minZ, p.Z ); maxZ = MathF.Max( maxZ, p.Z );
			}
		}
		// The skull, plus the ears and eyeballs that stick out at this height.
		Include( rest );
		for ( int i = 0; i < 2; i++ )
		{
			Include( sim.Ears[i]?.Pos );
			Include( sim.Eyes[i]?.Pos );
		}
		float scale = _face.WorldScale.x;
		var radii = new Vector2( (maxX - minX) * 0.5f, (maxZ - minZ) * 0.5f ) * scale;
		// Simulation +Z is towards the camera; the path's depth is away from it.
		var centre = new Vector2( (minX + maxX) * 0.5f - hooked.X, -((minZ + maxZ) * 0.5f - hooked.Z) ) * scale;
		return (radii, centre);
	}

	/// <summary>A triangle on the left side of his head, level with its middle (seen from the camera).</summary>
	int LeftSideTriangle()
	{
		var sim = _face.Sim;
		var rest = _face.InitialPositions;
		var dir = new Vec3( -1f, 0f, 0f ); // sim space: his left, level with the middle of his face, on the silhouette
		int best = 0;
		float bestDot = float.MinValue;
		for ( int v = 0; v < rest.Length; v++ )
		{
			float d = Vec3.Dot( rest[v], dir );
			if ( d > bestDot ) { bestDot = d; best = v; }
		}
		// A triangle that starts at that vertex (the grab carries a triangle's first corner).
		for ( int t = 0; t < sim.TriangleCount; t++ )
			if ( sim.Indices[t * 3] == best ) return t;
		for ( int t = 0; t < sim.Indices.Length; t++ )
			if ( sim.Indices[t] == best ) return t / 3;
		return 0;
	}

	/// <summary>The hook chases its goal on a damped spring (and tilts as it accelerates).</summary>
	void StepHook( float dt, float stiffness, float dampingRatio )
	{
		var accel = (_goal - _hook) * stiffness - _hookVel * (2f * dampingRatio * MathF.Sqrt( stiffness ));
		_hookVel += accel * dt;
		_hook += _hookVel * dt;

		// Tilt: a heavy pole lags its own acceleration, then springs back level.
		float torque = -Vector3.Dot( accel, _right ) * 0.0012f + Vector3.Dot( accel, _up ) * 0.0006f;
		_tiltVel += (torque - _tilt * 60f - _tiltVel * 7f) * dt;
		_tilt += _tiltVel * dt;


	}

	/// <summary>The head follows the hook through the stretched skin: a spring from the jaw's rest spot.</summary>
	void StepHead( float dt )
	{
		// The hook grabs him exactly like the mouse: the face stretches towards it with the normal physics.
		_face.HookGrab( _triangle, _hook );

		// He stays where he is until he's stretched as far as a big pull, then the rest of him gets dragged along.
		var stretch = _hook - (_catch + _head);
		stretch -= _forward * Vector3.Dot( stretch, _forward ); // across the stage, never towards the camera
		float length = stretch.Length;
		float excess = MathF.Max( length - Grip, 0f );
		var accel = excess > 0f
			? stretch / length * (excess * HeadStiffness) + (_hookVel - _headVel) * (2f * 0.5f * MathF.Sqrt( HeadStiffness ))
			: -_headVel * 4f;
		_headVel += accel * dt;
		_head += _headVel * dt;
		_face.Accelerate( accel * Wobble, dt );
		_face.HeadOffset = _head;
	}

	/// <summary>He's gone: clear the stage and send in a fresh Terry from the left.</summary>
	void NextAct()
	{
		_face.HookRelease();
		_face.ResetFace();
		_knife?.EndFatality();
		_object.Enabled = false;

		_head = -_right * _offstage;
		_headVel = _right * 20f;
		_face.HeadOffset = _head;
		_phase = Phase.Return;
		_t = 0f;
	}

	// --- Visual ---

	void UpdateVisual()
	{
		if ( !_object.Enabled ) return;
		_object.WorldPosition = _hook;
		// Local X is away from the camera, +Y left, Z up: the crook lies flat, round his head.
		_object.WorldRotation = Rotation.LookAt( _forward, _up ) * Rotation.FromRoll( _tilt.RadianToDegree() );
	}

	/// <summary>
	/// A shepherd's crook lying flat, like a collar round his head: <paramref name="crook"/> is the
	/// inside half width / half depth, <paramref name="centre"/> his head's centre. The path is in the
	/// horizontal plane (x right, y away from the camera) with its origin where it hooks his left side:
	/// from the tip in front of his chin, round his left side, behind his head, then the long pole off
	/// to the right behind him.
	/// </summary>
	void BuildMesh( Vector2 crook, Vector2 centre )
	{
		_builtFor = crook;
		_builtCentre = centre;
		float rx = crook.x + Thickness, ry = crook.y + Thickness; // centreline
		var path = new List<Vector2>();
		for ( int i = 0; i <= 40; i++ )
		{
			// -75° (tip, in front and a little right) round through -90° (front), -180° (left), -270° (behind).
			float a = (-75f - 195f * i / 40f).DegreeToRadian();
			path.Add( centre + new Vector2( MathF.Cos( a ) * rx, MathF.Sin( a ) * ry ) );
		}
		for ( int i = 1; i <= 6; i++ )
			path.Add( centre + new Vector2( PoleLength * i / 6f, ry ) );

		var mesh = TubeMesh( Material.Load( "materials/default.vmat" ), path, Thickness );
		_renderer.Model = Model.Builder.AddMesh( mesh ).Create();
	}

	/// <summary>A round tube along a path in the horizontal plane (x right, y away from the camera): local X forward, -Y right, Z up.</summary>
	static Mesh TubeMesh( Material material, List<Vector2> path, float radius )
	{
		static Vector3 To3( Vector2 p ) => new( p.y, -p.x, 0f );

		var verts = new List<Vertex>();
		var indices = new List<int>();
		for ( int i = 0; i < path.Count; i++ )
		{
			var tangent2 = (path[Math.Min( i + 1, path.Count - 1 )] - path[Math.Max( i - 1, 0 )]).Normal;
			var tangent = To3( tangent2 );
			var side = To3( new Vector2( -tangent2.y, tangent2.x ) );
			var depth = Vector3.Up;
			var centre = To3( path[i] );
			for ( int k = 0; k < Sides; k++ )
			{
				float a = k * MathF.Tau / Sides;
				var n = side * MathF.Cos( a ) + depth * MathF.Sin( a );
				verts.Add( new Vertex( centre + n * radius, n, tangent, new Vector4( (float)i / path.Count, (float)k / Sides, 0, 0 ) ) );
			}
			if ( i == 0 ) continue;
			int a0 = (i - 1) * Sides, b0 = i * Sides;
			for ( int k = 0; k < Sides; k++ )
			{
				int k1 = (k + 1) % Sides;
				indices.AddRange( new[] { a0 + k, b0 + k, a0 + k1, a0 + k1, b0 + k, b0 + k1 } );
			}
		}

		// Round-ish caps.
		foreach ( var (end, dir, ring) in new[] { (0, -1, 0), (path.Count - 1, 1, (path.Count - 1) * Sides) } )
		{
			var tangent2 = (path[Math.Min( end + 1, path.Count - 1 )] - path[Math.Max( end - 1, 0 )]).Normal * dir;
			var tip = To3( path[end] + tangent2 * radius * 0.8f );
			int c = verts.Count;
			verts.Add( new Vertex( tip, To3( tangent2 ), Vector3.Forward, Vector4.Zero ) );
			for ( int k = 0; k < Sides; k++ )
			{
				int k1 = (k + 1) % Sides;
				if ( dir > 0 ) indices.AddRange( new[] { ring + k, c, ring + k1 } );
				else indices.AddRange( new[] { ring + k1, c, ring + k } );
			}
		}

		var mesh = new Mesh( material );
		mesh.CreateVertexBuffer( verts.Count, Vertex.Layout, verts );
		mesh.CreateIndexBuffer( indices.Count, indices );
		mesh.Bounds = BBox.FromPositionAndSize( Vector3.Zero, PoleLength * 2.5f );
		return mesh;
	}
}