using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using PerfectRandom.Sulfur.Core;
using PerfectRandom.Sulfur.Core.LevelGeneration;
using PerfectRandom.Sulfur.Core.Units;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("Dynamic Pressure")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Dynamic Pressure")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("bf243145-c378-4bad-8447-4747dbf03372")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace Ryuka.Sulfur.DynamicPressure;
[BepInPlugin("ryuka.sulfur.dynamicpressure", "Dynamic Pressure", "0.1.0")]
public sealed class DynamicPressurePlugin : BaseUnityPlugin
{
private struct SpawnPointChoice
{
public Vector3 position;
public Room room;
public float distanceToPlayer;
public string source;
}
private sealed class Snapshot
{
public bool hasGameManager;
public bool hasPlayer;
public bool playerAlive;
public bool inSafeZone;
public string gameState = "Unknown";
public string currentEnvironment = "Unknown";
public float playerHp;
public float playerTimeSinceDamage;
public string playerRoomName = "null";
public bool playerRoomIsEndRoom;
public int aliveNpcs;
public int originalHostileAlive;
public int originalEngagedHostiles;
public int nearbyOriginalHostiles;
public int originalTargetingPlayer;
public int modHostileAlive;
public int nearbyModHostiles;
public int modTargetingPlayer;
public int originalPressure;
public int modPressure;
public int currentPressure;
public int targetPressure;
public int deficit;
public int candidateCount;
public string candidateSource = "None";
public int spawnedThisLevel;
public int spawnedSinceLastOriginalKill;
public float timeSinceLastOriginalKill;
public float timeSinceLastModKill;
public float nextSpawnIn;
public bool wouldDelayOnAllEnemiesDead;
}
[CompilerGenerated]
private sealed class <ReportPlayerPositionLater>d__70 : IEnumerator<object>, IDisposable, IEnumerator
{
private int <>1__state;
private object <>2__current;
public Npc npc;
public DynamicPressureSpawnMarker marker;
public DynamicPressurePlugin <>4__this;
private GameManager <gm>5__1;
private Exception <e>5__2;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <ReportPlayerPositionLater>d__70(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<gm>5__1 = null;
<e>5__2 = null;
<>1__state = -2;
}
private bool MoveNext()
{
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
//IL_0030: Expected O, but got Unknown
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = (object)new WaitForSeconds(0.5f);
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
<gm>5__1 = StaticInstance<GameManager>.Instance;
if ((Object)(object)<gm>5__1 == (Object)null || (Object)(object)<gm>5__1.PlayerObject == (Object)null || (Object)(object)<gm>5__1.PlayerUnit == (Object)null)
{
<>4__this._lastAggroReport = "Failed: no player";
return false;
}
if ((Object)(object)npc == (Object)null || !((Unit)npc).IsAlive || (Object)(object)npc.AiAgent == (Object)null)
{
<>4__this._lastAggroReport = "Failed: invalid npc";
return false;
}
try
{
npc.AiAgent.ReportLastSeen(<gm>5__1.PlayerObject);
marker.aggroReported = true;
marker.lastAggroReportTime = Time.time;
marker.targetingPlayerAfterReport = <>4__this.SafeTargetIsPlayer(npc);
marker.hasKnownPlayerPositionAfterReport = <>4__this.SafeHasKnownPlayerPosition(npc, <gm>5__1.PlayerUnit);
<>4__this._lastAggroReport = ((marker.targetingPlayerAfterReport || marker.hasKnownPlayerPositionAfterReport) ? "Success" : "Reported but not targeting yet");
<>4__this.AddEvent("ReportLastSeen: " + <>4__this._lastAggroReport);
}
catch (Exception ex)
{
<e>5__2 = ex;
<>4__this._lastAggroReport = "Exception: " + <e>5__2.GetType().Name;
((BaseUnityPlugin)<>4__this).Logger.LogWarning((object)<e>5__2);
}
return false;
}
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
public const string PluginGuid = "ryuka.sulfur.dynamicpressure";
public const string PluginName = "Dynamic Pressure";
public const string PluginVersion = "0.1.0";
private static DynamicPressurePlugin _instance;
private Harmony _harmony;
private ConfigEntry<bool> _enableMod;
private ConfigEntry<bool> _enableAutoSpawn;
private ConfigEntry<int> _pressureStyle;
private ConfigEntry<bool> _enableOverlay;
private ConfigEntry<Key> _overlayToggleKey;
private ConfigEntry<Key> _manualSpawnKey;
private ConfigEntry<float> _scanInterval;
private ConfigEntry<float> _enemyScanRadius;
private ConfigEntry<float> _closeEnemyDistance;
private ConfigEntry<float> _lowHealthStopThreshold;
private ConfigEntry<float> _recentDamageStopWindow;
private ConfigEntry<int> _minOriginalEngagedHostilesForSpawn;
private ConfigEntry<float> _cooldownAfterModKillOnly;
private ConfigEntry<float> _maxSecondsWithoutOriginalKill;
private ConfigEntry<int> _maxModSpawnsPerOriginalEngaged;
private ConfigEntry<bool> _allowEnvironmentFallbackCandidates;
private ConfigEntry<float> _spawnDistanceMin;
private ConfigEntry<float> _spawnDistanceMax;
private ConfigEntry<float> _style1Cooldown;
private ConfigEntry<float> _style2Cooldown;
private ConfigEntry<float> _style3Cooldown;
private ConfigEntry<int> _style1TargetPressure;
private ConfigEntry<int> _style2TargetPressure;
private ConfigEntry<int> _style3TargetPressure;
private ConfigEntry<int> _style1MaxSpawnPerWave;
private ConfigEntry<int> _style2MaxSpawnPerWave;
private ConfigEntry<int> _style3MaxSpawnPerWave;
private ConfigEntry<int> _style1MaxModAlive;
private ConfigEntry<int> _style2MaxModAlive;
private ConfigEntry<int> _style3MaxModAlive;
private ConfigEntry<int> _style1MaxModPerRoom;
private ConfigEntry<int> _style2MaxModPerRoom;
private ConfigEntry<int> _style3MaxModPerRoom;
private ConfigEntry<int> _style1MaxModPerLevel;
private ConfigEntry<int> _style2MaxModPerLevel;
private ConfigEntry<int> _style3MaxModPerLevel;
private readonly List<UnitSO> _candidatePool = new List<UnitSO>();
private readonly List<string> _recentEvents = new List<string>();
private readonly Dictionary<int, int> _modSpawnedPerRoom = new Dictionary<int, int>();
private readonly HashSet<int> _deadNpcIds = new HashSet<int>();
private object _lastGraphContext;
private float _nextScanTime;
private float _nextSpawnTime;
private bool _spawnInProgress;
private bool _inLevelTransition;
private int _waveId;
private int _spawnedThisLevel;
private int _spawnedSinceLastOriginalKill;
private float _lastOriginalKillTime = -9999f;
private float _lastModKillTime = -9999f;
private Snapshot _snapshot = new Snapshot();
private SpawnBlockReason _lastBlockReason = SpawnBlockReason.None;
private string _lastDecision = "None";
private string _lastSpawnSummary = "None";
private string _lastAggroReport = "None";
private void Awake()
{
//IL_0014: Unknown result type (might be due to invalid IL or missing references)
//IL_001e: Expected O, but got Unknown
_instance = this;
BindConfig();
_harmony = new Harmony("ryuka.sulfur.dynamicpressure");
TryPatchTransitions();
TryPatchNpcDie();
Log("Loaded.");
}
private void OnDestroy()
{
try
{
Harmony harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
catch
{
}
if ((Object)(object)_instance == (Object)(object)this)
{
_instance = null;
}
}
private void BindConfig()
{
_enableMod = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableMod", true, "Enable Dynamic Pressure.");
_enableAutoSpawn = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableAutoSpawn", true, "Enable automatic dynamic pressure spawning. Keep false for first debug testing.");
_pressureStyle = ((BaseUnityPlugin)this).Config.Bind<int>("General", "PressureStyle", 1, "1 = Light, 2 = Heavy, 3 = Nightmare.");
_enableOverlay = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug Overlay", "EnableOverlay", false, "Show debug overlay.");
_overlayToggleKey = ((BaseUnityPlugin)this).Config.Bind<Key>("Debug Overlay", "OverlayToggleKey", (Key)102, "Toggle overlay key.");
_manualSpawnKey = ((BaseUnityPlugin)this).Config.Bind<Key>("Debug Overlay", "ManualSpawnKey", (Key)101, "Manual debug spawn key.");
_scanInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "ScanInterval", 1f, "Seconds between pressure scans.");
_enemyScanRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "EnemyScanRadius", 25f, "Nearby enemy scan radius.");
_closeEnemyDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "CloseEnemyDistance", 8f, "Enemies closer than this add extra pressure.");
_lowHealthStopThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("Safety", "LowHealthStopThreshold", 0.35f, "Do not spawn if player health is below this normalized value.");
_recentDamageStopWindow = ((BaseUnityPlugin)this).Config.Bind<float>("Safety", "RecentDamageStopWindow", 4f, "Do not spawn if player was damaged within this many seconds.");
_minOriginalEngagedHostilesForSpawn = ((BaseUnityPlugin)this).Config.Bind<int>("Anti Loop", "MinOriginalEngagedHostilesForSpawn", 1, "Require this many engaged original hostiles before spawning mod enemies.");
_cooldownAfterModKillOnly = ((BaseUnityPlugin)this).Config.Bind<float>("Anti Loop", "CooldownAfterModKillOnly", 8f, "If only mod enemies are dying, pause spawning for this many seconds.");
_maxSecondsWithoutOriginalKill = ((BaseUnityPlugin)this).Config.Bind<float>("Anti Loop", "MaxSecondsWithoutOriginalKill", 30f, "Stop spawning if no original enemy has died for this long after mod spawns.");
_maxModSpawnsPerOriginalEngaged = ((BaseUnityPlugin)this).Config.Bind<int>("Anti Loop", "MaxModSpawnsPerOriginalEngaged", 2, "Soft budget: each engaged original enemy can support this many mod spawns.");
_allowEnvironmentFallbackCandidates = ((BaseUnityPlugin)this).Config.Bind<bool>("Candidates", "AllowEnvironmentFallbackCandidates", false, "Use current environment enemy metadata if current level alive NPC candidate pool is empty.");
_spawnDistanceMin = ((BaseUnityPlugin)this).Config.Bind<float>("Spawn", "SpawnDistanceMin", 8f, "Minimum spawn distance from player.");
_spawnDistanceMax = ((BaseUnityPlugin)this).Config.Bind<float>("Spawn", "SpawnDistanceMax", 60f, "Maximum spawn distance from player.");
_style1TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "TargetPressure", 6, "Target pressure.");
_style1Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 1 - Light", "SpawnCooldown", 16f, "Spawn cooldown.");
_style1MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxSpawnPerWave", 1, "Max spawns per wave.");
_style1MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedAlive", 2, "Max alive mod-spawned NPCs.");
_style1MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedPerRoom", 3, "Max mod spawns per room.");
_style1MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedPerLevel", 8, "Max mod spawns per level.");
_style2TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "TargetPressure", 9, "Target pressure.");
_style2Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 2 - Heavy", "SpawnCooldown", 10f, "Spawn cooldown.");
_style2MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxSpawnPerWave", 1, "Max spawns per wave.");
_style2MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedAlive", 4, "Max alive mod-spawned NPCs.");
_style2MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedPerRoom", 5, "Max mod spawns per room.");
_style2MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedPerLevel", 16, "Max mod spawns per level.");
_style3TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "TargetPressure", 13, "Target pressure.");
_style3Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 3 - Nightmare", "SpawnCooldown", 7f, "Spawn cooldown.");
_style3MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxSpawnPerWave", 2, "Max spawns per wave.");
_style3MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedAlive", 6, "Max alive mod-spawned NPCs.");
_style3MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedPerRoom", 8, "Max mod spawns per room.");
_style3MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedPerLevel", 28, "Max mod spawns per level.");
}
private void Update()
{
HandleKeys();
if (!_enableMod.Value)
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.Disabled;
return;
}
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance != (Object)null && (Object)(object)instance.graphContext != (Object)null && _lastGraphContext != instance.graphContext)
{
_lastGraphContext = instance.graphContext;
ResetRuntimeState("New graphContext");
}
if (Time.time >= _nextScanTime)
{
_nextScanTime = Time.time + Mathf.Max(0.2f, _scanInterval.Value);
ScanPressure();
}
if (_enableAutoSpawn.Value && !_spawnInProgress && Time.time >= _nextSpawnTime)
{
TryAutoSpawn();
}
}
private void HandleKeys()
{
//IL_0019: Unknown result type (might be due to invalid IL or missing references)
//IL_004f: Unknown result type (might be due to invalid IL or missing references)
Keyboard current = Keyboard.current;
if (current != null)
{
if (((ButtonControl)current[_overlayToggleKey.Value]).wasPressedThisFrame)
{
_enableOverlay.Value = !_enableOverlay.Value;
}
if (((ButtonControl)current[_manualSpawnKey.Value]).wasPressedThisFrame && !_spawnInProgress)
{
SpawnWaveAsync(1, "Manual debug spawn");
}
}
}
private void ScanPressure()
{
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_0048: Unknown result type (might be due to invalid IL or missing references)
//IL_01ca: Unknown result type (might be due to invalid IL or missing references)
//IL_01d0: Unknown result type (might be due to invalid IL or missing references)
Snapshot snapshot = new Snapshot();
_candidatePool.Clear();
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance == (Object)null)
{
_snapshot = snapshot;
_lastBlockReason = SpawnBlockReason.NoGameManager;
return;
}
snapshot.hasGameManager = true;
GameState gameState = instance.gameState;
snapshot.gameState = ((object)(GameState)(ref gameState)).ToString();
snapshot.inSafeZone = instance.InSafeZone;
snapshot.currentEnvironment = (((Object)(object)instance.currentEnvironment != (Object)null) ? ((object)(WorldEnvironmentIds)(ref instance.currentEnvironment.id)).ToString() : "null");
if ((Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null)
{
_snapshot = snapshot;
_lastBlockReason = SpawnBlockReason.NoPlayer;
return;
}
Unit playerUnit = instance.PlayerUnit;
Room unitRoom = GetUnitRoom(playerUnit);
snapshot.hasPlayer = true;
snapshot.playerAlive = playerUnit.IsAlive;
snapshot.playerHp = SafeGetPlayerHp(playerUnit);
snapshot.playerTimeSinceDamage = playerUnit.TimeSinceLastDamageTaken;
snapshot.playerRoomName = (((Object)(object)unitRoom != (Object)null) ? ((Object)unitRoom).name : "null");
snapshot.playerRoomIsEndRoom = (Object)(object)unitRoom != (Object)null && unitRoom.IsEndRoom;
List<Npc> aliveNpcs = instance.aliveNpcs;
snapshot.aliveNpcs = aliveNpcs?.Count ?? 0;
if (aliveNpcs != null)
{
for (int i = 0; i < aliveNpcs.Count; i++)
{
Npc val = aliveNpcs[i];
if (!IsValidAliveNpc(val))
{
continue;
}
DynamicPressureSpawnMarker component = ((Component)val).GetComponent<DynamicPressureSpawnMarker>();
bool flag = (Object)(object)component != (Object)null;
if (!IsHostileToPlayer(val))
{
continue;
}
float num = Vector3.Distance(((Component)val).transform.position, instance.PlayerPosition);
bool flag2 = num <= _enemyScanRadius.Value;
bool flag3 = SafeTargetIsPlayer(val);
bool flag4 = SafeHasKnownPlayerPosition(val, instance.PlayerUnit);
bool flag5 = IsSameOrConnectedRoom(GetUnitRoom((Unit)(object)val), unitRoom);
bool flag6 = flag2 || flag3 || flag4 || flag5;
int num2 = CalculateNpcPressure(val, num, flag3, flag4);
if (flag)
{
snapshot.modHostileAlive++;
if (flag2)
{
snapshot.nearbyModHostiles++;
snapshot.modPressure += num2;
}
if (flag3)
{
snapshot.modTargetingPlayer++;
}
continue;
}
snapshot.originalHostileAlive++;
if (flag6)
{
snapshot.originalEngagedHostiles++;
}
if (flag2)
{
snapshot.nearbyOriginalHostiles++;
snapshot.originalPressure += num2;
}
if (flag3)
{
snapshot.originalTargetingPlayer++;
}
TryAddCandidateFromNpc(val);
}
}
if (_candidatePool.Count == 0 && _allowEnvironmentFallbackCandidates.Value)
{
AddEnvironmentFallbackCandidates(instance);
snapshot.candidateSource = ((_candidatePool.Count > 0) ? "EnvironmentMetadata" : "None");
}
else
{
snapshot.candidateSource = ((_candidatePool.Count > 0) ? "CurrentLevelAliveNpcs" : "None");
}
snapshot.candidateCount = _candidatePool.Count;
snapshot.targetPressure = GetTargetPressure();
snapshot.currentPressure = snapshot.originalPressure + snapshot.modPressure;
snapshot.deficit = snapshot.targetPressure - snapshot.currentPressure;
snapshot.spawnedThisLevel = _spawnedThisLevel;
snapshot.spawnedSinceLastOriginalKill = _spawnedSinceLastOriginalKill;
snapshot.timeSinceLastOriginalKill = Time.time - _lastOriginalKillTime;
snapshot.timeSinceLastModKill = Time.time - _lastModKillTime;
snapshot.nextSpawnIn = Mathf.Max(0f, _nextSpawnTime - Time.time);
snapshot.wouldDelayOnAllEnemiesDead = snapshot.originalHostileAlive == 0 && snapshot.modHostileAlive > 0;
_snapshot = snapshot;
}
private void TryAutoSpawn()
{
if (!CanSpawn(out var blockReason, out var spawnCount))
{
_lastDecision = "BLOCKED";
_lastBlockReason = blockReason;
}
else
{
SpawnWaveAsync(spawnCount, "Auto pressure deficit");
}
}
private bool CanSpawn(out SpawnBlockReason blockReason, out int spawnCount)
{
//IL_006c: Unknown result type (might be due to invalid IL or missing references)
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
spawnCount = 0;
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance == (Object)null)
{
blockReason = SpawnBlockReason.NoGameManager;
return false;
}
if (_inLevelTransition)
{
blockReason = SpawnBlockReason.LevelTransition;
return false;
}
if ((Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null)
{
blockReason = SpawnBlockReason.NoPlayer;
return false;
}
GameState gameState = instance.gameState;
if (((object)(GameState)(ref gameState)).ToString() != "Running")
{
blockReason = SpawnBlockReason.GameStateNotRunning;
return false;
}
if (instance.InSafeZone)
{
blockReason = SpawnBlockReason.SafeZone;
return false;
}
Unit playerUnit = instance.PlayerUnit;
if (!playerUnit.IsAlive)
{
blockReason = SpawnBlockReason.PlayerDead;
return false;
}
if (SafeGetPlayerHp(playerUnit) <= _lowHealthStopThreshold.Value)
{
blockReason = SpawnBlockReason.LowHealth;
return false;
}
if (playerUnit.TimeSinceLastDamageTaken <= _recentDamageStopWindow.Value)
{
blockReason = SpawnBlockReason.RecentDamage;
return false;
}
Room unitRoom = GetUnitRoom(playerUnit);
if ((Object)(object)unitRoom != (Object)null && unitRoom.IsEndRoom)
{
blockReason = SpawnBlockReason.EndRoomBlocked;
return false;
}
if (_snapshot.originalEngagedHostiles < _minOriginalEngagedHostilesForSpawn.Value)
{
blockReason = SpawnBlockReason.NoEngagedOriginalHostiles;
return false;
}
if (_snapshot.currentPressure >= _snapshot.targetPressure)
{
blockReason = SpawnBlockReason.PressureAlreadyHigh;
return false;
}
if (_snapshot.modHostileAlive >= GetMaxModAlive())
{
blockReason = SpawnBlockReason.MaxModSpawnedAlive;
return false;
}
if (_spawnedThisLevel >= GetMaxModPerLevel())
{
blockReason = SpawnBlockReason.LevelSpawnBudgetExceeded;
return false;
}
int num = Mathf.Max(1, _snapshot.originalEngagedHostiles) * Mathf.Max(1, _maxModSpawnsPerOriginalEngaged.Value);
if (_spawnedSinceLastOriginalKill >= num)
{
blockReason = SpawnBlockReason.SpawnBudgetPerOriginalExceeded;
return false;
}
if (_spawnedSinceLastOriginalKill > 0 && Time.time - _lastOriginalKillTime > _maxSecondsWithoutOriginalKill.Value)
{
blockReason = SpawnBlockReason.NoOriginalKillProgress;
return false;
}
if (_lastModKillTime > _lastOriginalKillTime && Time.time - _lastModKillTime < _cooldownAfterModKillOnly.Value)
{
blockReason = SpawnBlockReason.RecentModKillOnly;
return false;
}
if (_candidatePool.Count == 0)
{
blockReason = SpawnBlockReason.NoCandidateUnits;
return false;
}
spawnCount = Mathf.Min(GetMaxSpawnPerWave(), Mathf.Max(1, _snapshot.deficit));
spawnCount = Mathf.Min(spawnCount, GetMaxModAlive() - _snapshot.modHostileAlive);
spawnCount = Mathf.Min(spawnCount, GetMaxModPerLevel() - _spawnedThisLevel);
if (spawnCount <= 0)
{
blockReason = SpawnBlockReason.MaxModSpawnedAlive;
return false;
}
blockReason = SpawnBlockReason.None;
return true;
}
private async Task SpawnWaveAsync(int count, string reason)
{
if (_spawnInProgress)
{
return;
}
_spawnInProgress = true;
_waveId++;
int spawned = 0;
try
{
ScanPressure();
for (int i = 0; i < count; i++)
{
UnitSO unitSo = PickCandidate();
if ((Object)(object)unitSo == (Object)null)
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.NoCandidateUnits;
AddEvent("Spawn blocked: no candidate.");
break;
}
if (!TryFindSpawnPoint(unitSo, out var spawnPoint))
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.NoValidNpcSpawnPoint;
AddEvent("Spawn blocked: no valid NPCSpawn point.");
break;
}
int roomId = GetRoomId(spawnPoint.room);
_modSpawnedPerRoom.TryGetValue(roomId, out var roomCount);
if (roomCount >= GetMaxModPerRoom())
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.RoomSpawnBudgetExceeded;
AddEvent("Spawn blocked: room budget exceeded.");
break;
}
if (!(await SpawnOneAsync(unitSo, spawnPoint, reason)))
{
_lastDecision = "FAILED";
_lastBlockReason = SpawnBlockReason.SpawnAsyncFailed;
break;
}
spawned++;
spawnPoint = default(SpawnPointChoice);
}
if (spawned > 0)
{
_lastDecision = "SPAWNED";
_lastBlockReason = SpawnBlockReason.None;
_nextSpawnTime = Time.time + GetSpawnCooldown();
}
}
catch (Exception ex)
{
Exception e = ex;
_lastDecision = "FAILED";
_lastBlockReason = SpawnBlockReason.SpawnAsyncFailed;
AddEvent("Spawn exception: " + e.GetType().Name);
((BaseUnityPlugin)this).Logger.LogError((object)e);
}
finally
{
_spawnInProgress = false;
ScanPressure();
}
}
private async Task<bool> SpawnOneAsync(UnitSO unitSo, SpawnPointChoice spawnPoint, string reason)
{
if ((Object)(object)unitSo == (Object)null)
{
return false;
}
Unit spawnedUnit = await unitSo.SpawnUnitAsync((MonoBehaviour)(object)this, spawnPoint.position, Quaternion.identity);
if ((Object)(object)spawnedUnit == (Object)null)
{
return false;
}
Npc npc = (Npc)(object)((spawnedUnit is Npc) ? spawnedUnit : null);
if ((Object)(object)npc == (Object)null)
{
npc = ((Component)spawnedUnit).GetComponent<Npc>();
}
if ((Object)(object)npc == (Object)null)
{
return false;
}
if ((Object)(object)spawnPoint.room != (Object)null)
{
((Unit)npc).currentRoom = spawnPoint.room;
((Unit)npc).lastValidCurrentRoom = spawnPoint.room;
((Unit)npc).lastRoomCalcPosition = ((Component)npc).transform.position;
}
DynamicPressureSpawnMarker marker = ((Component)npc).gameObject.AddComponent<DynamicPressureSpawnMarker>();
marker.sourceUnitSo = unitSo;
marker.spawnTime = Time.time;
marker.spawnPosition = spawnPoint.position;
marker.spawnRoom = spawnPoint.room;
marker.spawnReason = reason;
marker.waveId = _waveId;
int roomId = GetRoomId(spawnPoint.room);
_modSpawnedPerRoom.TryGetValue(roomId, out var roomCount);
_modSpawnedPerRoom[roomId] = roomCount + 1;
_spawnedThisLevel++;
_spawnedSinceLastOriginalKill++;
_lastSpawnSummary = string.Format("{0} at {1}, room={2}, source={3}", ((Object)unitSo).name, spawnPoint.position, ((Object)(object)spawnPoint.room != (Object)null) ? ((Object)spawnPoint.room).name : "null", spawnPoint.source);
AddEvent("Spawned: " + ((Object)unitSo).name);
((MonoBehaviour)this).StartCoroutine(ReportPlayerPositionLater(npc, marker));
return true;
}
[IteratorStateMachine(typeof(<ReportPlayerPositionLater>d__70))]
private IEnumerator ReportPlayerPositionLater(Npc npc, DynamicPressureSpawnMarker marker)
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <ReportPlayerPositionLater>d__70(0)
{
<>4__this = this,
npc = npc,
marker = marker
};
}
private UnitSO PickCandidate()
{
if (_candidatePool.Count == 0)
{
return null;
}
return _candidatePool[Random.Range(0, _candidatePool.Count)];
}
private void TryAddCandidateFromNpc(Npc npc)
{
if (!((Object)(object)npc == (Object)null) && !((Object)(object)((Unit)npc).unitSO == (Object)null))
{
UnitSO unitSO = ((Unit)npc).unitSO;
if (IsValidCandidateUnitSo(unitSO) && !_candidatePool.Contains(unitSO))
{
_candidatePool.Add(unitSO);
}
}
}
private void AddEnvironmentFallbackCandidates(GameManager gm)
{
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_0048: Unknown result type (might be due to invalid IL or missing references)
//IL_0049: Unknown result type (might be due to invalid IL or missing references)
//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
//IL_0061: Unknown result type (might be due to invalid IL or missing references)
//IL_0068: Unknown result type (might be due to invalid IL or missing references)
try
{
if ((Object)(object)gm == (Object)null || (Object)(object)gm.environmentsInOrder == (Object)null || (Object)(object)gm.currentEnvironment == (Object)null)
{
return;
}
Metadata metadata = gm.environmentsInOrder.GetMetadata(gm.currentEnvironment.id);
if (metadata.availableEnemiesReadOnly == null)
{
return;
}
for (int i = 0; i < metadata.availableEnemiesReadOnly.Length; i++)
{
UnitSO asset = AssetAccess.GetAsset(metadata.availableEnemiesReadOnly[i]);
if ((Object)(object)asset != (Object)null && IsValidCandidateUnitSo(asset) && !_candidatePool.Contains(asset))
{
_candidatePool.Add(asset);
}
}
}
catch (Exception ex)
{
AddEvent("Candidate fallback failed: " + ex.GetType().Name);
}
}
private bool IsValidCandidateUnitSo(UnitSO so)
{
//IL_0047: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Unknown result type (might be due to invalid IL or missing references)
//IL_004f: Invalid comparison between Unknown and I4
if ((Object)(object)so == (Object)null)
{
return false;
}
if (so.isCivilian)
{
return false;
}
if (so.isProtectedNpc)
{
return false;
}
if (so.ExperienceOnKill <= 0)
{
return false;
}
if ((so.unitType & 8) > 0)
{
return false;
}
return true;
}
private bool TryFindSpawnPoint(UnitSO unitSo, out SpawnPointChoice result)
{
//IL_004c: Unknown result type (might be due to invalid IL or missing references)
//IL_0051: Unknown result type (might be due to invalid IL or missing references)
//IL_0231: Unknown result type (might be due to invalid IL or missing references)
//IL_0254: Unknown result type (might be due to invalid IL or missing references)
//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
//IL_0116: Unknown result type (might be due to invalid IL or missing references)
//IL_0118: Unknown result type (might be due to invalid IL or missing references)
//IL_011e: Unknown result type (might be due to invalid IL or missing references)
//IL_0123: Unknown result type (might be due to invalid IL or missing references)
//IL_0125: Invalid comparison between Unknown and I4
//IL_0132: Unknown result type (might be due to invalid IL or missing references)
//IL_0134: Unknown result type (might be due to invalid IL or missing references)
//IL_0139: Unknown result type (might be due to invalid IL or missing references)
//IL_014b: Unknown result type (might be due to invalid IL or missing references)
//IL_014d: Unknown result type (might be due to invalid IL or missing references)
//IL_0152: Unknown result type (might be due to invalid IL or missing references)
result = default(SpawnPointChoice);
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance == (Object)null || (Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null || (Object)(object)unitSo == (Object)null)
{
return false;
}
Vector3 playerPosition = instance.PlayerPosition;
List<SpawnPointChoice> list = new List<SpawnPointChoice>();
List<SpawnPointChoice> list2 = new List<SpawnPointChoice>();
List<Room> orderedRooms = instance.orderedRooms;
if (orderedRooms == null || orderedRooms.Count == 0)
{
return false;
}
for (int i = 0; i < orderedRooms.Count; i++)
{
Room val = orderedRooms[i];
if (!IsValidSpawnRoom(val))
{
continue;
}
Data[] nPCSpawns = val.GetNPCSpawns();
if (nPCSpawns == null || nPCSpawns.Length == 0)
{
continue;
}
foreach (Data val2 in nPCSpawns)
{
Room room = (((Object)(object)val2.room != (Object)null) ? val2.room : val);
if (IsValidSpawnRoom(room) && (val2.usableByTypes & unitSo.unitType) != 0)
{
float num = Vector3.Distance(val2.position, playerPosition);
SpawnPointChoice spawnPointChoice = default(SpawnPointChoice);
spawnPointChoice.position = val2.position;
spawnPointChoice.room = room;
spawnPointChoice.distanceToPlayer = num;
spawnPointChoice.source = "GameManager.orderedRooms NPCSpawn";
SpawnPointChoice item = spawnPointChoice;
if (num >= _spawnDistanceMin.Value && num <= _spawnDistanceMax.Value)
{
list.Add(item);
}
else if (num > _spawnDistanceMax.Value && num <= _spawnDistanceMax.Value * 2f)
{
list2.Add(item);
}
}
}
}
if (list.Count > 0)
{
result = PickNearestRandomized(list, playerPosition);
return true;
}
if (list2.Count > 0)
{
result = PickNearestRandomized(list2, playerPosition);
result.source += " / loose distance";
return true;
}
return false;
}
private bool IsValidSpawnRoom(Room room)
{
if ((Object)(object)room == (Object)null)
{
return false;
}
if (room.IsStartRoom)
{
return false;
}
if (room.IsEndRoom)
{
return false;
}
if (room.disallowEnemySpawn)
{
return false;
}
return true;
}
private SpawnPointChoice PickNearestRandomized(List<SpawnPointChoice> choices, Vector3 playerPos)
{
choices.Sort((SpawnPointChoice a, SpawnPointChoice b) => a.distanceToPlayer.CompareTo(b.distanceToPlayer));
int num = Mathf.Min(choices.Count, 5);
return choices[Random.Range(0, num)];
}
private int CalculateNpcPressure(Npc npc, float distance, bool targeting, bool knowsPlayer)
{
int num = 1;
if ((Object)(object)npc != (Object)null && (Object)(object)((Unit)npc).unitSO != (Object)null)
{
num = Mathf.Clamp(((Unit)npc).unitSO.SpawnCost, 1, 4);
}
if (distance <= _closeEnemyDistance.Value)
{
num++;
}
if (targeting)
{
num += 2;
}
else if (knowsPlayer)
{
num++;
}
return num;
}
private bool IsValidAliveNpc(Npc npc)
{
return (Object)(object)npc != (Object)null && (Object)(object)((Component)npc).gameObject != (Object)null && ((Unit)npc).IsAlive;
}
private bool IsHostileToPlayer(Npc npc)
{
try
{
return (Object)(object)npc != (Object)null && ((Unit)npc).IsHostileTo((FactionIds)16);
}
catch
{
return false;
}
}
private bool SafeTargetIsPlayer(Npc npc)
{
try
{
return (Object)(object)npc != (Object)null && npc.targetIsPlayer;
}
catch
{
return false;
}
}
private bool SafeHasKnownPlayerPosition(Npc npc, Unit playerUnit)
{
try
{
return (Object)(object)npc != (Object)null && (Object)(object)npc.AiAgent != (Object)null && (Object)(object)playerUnit != (Object)null && npc.AiAgent.HasKnownPosition(playerUnit);
}
catch
{
return false;
}
}
private float SafeGetPlayerHp(Unit playerUnit)
{
try
{
return ((Object)(object)playerUnit != (Object)null) ? playerUnit.GetNormalizedHealth() : 0f;
}
catch
{
return 0f;
}
}
private Room GetUnitRoom(Unit unit)
{
if ((Object)(object)unit == (Object)null)
{
return null;
}
if ((Object)(object)unit.currentRoom != (Object)null)
{
return unit.currentRoom;
}
return unit.lastValidCurrentRoom;
}
private bool IsSameOrConnectedRoom(Room npcRoom, Room playerRoom)
{
if ((Object)(object)npcRoom == (Object)null || (Object)(object)playerRoom == (Object)null)
{
return false;
}
if ((Object)(object)npcRoom == (Object)(object)playerRoom)
{
return true;
}
if (playerRoom.connectedRooms == null)
{
return false;
}
for (int i = 0; i < playerRoom.connectedRooms.Length; i++)
{
if ((Object)(object)playerRoom.connectedRooms[i] == (Object)(object)npcRoom)
{
return true;
}
}
return false;
}
private int GetRoomId(Room room)
{
return ((Object)(object)room != (Object)null) ? ((Object)room).GetInstanceID() : 0;
}
private int GetStyle()
{
return Mathf.Clamp(_pressureStyle.Value, 1, 3);
}
private int GetTargetPressure()
{
return GetStyle() switch
{
1 => _style1TargetPressure.Value,
2 => _style2TargetPressure.Value,
_ => _style3TargetPressure.Value,
};
}
private float GetSpawnCooldown()
{
return GetStyle() switch
{
1 => _style1Cooldown.Value,
2 => _style2Cooldown.Value,
_ => _style3Cooldown.Value,
};
}
private int GetMaxSpawnPerWave()
{
return GetStyle() switch
{
1 => _style1MaxSpawnPerWave.Value,
2 => _style2MaxSpawnPerWave.Value,
_ => _style3MaxSpawnPerWave.Value,
};
}
private int GetMaxModAlive()
{
return GetStyle() switch
{
1 => _style1MaxModAlive.Value,
2 => _style2MaxModAlive.Value,
_ => _style3MaxModAlive.Value,
};
}
private int GetMaxModPerRoom()
{
return GetStyle() switch
{
1 => _style1MaxModPerRoom.Value,
2 => _style2MaxModPerRoom.Value,
_ => _style3MaxModPerRoom.Value,
};
}
private int GetMaxModPerLevel()
{
return GetStyle() switch
{
1 => _style1MaxModPerLevel.Value,
2 => _style2MaxModPerLevel.Value,
_ => _style3MaxModPerLevel.Value,
};
}
private void ResetRuntimeState(string reason)
{
_candidatePool.Clear();
_recentEvents.Clear();
_modSpawnedPerRoom.Clear();
_deadNpcIds.Clear();
_waveId = 0;
_spawnedThisLevel = 0;
_spawnedSinceLastOriginalKill = 0;
_lastOriginalKillTime = Time.time;
_lastModKillTime = -9999f;
_lastDecision = "Reset";
_lastBlockReason = SpawnBlockReason.None;
_lastSpawnSummary = "None";
_lastAggroReport = "None";
_inLevelTransition = false;
_nextSpawnTime = Time.time + 3f;
AddEvent("Reset: " + reason);
}
private static void OnNpcDiePostfix(Npc __instance)
{
if (!((Object)(object)_instance == (Object)null) && !((Object)(object)__instance == (Object)null))
{
_instance.HandleNpcDeath(__instance);
}
}
private void HandleNpcDeath(Npc npc)
{
int instanceID = ((Object)npc).GetInstanceID();
if (_deadNpcIds.Contains(instanceID))
{
return;
}
_deadNpcIds.Add(instanceID);
bool flag = (Object)(object)((Component)npc).GetComponent<DynamicPressureSpawnMarker>() != (Object)null;
if (IsHostileToPlayer(npc))
{
if (flag)
{
_lastModKillTime = Time.time;
AddEvent("Mod NPC died: " + ((Object)npc).name);
}
else
{
_lastOriginalKillTime = Time.time;
_spawnedSinceLastOriginalKill = 0;
AddEvent("Original NPC died: " + ((Object)npc).name);
}
}
}
private static void OnLevelTransitionPrefix()
{
if (!((Object)(object)_instance == (Object)null))
{
_instance._inLevelTransition = true;
_instance.ResetRuntimeState("Level transition");
}
}
private void TryPatchTransitions()
{
MethodInfo prefix = AccessTools.Method(typeof(DynamicPressurePlugin), "OnLevelTransitionPrefix", (Type[])null, (Type[])null);
TryPatchMethod(typeof(NextLevelTrigger), "MakeTransition", prefix);
TryPatchMethod(typeof(GameManager), "CompleteLevel", prefix);
TryPatchAllNamedMethods(typeof(GameManager), "GoToLevel", prefix);
TryPatchAllNamedMethods(typeof(GameManager), "GoToChurchHub", prefix);
TryPatchAllNamedMethods(typeof(GameManager), "GoToCarHub", prefix);
TryPatchAllNamedMethods(typeof(GameManager), "SwitchLevel", prefix);
}
private void TryPatchNpcDie()
{
//IL_0051: Unknown result type (might be due to invalid IL or missing references)
//IL_005e: Expected O, but got Unknown
try
{
MethodInfo methodInfo = AccessTools.Method(typeof(Npc), "Die", (Type[])null, (Type[])null);
MethodInfo methodInfo2 = AccessTools.Method(typeof(DynamicPressurePlugin), "OnNpcDiePostfix", (Type[])null, (Type[])null);
if (methodInfo != null && methodInfo2 != null)
{
_harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
Log("Patched Npc.Die.");
}
else
{
((BaseUnityPlugin)this).Logger.LogWarning((object)"Failed to patch Npc.Die: method not found.");
}
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to patch Npc.Die: " + ex.Message));
}
}
private void TryPatchMethod(Type type, string methodName, MethodInfo prefix)
{
//IL_0047: Unknown result type (might be due to invalid IL or missing references)
//IL_0055: Expected O, but got Unknown
try
{
MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null);
if (methodInfo == null)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch skipped: " + type.Name + "." + methodName));
return;
}
_harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(prefix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
Log("Patched " + type.Name + "." + methodName);
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch failed: " + type.Name + "." + methodName + " / " + ex.Message));
}
}
private void TryPatchAllNamedMethods(Type type, string methodName, MethodInfo prefix)
{
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Expected O, but got Unknown
try
{
MethodInfo[] array = (from m in AccessTools.GetDeclaredMethods(type)
where m.Name == methodName
select m).ToArray();
if (array.Length == 0)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch skipped: " + type.Name + "." + methodName));
return;
}
for (int i = 0; i < array.Length; i++)
{
_harmony.Patch((MethodBase)array[i], new HarmonyMethod(prefix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
}
Log("Patched " + type.Name + "." + methodName + " x" + array.Length);
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch failed: " + type.Name + "." + methodName + " / " + ex.Message));
}
}
private void AddEvent(string text)
{
_recentEvents.Add(DateTime.Now.ToString("HH:mm:ss") + " " + text);
while (_recentEvents.Count > 8)
{
_recentEvents.RemoveAt(0);
}
}
private void Log(string text)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)text);
}
private void OnGUI()
{
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
//IL_0074: Unknown result type (might be due to invalid IL or missing references)
if (_enableOverlay.Value)
{
Rect val = default(Rect);
((Rect)(ref val))..ctor(20f, 120f, 620f, 620f);
GUI.Box(val, "Dynamic Pressure Debug");
GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 10f, ((Rect)(ref val)).y + 25f, ((Rect)(ref val)).width - 20f, ((Rect)(ref val)).height - 35f));
GUILayout.Label("Enabled: " + _enableMod.Value + " | AutoSpawn: " + _enableAutoSpawn.Value + " | Style: " + GetStyle(), Array.Empty<GUILayoutOption>());
GUILayout.Label("GameState: " + _snapshot.gameState + " | SafeZone: " + _snapshot.inSafeZone + " | Env: " + _snapshot.currentEnvironment, Array.Empty<GUILayoutOption>());
GUILayout.Label("Player: " + (_snapshot.hasPlayer ? "OK" : "Missing") + " | Alive: " + _snapshot.playerAlive + " | HP: " + Mathf.RoundToInt(_snapshot.playerHp * 100f) + "% | DamageAgo: " + _snapshot.playerTimeSinceDamage.ToString("0.0") + "s", Array.Empty<GUILayoutOption>());
GUILayout.Label("Room: " + _snapshot.playerRoomName + " | EndRoom: " + _snapshot.playerRoomIsEndRoom, Array.Empty<GUILayoutOption>());
GUILayout.Space(8f);
GUILayout.Label("Pressure", Array.Empty<GUILayoutOption>());
GUILayout.Label("Original Hostile Alive: " + _snapshot.originalHostileAlive + " | Engaged: " + _snapshot.originalEngagedHostiles + " | Targeting: " + _snapshot.originalTargetingPlayer, Array.Empty<GUILayoutOption>());
GUILayout.Label("Mod Hostile Alive: " + _snapshot.modHostileAlive + " | Targeting: " + _snapshot.modTargetingPlayer, Array.Empty<GUILayoutOption>());
GUILayout.Label("Original Pressure: " + _snapshot.originalPressure + " | Mod Pressure: +" + _snapshot.modPressure + " | Current: " + _snapshot.currentPressure + " / Target: " + _snapshot.targetPressure + " | Deficit: " + _snapshot.deficit, Array.Empty<GUILayoutOption>());
GUILayout.Space(8f);
GUILayout.Label("Spawn Decision", Array.Empty<GUILayoutOption>());
GUILayout.Label("Last Decision: " + _lastDecision + " | Reason: " + _lastBlockReason, Array.Empty<GUILayoutOption>());
GUILayout.Label("Next Spawn In: " + _snapshot.nextSpawnIn.ToString("0.0") + "s | Candidate Source: " + _snapshot.candidateSource + " | Candidates: " + _snapshot.candidateCount, Array.Empty<GUILayoutOption>());
GUILayout.Space(8f);
GUILayout.Label("Mod Impact", Array.Empty<GUILayoutOption>());
GUILayout.Label("Spawned This Level: " + _spawnedThisLevel + " / " + GetMaxModPerLevel(), Array.Empty<GUILayoutOption>());
GUILayout.Label("Spawned Since Last Original Kill: " + _spawnedSinceLastOriginalKill, Array.Empty<GUILayoutOption>());
GUILayout.Label("Time Since Original Kill: " + _snapshot.timeSinceLastOriginalKill.ToString("0.0") + "s | Time Since Mod Kill: " + _snapshot.timeSinceLastModKill.ToString("0.0") + "s", Array.Empty<GUILayoutOption>());
GUILayout.Label("Would Delay OnAllEnemiesDead: " + _snapshot.wouldDelayOnAllEnemiesDead, Array.Empty<GUILayoutOption>());
GUILayout.Label("Last Spawn: " + _lastSpawnSummary, Array.Empty<GUILayoutOption>());
GUILayout.Label("Last Aggro Report: " + _lastAggroReport, Array.Empty<GUILayoutOption>());
GUILayout.Space(8f);
GUILayout.Label("Keys: F8 Manual Spawn | F9 Toggle Overlay", Array.Empty<GUILayoutOption>());
GUILayout.Space(8f);
GUILayout.Label("Recent Events", Array.Empty<GUILayoutOption>());
for (int i = 0; i < _recentEvents.Count; i++)
{
GUILayout.Label(_recentEvents[i], Array.Empty<GUILayoutOption>());
}
GUILayout.EndArea();
}
}
}
public sealed class DynamicPressureSpawnMarker : MonoBehaviour
{
public UnitSO sourceUnitSo;
public float spawnTime;
public Vector3 spawnPosition;
public Room spawnRoom;
public string spawnReason;
public int waveId;
public bool aggroReported;
public float lastAggroReportTime;
public bool targetingPlayerAfterReport;
public bool hasKnownPlayerPositionAfterReport;
}
public enum SpawnBlockReason
{
None,
Disabled,
NoGameManager,
NoPlayer,
PlayerDead,
SafeZone,
GameStateNotRunning,
LowHealth,
RecentDamage,
EndRoomBlocked,
SpawnRoomIsEndRoom,
NoOriginalHostiles,
NoEngagedOriginalHostiles,
NotEnoughOriginalHostiles,
NoCandidateUnits,
NoValidNpcSpawnPoint,
Cooldown,
MaxModSpawnedAlive,
RoomSpawnBudgetExceeded,
LevelSpawnBudgetExceeded,
PressureAlreadyHigh,
SpawnAsyncFailed,
LevelTransition,
RecentModKillOnly,
NoOriginalKillProgress,
SpawnBudgetPerOriginalExceeded
}