LotRunSystem.cs

Adventure mode run system for the game. Manages states for flushing a fish into toilet pipes, selecting a map and team, resolving rooms (look, loot, hazard, fight, choice), handling ambushes, rewards, injuries, faucet return and saving/loading progress.

File AccessNetworking
namespace NoChillquarium;

public enum LotRunPhase
{
	Closed,
	/// <summary>Toilet flush cutscene — Adventure Mode cold open.</summary>
	Flush,
	/// <summary>Pick a map location.</summary>
	Map,
	/// <summary>Pick fish team for the run.</summary>
	Team,
	/// <summary>Standing in a room — act or continue.</summary>
	Room,
	/// <summary>After a fight resolve, before next room.</summary>
	RoomResult,
	/// <summary>Died on adventure — spit back out the kitchen faucet.</summary>
	Faucet,
	/// <summary>Run complete or wiped (post-faucet / normal maps).</summary>
	Done
}

/// <summary>
/// Adventure Mode: flush a fish → toilet-pipe crawl → more lot dungeons
/// (jr high, alley, neighbor pool, gas, storm drain, bingo basement).
/// </summary>
public static class LotRunSystem
{
	public const string ToiletLocationId = "toilet_pipes";

	// One-beat cutscenes — click once and crawl (don't force a novella).
	static readonly string[] FlushLines =
	{
		"Handle pulled. They're in the pipes under the lot. Follow or they don't come back easy."
	};

	/// <summary>Wipe only — die on adventure → always faucet home.</summary>
	static readonly string[] FaucetLines =
	{
		"Kitchen faucet knocks. Hurt, angry, alive. Grandma doesn't look up from the sink."
	};

	/// <summary>Random ambush enemies when Look/Loot/Hazard turns into a skirmish.</summary>
	static readonly string[] AmbushNames =
	{
		"Drain Runt", "Lot Skater", "Pipe Bully", "Silt Gang", "Alley Fry",
		"Wet Ratfish", "Gutter Spec", "Trap Gremlin", "Scale Flake", "Mop Eel"
	};

	static readonly Random _rng = new();
	static readonly HashSet<string> _cleared = new( StringComparer.OrdinalIgnoreCase );
	static readonly List<string> _selectedIds = new();
	static readonly List<string> _log = new();

	static LotRunPhase _phase = LotRunPhase.Closed;
	static LotLocationDef _location;
	static int _roomIndex;
	static int _version;
	static string _roomResultTitle = "";
	static string _roomResultBody = "";
	static string _doneTitle = "";
	static string _doneBody = "";
	static double _runEarnings;
	static bool _runWon;
	static string _lastEnemyName = "";
	/// <summary>Player has flushed once — Adventure map is open.</summary>
	static bool _hasFlushed;
	static int _flushLine;
	static string _flushFishId = "";
	static string _flushFishName = "";
	static int _faucetLine;
	/// <summary>True while a flushed fish is away on a crawl (must faucet-return on wipe/bail).</summary>
	static bool _fishAwayOnAdventure;

	public static int Version => _version;
	public static LotRunPhase Phase => _phase;
	public static bool IsOpen => _phase != LotRunPhase.Closed;
	public static bool IsFlushing => _phase == LotRunPhase.Flush;
	public static bool IsFaucetReturn => _phase == LotRunPhase.Faucet;
	public static LotLocationDef Location => _location;
	public static int RoomIndex => _roomIndex;
	public static int RoomCount => _location?.Rooms?.Count ?? 0;
	public static LotRoomDef CurrentRoom =>
		_location?.Rooms is null || _roomIndex < 0 || _roomIndex >= _location.Rooms.Count
			? null
			: _location.Rooms[_roomIndex];
	public static IReadOnlyList<string> SelectedIds => _selectedIds;
	public static int SelectedCount => _selectedIds.Count;
	public static IReadOnlyList<string> Log => _log;
	public static string RoomResultTitle => _roomResultTitle;
	public static string RoomResultBody => _roomResultBody;
	public static string DoneTitle => _doneTitle;
	public static string DoneBody => _doneBody;
	public static double RunEarnings => _runEarnings;
	public static bool RunWon => _runWon;
	public static string LastEnemyName => _lastEnemyName;
	/// <summary>Selected team combat power (END/SPD/AGI) — train raises this.</summary>
	public static float SelectedTeamPower => TeamPower();
	/// <summary>Ð bagged this crawl (shown in room header so risk/reward is always visible).</summary>
	public static string RunEarningsLabel =>
		_runEarnings > 0.01
			? "bag Ð" + Economy.FormatDoge( _runEarnings )
			: "bag Ð0";
	public static bool HasFlushed => _hasFlushed;
	public static int FlushLineIndex => _flushLine;
	public static int FlushLineCount => FlushLines.Length;
	public static string FlushFishName =>
		string.IsNullOrEmpty( _flushFishName ) ? "your fish" : _flushFishName;
	public static string FlushVisibleLine
	{
		get
		{
			if ( _flushLine < 0 || _flushLine >= FlushLines.Length )
				return "";
			if ( _flushLine == 0 && !string.IsNullOrEmpty( _flushFishName ) )
				return _flushFishName + " swirls down. Pipes under the lot — follow or they don't come back easy.";
			return FlushLines[_flushLine];
		}
	}
	public static int FaucetLineIndex => _faucetLine;
	public static int FaucetLineCount => FaucetLines.Length;
	public static string FaucetVisibleLine
	{
		get
		{
			if ( _faucetLine < 0 || _faucetLine >= FaucetLines.Length )
				return "";
			if ( _faucetLine == 0 && !string.IsNullOrEmpty( _flushFishName ) )
				return "Faucet knocks. " + _flushFishName + " comes through — hurt, angry, alive.";
			return FaucetLines[_faucetLine];
		}
	}

	/// <summary>Player-facing room kind label (short, not UI enum noise).</summary>
	public static string RoomKindLabel
	{
		get
		{
			var room = CurrentRoom;
			if ( room is null )
				return "";
			return room.Kind switch
			{
				LotRoomKind.Look => "SCOUT",
				LotRoomKind.Fight => "FIGHT",
				LotRoomKind.Loot => "LOOT",
				LotRoomKind.Hazard => "HAZARD",
				LotRoomKind.Choice => "CHOICE",
				LotRoomKind.Boss => "BOSS",
				_ => ""
			};
		}
	}

	public static string RoomKindCss
	{
		get
		{
			var room = CurrentRoom;
			if ( room is null )
				return "";
			return room.Kind switch
			{
				LotRoomKind.Look => "kind-look",
				LotRoomKind.Fight => "kind-fight",
				LotRoomKind.Loot => "kind-loot",
				LotRoomKind.Hazard => "kind-hazard",
				LotRoomKind.Choice => "kind-choice",
				LotRoomKind.Boss => "kind-boss",
				_ => ""
			};
		}
	}
	public static bool FishAwayOnAdventure => _fishAwayOnAdventure;
	public static string ProgressLabel
	{
		get
		{
			if ( _location is null || RoomCount <= 0 )
				return "";
			var n = Math.Clamp( _roomIndex + 1, 1, RoomCount );
			return n + "/" + RoomCount;
		}
	}

	public static bool IsCleared( string locationId ) =>
		!string.IsNullOrEmpty( locationId ) && _cleared.Contains( locationId );

	/// <summary>
	/// Flush / Tools → Adventure after the ladder: care → fusion intro → fight intro → adventure intro.
	/// Last gift in the lot cast sequence.
	/// </summary>
	public static bool Unlocked
	{
		get
		{
			if ( !TankSim.IsActive || !GrandmaGifts.HasTank )
				return false;
			if ( TankSim.TotalFishCount < 1 )
				return false;
			// Already mid-adventure (flushed before) — stay open.
			if ( _hasFlushed )
				return true;
			// Soft unlock: adventure intro gift (or long save seed).
			return GrandmaGifts.HasAdventureIntro;
		}
	}

	public static string UnlockHint
	{
		get
		{
			if ( !GrandmaGifts.HasTank )
				return "Need the tank first";
			if ( TankSim.TotalFishCount < 1 )
				return "Need a fish to flush";
			if ( !StorySystem.HasFedOnce )
				return "Feed once first";
			var fusionDone = GrandmaGifts.HasReceived( "janitor_fusion" ) || StorySystem.HasCombinedOnce;
			var fightDone = GrandmaGifts.HasReceived( "lot_bully_fight" )
				|| BattleSystem.Wins + BattleSystem.Losses > 0;
			if ( !fusionDone )
				return "Combine first · then Adventure";
			if ( !fightDone )
				return "Fight once · then Adventure";
			if ( !GrandmaGifts.HasAdventureIntro )
				return "Adventure intro coming…";
			return "";
		}
	}

	/// <summary>Full map after first toilet adventure; before that only flush path.</summary>
	public static bool MapUnlocked => _hasFlushed;

	public static bool IsLocationUnlocked( LotLocationDef loc )
	{
		if ( loc is null || !Unlocked )
			return false;
		// Toilet pipes: map only after you've flushed once (re-runs).
		if ( loc.FlushOrigin )
			return _hasFlushed;
		if ( !_hasFlushed )
			return false;
		if ( loc.RequireTrained && !StorySystem.HasTrainedOnce )
			return false;
		if ( Economy.PlaytimeSeconds < loc.UnlockPlaytime )
			return false;
		var fights = BattleSystem.Wins + BattleSystem.Losses;
		if ( fights < loc.UnlockFights )
			return false;
		return true;
	}

	public static string LocationLockHint( LotLocationDef loc )
	{
		if ( loc is null )
			return "";
		if ( !Unlocked )
			return UnlockHint;
		if ( loc.FlushOrigin && !_hasFlushed )
			return "Flush a fish to start Adventure";
		if ( !_hasFlushed )
			return "Flush a fish first (fish menu → Flush)";
		if ( loc.RequireTrained && !StorySystem.HasTrainedOnce )
			return "Train once first";
		if ( Economy.PlaytimeSeconds < loc.UnlockPlaytime )
		{
			var left = loc.UnlockPlaytime - Economy.PlaytimeSeconds;
			var m = Math.Max( 1, (int)MathF.Ceiling( left / 60f ) );
			return "Play ~" + m + "m more";
		}
		var fights = BattleSystem.Wins + BattleSystem.Losses;
		if ( fights < loc.UnlockFights )
			return "Fight " + (loc.UnlockFights - fights) + " more bout(s)";
		return "";
	}

	/// <summary>Map card risk/reward line (always plain C# — no Ð@ in Razor).</summary>
	public static string LocationRewardSummary( LotLocationDef loc )
	{
		if ( loc is null )
			return "";
		var rooms = loc.Rooms?.Count ?? 0;
		var fee = loc.EntryFee <= 0.01
			? "free entry"
			: "Ð" + Economy.FormatDoge( loc.EntryFee ) + " entry";
		var clear = loc.ClearBonus > 0
			? " · clear ~Ð" + Economy.FormatDoge( loc.ClearBonus )
			: "";
		var first = !IsCleared( loc.Id ) && loc.ClearBonus > 0
			? " · first-clear bonus"
			: "";
		return fee + " · " + rooms + " rooms" + clear + first + " · team " + loc.TeamSize;
	}

	/// <summary>
	/// Room stakes line: expected loot / fight odds / hazard risk.
	/// Keeps Adventure from feeling like blind button mashing.
	/// </summary>
	public static string RoomStakesLabel
	{
		get
		{
			var room = CurrentRoom;
			if ( room is null )
				return "";
			var power = TeamPower();
			return room.Kind switch
			{
				LotRoomKind.Look => "Scout · sometimes ambush · train = safer",
				LotRoomKind.Loot => LootStakes( room ),
				LotRoomKind.Hazard => HazardStakes( room ),
				LotRoomKind.Fight or LotRoomKind.Boss => FightStakes( room, power ),
				LotRoomKind.Choice => ChoiceStakes( room ),
				_ => ""
			};
		}
	}

	static string LootStakes( LotRoomDef room )
	{
		var lo = room.LootMin;
		var hi = Math.Max( lo, room.LootMax );
		return "Loot ~Ð" + Economy.FormatDoge( lo ) + "–" + Economy.FormatDoge( hi )
			+ " · SPD/AGI scouts more";
	}

	static string HazardStakes( LotRoomDef room )
	{
		var chance = (int)MathF.Round( ScaleInjuryChance( room.InjuryChance ) * 100f );
		return "Injury risk ~" + chance + "% · END/AGI shrugs pipes";
	}

	static string FightStakes( LotRoomDef room, float power )
	{
		var them = EstimateRoomThreat( room );
		var ratio = power / MathF.Max( 0.5f, them );
		var lo = room.FightRewardMin;
		var hi = Math.Max( lo, room.FightRewardMax );
		var purse = "purse ~Ð" + Economy.FormatDoge( lo ) + "–" + Economy.FormatDoge( hi );
		var odds = ratio >= 1.15f ? "FAVOR"
			: ratio >= 0.85f ? "EVEN"
			: ratio >= 0.55f ? "UNDERDOG"
			: "LONG SHOT";
		var boss = room.Kind == LotRoomKind.Boss ? " · BOSS" : "";
		return odds + boss + " · you " + power.ToString( "0.#" )
			+ " vs ~" + them.ToString( "0.#" ) + " · " + purse;
	}

	static string ChoiceStakes( LotRoomDef room )
	{
		var a = room.ChoiceALoot > 0 ? "A ~Ð" + Economy.FormatDoge( room.ChoiceALoot ) : "A safe";
		var b = room.ChoiceBLoot > 0 ? "B ~Ð" + Economy.FormatDoge( room.ChoiceBLoot ) : "B risky";
		if ( room.ChoiceBInjuryChance > 0.05f )
			b += " · injury";
		return a + " · " + b;
	}

	/// <summary>Mid threat estimate (same scale as TeamPower / CombatPower).</summary>
	public static float EstimateRoomThreat( LotRoomDef room )
	{
		if ( room is null )
			return 8f;
		var lo = room.AiStatMin > 0.1f ? room.AiStatMin : 2f;
		var hi = room.AiStatMax > lo ? room.AiStatMax : lo + 1.4f;
		var mid = (lo + hi) * 0.5f;
		var pow = mid * 3.45f;
		var n = Math.Max( 1, _selectedIds.Count );
		pow *= 1f + (n - 1) * 0.28f;
		if ( room.Kind == LotRoomKind.Boss )
			pow *= 1.08f;
		return MathF.Max( 1.5f, pow );
	}

	public static void ResetProgress()
	{
		_cleared.Clear();
		_hasFlushed = false;
		Clear();
	}

	public static void Clear()
	{
		// Safety: never leave a fish stuck in the pipes across sessions.
		ReturnAllAdventureFish( quiet: true, applyInjury: false );
		_phase = LotRunPhase.Closed;
		_location = null;
		_roomIndex = 0;
		_selectedIds.Clear();
		_log.Clear();
		_roomResultTitle = "";
		_roomResultBody = "";
		_doneTitle = "";
		_doneBody = "";
		_runEarnings = 0;
		_runWon = false;
		_lastEnemyName = "";
		_flushLine = 0;
		_flushFishId = "";
		_flushFishName = "";
		_faucetLine = 0;
		_fishAwayOnAdventure = false;
		_version++;
	}

	public static void Load( SaveData data )
	{
		_cleared.Clear();
		// Clear without double-return during load.
		_phase = LotRunPhase.Closed;
		_location = null;
		_selectedIds.Clear();
		_log.Clear();
		_fishAwayOnAdventure = false;
		_flushFishId = "";
		_flushFishName = "";
		_faucetLine = 0;
		_hasFlushed = data?.AdventureFlushed == true;
		if ( data?.LotRunsCleared is not null )
		{
			foreach ( var id in data.LotRunsCleared )
			{
				if ( !string.IsNullOrWhiteSpace( id ) )
					_cleared.Add( id.Trim() );
			}
		}
		if ( _cleared.Count > 0 )
			_hasFlushed = true;
		// Any fish flagged from a crashed session — home without injury spam.
		ReturnAllAdventureFish( quiet: true, applyInjury: false );
		_version++;
	}

	public static void WriteToSave( SaveData data )
	{
		if ( data is null )
			return;
		data.LotRunsCleared = _cleared.ToList();
		data.AdventureFlushed = _hasFlushed;
	}

	// ---- Open / flush / close ----

	/// <summary>Fish menu → Flush: Adventure Mode cold open.</summary>
	public static bool TryFlushFish( string fishId )
	{
		if ( !TankSim.IsActive || !Unlocked )
		{
			TankSim.ShowBanner( string.IsNullOrEmpty( UnlockHint ) ? "Can't flush right now." : UnlockHint );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		var fish = TankSim.FindFish( fishId );
		if ( fish is null || fish.Species is null )
		{
			TankSim.ShowBanner( "Pick a real fish." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		if ( fish.IsFightInjured )
		{
			TankSim.ShowBanner( "Hurt fish — wait out cooldown before flushing." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		if ( GrandmaGifts.IsBusy || StorySystem.CardOpen )
			return false;
		if ( BattleSystem.IsOpen )
			BattleSystem.Close();
		if ( TrainingSystem.TrainOpen )
			TrainingSystem.CancelTrain();
		if ( TrainingSystem.InjectOpen )
			TrainingSystem.CancelInject();
		if ( ExplodeSystem.ModeActive )
			ExplodeSystem.ModeActive = false;

		_flushFishId = fish.InstanceId;
		_flushFishName = fish.DisplayName;
		_flushLine = 0;
		_faucetLine = 0;
		_phase = LotRunPhase.Flush;
		_location = null;
		_selectedIds.Clear();
		_log.Clear();
		_runEarnings = 0;
		_runWon = false;
		_fishAwayOnAdventure = false;
		Chaos.Add( 2f );
		Karma.Add( -0.5f ); // she noticed
		GameAudio.PlayUi( GameAudio.Notice );
		TankSim.ShowBanner( _flushFishName + " is in the pipes." );
		_version++;
		return true;
	}

	public static void AdvanceFlush()
	{
		if ( _phase != LotRunPhase.Flush )
			return;
		if ( _flushLine < FlushLines.Length - 1 )
		{
			_flushLine++;
			GameAudio.PlayUi( GameAudio.Pop );
			_version++;
			return;
		}
		// End cutscene → toilet pipe run with that fish.
		BeginToiletAdventure();
	}

	/// <summary>Wipe path: click through faucet return until fish is home.</summary>
	public static void AdvanceFaucet()
	{
		if ( _phase != LotRunPhase.Faucet )
			return;
		if ( _faucetLine < FaucetLines.Length - 1 )
		{
			_faucetLine++;
			GameAudio.PlayUi( GameAudio.Pop );
			_version++;
			return;
		}
		CompleteFaucetReturn();
	}

	static void BeginToiletAdventure()
	{
		var loc = LotLocationDef.Find( ToiletLocationId );
		if ( loc is null )
		{
			Clear();
			return;
		}
		var fish = TankSim.FindFish( _flushFishId );
		if ( fish is null )
		{
			TankSim.ShowBanner( "Fish vanished before the swirl. Spooky." );
			Clear();
			return;
		}

		_hasFlushed = true;
		_location = loc;
		_selectedIds.Clear();
		_selectedIds.Add( fish.InstanceId );
		// Leave the glass — adventure until win (continue mode) or die (faucet).
		fish.OnAdventure = true;
		_fishAwayOnAdventure = true;
		TankSim.BumpFrame();

		_roomIndex = 0;
		_runEarnings = 0;
		_runWon = false;
		_log.Clear();
		_log.Add( FlushFishName + " · toilet pipes." );
		_phase = LotRunPhase.Room;
		GameAudio.PlayUi( GameAudio.Confirm );
		TankSim.ShowBanner( "Adventure Mode · Toilet Pipes." );
		SaveGame.TrySave( quiet: true );
		_version++;
	}

	/// <summary>Tools → Adventure: map if flushed before; else tell player to flush.</summary>
	public static bool TryOpen()
	{
		if ( !TankSim.IsActive )
			return false;
		if ( !Unlocked )
		{
			TankSim.ShowBanner( UnlockHint );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}
		if ( GrandmaGifts.IsBusy || StorySystem.CardOpen )
			return false;
		if ( BattleSystem.IsOpen )
			BattleSystem.Close();
		if ( TrainingSystem.TrainOpen )
			TrainingSystem.CancelTrain();
		if ( TrainingSystem.InjectOpen )
			TrainingSystem.CancelInject();
		if ( ExplodeSystem.ModeActive )
			ExplodeSystem.ModeActive = false;

		if ( !_hasFlushed )
		{
			TankSim.ShowBanner( "Adventure starts dirty · click a fish → Flush" );
			GameAudio.PlayUi( GameAudio.Notice );
			return false;
		}

		_phase = LotRunPhase.Map;
		_location = null;
		_roomIndex = 0;
		_selectedIds.Clear();
		_log.Clear();
		_runEarnings = 0;
		_runWon = false;
		GameAudio.PlayUi( GameAudio.Notice );
		_version++;
		return true;
	}

	public static void Close()
	{
		if ( _phase == LotRunPhase.Closed )
			return;

		// Mid-crawl with fish in the pipes — they still come home via faucet.
		if ( _fishAwayOnAdventure
			&& _phase is LotRunPhase.Room or LotRunPhase.RoomResult )
		{
			TankSim.ShowBanner( "Bailed. Faucet's gonna spit them out anyway." );
			StartFaucetReturn(
				"BAILED",
				"You left them in the pipes. The faucet still brings them home." );
			GameAudio.PlayUi( GameAudio.Close );
			return;
		}

		if ( _phase is LotRunPhase.Team && _location is not null && _location.EntryFee <= 0 )
		{
			// no fee
		}
		else if ( _phase is LotRunPhase.Team or LotRunPhase.Room or LotRunPhase.RoomResult )
		{
			if ( _location is not null && _location.EntryFee > 0 )
				TankSim.ShowBanner( "Bailed the run. Entry fee stays spent." );
		}

		if ( _phase == LotRunPhase.Flush )
			TankSim.ShowBanner( "Flush cancelled. Fish stays in the tank. Coward." );

		if ( _phase == LotRunPhase.Faucet )
		{
			// Force-complete faucet so fish isn't stuck away.
			CompleteFaucetReturn();
			return;
		}

		Clear();
		GameAudio.PlayUi( GameAudio.Close );
	}

	public static void SelectLocation( LotLocationDef loc )
	{
		if ( _phase != LotRunPhase.Map || loc is null )
			return;
		if ( !IsLocationUnlocked( loc ) )
		{
			TankSim.ShowBanner( LocationLockHint( loc ) );
			GameAudio.PlayUi( GameAudio.Error );
			_version++;
			return;
		}
		if ( Economy.Balance + 0.001 < loc.EntryFee )
		{
			TankSim.ShowBanner( "Need Ð" + Economy.FormatDoge( loc.EntryFee ) + " entry." );
			GameAudio.PlayUi( GameAudio.Error );
			_version++;
			return;
		}

		_location = loc;
		_selectedIds.Clear();
		AutoPickTeam( loc.TeamSize );
		_phase = LotRunPhase.Team;
		GameAudio.PlayUi( GameAudio.Pop );
		_version++;
	}

	public static void ToggleFish( string instanceId )
	{
		if ( _phase != LotRunPhase.Team || _location is null || string.IsNullOrEmpty( instanceId ) )
			return;
		var fish = TankSim.FindFish( instanceId );
		if ( !CanBring( fish ) )
		{
			GameAudio.PlayUi( GameAudio.Error );
			return;
		}
		if ( _selectedIds.Contains( instanceId ) )
		{
			_selectedIds.Remove( instanceId );
			GameAudio.PlayUi( GameAudio.Pop );
			_version++;
			return;
		}
		if ( _selectedIds.Count >= _location.TeamSize )
		{
			// Replace weakest.
			_selectedIds.RemoveAt( 0 );
		}
		_selectedIds.Add( instanceId );
		GameAudio.PlayUi( GameAudio.Pop );
		_version++;
	}

	public static bool CanBring( FishActor fish ) =>
		fish is not null
		&& fish.Species is not null
		&& !fish.IsFightInjured
		&& !fish.OnAdventure;

	public static bool CanStartRun()
	{
		if ( _phase != LotRunPhase.Team || _location is null )
			return false;
		if ( _selectedIds.Count < _location.TeamSize )
			return false;
		if ( Economy.Balance + 0.001 < _location.EntryFee )
			return false;
		foreach ( var id in _selectedIds )
		{
			if ( !CanBring( TankSim.FindFish( id ) ) )
				return false;
		}
		return true;
	}

	public static void StartRun()
	{
		if ( !CanStartRun() )
		{
			TankSim.ShowBanner( "Pick " + (_location?.TeamSize ?? 1) + " healthy fish · pay entry." );
			GameAudio.PlayUi( GameAudio.Error );
			_version++;
			return;
		}
		if ( !Economy.TrySpend( _location.EntryFee ) )
		{
			GameAudio.PlayUi( GameAudio.Error );
			_version++;
			return;
		}

		_roomIndex = 0;
		_runEarnings = 0;
		_runWon = false;
		_log.Clear();
		_log.Add( "Entered " + _location.Name + " (−Ð" + Economy.FormatDoge( _location.EntryFee ) + ")." );
		_phase = LotRunPhase.Room;
		Chaos.Add( 1f );
		GameAudio.PlayUi( GameAudio.Confirm );
		_version++;
	}

	public static void BackToMap()
	{
		if ( _phase != LotRunPhase.Team )
			return;
		_location = null;
		_selectedIds.Clear();
		_phase = LotRunPhase.Map;
		GameAudio.PlayUi( GameAudio.Pop );
		_version++;
	}

	// ---- Room actions ----

	public static void ActPrimary()
	{
		if ( _phase != LotRunPhase.Room )
			return;
		var room = CurrentRoom;
		if ( room is null )
			return;

		switch ( room.Kind )
		{
			case LotRoomKind.Look:
				// Sometimes the "scout" is an ambush — train or limp.
				if ( TryAmbush( room, chance: 0.34f, afterWin: AmbushAfter.Continue ) )
					return;
				AdvanceRoom( "Moved on.", quiet: true );
				break;
			case LotRoomKind.Loot:
				// Fight for the pocket sometimes.
				if ( TryAmbush( room, chance: 0.22f, afterWin: AmbushAfter.Loot ) )
					return;
				DoLoot( room );
				break;
			case LotRoomKind.Hazard:
				// Living hazard: fight instead of pure scrape roll.
				if ( TryAmbush( room, chance: 0.18f, afterWin: AmbushAfter.HazardSafe ) )
					return;
				DoHazard( room );
				break;
			case LotRoomKind.Fight:
			case LotRoomKind.Boss:
				DoFight( room, isBoss: room.Kind == LotRoomKind.Boss );
				break;
			case LotRoomKind.Choice:
				// Need A/B — primary defaults to A
				DoChoice( room, pickB: false );
				break;
		}
	}

	public static void ActChoiceA()
	{
		if ( _phase != LotRunPhase.Room || CurrentRoom?.Kind != LotRoomKind.Choice )
			return;
		DoChoice( CurrentRoom, pickB: false );
	}

	public static void ActChoiceB()
	{
		if ( _phase != LotRunPhase.Room || CurrentRoom?.Kind != LotRoomKind.Choice )
			return;
		DoChoice( CurrentRoom, pickB: true );
	}

	public static void ContinueFromRoomResult()
	{
		if ( _phase != LotRunPhase.RoomResult )
			return;
		if ( _runWon || AliveTeamCount() <= 0 )
		{
			// Done already staged
			return;
		}
		_roomIndex++;
		if ( _location is null || _roomIndex >= _location.Rooms.Count )
		{
			FinishWin();
			return;
		}
		_phase = LotRunPhase.Room;
		_roomResultTitle = "";
		_roomResultBody = "";
		_version++;
	}

	public static void FinishToMap()
	{
		if ( _phase != LotRunPhase.Done )
			return;
		_phase = LotRunPhase.Map;
		_location = null;
		_selectedIds.Clear();
		_roomIndex = 0;
		_log.Clear();
		_doneTitle = "";
		_doneBody = "";
		GameAudio.PlayUi( GameAudio.Pop );
		_version++;
	}

	// ---- Resolve helpers ----

	enum AmbushAfter
	{
		Continue,
		Loot,
		HazardSafe
	}

	/// <summary>
	/// Sometimes a non-fight room is a skirmish. Stats decide win / pay / injury.
	/// Returns true if an ambush was resolved (caller should not also resolve the room).
	/// </summary>
	static bool TryAmbush( LotRoomDef room, float chance, AmbushAfter afterWin )
	{
		if ( room is null || chance <= 0f )
			return false;
		if ( _rng.NextDouble() >= chance )
			return false;

		// Scale ambush AI from map room index + any fight stats on this room.
		var baseMin = room.AiStatMin > 0.1f ? room.AiStatMin : 1.5f;
		var baseMax = room.AiStatMax > baseMin ? room.AiStatMax : baseMin + 1.4f;
		// Soft rooms (look/loot) fight slightly weaker pests.
		if ( room.Kind is LotRoomKind.Look or LotRoomKind.Loot )
		{
			baseMin = MathF.Max( 1.2f, baseMin * 0.85f );
			baseMax = MathF.Max( baseMin + 0.6f, baseMax * 0.9f );
		}

		var synth = new LotRoomDef
		{
			Title = "Ambush",
			Body = "Something mean in the dark.",
			Kind = LotRoomKind.Fight,
			Button = "Fight",
			AiStatMin = baseMin,
			AiStatMax = baseMax,
			EnemyNames = room.EnemyNames is { Length: > 0 } ? room.EnemyNames : AmbushNames,
			EnemyColor = room.EnemyColor ?? "#607080",
			FightRewardMin = Math.Max( 4, room.FightRewardMin > 0 ? room.FightRewardMin * 0.55 : 6 ),
			FightRewardMax = Math.Max( 8, room.FightRewardMax > 0 ? room.FightRewardMax * 0.65 : 14 )
		};

		var won = ResolveFightCore( synth, isBoss: false, out var pay, out var margin );
		if ( won )
		{
			// Ambush win: pay, then maybe the room reward too.
			if ( afterWin == AmbushAfter.Loot )
			{
				var loot = ScaleLoot( Roll( room.LootMin, room.LootMax ) );
				if ( loot > 0 )
				{
					Economy.Add( loot );
					_runEarnings += loot;
				}
				ShowRoomResult(
					"Ambush loot",
					"Beat " + _lastEnemyName + ". +Ð" + Economy.FormatDoge( pay )
					+ (loot > 0 ? " · pocket +Ð" + Economy.FormatDoge( loot ) : "")
					+ StatMarginNote( margin ) + "." );
				GameAudio.PlayDogeBark();
			}
			else if ( afterWin == AmbushAfter.HazardSafe )
			{
				ShowRoomResult(
					"Fought through",
					"Beat " + _lastEnemyName + ". +Ð" + Economy.FormatDoge( pay )
					+ " · no scrape" + StatMarginNote( margin ) + "." );
				GameAudio.PlayUi( GameAudio.Confirm );
			}
			else
			{
				ShowRoomResult(
					"Ambush",
					"Beat " + _lastEnemyName + ". +Ð" + Economy.FormatDoge( pay )
					+ StatMarginNote( margin ) + "." );
				GameAudio.PlayUi( GameAudio.Confirm );
			}
			QueueAdvanceAfterResult();
		}
		// Loss path already queued result / wipe inside ResolveFightCore.
		return true;
	}

	static void DoLoot( LotRoomDef room )
	{
		var raw = Roll( room.LootMin, room.LootMax );
		var amt = ScaleLoot( raw );
		if ( amt > 0 )
		{
			Economy.Add( amt );
			_runEarnings += amt;
		}
		var scout = TeamScoutNorm();
		var tip = scout >= 0.55f ? " · quick fins" : scout < 0.35f ? " · slow hands" : "";
		_log.Add( "Loot +Ð" + Economy.FormatDoge( amt ) );
		ShowRoomResult( "Loot", "+" + "Ð" + Economy.FormatDoge( amt ) + tip + ". " + (room.Body ?? "") );
		GameAudio.PlayDogeBark();
		Karma.Add( 0.3f );
		QueueAdvanceAfterResult();
	}

	static void DoHazard( LotRoomDef room )
	{
		Chaos.Add( room.ChaosOnHazard );
		// Tough fish (END/AGI) shrug pipes; soft fish get scraped.
		var chance = ScaleInjuryChance( room.InjuryChance );
		var hurt = false;
		if ( _rng.NextDouble() < chance )
			hurt = InjureRandomTeamMember();
		var tough = TeamToughnessNorm();
		var body = hurt
			? "Ouch. One fish is hurt (fight cooldown)."
			: tough >= 0.55f
				? "Trained body. Scraped through clean."
				: "Scraped through. Barely.";
		_log.Add( hurt ? "Hazard · injury" : "Hazard · ok" );
		ShowRoomResult( room.Title ?? "Hazard", body );
		GameAudio.PlayUi( hurt ? GameAudio.Error : GameAudio.Pop );
		if ( AliveTeamCount() <= 0 )
		{
			FinishWipe( "Team wiped on a hazard. Lot laughs." );
			return;
		}
		QueueAdvanceAfterResult();
	}

	static void DoChoice( LotRoomDef room, bool pickB )
	{
		var result = pickB ? room.ChoiceBResult : room.ChoiceAResult;
		var loot = pickB ? room.ChoiceBLoot : room.ChoiceALoot;
		var chaos = pickB ? room.ChoiceBChaos : room.ChoiceAChaos;
		var injChance = pickB ? room.ChoiceBInjuryChance : 0f;
		if ( loot > 0 )
		{
			loot = ScaleLoot( loot );
			Economy.Add( loot );
			_runEarnings += loot;
		}
		if ( chaos > 0 )
			Chaos.Add( chaos );
		var hurt = injChance > 0
			&& _rng.NextDouble() < ScaleInjuryChance( injChance )
			&& InjureRandomTeamMember();
		var body = (result ?? "Done.") + (loot > 0 ? " +Ð" + Economy.FormatDoge( loot ) + "." : "");
		if ( hurt )
			body += " Fish hurt.";
		_log.Add( (pickB ? "Choice B" : "Choice A") + (loot > 0 ? " +Ð" + Economy.FormatDoge( loot ) : "") );
		ShowRoomResult( room.Title ?? "Choice", body );
		if ( loot > 0 )
			GameAudio.PlayCoin();
		else
			GameAudio.PlayUi( GameAudio.Pop );
		if ( AliveTeamCount() <= 0 )
		{
			FinishWipe( "Bad choice. Team down." );
			return;
		}
		QueueAdvanceAfterResult();
	}

	static void DoFight( LotRoomDef room, bool isBoss )
	{
		var won = ResolveFightCore( room, isBoss, out var pay, out var margin );
		if ( won )
		{
			ShowRoomResult(
				isBoss ? "Boss down" : "Win",
				"Beat " + _lastEnemyName + ". +Ð" + Economy.FormatDoge( pay )
				+ StatMarginNote( margin ) + "." );
			GameAudio.PlayUi( GameAudio.Confirm );
			if ( isBoss )
				_runWon = true;
			QueueAdvanceAfterResult();
		}
		// Loss already handled in ResolveFightCore.
	}

	/// <summary>
	/// Shared fight resolve. AI stats map into CombatPower-scale so training matters.
	/// Returns win; pays on win; injures on loss. margin = player/ai power ratio.
	/// </summary>
	static bool ResolveFightCore( LotRoomDef room, bool isBoss, out double pay, out float margin )
	{
		pay = 0;
		var playerPow = TeamPower();
		var aiPow = RollAiPower( room );
		margin = playerPow / MathF.Max( 0.5f, aiPow );

		// Swing + ratio → real chance to lose if soft.
		var swing = 0.82f + (float)_rng.NextDouble() * 0.36f;
		var won = playerPow * swing >= aiPow;

		_lastEnemyName = PickName( room.EnemyNames );

		if ( won )
		{
			var raw = Roll( room.FightRewardMin, room.FightRewardMax );
			// Trained / strong teams extract more Ð; weak squeak by with less.
			var rewardMul = Math.Clamp( 0.55f + margin * 0.4f, 0.55f, 1.7f );
			pay = Math.Round( raw * rewardMul, 1 );
			if ( pay > 0 )
			{
				Economy.Add( pay );
				_runEarnings += pay;
			}
			Karma.Add( isBoss ? 2f : 0.8f );
			Chaos.Add( isBoss ? 2f : 1f );
			_log.Add( "Win vs " + _lastEnemyName + " +Ð" + Economy.FormatDoge( pay ) );
			return true;
		}

		// Loss: always injure one; very outmatched may injure a second if team >1.
		InjureRandomTeamMember();
		if ( margin < 0.55f && AliveTeamCount() > 1 && _rng.NextDouble() < 0.4 )
			InjureRandomTeamMember();
		Karma.Add( 0.4f );
		Chaos.Add( isBoss ? 2.5f : 2f );
		_log.Add( "Loss vs " + _lastEnemyName );
		ShowRoomResult(
			"Loss",
			_lastEnemyName + " slapped your team. Fish hurt"
			+ (margin < 0.7f ? " · train harder next time" : "") + "." );
		GameAudio.PlayUi( GameAudio.Error );
		if ( AliveTeamCount() <= 0 || isBoss )
		{
			FinishWipe( isBoss
				? "Boss ended the run. " + _lastEnemyName + " owns this block."
				: "Team wiped. Crawl home." );
			return false;
		}
		QueueAdvanceAfterResult();
		return false;
	}

	/// <summary>
	/// Map AiStat (≈1–5 rating) into CombatPower space.
	/// Untrained fish ~10 power; AiStat 3 → ~10; trained 10s ~34 crush early pests.
	/// </summary>
	static float RollAiPower( LotRoomDef room )
	{
		var lo = room?.AiStatMin ?? 2f;
		var hi = room?.AiStatMax ?? 4f;
		if ( hi < lo )
			(hi, lo) = (lo, hi);
		var rating = lo + (float)_rng.NextDouble() * MathF.Max( 0.1f, hi - lo );
		// ~3.45× rating ≈ same formula weight as END+SPD+AGI at that level.
		var pow = rating * 3.45f;
		// Multi-fish teams face harder packs.
		var n = Math.Max( 1, _selectedIds.Count );
		pow *= 1f + (n - 1) * 0.28f;
		// Boss rooms already have higher AiStat; tiny extra grit.
		if ( room?.Kind == LotRoomKind.Boss )
			pow *= 1.08f;
		return MathF.Max( 1.5f, pow );
	}

	/// <summary>0–1 mean END/AGI of live team — hazard resistance.</summary>
	static float TeamToughnessNorm()
	{
		var sum = 0f;
		var n = 0;
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			if ( f is null || f.IsFightInjured )
				continue;
			sum += (f.EnduranceNorm + f.AgilityNorm) * 0.5f;
			n++;
		}
		return n <= 0 ? 0.25f : Math.Clamp( sum / n, 0f, 1f );
	}

	/// <summary>0–1 mean SPD/AGI — loot scouting.</summary>
	static float TeamScoutNorm()
	{
		var sum = 0f;
		var n = 0;
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			if ( f is null || f.IsFightInjured )
				continue;
			sum += (f.SpeedNorm + f.AgilityNorm) * 0.5f;
			n++;
		}
		return n <= 0 ? 0.25f : Math.Clamp( sum / n, 0f, 1f );
	}

	static float ScaleInjuryChance( float baseChance )
	{
		if ( baseChance <= 0f )
			return 0f;
		// Soft fish: up to ~1.15× injury. Jacked/trained: down to ~0.35×.
		var tough = TeamToughnessNorm();
		var mul = 1.15f - tough * 0.8f;
		if ( TeamHasJacked() )
			mul *= 0.75f;
		return Math.Clamp( baseChance * mul, 0.04f, 0.92f );
	}

	static double ScaleLoot( double raw )
	{
		if ( raw <= 0 )
			return 0;
		// Soft: ~0.7×. Scouted: up to ~1.35×.
		var scout = TeamScoutNorm();
		var mul = 0.7 + scout * 0.65;
		return Math.Round( raw * mul, 1 );
	}

	static bool TeamHasJacked()
	{
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			if ( f is not null && !f.IsFightInjured && f.Jacked )
				return true;
		}
		return false;
	}

	static string StatMarginNote( float margin )
	{
		if ( margin >= 1.6f )
			return " · overpower";
		if ( margin >= 1.15f )
			return " · clean";
		if ( margin >= 0.9f )
			return " · close";
		return " · lucky";
	}

	static void QueueAdvanceAfterResult()
	{
		_phase = LotRunPhase.RoomResult;
		_version++;
	}

	static void AdvanceRoom( string note, bool quiet )
	{
		_log.Add( note );
		_roomIndex++;
		if ( _location is null || _roomIndex >= _location.Rooms.Count )
		{
			FinishWin();
			return;
		}
		_phase = LotRunPhase.Room;
		if ( !quiet )
			GameAudio.PlayUi( GameAudio.Pop );
		_version++;
	}

	/// <summary>After RoomResult, if boss won or last room.</summary>
	public static void ContinueAfterResult()
	{
		if ( _phase != LotRunPhase.RoomResult )
			return;

		if ( AliveTeamCount() <= 0 )
		{
			FinishWipe( "Nobody left swimming." );
			return;
		}

		var room = CurrentRoom;
		var wasBoss = room?.Kind == LotRoomKind.Boss;
		if ( _runWon || wasBoss )
		{
			// Only finish win if we actually beat the boss (flag set on boss win).
			if ( _runWon )
			{
				FinishWin();
				return;
			}
			// Boss loss already went to FinishWipe.
		}

		_roomIndex++;
		if ( _location is null || _roomIndex >= _location.Rooms.Count )
		{
			FinishWin();
			return;
		}
		_phase = LotRunPhase.Room;
		_roomResultTitle = "";
		_roomResultBody = "";
		_version++;
	}

	static void FinishWin()
	{
		_runWon = true;
		var bonus = _location?.ClearBonus ?? 0;
		var first = _location is not null && !_cleared.Contains( _location.Id );
		// First clear of a map pays better — risk of unknown rooms → real reward.
		if ( first && bonus > 0 )
			bonus = Math.Round( bonus * 1.35, 1 );
		else if ( first && bonus <= 0 )
			bonus = 12; // survival tip even if map forgot a ClearBonus
		// Healthy trained survivors extract a little more on clear.
		if ( bonus > 0 )
		{
			var clearMul = 0.9 + TeamToughnessNorm() * 0.25 + TeamScoutNorm() * 0.1;
			bonus = Math.Round( bonus * clearMul, 1 );
			Economy.Add( bonus );
			_runEarnings += bonus;
		}
		var flushOrigin = _location?.FlushOrigin == true || _fishAwayOnAdventure;
		if ( _location is not null )
		{
			_cleared.Add( _location.Id );
			if ( first )
			{
				Karma.Add( 3.5f );
				if ( !string.IsNullOrEmpty( _location.ClearBanner ) )
					TankSim.ShowBanner( _location.ClearBanner + " +Ð" + Economy.FormatDoge( bonus ) );
				else
					TankSim.ShowBanner( _location.Name + " first clear! +Ð" + Economy.FormatDoge( bonus ) );
			}
			else
			{
				Karma.Add( 1f );
				TankSim.ShowBanner( _location.Name + " cleared again. +Ð" + Economy.FormatDoge( bonus ) );
			}
		}

		// Survive → fish comes home quietly and Adventure Mode continues (map).
		if ( _fishAwayOnAdventure )
		{
			ReturnAllAdventureFish( quiet: false, applyInjury: false );
			_flushFishId = "";
			// Survived the flush path — no injury tax, extra tip for bringing them home.
			var homeTip = first ? 8.0 : 3.0;
			Economy.Add( homeTip );
			_runEarnings += homeTip;
			TankSim.ShowBanner( FlushFishName + " home. +Ð" + Economy.FormatDoge( homeTip ) );
		}

		_doneTitle = flushOrigin ? "PIPES CLEAR" : "MAP CLEAR";
		var next = NextOpenMapHint();
		_doneBody = "Bagged Ð" + Economy.FormatDoge( _runEarnings )
			+ (bonus > 0 ? " · clear Ð" + Economy.FormatDoge( bonus ) : "")
			+ (flushOrigin ? " · map board open" : "")
			+ (string.IsNullOrEmpty( next ) ? "" : " · next: " + next)
			+ ".";
		_phase = LotRunPhase.Done;
		GameAudio.PlayDogeBark();
		SaveGame.TrySave( quiet: true );
		_version++;
	}

	/// <summary>Soft "what next" after a clear — unlocked uncleared map, else first open map name.</summary>
	static string NextOpenMapHint()
	{
		LotLocationDef firstOpen = null;
		foreach ( var loc in LotLocationDef.Catalog )
		{
			if ( loc is null || !IsLocationUnlocked( loc ) )
				continue;
			if ( firstOpen is null )
				firstOpen = loc;
			if ( !IsCleared( loc.Id ) )
				return loc.Name;
		}
		return firstOpen?.Name ?? "";
	}

	static void FinishWipe( string body )
	{
		_runWon = false;
		// Died / wiped on adventure with fish away → always faucet return (they come home).
		if ( _fishAwayOnAdventure )
		{
			StartFaucetReturn(
				"DOWN",
				( body ?? "Team lost." )
				+ " Kept Ð" + Economy.FormatDoge( _runEarnings )
				+ ". Faucet return next." );
			return;
		}

		_doneTitle = "RUN OVER";
		_doneBody = ( body ?? "" ) + " Kept Ð" + Economy.FormatDoge( _runEarnings ) + ".";
		_phase = LotRunPhase.Done;
		GameAudio.PlayUi( GameAudio.Error );
		_version++;
	}

	static void StartFaucetReturn( string title, string body )
	{
		_doneTitle = title ?? "FAUCET";
		_doneBody = body ?? "";
		_faucetLine = 0;
		_phase = LotRunPhase.Faucet;
		GameAudio.PlayUi( GameAudio.Error );
		_version++;
	}

	static void CompleteFaucetReturn()
	{
		// Died on adventure → home via faucet, benched but alive. Map stays open.
		ReturnAllAdventureFish( quiet: false, applyInjury: true );
		_doneTitle = string.IsNullOrEmpty( _doneTitle ) ? "HOME" : _doneTitle;
		if ( string.IsNullOrEmpty( _doneBody ) )
			_doneBody = FlushFishName + " is back in the tank via the kitchen faucet.";
		else
			_doneBody = _doneBody + " They're home. Adventure Mode continues when ready.";
		_phase = LotRunPhase.Done;
		TankSim.ShowBanner( FlushFishName + " returned via faucet." );
		GameAudio.PlayUi( GameAudio.Confirm );
		SaveGame.TrySave( quiet: true );
		_version++;
	}

	static void ReturnAllAdventureFish( bool quiet, bool applyInjury )
	{
		foreach ( var f in TankSim.AllFish() )
		{
			if ( f is null || !f.OnAdventure )
				continue;
			f.OnAdventure = false;
			// Wipe path: benched. Survive path: no extra injury.
			if ( applyInjury && !f.IsFightInjured )
				f.ApplyFightLossInjury();
		}
		_fishAwayOnAdventure = false;
		TankSim.BumpFrame();
		if ( !quiet )
			_version++;
	}

	static void ShowRoomResult( string title, string body )
	{
		_roomResultTitle = title ?? "";
		_roomResultBody = body ?? "";
	}

	static void AutoPickTeam( int size )
	{
		_selectedIds.Clear();
		var picks = TankSim.Fish
			.Where( CanBring )
			.OrderByDescending( BattleSystem.CombatPower )
			.Take( Math.Max( 1, size ) )
			.Select( f => f.InstanceId );
		foreach ( var id in picks )
			_selectedIds.Add( id );
	}

	static float TeamPower()
	{
		var p = 0f;
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			if ( f is null || f.IsFightInjured )
				continue;
			// Adventure fish still fight even while hidden from the glass.
			p += BattleSystem.CombatPower( f );
		}
		return MathF.Max( p, 0.01f );
	}

	static int AliveTeamCount()
	{
		var n = 0;
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			// On adventure: "dead" = all injured / missing.
			if ( f is not null && !f.IsFightInjured )
				n++;
		}
		return n;
	}

	static bool InjureRandomTeamMember()
	{
		var live = new List<FishActor>();
		foreach ( var id in _selectedIds )
		{
			var f = TankSim.FindFish( id );
			if ( f is not null && !f.IsFightInjured )
				live.Add( f );
		}
		if ( live.Count == 0 )
			return false;
		var pick = live[_rng.Next( live.Count )];
		pick.ApplyFightLossInjury();
		return true;
	}

	static double Roll( double min, double max )
	{
		if ( max < min )
			(max, min) = (min, max);
		if ( max <= 0 )
			return 0;
		return min + _rng.NextDouble() * (max - min);
	}

	static string PickName( string[] names )
	{
		if ( names is null || names.Length == 0 )
			return "Local Pest";
		return names[_rng.Next( names.Length )];
	}
}