core/player/PacViewNode.cs
using Sandbox.UI;

[Icon("videocam")]
public class PacViewNode : Component {
	[Property] public string NodeGroup {get; set;}
	public struct TargetNode {
		public GameObject Node {get; set;}
		public string ExtNodeName {get; set;}
		[Range(0, 8)] public int EntryAngle {get; set;}
		[Range(0, 8)] public int ReverseEntryAngle {get; set;}
		public bool DenyReverseEntry {get; set;}
		//public Door BlockedByDoor {get; set;}
		public bool BlockOnOpen {get; set;}
		//public Elevator BlockedByElevator {get; set;}
		public string ExtElevatorName {get; set;}
		public int ElevatorFloor {get; set;}
		public Action OnTravel {get; set;}
		public override string ToString() {
			return Node.ToString();
		}
	}
	[Button] public void AutoConnectNodes() {
		var group = GameObject.Name.Replace("_" + GameObject.Name.Split("_").Last(), "");
		var index = int.Parse(GameObject.Name.Split("_").Last());
		foreach (var gameobject in Scene.GetAllObjects(true)) {
			var camnode = gameobject.Components.Get<PacViewNode>();
			if (camnode == null)
				continue;
			bool alreadyset = false;
			foreach (var node in ConnectedNodes)
				if (node.Node == gameobject)
					alreadyset = true;
			if (alreadyset)
				continue;
			if (group != gameobject.Name.Replace("_" + gameobject.Name.Split("_").Last(), ""))
				continue;
			if (Math.Abs(int.Parse(gameobject.Name.Split("_").Last()) - index) != 1)
				continue;
			var refnormal = Vector3.Direction(Transform.World.Position, gameobject.Transform.World.Position);
			int entryangle = 0;
			for (int i = 0; i < camnode.ViewAngles.Count; i++)
				if (gameobject.Transform.World.RotationToWorld(camnode.ViewAngles[i].ToRotation()).Forward.Dot(refnormal)
					> gameobject.Transform.World.RotationToWorld(camnode.ViewAngles[entryangle].ToRotation()).Forward.Dot(refnormal))
					entryangle = i;
			int reverseentryangle = 0;
			for (int i = 0; i < camnode.ViewAngles.Count; i++)
				if (gameobject.Transform.World.RotationToWorld(camnode.ViewAngles[i].ToRotation()).Forward.Dot(refnormal)
					< gameobject.Transform.World.RotationToWorld(camnode.ViewAngles[reverseentryangle].ToRotation()).Forward.Dot(refnormal))
					reverseentryangle = i;
			ConnectedNodes.Add(new() {
				Node = gameobject,
				EntryAngle = entryangle,
				ReverseEntryAngle = reverseentryangle,
				DenyReverseEntry = false,
				//BlockedByDoor = null,
			});
		}
	}
	[Property] public List<TargetNode> ConnectedNodes {get; set;} = new();
	[Property, ReadOnly] public BasePlayer AttachedPlayer {get; set;}
	[Property, ReadOnly] public int ViewDirection {get; set;} = 0;
	[Button] public void FourViewAngles() {
		ViewAngles = new List<Angles>{
			Angles.Zero.WithYaw(0f),
			Angles.Zero.WithYaw(90f),
			Angles.Zero.WithYaw(180f),
			Angles.Zero.WithYaw(270f)
		};
	}
	[Button] public void SixViewAngles() {
		ViewAngles = new List<Angles>{
			Angles.Zero.WithYaw(0f),
			Angles.Zero.WithYaw(60f),
			Angles.Zero.WithYaw(120f),
			Angles.Zero.WithYaw(180f),
			Angles.Zero.WithYaw(240f),
			Angles.Zero.WithYaw(300f)
		};
	}
	[Property] public List<Angles> ViewAngles {get; set;} = new List<Angles>{
		Angles.Zero.WithYaw(0f),
		Angles.Zero.WithYaw(60f),
		Angles.Zero.WithYaw(120f),
		Angles.Zero.WithYaw(180f),
		Angles.Zero.WithYaw(240f),
		Angles.Zero.WithYaw(300f)
	};
	[Property] public float Fov {get; set;} = 100f;
	[Property] public bool RainOnCamera {get; set;} = false;
	[Property] public bool SnowOnCamera {get; set;} = false;
	[Property] public List<int> AdditionalSnowOnCameraAngles {get; set;} = new();
	[Property] public bool FullScreenFMV {get; set;} = false;
	[Property] public bool CantMoveInFullScreenFMV {get; set;} = false;
	[Property] public bool UseCustomTextureOverlay {get; set;}
	[Property] public Texture CustomTextureOverlay {get; set;}
	public struct ViewAngleIndexSet {
		[Range(0, 8)] public int A {get; set;}
		[Range(0, 8)] public int B {get; set;}
	}
	[Property] public List<ViewAngleIndexSet> DisconnectedAngles {get; set;} = new();
	[Property] public Dictionary<int,int> NodeMoveOverrides {get; set;} = new();
	[Property] public Dictionary<int,int> NodeReverseMoveOverrides {get; set;} = new();
	[Property] public List<int> DisableMovementNodes {get; set;} = new();
	[Property, Range(0,2)] public float FmvGreenScale {get; set;} = 1f;
	[Property, Hide] public List<Texture> BaseLayerRenders {get; set;} = new();
	[Property] public Action OnEnterNode {get; set;}
	[Property] public Action OnExitNode {get; set;}
	[Property] public Action OnAttemptInputLeft {get; set;}
	[Property] public Action OnAttemptInputRight {get; set;}

	protected override void DrawGizmos() {
		Gizmo.Draw.IgnoreDepth = Gizmo.IsSelected;
		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.LineSphere(0f, 1f);
		Gizmo.Hitbox.Sphere(new Sphere(0f, 2f));
		foreach (var node in ConnectedNodes) {
			if (node.Node == null)
				continue;
			Gizmo.Draw.IgnoreDepth = false;
			if (Gizmo.IsSelected) {
				Gizmo.Draw.Color = Color.Green;
				Gizmo.Draw.LineThickness = 2;
			} else {
				Gizmo.Draw.Color = Color.Gray;
				Gizmo.Draw.LineThickness = 1;
			}
			#if false
			#endif
				Gizmo.Draw.Line(0f, Transform.World.ToLocal(node.Node.Transform.World).Position);
			if (Gizmo.IsSelected) {
				Gizmo.Draw.IgnoreDepth= true;
				Gizmo.Draw.LineThickness = 1;
				Gizmo.Draw.Color = Color.White;
				var nodecomp = node.Node.Components.Get<PacViewNode>();
				if (nodecomp != null) {
					Gizmo.Transform = node.Node.Transform.World;
					foreach (var angle in nodecomp.ViewAngles) {
						if (node.EntryAngle == nodecomp.ViewAngles.IndexOf(angle)) {
							Gizmo.Draw.LineThickness = 5f;
							Gizmo.Draw.Color = Color.Green;
						}
						if (node.ReverseEntryAngle == nodecomp.ViewAngles.IndexOf(angle) && !node.DenyReverseEntry) {
							Gizmo.Draw.LineThickness = 5f;
							Gizmo.Draw.Color = Color.Orange;
						}
						Gizmo.Draw.Line(0f, angle.Forward * 3f);
						Gizmo.Draw.Color = Color.White;
						Gizmo.Draw.LineThickness = 1;
						Gizmo.Draw.Text(nodecomp.ViewAngles.IndexOf(angle).ToString(), new Transform(angle.Forward * 3.2f));
					}
				}
				Gizmo.Transform = Transform.World;
			}
		}
		if (Gizmo.IsSelected) {
			Gizmo.Draw.IgnoreDepth= true;
			Gizmo.Draw.LineThickness = 2;
			Gizmo.Draw.Color = Color.Red;
			foreach (var angle in ViewAngles) {
				Gizmo.Draw.Line(0f, angle.Forward * 3f);
				Gizmo.Draw.Text(ViewAngles.IndexOf(angle).ToString(), new Transform(angle.Forward * 3.2f));
			}
			
			Gizmo.Draw.Color = Color.Gray;
			Gizmo.Draw.Line(0f, Vector3.Down * 72f);
			Gizmo.Draw.LineCircle(Vector3.Down * 72f, Vector3.Up, 12f);
			Gizmo.Draw.IgnoreDepth = false;
			Gizmo.Draw.Color = Gizmo.Colors.Red;
			Gizmo.Draw.LineThickness = 2.5f;
			Gizmo.Draw.Line(0f, Vector3.Down * 72f);
			Gizmo.Draw.LineCircle(Vector3.Down * 72f, Vector3.Up, 12f);
		}
		base.DrawGizmos();
	}

	private Action DelayedAction = null;
	private TimeUntil DelayedActionDelay;
	private bool Zoomed = false;
	protected override void OnUpdate() {
		if (!AttachedPlayer.IsValid())
			return;
#pragma warning disable CS0642
		if (DelayedAction != null && !AllowInteraction())
			DelayedActionDelay = 0.1f;
		if (DelayedAction != null && AllowInteraction() && DelayedActionDelay) {
			AttachedPlayer.PacCamera.InvalidateView();
			AttachedPlayer.PacCamera.Update();
			DelayedAction.Invoke();
			DelayedAction = null;
		} else {
			var attack1handled = false;
			if (CustomTextureOverlay.IsValid()) {
				AttachedPlayer.PacCamera.OverlayTexture = CustomTextureOverlay;
				AttachedPlayer.PacCamera.OverlayStrength = AttachedPlayer.PacCamera.OverlayStrength.Approach(UseCustomTextureOverlay ? 1 : 0, Time.Delta);
			} else if (UseCustomTextureOverlay) {
				AttachedPlayer.PacCamera.OverlayStrength = 0f;
			} else {
				var mousepos = (Mouse.Position / Screen.Size);
				if (mousepos.y > 0.868f || Zoomed) {
					attack1handled = true;
					mousepos.x -= 0.5f;
					mousepos.x *= (float)Screen.Size.x / Screen.Size.y;
					mousepos.x /= 640f/480f;
					mousepos.x += 0.5f;
					if (!Zoomed) {
						Zoomed = Input.Pressed("Attack1") && mousepos.x > 0.041f && mousepos.y > 0.888f && mousepos.x < 0.187f && mousepos.y < 0.987f;
					} else if (mousepos.x < 0.097f || mousepos.y < 0.206f || mousepos.x > 0.896f || mousepos.y > 0.741f) {
						if (Input.Pressed("Attack1"))
							Zoomed = false;
					}
				}
				if (Zoomed)
					AttachedPlayer.PacCamera.OverlayTexture = Texture.Load("materials/interface/choreo/postcard_zoom.vtex");
				else
					AttachedPlayer.PacCamera.OverlayTexture = Texture.Load("materials/interface/choreo/postcard_bar.vtex");
				AttachedPlayer.PacCamera.OverlayStrength = (FullScreenFMV || AttachedPlayer.PacCamera.FullScreenFMV) ? 0f : 1f;
			}
			AttachedPlayer.PacCamera.Turn = 0;
			if (!attack1handled && InputTurn(false, true)) {
				AttachedPlayer.PacCamera.Turn = Math.Sign((Mouse.Position / Screen.Size).x - 0.5f);
			}
			if (Input.Pressed("Attack1") && AllowInteraction() && !attack1handled && (InputTurn(true, true) || InputInteraction() || InputMove(true, false)));
			if (Input.Pressed("Forward") && AllowInteraction() && (InputMove(false, false)));
			if (Input.Pressed("Backward") && AllowInteraction() && (InputMove(false, true)));
			if (Input.Pressed("Left") && AllowInteraction() && (InputTurn(true, false, true)));
			if (Input.Pressed("Right") && AllowInteraction() && (InputTurn(true, false, false)));
		}
#pragma warning restore CS0642
		if (!AttachedPlayer.IsValid())
			return;
		if (ViewDirection >= ViewAngles.Count) ViewDirection = 0; else if (ViewDirection < 0) ViewDirection = ViewAngles.Count - 1;
		AttachedPlayer.LocalRotation = ViewAngles[ViewDirection];
		AttachedPlayer.PacCamera.FullScreenFMV = FullScreenFMV;
		AttachedPlayer.PacCamera.RenderCamera.FieldOfView = Fov;
		InteractionCooldown = false;
		base.OnUpdate();
	}
	
	public bool InteractionCooldown = false;
	public bool AllowInteraction() {
		if (!AttachedPlayer.PacCamera.TransitionTime)
			return false;
		if (CantMoveInFullScreenFMV && FullScreenFMV)
			return false;
		if (InteractionCooldown)
			return false;
		return true;
	}

	public bool InputTurn(bool real, bool usemouse, bool right = false) {
		var mousePosition = Mouse.Position / Screen.Size;
		if (real) {
			if (usemouse && MathF.Abs(mousePosition.x - 0.5f) > 0.25f) {
				if (mousePosition.x < 0.5f)
					OnAttemptInputRight?.Invoke();
				else
					OnAttemptInputLeft?.Invoke();
			} else {
				if (right)
					OnAttemptInputRight?.Invoke();
				else
					OnAttemptInputLeft?.Invoke();
			}
		}
		if (ViewAngles.Count == 1)
			return false;
		var originalViewDirection = ViewDirection;
		if (usemouse) {
			if (MathF.Abs(mousePosition.x - 0.5f) < 0.25f)
				return false;
			if (mousePosition.x > 0.5f)
				ViewDirection--;
			else
				ViewDirection++;
		} else {
			if (right)
				ViewDirection++;
			else
				ViewDirection--;
		}
		foreach (var set in DisconnectedAngles) {
			if (set.A == originalViewDirection && set.B == ViewDirection) {
				ViewDirection = originalViewDirection;
				return false;
			}
			if (set.B == originalViewDirection && set.A == ViewDirection) {
				ViewDirection = originalViewDirection;
				return false;
			}
		}
		if (!real) {
			ViewDirection = originalViewDirection;
			return true;
		}
		if (AttachedPlayer.PacCamera.DelayMoveAction != null) {
			AttachedPlayer.PacCamera.DelayMoveAction.Invoke();
			AttachedPlayer.PacCamera.DelayMoveAction = null;
			var dir = ViewDirection;
			DelayedAction = () => {
				if (ViewDirection > originalViewDirection)
					AttachedPlayer.PacCamera.DoTransition = PlayerPacCamera.TransitionType.SlideRight;
				else
					AttachedPlayer.PacCamera.DoTransition = PlayerPacCamera.TransitionType.SlideLeft;
				foreach (var animator in Scene.GetAllComponents<NpcAnimator>())
					animator.ResetToIdle();
				ViewDirection = dir;
			};
			ViewDirection = originalViewDirection;
			return true;
		}
		if (ViewDirection > originalViewDirection)
			AttachedPlayer.PacCamera.DoTransition = PlayerPacCamera.TransitionType.SlideRight;
		else
			AttachedPlayer.PacCamera.DoTransition = PlayerPacCamera.TransitionType.SlideLeft;
		foreach (var animator in Scene.GetAllComponents<NpcAnimator>())
			animator.ResetToIdle();
		return true;
	}

	public bool InputInteraction() {
		var pos = Mouse.Position / Screen.Size;
		pos *= AttachedPlayer.PacCamera.RenderCamera.ScreenRect.Size;
		var ray = AttachedPlayer.PacCamera.RenderCamera.ScreenPixelToRay(pos);
		foreach (var interactable in Scene.GetAllComponents<IsoInteractable>()) {
			if (!interactable.ValidNodes.Contains(this))
				continue;
			if (interactable.Box.Trace(ray.ToLocal(interactable.WorldTransform), 512, out var dist)) {
				interactable.Interact();
				return true;
			}
		}
		return false;
	}

	public bool InputMove(bool usemouse, bool reverse) {
		var bestnode = new TargetNode();
		var bestdot = 0.76f;
		if (usemouse && DisableMovementNodes.Contains(ViewDirection))
			return false;
		if (!reverse && NodeMoveOverrides.TryGetValue(ViewDirection, out int value)) {
			bestnode = ConnectedNodes[value];
		} else if (reverse && NodeReverseMoveOverrides.TryGetValue(ViewDirection, out int reverseValue)) {
			bestnode = ConnectedNodes[reverseValue];
		} else {
			foreach (var node in ConnectedNodes) {
				if (node.Node.Components.Get<PacViewNode>() == null)
					continue;
				var normal = BasePlayer.Local.PacCamera.WorldRotation.Forward.WithZ(0f).Normal;
				if (usemouse)
					normal = (BasePlayer.Local.PacCamera.RenderCamera.ScreenNormalToRay(Mouse.Position/Screen.Size).Project(WorldPosition.Distance(node.Node.WorldPosition)))-WorldPosition.WithZ(0f).Normal;
				if (reverse)
					normal *= -1f;
				var dot = normal.Dot((node.Node.WorldPosition-WorldPosition).WithZ(0f).Normal);
				if (node.DenyReverseEntry && reverse)
					dot = 0f;
				#if false
				#endif
				if (dot > bestdot) {
					bestdot = dot;
					bestnode = node;
				}
			}
		}
		if (bestnode.Node == null)
			return false;
		DelayedAction = () => {
			OnExitNode?.Invoke();
			AttachedPlayer = null;
			if (reverse)
				bestnode.Node.Components.Get<PacViewNode>().ConnectToNode(bestnode.ReverseEntryAngle, PlayerPacCamera.TransitionType.Fade);
			else
				bestnode.Node.Components.Get<PacViewNode>().ConnectToNode(bestnode.EntryAngle, PlayerPacCamera.TransitionType.Fade);
			bestnode.OnTravel?.Invoke();
			foreach (var animator in Scene.GetAllComponents<NpcAnimator>())
				animator.ResetToIdle();
		};
		if (AttachedPlayer.PacCamera.DelayMoveAction != null) {
			AttachedPlayer.PacCamera.DelayMoveAction.Invoke();
			AttachedPlayer.PacCamera.DelayMoveAction = null;
			return true;
		}
		DelayedAction.Invoke();
		DelayedAction = null;
		return true;
	}

	[Button] public void ConnectToNode(int direction = 0, PlayerPacCamera.TransitionType transition = PlayerPacCamera.TransitionType.None) {
		if (!BasePlayer.Local.IsValid())
			return;
		foreach (var node in Scene.GetAllComponents<PacViewNode>())
			node.AttachedPlayer = null;
		ViewDirection = direction;
		AttachedPlayer = BasePlayer.Local;
		AttachedPlayer.PacCamera.DoTransition = transition;
		AttachedPlayer.GameObject.SetParent(GameObject);
		if (!BasePlayer.Local.GameObject.Parent.IsValid())
			return;
		AttachedPlayer.LocalTransform = global::Transform.Zero;
		AttachedPlayer.LocalRotation = ViewAngles[ViewDirection];
		AttachedPlayer.PacCamera.FullScreenFMV = FullScreenFMV;
		InteractionCooldown = true;
		OnEnterNode?.Invoke();
	}

	public bool DrawSnowOnCamera() {
		if (SnowOnCamera)
			return true;
		if (AdditionalSnowOnCameraAngles.Contains(ViewDirection))
			return true;
		return false;
	}
}