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;
}
}