Object/ObjectHex.cs

Hex game object component. Manages hex tile state including type, production, ownership, fog, queued unit/building data, spawned unit/building objects, highlighting, reveal/hide, and recursive operations across neighbour hexes.

NetworkingFile Access
using Forp.Object.Unit;
using Sandbox;
using Sandbox.Diagnostics;
using System;
using Sandbox.UI;
using Forp.Game;
using Forp.Object.Building;
using System.Transactions;
using System.Runtime.CompilerServices;

namespace Forp.Object;

public enum EHexType
{
	Grass,
	Sand,
	Stone,
	Water,
}

public record FQueueObject
{
	public Guid GameObjectId { get; init; }

	public string ObjectId { get; init; }
	public string ObjectName { get; init; }
	public int ProductionToBuild { get; init; }
	public int GoldToBuild { get; init; }

	public int Production { get; set; }
	public bool IsReadyToBuild()
	{
		return Production >= ProductionToBuild;
	}

	public bool CanBeBuilt()
	{
		return GamePlayer.Local.Gold >= GoldToBuild;
	}

	public string GetBuildString()
	{
		return $"{ObjectName} {Production} / {ProductionToBuild}";
	}
}

public sealed class Hex : Obj
{
	[RequireComponent] public HighlightOutline HighlightOutline { get; set; }
	[Property] GameObject FogPrefab { get; set; }
	GameObject FogObject = null;

	[Sync(SyncFlags.FromHost)] public NetList<FQueueObject> QueuedUnits { get; set; } = new();
	[Sync(SyncFlags.FromHost), Property] public int Production { get; set; } = 0;
	[Sync(SyncFlags.FromHost), Property] public EHexType Type { get; set; } = EHexType.Grass;
	[Sync(SyncFlags.FromHost), Change, Property] public Color BaseColour { get; set; } = Color.White;

	[Property] public Dictionary<EHexType, Model> TypeModels { get; private set; }

	/////////////////////////////////////////////////////////////////////////////////////////////////

	[Sync(SyncFlags.FromHost), Change] public FBuilding BuildingData { get; set; } = null;
	public TextRenderer ConstructionObject { get; set; } = null;
	public ObjectBuilding BuildingObject { get; set; } = null;
	private void OnBuildingDataChanged(FBuilding OldBuildingData, FBuilding NewBuildingData)
	{
		if (BuildingData == null)
		{
			if (BuildingObject.IsValid())
			{
				BuildingObject.GameObject.Destroy();
				BuildingObject = null;
			}
			return;
		}

		if (BuildingData.TurnsAlive < BuildingData.ProductionToBuild)
		{
			if (!ConstructionObject.IsValid())
			{
				var ConstructionClone = GameManager.Instance.GameTextPrefab.Clone();
				ConstructionClone.NetworkMode = NetworkMode.Never;
				ConstructionClone.WorldTransform = BuildingData.Transform;
				ConstructionClone.WorldPosition += Vector3.Up * 100;
				ConstructionObject = ConstructionClone.GetComponent<TextRenderer>();
			}

			ConstructionObject.Text = $"{BuildingData.TurnsAlive} / {BuildingData.ProductionToBuild}";
			return;
		}

		if (ConstructionObject.IsValid())
		{
			ConstructionObject.GameObject.Destroy();
		}

		if (BuildingData.OwnerGuid == Connection.Local.Id)
		{
			RevealHexesRecusrive(this, true, BuildingData.ViewRange + 1);
		}

		// we only build once
		if (IsRevealed && !BuildingObject.IsValid())
		{
			CloneConfig BuildingCloneConfig = new()
			{
				StartEnabled = true,
				Parent = GameObject,
			};

			var Clone = GameManager.Instance.GetObject(BuildingData.ObjectId).Clone(BuildingCloneConfig);
			BuildingObject = Clone.GetComponent<ObjectBuilding>();
			BuildingObject.OwnerHex = this;
			BuildingObject.ModelRenderer?.Tint = GameManager.Instance.GetTintColour(BuildingData.OwnerGuid);

			if (GameManager.Instance.GetGamePlayer(BuildingData.OwnerGuid) is { } OwnerPlayer)
			{
				BuildingObject.OwnerPlayer = OwnerPlayer;
			}
		}

		if (BuildingObject.IsValid())
		{
			BuildingObject.Health = BuildingData.Health;
			BuildingObject.Attack = BuildingData.Attack;
			if (BuildingData.Health <= 0)
			{
				BuildingObject.DestroyGameObject();
				return;
			}
		}
	}

	[Sync(SyncFlags.FromHost), Change] public FUnit UnitData { get; set; } = null;
	public ObjectUnit UnitObject { get; set; } = null;
	private void OnUnitDataChanged(FUnit OldUnitData, FUnit NewUnitData)
	{
		if (UnitData == null)
		{
			if (UnitObject.IsValid())
			{
				UnitObject.GameObject.Destroy();
				UnitObject = null;
			}
			return;
		}

		if (UnitData.OwnerGuid == Connection.Local.Id)
		{
			RevealHexesRecusrive(this, true, UnitData.ViewRange + 1);
		}

		if (IsRevealed && !UnitObject.IsValid())
		{
			CloneConfig UnitCloneConfig = new()
			{
				StartEnabled = true,
				Parent = GameObject,
			};

			var Clone = GameManager.Instance.GetObject(UnitData.ObjectId).Clone(UnitCloneConfig);
			UnitObject = Clone.GetComponent<ObjectUnit>();
			UnitObject.OwnerHex = this;
			UnitObject.WorldPosition += Vector3.Up * 20f;
			UnitObject.WorldRotation = Rotation.FromYaw(180);
			UnitObject.ModelRenderer?.Tint = GameManager.Instance.GetTintColour(UnitData.OwnerGuid);

			if (GameManager.Instance.Mode != EGameManagerMode.Menu)
			{
				UnitObject.ModelRenderer.RenderOptions.Overlay = true;
			}

			if (!UnitData.IsAi && GameManager.Instance.GetGamePlayer(UnitData.OwnerGuid) is { } OwnerPlayer)
			{
				UnitObject.OwnerPlayer = OwnerPlayer;
			}
			else
			{
				UnitObject.AddComponent<AiUnit>();
			}
		}

		if (UnitObject.IsValid())
		{
			UnitObject.Health = UnitData.Health;
			UnitObject.Attack = UnitData.Attack;
			if (UnitData.Health <= 0)
			{
				Log.Info($"you! yes you are DEAD");
				UnitObject.DestroyGameObject();
				return;
			}

			if (UnitObject.Upgrade != null)
			{
				UnitObject.ApplyUpgrade(UnitData.Upgrade);
			}
		}
	}

	[Sync(SyncFlags.FromHost), Change] public FObj ObjectData { get; set; } = null;
	public Obj Obj { get; set; } = null;
	private void OnObjectDataChanged(FObj OldObjectData, FObj NewObjectData)
	{
		if (ObjectData == null)
		{
			if (Obj.IsValid())
			{
				Obj.GameObject.Destroy();
				Obj = null;
			}
			return;
		}

		if (IsRevealed && !Obj.IsValid())
		{
			CloneConfig ObjCloneConfig = new()
			{
				StartEnabled = true,
				Parent = GameObject,
			};

			var Clone = GameManager.Instance.GetObject(ObjectData.ObjectId).Clone(ObjCloneConfig);
			Clone.WorldPosition += Vector3.Up * 30f; // HACK
		}
	}

	[Sync(SyncFlags.FromHost)] public Guid OwningConnectionGuid { get; set; }

	public void SetOwner_ServerOnly(Guid OwnerIn)
	{
		Assert.True(Networking.IsHost);

		OwningConnectionGuid = OwnerIn;
		SetBaseColour_ServerOnly(GameManager.Instance.GetTintColour(OwningConnectionGuid));
	}

	public void RemoveOwner_ServerOnly()
	{
		Assert.True(Networking.IsHost);

		OwningConnectionGuid = Guid.Empty;
		SetBaseColour_ServerOnly(Color.White);
	}

	public void AddQueuedObject_ServerOnly(FQueueObject QueueObject)
	{
		Assert.True(Networking.IsHost);
		QueuedUnits.Add(QueueObject);
	}

	private void OnBaseColourChanged(Color OldColour, Color NewColour)
	{
		ModelRenderer.Tint = BaseColour;//.Darken(1f / Production);
	}

	private void SetBaseColour_ServerOnly(Color Colour)
	{
		BaseColour = Colour;
	}

	public static readonly List<Vector3> BrotherOffsets = [new(170, 100), new(170, -100), new(0, -200), new(-170, -100), new(-170, 100), new(0, 200)];

	protected override void OnAwake()
	{
		base.OnAwake();

		Assert.IsValid(FogPrefab);

		if (!IsRevealed)
		{
			OnHide();
		}
	}

	protected override void OnStart()
	{
		Assert.IsValid(FogPrefab);

		base.OnStart();

		QueuedUnits.OnChanged += OnQueuedObjectsChanged; // TODO : unbind?

		if (Networking.IsHost)
		{
			InitHex_ServerOnly();
		}

		ModelRenderer.Model = TypeModels[Type];
		HighlightOutline.Enabled = false;
		HighlightOutline.Color = Color.Blue;
	}

	protected override void OnDestroy()
	{
		LocalCameraMode = ECameraMode.Normal;

		base.OnDestroy();
	}

	public void InitHex_ServerOnly()
	{
		Assert.True(Networking.IsHost);

		Type = Random.Shared.FromArray(Enum.GetValues<EHexType>());

		switch (Type)
		{
			case EHexType.Grass:
				Production = Random.Shared.Int(3, 4);
				break;
			case EHexType.Sand:
				Production = Random.Shared.Int(1, 6);
				break;
			case EHexType.Stone:
				Production = Random.Shared.Int(3, 6);
				break;
			case EHexType.Water:
				Production = Random.Shared.Int(1, 5);
				break;
		}

		if (Random.Shared.Next(7) == 1 && Type == EHexType.Grass)
		{
			GameManager.Instance.Server_CreateHexObject("tree", this, Connection.Host.Id);
			GameManager.Instance.Server_CreateHexUnitObject("unit-combat", this, Connection.Host.Id, true);
			SetOwner_ServerOnly(GameManager.AiGuid);
		}
	}

	private void OnQueuedObjectsChanged(NetListChangeEvent<FQueueObject> QueueObjectChanged)
	{
		if (IsLocallyOwned())
		{
			DrawUnitQueue();
		}
	}

	public override void OnClick()
	{
		base.OnClick();

		IsSelected = !IsSelected;
	}

	public Transform GetObjectSpawnLocation()
	{
		var BaseTransform = WorldTransform;
		BaseTransform.Position += Vector3.Up * 40; // 40 = hex width
		return BaseTransform;
	}

	private readonly Dictionary<Guid, GameText> QueuedObjectTexts = new();

	public void DrawUnitQueue()
	{
		List<Guid> GuidsToRemove = new();

		int TextYPadding = 100;
		foreach (var QueuedObject in QueuedUnits)
		{
			TextYPadding += 50;

			if (QueuedObjectTexts.TryGetValue(QueuedObject.GameObjectId, out var FoundText))
			{
				FoundText.SetText($"{QueuedObject.GetBuildString()}");

				if (QueuedObject.IsReadyToBuild())
				{
					GuidsToRemove.Add(QueuedObject.GameObjectId);
				}
				continue;
			}

			var TextTransform = BuildingData.Transform;
			TextTransform.Position += Vector3.Up * TextYPadding;

			var TextObject = GameText.CreateTextObject<GameText>(TextTransform, $"{QueuedObject.GetBuildString()}");
			QueuedObjectTexts.Add(QueuedObject.GameObjectId, TextObject);
		}

		foreach (var Guid in GuidsToRemove)
		{
			RemoveQueuedObjectText(Guid);
		}
	}

	public void RemoveQueuedObjectText(Guid GameObjectId)
	{
		if (QueuedObjectTexts.TryGetValue(GameObjectId, out var FoundText))
		{
			FoundText.DestroyGameObject();
			QueuedObjectTexts.Remove(GameObjectId);
		}
	}

	private readonly List<GameText> ProductionTexts = new();

	private void ProductionShown(bool Shown)
	{
		foreach (var ProductionText in ProductionTexts)
		{
			ProductionText.DestroyGameObject();
		}
		ProductionTexts.Clear();

		if (Shown && IsRevealed)
		{
			var TextTransform = WorldTransform;
			TextTransform.Position += Vector3.Up * 25;

			var ProductionText = GameText.CreateTextObject<GameText>(TextTransform, $"{Production}⬡");

			if (Production <= 2)
			{
				ProductionText.GetComponent<TextRenderer>().Color = Color.Red;
			}
			else if (Production > 6)
			{
				ProductionText.GetComponent<TextRenderer>().Color = Color.Green;
			}

			ProductionTexts.Add(ProductionText);
		}
	}

	private void OnHighlight()
	{
		if (!CanWalkOn())
		{
			return;
		}

		HighlightOutline.Enabled = true;
	}

	private void OnDeHighlight()
	{
		ModelRenderer.MaterialOverride = null;
		HighlightOutline.Enabled = false;
	}

	private void OnReveal()
	{
		if (FogObject.IsValid())
		{
			FogObject.Destroy();
			FogObject = null;
		}

		SetBaseColour_ServerOnly(GameManager.Instance.GetTintColour(OwningConnectionGuid));

		// TODO : revisit
		OnUnitDataChanged(new(), new());
		OnBuildingDataChanged(new(), new());
		OnObjectDataChanged(new(), new());

		// TODO : REALLY revisit
		if (GamePlayer.Local != null)
		{
			LocalCameraMode = GamePlayer.Local.CameraMode;
		}
	}

	private void OnHide()
	{
		CloneConfig FogConfig = new(WorldTransform);
		FogObject = FogPrefab.Clone(FogConfig);

		// TODO : revisit
		OnUnitDataChanged(new(), new());
		OnBuildingDataChanged(new(), new());
		OnObjectDataChanged(new(), new());
	}

	private void OnSelect()
	{
		// ModelRenderer.Tint = BaseColour.Darken(0.66f);
	}

	private void OnDeselect()
	{
		// ModelRenderer.Tint = BaseColour;
	}

	/////////////////////////////////////////////////////////////////////////////////////////////////

	[Sync(SyncFlags.FromHost)] public NetList<Hex> AllBrothers { get; set; } = [null, null, null, null, null, null];

	private ECameraMode _LocalCameraMode = ECameraMode.Normal;
	public ECameraMode LocalCameraMode
	{
		get => _LocalCameraMode;
		set
		{
			_LocalCameraMode = value;

			if (UnitObject.IsValid())
			{
				UnitObject.SetCameraMode(_LocalCameraMode);
			}

			ProductionShown(_LocalCameraMode == ECameraMode.Build);
		}
	}

	private bool _isHighlighted = false;
	public bool IsHighlighted
	{
		get => _isHighlighted;
		set
		{
			if (_isHighlighted == value)
			{
				return;
			}

			_isHighlighted = value;

			if (_isHighlighted)
			{
				OnHighlight();
			}
			else
			{
				OnDeHighlight();
			}
		}
	}

	private bool _isRevealed = false;
	public bool IsRevealed
	{
		get => _isRevealed;
		set
		{
			if (_isRevealed == value)
			{
				return;
			}

			_isRevealed = value;

			if (_isRevealed)
			{
				OnReveal();
			}
			else
			{
				OnHide();
			}
		}
	}

	private bool _isSelected = false;
	public bool IsSelected
	{
		get => _isSelected;
		set
		{
			if (_isSelected == value)
			{
				return;
			}

			_isSelected = value;

			if (_isSelected)
			{
				OnSelect();
			}
			else
			{
				OnDeselect();
			}
		}
	}

	public bool HasOwner()
	{
		return OwningConnectionGuid != Guid.Empty;
	}

	public bool IsLocallyOwned()
	{
		return GetOwnerId() == GamePlayer.Local.ConnectionId;
	}

	public bool CanWalkOn()
	{
		return Type != EHexType.Water;
	}

	public Guid GetOwnerId()
	{
		return OwningConnectionGuid;
	}

	public List<Hex> GetSurroundingHexesInRange(int Range, bool IncludeSelf = false)
	{
		List<Hex> SurroundingHexes = [];

		if (IncludeSelf)
		{
			SurroundingHexes.Add(this);
		}

		HashSet<Hex> Visited = [this];
		List<Hex> CurrentRing = [this];

		for (int RangeIndex = 0; RangeIndex < Range; ++RangeIndex)
		{
			List<Hex> NextRing = [];

			foreach (var Hex in CurrentRing)
			{
				foreach (var Brother in Hex.AllBrothers)
				{
					if (Brother == null)
					{
						continue;
					}

					var BrotherHex = Brother.GetComponent<Hex>();

					if (BrotherHex == null || !Visited.Add(BrotherHex))
					{
						continue;
					}

					SurroundingHexes.Add(BrotherHex);
					NextRing.Add(BrotherHex);
				}
			}

			CurrentRing = NextRing;
		}

		return SurroundingHexes;
	}

	public static void SetOwnerHexesRecursive_ServerOnly(Hex Hex, Guid OwningConnectionGuid, int Depth)
	{
		Assert.True(Networking.IsHost);
		if (Hex == null || Depth <= 0) return;

		if (OwningConnectionGuid != Guid.Empty)
		{
			Hex.SetOwner_ServerOnly(OwningConnectionGuid);
		}
		else
		{
			Hex.RemoveOwner_ServerOnly();
		}

		foreach (var Brother in Hex.AllBrothers)
		{
			if (Brother == null) continue;
			SetOwnerHexesRecursive_ServerOnly(Brother.GetComponent<Hex>(), OwningConnectionGuid, Depth - 1);
		}
	}

	public static void RevealHexesRecusrive(Hex Hex, bool Reveal, int Depth)
	{
		if (Hex == null || Depth <= 0) return;

		Hex.IsRevealed = Reveal;

		foreach (var Brother in Hex.AllBrothers)
		{
			if (Brother == null) continue;
			RevealHexesRecusrive(Brother.GetComponent<Hex>(), Reveal, Depth - 1);
		}
	}

	public static void HighlightHexesRecusrive(Hex Hex, bool Highlight, int Depth)
	{
		if (Hex == null || Depth <= 0) return;

		Hex.IsHighlighted = Highlight;

		foreach (var Brother in Hex.AllBrothers)
		{
			if (Brother == null) continue;
			HighlightHexesRecusrive(Brother.GetComponent<Hex>(), Highlight, Depth - 1);
		}
	}

	public static bool AreHexesUnowned(Hex Hex, int Depth, Guid AllowedOwner)
	{
		if (Hex == null || Depth <= 0) return true;

		if (Hex.OwningConnectionGuid != Guid.Empty && Hex.OwningConnectionGuid != AllowedOwner)
		{
			return false;
		}

		foreach (var Brother in Hex.AllBrothers)
		{
			if (Brother == null) continue;
			if (!AreHexesUnowned(Brother.GetComponent<Hex>(), Depth - 1, AllowedOwner))
			{
				return false;
			}
		}

		return true;
	}
}