FishFusion.cs

Static helper for the fish fusion mechanic. Manages starting/cancelling a pending fusion, validating targets, and resolving resulting species for fish+fish or fish+decor combinations.

namespace NoChillquarium;

/// <summary>
/// Combine fish (and fish + decor) into abominations.
/// Flow: fish menu → Combine → click second fish <b>or</b> a decoration.
/// Any two fish can fuse. Special recipes first; otherwise:
///   base × base → Type 1–10 abom (hash)
///   monster × normal (or any unhandled monster pair) → Tentacle Beast
/// Evolution chain (Gold / Betta line) still applies when the pair matches.
/// </summary>
public static class FishFusion
{
	static string _pendingId;
	static int _version;

	public static int Version => _version;
	/// <summary>True while waiting for the second target (fish or decor).</summary>
	public static bool PickingSecond => !string.IsNullOrEmpty( _pendingId );
	public static string PendingId => _pendingId;

	/// <summary>
	/// Fusion unlocks after the basic care loop: own food + feed once.
	/// Keeps early game on grandma → wallet → food → feed before abominations.
	/// </summary>
	public static bool IsUnlocked => Progression.CombineUnlocked;

	public static string UnlockHint
	{
		get
		{
			if ( IsUnlocked )
				return "";
			if ( !FeedSystem.HasAnyFood )
				return "Buy food first (Shop → Food)";
			if ( !StorySystem.HasFedOnce )
				return "Feed your fish once first";
			return "Not yet";
		}
	}

	public static void Clear()
	{
		_pendingId = null;
		_version++;
	}

	/// <summary>Start combine from fish menu (first parent). Any fish — monsters included.</summary>
	public static bool Begin( string fishId )
	{
		if ( string.IsNullOrEmpty( fishId ) || !TankSim.IsActive )
			return false;

		if ( !IsUnlocked )
		{
			TankSim.ShowBanner( UnlockHint );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		var a = TankSim.FindFish( fishId );
		if ( a is null )
		{
			TankSim.ShowBanner( "That fish swam off." );
			return false;
		}

		if ( a.OnAdventure )
		{
			TankSim.ShowBanner( "They're in the pipes. Not now." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		var canFish = CountEligibleFish( a ) > 0;
		// Base fish can still fuse with furniture; monsters need a body.
		var canDecor = !a.IsMonster && TankSim.DecorCount > 0;
		if ( !canFish && !canDecor )
		{
			TankSim.ShowBanner( a.IsMonster
				? "Need another fish in this tank."
				: "Need another fish or a decor piece." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		_pendingId = fishId;
		_version++;
		TankSim.ShowBanner( a.IsMonster
			? "Combine · click any other fish (normal → Tentacle Beast)."
			: "Combine · click fish OR decor." );
		GameAudio.PlayUi( GameAudio.Notice );
		return true;
	}

	public static void Cancel()
	{
		if ( !PickingSecond )
			return;
		_pendingId = null;
		_version++;
		TankSim.ShowBanner( "Combine cancelled." );
		GameAudio.PlayUi( GameAudio.Close );
	}

	static int CountEligibleFish( FishActor pending )
	{
		if ( pending?.Species is null )
			return 0;
		var n = 0;
		foreach ( var f in TankSim.Fish )
		{
			if ( f is null || f.InstanceId == pending.InstanceId )
				continue;
			if ( f.OnAdventure || f.Species is null )
				continue;
			// Any other living fish in this tank can fuse now.
			if ( ResolveMonster( pending.Species.Id, f.Species.Id ) is not null )
				n++;
		}
		return n;
	}

	/// <summary>Second fish chosen — fuse or fail.</summary>
	public static bool TryComplete( string secondId )
	{
		if ( !PickingSecond )
			return false;
		if ( !IsUnlocked )
		{
			Cancel();
			TankSim.ShowBanner( UnlockHint );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		if ( string.IsNullOrEmpty( secondId ) )
			return false;

		var firstId = _pendingId;
		_pendingId = null;
		_version++;

		if ( firstId == secondId )
		{
			TankSim.ShowBanner( "Need two different fish — or click a decor." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		return TankSim.TryCombineFish( firstId, secondId );
	}

	/// <summary>Fuse pending fish with a decoration piece (base fish only).</summary>
	public static bool TryCompleteWithDecor( DecorPiece piece )
	{
		if ( !PickingSecond )
			return false;
		if ( !IsUnlocked )
		{
			Cancel();
			TankSim.ShowBanner( UnlockHint );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		if ( piece is null )
			return false;

		var firstId = _pendingId;
		var fish = TankSim.FindFish( firstId );
		_pendingId = null;
		_version++;

		if ( fish?.IsMonster == true )
		{
			TankSim.ShowBanner( "Monsters evolve with fish, not furniture." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		return TankSim.TryCombineFishWithDecor( firstId, piece );
	}

	/// <summary>
	/// Resolve result species for two parent species ids (order-independent).
	/// Always returns a result for any two known species — no dead ends.
	/// </summary>
	public static FishSpecies ResolveMonster( string idA, string idB )
	{
		if ( string.IsNullOrEmpty( idA ) || string.IsNullOrEmpty( idB ) )
			return null;

		idA = idA.ToLowerInvariant();
		idB = idB.ToLowerInvariant();

		var a = FishSpecies.Find( idA );
		var b = FishSpecies.Find( idB );
		if ( a is null || b is null )
			return null;

		// Same species doubles → mono-monster (bases). Monster doubles → tentacle.
		if ( idA == idB )
		{
			var mono = idA switch
			{
				"goldfish" => FishSpecies.MonsterGoldfish,
				"betta" => FishSpecies.MonsterBetta,
				"neon_tetra" or "guppy" or "molly" => FishSpecies.MonsterNeon,
				"clownfish" or "seahorse" or "puffer" => FishSpecies.MonsterClown,
				"blue_tang" or "lionfish" or "mandarin" or "barracuda" => FishSpecies.MonsterTang,
				"koi" or "oscar" or "angelfish" or "piranha" or "axolotl" => FishSpecies.MonsterGoldfish,
				// Already an abom: two of the same → deeper horror.
				_ when a.IsMonster => FishSpecies.MonsterTentacle,
				_ => null
			};
			if ( mono is not null )
				return mono;
		}

		// Canonical key: sorted pair
		var lo = string.CompareOrdinal( idA, idB ) <= 0 ? idA : idB;
		var hi = string.CompareOrdinal( idA, idB ) <= 0 ? idB : idA;
		var key = lo + "+" + hi;

		// --- Gold / Betta evolution chain (stage 2 + 3) ---
		if ( key is "monster_betta+monster_goldfish"
			or "evo_gold_betta+monster_betta"
			or "evo_gold_betta+monster_goldfish"
			or "evo_gold_betta+monster_01" )
			return FishSpecies.EvoGoldBettaApex;

		if ( key is "betta+monster_goldfish"
			or "goldfish+monster_betta"
			or "monster_01+monster_goldfish"
			or "monster_01+monster_betta" )
			return FishSpecies.EvoGoldBetta;

		// Signature mixed pairs (base × base)
		var known = key switch
		{
			"betta+goldfish" => FishSpecies.Monster1,
			"goldfish+neon_tetra" => FishSpecies.Monster2,
			"clownfish+goldfish" => FishSpecies.Monster3,
			"blue_tang+goldfish" => FishSpecies.Monster4,
			"betta+neon_tetra" => FishSpecies.Monster5,
			"betta+clownfish" => FishSpecies.Monster6,
			"betta+blue_tang" => FishSpecies.Monster7,
			"clownfish+neon_tetra" => FishSpecies.Monster8,
			"blue_tang+neon_tetra" => FishSpecies.Monster9,
			"blue_tang+clownfish" => FishSpecies.Monster10,
			_ => null
		};
		if ( known is not null )
			return known;

		// Any two non-monster bases → hash into Type 1–10
		if ( !a.IsMonster && !b.IsMonster )
		{
			var h = Math.Abs( StringComparer.Ordinal.GetHashCode( key ) );
			return (h % 10) switch
			{
				0 => FishSpecies.Monster1,
				1 => FishSpecies.Monster2,
				2 => FishSpecies.Monster3,
				3 => FishSpecies.Monster4,
				4 => FishSpecies.Monster5,
				5 => FishSpecies.Monster6,
				6 => FishSpecies.Monster7,
				7 => FishSpecies.Monster8,
				8 => FishSpecies.Monster9,
				_ => FishSpecies.Monster10
			};
		}

		// Monster + normal fish (or any other monster pair without a special recipe)
		// → giant Lovecraftian tentacle beast.
		return FishSpecies.MonsterTentacle;
	}

	/// <summary>Fish + decor hybrid (any base fish × this decor id).</summary>
	public static FishSpecies ResolveDecorHybrid( string decorId )
	{
		if ( string.IsNullOrEmpty( decorId ) )
			return null;
		return decorId.ToLowerInvariant() switch
		{
			"plant_green" or "fake_coral" or "rock_pile" => FishSpecies.HybridPlant,
			"treasure_chest" or "cheap_trophy" or "plastic_castle" => FishSpecies.HybridChest,
			"bubble_wand" or "lava_lamp" => FishSpecies.HybridBubble,
			"forty_oz" or "pizza_box" => FishSpecies.HybridForty,
			"handcuffs_skeleton" or "road_cone" => FishSpecies.HybridCuffs,
			"old_boot" or "rusty_anchor" => FishSpecies.HybridBoot,
			"medicine_bottles" => FishSpecies.HybridMeds,
			"doge_statue" => FishSpecies.HybridDoge,
			"m80_crate" => FishSpecies.HybridM80,
			_ => FishSpecies.HybridBoot
		};
	}

	/// <summary>Short chain blurb for UI / help.</summary>
	public static string EvolutionChainHint =>
		"Any two fish fuse. Monster + normal → Tentacle Beast. Gold line still goes Crown Atrocity.";

	public static string RecipeHint( FishSpecies a, FishSpecies b )
	{
		var m = ResolveMonster( a?.Id, b?.Id );
		return m is null ? "No fusion" : m.Name;
	}
}