using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.SceneManagement;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("LevelDeath")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("LevelDeath")]
[assembly: AssemblyTitle("LevelDeath")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace LevelDeath
{
[BepInPlugin("oai.ultrakill.leveldeath", "Level Death", "0.8.4")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public sealed class LevelDeathPlugin : BaseUnityPlugin
{
private sealed class PendingWaveEnemy
{
public readonly GameObject Source;
public readonly EnemyProfile Profile;
public readonly float TimeStarted;
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly Transform Parent;
private readonly int _sourceId;
public int SourceId => _sourceId;
public PendingWaveEnemy(GameObject source, EnemyProfile profile, float timeStarted)
{
//IL_0032: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: Unknown result type (might be due to invalid IL or missing references)
//IL_0053: Unknown result type (might be due to invalid IL or missing references)
//IL_0046: Unknown result type (might be due to invalid IL or missing references)
//IL_0058: Unknown result type (might be due to invalid IL or missing references)
Source = source;
Profile = profile;
TimeStarted = timeStarted;
Position = (((Object)(object)source != (Object)null) ? source.transform.position : Vector3.zero);
Rotation = (((Object)(object)source != (Object)null) ? source.transform.rotation : Quaternion.identity);
Parent = (((Object)(object)source != (Object)null) ? source.transform.parent : null);
_sourceId = (((Object)(object)source != (Object)null) ? ((Object)source).GetInstanceID() : 0);
}
}
private sealed class WeightedWaveEnemy
{
public readonly PendingWaveEnemy Enemy;
public readonly float Weight;
public WeightedWaveEnemy(PendingWaveEnemy enemy, float weight)
{
Enemy = enemy;
Weight = Mathf.Max(0.001f, weight);
}
}
private sealed class EnemySpawnCandidate
{
public readonly EnemyTemplatePool Pool;
public readonly int EffectiveCost;
public readonly bool IsGlobalOnly;
public bool IsLarge
{
get
{
if (Pool != null)
{
return Pool.IsLarge;
}
return false;
}
}
public int BaseCost
{
get
{
if (Pool == null)
{
return EffectiveCost;
}
return Pool.Cost;
}
}
public EnemySpawnCandidate(EnemyTemplatePool pool, int effectiveCost, bool isGlobalOnly)
{
Pool = pool;
EffectiveCost = Mathf.Max(1, effectiveCost);
IsGlobalOnly = isGlobalOnly;
}
}
private sealed class EnemyCooldownState
{
public float Heat;
public float LastUpdatedTime;
}
private sealed class WeightedCandidate
{
public readonly EnemySpawnCandidate Candidate;
public readonly float Weight;
public WeightedCandidate(EnemySpawnCandidate candidate, float weight)
{
Candidate = candidate;
Weight = Mathf.Max(0.001f, weight);
}
}
[CompilerGenerated]
private sealed class <DestroyOriginalAfterReplacementValid>d__131 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public GameObject source;
public LevelDeathPlugin <>4__this;
public GameObject clone;
public string key;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <DestroyOriginalAfterReplacementValid>d__131(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<>1__state = -2;
}
private bool MoveNext()
{
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected O, but got Unknown
int num = <>1__state;
LevelDeathPlugin levelDeathPlugin = <>4__this;
switch (num)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = (object)new WaitForSeconds(0.2f);
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
if ((Object)(object)source == (Object)null)
{
return false;
}
if (levelDeathPlugin.IsSpawnedCloneValid(clone, key))
{
Object.Destroy((Object)(object)source);
}
else
{
levelDeathPlugin.DestroyCloneIfPresent(clone);
if (levelDeathPlugin._debugLogging != null && levelDeathPlugin._debugLogging.Value)
{
((BaseUnityPlugin)levelDeathPlugin).Logger.LogInfo((object)("Level Death kept original enemy because its replacement failed validation after spawn: " + ((Object)source).name));
}
}
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();
}
}
[CompilerGenerated]
private sealed class <FindEnemyIdentifierComponents>d__180 : IEnumerable<Component>, IEnumerable, IEnumerator<Component>, IEnumerator, IDisposable
{
private int <>1__state;
private Component <>2__current;
private int <>l__initialThreadId;
private GameObject gameObject;
public GameObject <>3__gameObject;
public LevelDeathPlugin <>4__this;
private Type <enemyIdentifierType>5__2;
private Component <self>5__3;
private Component[] <>7__wrap3;
private int <>7__wrap4;
Component IEnumerator<Component>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <FindEnemyIdentifierComponents>d__180(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<enemyIdentifierType>5__2 = null;
<self>5__3 = null;
<>7__wrap3 = null;
<>1__state = -2;
}
private bool MoveNext()
{
int num = <>1__state;
LevelDeathPlugin levelDeathPlugin = <>4__this;
Component[] componentsInChildren;
switch (num)
{
default:
return false;
case 0:
<>1__state = -1;
if ((Object)(object)gameObject == (Object)null)
{
return false;
}
<enemyIdentifierType>5__2 = levelDeathPlugin.GetEnemyIdentifierType();
if (<enemyIdentifierType>5__2 == null)
{
return false;
}
<self>5__3 = gameObject.GetComponent(<enemyIdentifierType>5__2);
if ((Object)(object)<self>5__3 != (Object)null)
{
<>2__current = <self>5__3;
<>1__state = 1;
return true;
}
goto IL_0096;
case 1:
<>1__state = -1;
goto IL_0096;
case 2:
{
<>1__state = -1;
goto IL_00fa;
}
IL_0108:
if (<>7__wrap4 < <>7__wrap3.Length)
{
Component val = <>7__wrap3[<>7__wrap4];
if ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)<self>5__3)
{
<>2__current = val;
<>1__state = 2;
return true;
}
goto IL_00fa;
}
<>7__wrap3 = null;
return false;
IL_0096:
componentsInChildren = gameObject.GetComponentsInChildren(<enemyIdentifierType>5__2, true);
if (componentsInChildren == null)
{
return false;
}
<>7__wrap3 = componentsInChildren;
<>7__wrap4 = 0;
goto IL_0108;
IL_00fa:
<>7__wrap4++;
goto IL_0108;
}
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
[DebuggerHidden]
IEnumerator<Component> IEnumerable<Component>.GetEnumerator()
{
<FindEnemyIdentifierComponents>d__180 <FindEnemyIdentifierComponents>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<FindEnemyIdentifierComponents>d__ = this;
}
else
{
<FindEnemyIdentifierComponents>d__ = new <FindEnemyIdentifierComponents>d__180(0)
{
<>4__this = <>4__this
};
}
<FindEnemyIdentifierComponents>d__.gameObject = <>3__gameObject;
return <FindEnemyIdentifierComponents>d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<Component>)this).GetEnumerator();
}
}
[CompilerGenerated]
private sealed class <PrimeGlobalEnemyPoolSoon>d__159 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public LevelDeathPlugin <>4__this;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <PrimeGlobalEnemyPoolSoon>d__159(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<>1__state = -2;
}
private bool MoveNext()
{
int num = <>1__state;
LevelDeathPlugin levelDeathPlugin = <>4__this;
switch (num)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = null;
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
<>2__current = null;
<>1__state = 2;
return true;
case 2:
<>1__state = -1;
if (levelDeathPlugin._enabled == null || !levelDeathPlugin._enabled.Value)
{
return false;
}
if (levelDeathPlugin._enemyPoolMode == null || levelDeathPlugin._enemyPoolMode.Value != EnemyPoolMode.GlobalEnemies)
{
return false;
}
if (levelDeathPlugin._cyberGrindOnly != null && levelDeathPlugin._cyberGrindOnly.Value)
{
string text = Normalize(levelDeathPlugin._currentSceneName);
if (!text.Contains("cyber") && !text.Contains("endless") && !text.Contains("grid"))
{
return false;
}
}
levelDeathPlugin.ScanGlobalEnemyPoolOncePerScene();
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 = "oai.ultrakill.leveldeath";
public const string PluginName = "Level Death";
public const string PluginVersion = "0.8.4";
internal static LevelDeathPlugin Instance;
internal static ManualLogSource Log;
private Harmony _harmony;
private Type _enemyIdentifierType;
private ConfigEntry<bool> _enabled;
private ConfigEntry<float> _maxMeterCarriedBetweenScenes;
private ConfigEntry<bool> _showHud;
private ConfigEntry<bool> _cyberGrindOnly;
private ConfigEntry<StartingLevelMode> _startingLevelMode;
private ConfigEntry<float> _startingMeterPercent;
private ConfigEntry<EnemyPoolMode> _enemyPoolMode;
private ConfigEntry<DirectorActionMode> _directorActionMode;
private ConfigEntry<bool> _allowUnknownEnemyTypes;
private ConfigEntry<bool> _debugLogging;
private ConfigEntry<float> _styleGainScale;
private ConfigEntry<float> _softCapGainMultiplier;
private ConfigEntry<float> _levelCompletionReward;
private ConfigEntry<float> _deathMeterRetainedMultiplier;
private ConfigEntry<float> _damagePenaltyFlat;
private ConfigEntry<float> _damagePenaltyScale;
private ConfigEntry<float> _damagePenaltyMultiplier;
private ConfigEntry<float> _isolatedHitPenaltyMultiplier;
private ConfigEntry<float> _consecutiveHitWindowSeconds;
private ConfigEntry<float> _consecutiveHitPenaltyStep;
private ConfigEntry<float> _maxConsecutiveHitPenaltyMultiplier;
private ConfigEntry<float> _minimumMeterForBonusSpawns;
private ConfigEntry<float> _toughPoolStartsAtMeter;
private ConfigEntry<float> _globalEnemyMinimumMeter;
private ConfigEntry<float> _spawnChanceMultiplier;
private ConfigEntry<int> _maxBonusBudget;
private ConfigEntry<int> _maxOvercapBonusBudget;
private ConfigEntry<float> _deathMarkBudgetMultiplier;
private ConfigEntry<int> _maxActiveBonusEnemies;
private ConfigEntry<float> _globalEnemyCostMultiplier;
private ConfigEntry<int> _globalEnemyFlatSurcharge;
private ConfigEntry<float> _minimumSecondsBetweenBonusSpawns;
private ConfigEntry<float> _minimumSceneAgeForSpawns;
private ConfigEntry<float> _spawnRadiusMin;
private ConfigEntry<float> _spawnRadiusMax;
private ConfigEntry<bool> _allowTypeReplacements;
private ConfigEntry<float> _replacementChanceLevel2;
private ConfigEntry<float> _replacementChanceLevel3;
private ConfigEntry<float> _replacementChanceLevel4;
private ConfigEntry<float> _waveBatchWindowSeconds;
private ConfigEntry<float> _lowTierWaveReadSeconds;
private ConfigEntry<int> _processWaveImmediatelyAtSize;
private ConfigEntry<bool> _allowSingleFodderReplacement;
private ConfigEntry<int> _fullDirectorStartsAtWaveSize;
private ConfigEntry<int> _mediumWaveMaxDuplicates;
private ConfigEntry<int> _mediumWaveMaxReplacements;
private ConfigEntry<int> _maxReplacementsLevel1;
private ConfigEntry<int> _maxReplacementsLevel2;
private ConfigEntry<int> _maxReplacementsLevel3;
private ConfigEntry<int> _maxReplacementsLevel4;
private ConfigEntry<int> _maxDuplicatesLevel1;
private ConfigEntry<int> _maxDuplicatesLevel2;
private ConfigEntry<int> _maxDuplicatesLevel3;
private ConfigEntry<int> _maxDuplicatesLevel4;
private ConfigEntry<float> _maxReplacementFractionOfWave;
private ConfigEntry<bool> _guaranteeSafeDuplicateAtLevel2;
private ConfigEntry<bool> _allowNaturalSpawnBuffs;
private ConfigEntry<float> _naturalEnrageChanceLevel2;
private ConfigEntry<float> _naturalRadiantChanceLevel3;
private ConfigEntry<bool> _allowHighMeterEnemyBuffs;
private ConfigEntry<float> _enrageChanceAt300;
private ConfigEntry<float> _enrageChanceAt1000;
private ConfigEntry<float> _radiantChanceAt400;
private ConfigEntry<float> _radiantChanceAt1000;
private ConfigEntry<float> _radianceTier;
private ConfigEntry<bool> _sanitizeInheritedIdolState;
private ConfigEntry<float> _cerberusPairChance;
private ConfigEntry<bool> _enableLargeEnemyPool;
private ConfigEntry<float> _largeEnemyRarityMultiplier;
private ConfigEntry<float> _largeEnemyGroundCheckDistance;
private ConfigEntry<float> _largeEnemyActionPenalty;
private ConfigEntry<int> _largeEnemyMinNonFilthWaveSize;
private ConfigEntry<bool> _enableEnemyVarietyCooldown;
private ConfigEntry<float> _enemyVarietyCooldownDecaySeconds;
private ConfigEntry<float> _enemyVarietyCooldownStrength;
private ConfigEntry<float> _largeEnemyCooldownBonus;
private readonly HashSet<int> _processedEnemies = new HashSet<int>();
private readonly List<PendingWaveEnemy> _pendingWave = new List<PendingWaveEnemy>();
private readonly Dictionary<string, EnemyTemplatePool> _limitedEnemyPools = new Dictionary<string, EnemyTemplatePool>();
private readonly Dictionary<string, EnemyTemplatePool> _globalEnemyPools = new Dictionary<string, EnemyTemplatePool>();
private readonly HashSet<string> _loggedUnknownTypes = new HashSet<string>();
private readonly Dictionary<string, EnemyCooldownState> _enemyCooldowns = new Dictionary<string, EnemyCooldownState>();
private GUIStyle _hudStyle;
private float _meter;
private float _nextAllowedBonusSpawnTime;
private float _sceneLoadedTime;
private bool _globalPoolScannedThisScene;
private string _currentSceneName = string.Empty;
private bool _levelCompletionRewardGiven;
private float _pendingWaveDeadline = -1f;
private float _lastDamageTime = -999f;
private float _lastDeathPenaltyTime = -999f;
private int _consecutiveDamageHits;
private bool _activeWaveLargeEnemySpawned;
private int _activeWaveNonFilthEnemyCount;
private void Awake()
{
//IL_0033: Unknown result type (might be due to invalid IL or missing references)
//IL_0038: Unknown result type (might be due to invalid IL or missing references)
//IL_006b: Unknown result type (might be due to invalid IL or missing references)
//IL_0075: Expected O, but got Unknown
Instance = this;
Log = ((BaseUnityPlugin)this).Logger;
BindConfig();
_enemyIdentifierType = AccessTools.TypeByName("EnemyIdentifier");
_sceneLoadedTime = Time.time;
Scene activeScene = SceneManager.GetActiveScene();
_currentSceneName = ((Scene)(ref activeScene)).name ?? string.Empty;
ApplyStartingMeterFloor();
SceneManager.sceneLoaded += OnSceneLoaded;
_harmony = new Harmony("oai.ultrakill.leveldeath");
PatchSafely();
((MonoBehaviour)this).StartCoroutine(PrimeGlobalEnemyPoolSoon());
((BaseUnityPlugin)this).Logger.LogInfo((object)"Level Death loaded. Wave director enabled. Levels: Normal, Enraged, Radiant, Level Death.");
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
try
{
Harmony harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to unpatch Level Death cleanly: " + ex.Message));
}
}
private ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, string description)
{
return ((BaseUnityPlugin)this).Config.Bind<T>(section, key, defaultValue, description);
}
private void BindConfig()
{
_enabled = Bind("General", "Enabled", defaultValue: true, "Master switch for Level Death. Turning this off makes the mod inert and hides the HUD.");
_enabled.SettingChanged += OnEnabledSettingChanged;
_showHud = Bind("General", "ShowMeter", defaultValue: true, "Show the Level Death meter on screen.");
_cyberGrindOnly = Bind("General", "CyberGrindOnly", defaultValue: false, "If true, Level Death only changes enemy spawns in Cyber Grind / Endless-style scenes.");
_startingLevelMode = Bind("General", "StartingDifficulty", StartingLevelMode.Normal, "Starting meter floor for new scenes. Normal=0%, Enraged=100%, Radiant=200%, LevelDeath=300%, Custom=CustomStartingMeterPercent.");
_startingMeterPercent = Bind("General", "CustomStartingMeterPercent", 0f, "Used only when StartingDifficulty is Custom. Clamped from 0% to 400%.");
_enemyPoolMode = Bind("General", "EnemyPool", EnemyPoolMode.GlobalEnemies, "GlobalEnemies uses a curated global enemy pool. LimitedEnemyPool only clones enemies naturally present in the current scene/session.");
_directorActionMode = Bind("General", "DirectorStyle", DirectorActionMode.MixedUpgradesAndReinforcements, "How Level Death spends pressure: AddReinforcementsOnly, MixedUpgradesAndReinforcements, or PreferUpgrades.");
_debugLogging = Bind("General", "DebugLogging", defaultValue: false, "Verbose logs for wave decisions, replacement choices, damage penalties, and unknown enemy types.");
_styleGainScale = Bind("Meter", "StyleGainScale", 0.02f, "Meter gained per style point before action bonuses are applied.");
_softCapGainMultiplier = Bind("Meter", "GainMultiplierAbove100Percent", 0.82f, "Meter gain multiplier after Level 2 begins at 100%.");
_levelCompletionReward = Bind("Meter", "LevelCompletionBonus", 25f, "Meter awarded once when the normal level-end rank screen appears.");
_deathMeterRetainedMultiplier = Bind("Meter", "DeathMeterRetainedMultiplier", 0.5f, "Meter retained after death/respawn. Default 0.5 means dying halves the current Level Death percentage instead of resetting it to zero.");
_damagePenaltyFlat = Bind("Meter", "BaseDamagePenalty", 6f, "Base meter removed when the player takes damage before isolated/consecutive hit scaling.");
_damagePenaltyScale = Bind("Meter", "DamagePenaltyPerDamagePoint", 0.06f, "Extra meter removed per point of damage, when the damage value can be read.");
_damagePenaltyMultiplier = Bind("Meter", "DamagePenaltyMultiplier", 1.3f, "Overall multiplier for damage meter loss. Default 1.3 makes damage remove about 30% more meter than previous versions.");
_isolatedHitPenaltyMultiplier = Bind("Meter", "OccasionalHitPenaltyMultiplier", 0.25f, "Multiplier for isolated damage hits before streak scaling.");
_consecutiveHitWindowSeconds = Bind("Meter", "ConsecutiveHitWindowSeconds", 4f, "Hits inside this window count as a damage streak and become increasingly punishing.");
_consecutiveHitPenaltyStep = Bind("Meter", "ConsecutiveHitPenaltyStep", 0.35f, "Additional penalty multiplier per consecutive hit after the first.");
_maxConsecutiveHitPenaltyMultiplier = Bind("Meter", "MaxConsecutiveHitPenaltyMultiplier", 1.8f, "Maximum multiplier for repeated hits in a short window.");
_maxMeterCarriedBetweenScenes = Bind("Meter", "MaxMeterCarriedBetweenScenes", 400f, "Maximum meter preserved when loading a new scene. Level Death runs from 0% to 400%.");
Bind("Meter", "OutOfCombatDecayPerSecond", 0f, "Deprecated/no-op. Time decay was removed so quiet levels do not punish the player.");
Bind("Meter", "OutOfCombatDecayDelay", 0f, "Deprecated/no-op. Time decay was removed so quiet levels do not punish the player.");
_minimumMeterForBonusSpawns = Bind("Director", "MinimumMeterForDirector", 18f, "Level Death will not add duplicate/replacement pressure below this meter value.");
_toughPoolStartsAtMeter = Bind("Director", "ToughEnemiesOnlyAfterPercent", 9999f, "Deprecated/no-op. Low-tier enemies such as Filth, Strays, and Drones are no longer suppressed at high meter.");
_globalEnemyMinimumMeter = Bind("Director", "GlobalEnemyAppearsAfterPercent", 70f, "Global-only enemy types cannot appear before this meter value, even when EnemyPool is GlobalEnemies.");
_spawnChanceMultiplier = Bind("Director", "PressureChanceMultiplier", 1f, "Multiplies Level Death's duplicate/replacement chances.");
_maxBonusBudget = Bind("Director", "BudgetAt100Percent", 12, "Base extra enemy-cost budget unlocked at 100% before the 100% budget jump.");
_maxOvercapBonusBudget = Bind("Director", "BudgetAt400Percent", 38, "Maximum extra enemy-cost budget unlocked at 400%.");
_deathMarkBudgetMultiplier = Bind("Director", "BudgetJumpAt100Percent", 2f, "When the meter reaches 100%, the director budget jumps by this multiplier before continuing toward the 400% cap.");
_maxActiveBonusEnemies = Bind("Director", "MaxLivingBonusEnemies", 24, "Hard cap on living Level Death-created enemies.");
_globalEnemyCostMultiplier = Bind("Director", "GlobalEnemyCostMultiplier", 1.75f, "Cost multiplier for enemies that are not already part of the current scene's limited pool.");
_globalEnemyFlatSurcharge = Bind("Director", "GlobalEnemyCostBonus", 1, "Flat extra cost for global-only enemies. This keeps enemies like Ferryman expensive in tiny early rooms.");
_minimumSecondsBetweenBonusSpawns = Bind("Director", "MinimumSecondsBetweenWaves", 0.45f, "Throttle after Level Death spawns duplicates/replacements.");
_minimumSceneAgeForSpawns = Bind("Director", "SceneStartGraceSeconds", 3f, "Grace period after loading a scene before Level Death changes spawns. Natural enemies that appear during this grace period are now held and processed after the grace period instead of being discarded.");
_waveBatchWindowSeconds = Bind("Director", "SpawnWaveReadSeconds", 1.45f, "How long Level Death keeps listening for drip-fed natural spawns before treating them as one wave. Higher values help rooms that drip-feed mixed enemies, but delay director actions slightly.");
_lowTierWaveReadSeconds = Bind("Director", "LowTierSpawnWaveReadSeconds", 0.45f, "Shorter read window for waves made only of Filth/Strays/Drones. This prevents fast fodder from dying before Level Death can process it.");
_processWaveImmediatelyAtSize = Bind("Director", "ProcessWaveImmediatelyAtSize", 4, "If a pending wave reaches this many enemies, process it almost immediately instead of waiting out the full read window.");
Bind("Director", "WaveBatchWindowSeconds", 0.65f, "Deprecated/no-op. Use SpawnWaveReadSeconds instead.");
_spawnRadiusMin = Bind("Director", "SpawnDistanceMin", 2.25f, "Minimum distance from the source spawn for bonus enemies.");
_spawnRadiusMax = Bind("Director", "SpawnDistanceMax", 6.5f, "Maximum distance from the source spawn for bonus enemies.");
_cerberusPairChance = Bind("Director", "CerberusPairChance", 0.55f, "When Level Death spawns a Cerberus, chance to spawn a second Cerberus nearby if safety caps allow it.");
_enableEnemyVarietyCooldown = Bind("Director", "EnableHighCostEnemyCooldown", defaultValue: true, "Soft variety cooldown for enemies costing 7 or more. It never blocks a spawn, but heavily lowers odds after that enemy was recently selected.");
_enemyVarietyCooldownDecaySeconds = Bind("Director", "HighCostEnemyCooldownDecaySeconds", 22f, "How quickly high-cost enemy cooldown pressure fades. Higher values keep rare enemies rare for longer.");
_enemyVarietyCooldownStrength = Bind("Director", "HighCostEnemyCooldownStrength", 1f, "Overall strength of the high-cost enemy cooldown. 0 disables the effect even if enabled.");
_largeEnemyCooldownBonus = Bind("Director", "LargeEnemyCooldownBonus", 3.5f, "Extra cooldown heat added to large enemies, making repeat large enemy picks substantially rarer.");
_enableLargeEnemyPool = Bind("Large Enemies", "EnableLargeEnemyPool", defaultValue: true, "Allow very large enemies to appear in large-enough waves. They still require ground below and cannot replace flying enemies.");
_largeEnemyRarityMultiplier = Bind("Large Enemies", "LargeEnemyRarityMultiplier", 0.5f, "Selection weight multiplier for large enemies. Default 0.5 makes them half as common as normal candidates.");
_largeEnemyGroundCheckDistance = Bind("Large Enemies", "GroundCheckDistance", 10f, "Large enemies require ground below within this distance.");
_largeEnemyActionPenalty = Bind("Large Enemies", "ActionPenaltyWhenLargeEnemySpawns", 1f, "When a large enemy spawns in a wave, reduce remaining duplicate/replacement caps by this amount.");
_largeEnemyMinNonFilthWaveSize = Bind("Large Enemies", "MinimumNonFilthWaveSize", 5, "Large enemies may only appear in waves with this many non-Filth natural enemies. Filth are ignored because they are often spawned as swarms in small spaces.");
_fullDirectorStartsAtWaveSize = Bind("Wave Rules", "FullDirectorStartsAtWaveSize", 4, "Waves this large use the normal per-level duplicate/replacement caps. Smaller waves are protected from obvious cloning/randomizer behavior.");
_mediumWaveMaxDuplicates = Bind("Wave Rules", "MediumWaveMaxDuplicates", 1, "Maximum duplicates for small waves below FullDirectorStartsAtWaveSize but larger than one enemy. Default means 2-3 enemy waves can only get one duplicate.");
_mediumWaveMaxReplacements = Bind("Wave Rules", "MediumWaveMaxReplacements", 1, "Maximum type replacements for small waves below FullDirectorStartsAtWaveSize but larger than one enemy. Default means 2-3 enemy waves can only get one replacement.");
_guaranteeSafeDuplicateAtLevel2 = Bind("Wave Rules", "GuaranteeDuplicateAt100Percent", defaultValue: true, "At 100% and above, try to add one safe local duplicate, but only when the natural wave has at least two enemies.");
_allowSingleFodderReplacement = Bind("Wave Rules", "AllowSingleFodderReplacement", defaultValue: true, "Allow lone Filth/Stray/Drone spawns to be type-replaced at 100%+. They still cannot be duplicated. This keeps drip-fed fodder waves active without cloning single scripted enemies.");
_maxReplacementFractionOfWave = Bind("Wave Rules", "MaxReplacementFractionOfWave", 0.35f, "Personality-preservation cap. By default, no more than about 35% of a natural wave is type-replaced.");
_maxDuplicatesLevel1 = Bind("Duplicate Caps", "Level1Normal", 1, "Maximum safe local duplicates per eligible spawn wave in Level 1: Normal. Single-enemy waves still receive zero duplicates.");
_maxDuplicatesLevel2 = Bind("Duplicate Caps", "Level2Enraged", 2, "Maximum safe local duplicates per eligible spawn wave in Level 2: Enraged.");
_maxDuplicatesLevel3 = Bind("Duplicate Caps", "Level3Radiant", 3, "Maximum safe local duplicates per eligible spawn wave in Level 3: Radiant.");
_maxDuplicatesLevel4 = Bind("Duplicate Caps", "LevelDeath", 3, "Maximum safe local duplicates per eligible spawn wave in Level Death. Kept at 3 by default so Level Death favors quality over spam.");
_allowTypeReplacements = Bind("Replacements", "AllowTypeReplacements", defaultValue: true, "Allow Level Death to replace some natural enemy spawns with more expensive global-pool enemies. Turn this off if a level behaves strangely.");
_replacementChanceLevel2 = Bind("Replacements", "Level2EnragedChance", 0.45f, "Per-wave chance for each eligible natural spawn to be replaced by a tougher enemy in Level 2: Enraged, up to the cap.");
_replacementChanceLevel3 = Bind("Replacements", "Level3RadiantChance", 0.62f, "Per-wave chance for each eligible natural spawn to be replaced by a tougher enemy in Level 3: Radiant, up to the cap.");
_replacementChanceLevel4 = Bind("Replacements", "LevelDeathChance", 0.74f, "Per-wave chance for each eligible natural spawn to be replaced by a tougher enemy in Level Death, up to the cap.");
_maxReplacementsLevel1 = Bind("Replacement Caps", "Level1Normal", 0, "Maximum type replacements per eligible spawn wave in Level 1: Normal.");
_maxReplacementsLevel2 = Bind("Replacement Caps", "Level2Enraged", 1, "Maximum type replacements per eligible spawn wave in Level 2: Enraged.");
_maxReplacementsLevel3 = Bind("Replacement Caps", "Level3Radiant", 3, "Maximum type replacements per eligible spawn wave in Level 3: Radiant.");
_maxReplacementsLevel4 = Bind("Replacement Caps", "LevelDeath", 4, "Maximum type replacements per eligible spawn wave in Level Death.");
_allowNaturalSpawnBuffs = Bind("Enemy Upgrades", "AllowNaturalEnemyUpgrades", defaultValue: true, "Allow natural, non-Level-Death spawns to be upgraded in place. This reduces enemy spam by making existing enemies more dangerous.");
_naturalEnrageChanceLevel2 = Bind("Enemy Upgrades", "NaturalEnrageChance", 0.16f, "Chance for a natural spawn to become enraged in Level 2: Enraged and above.");
_naturalRadiantChanceLevel3 = Bind("Enemy Upgrades", "NaturalRadiantChance", 0.14f, "Chance for a natural spawn to receive tier-1 radiant buffs in Level 3: Radiant and above.");
_allowHighMeterEnemyBuffs = Bind("Enemy Upgrades", "AllowBonusEnemyUpgrades", defaultValue: true, "Allow Level Death-created enemies to occasionally become enraged or radiant.");
_enrageChanceAt300 = Bind("Enemy Upgrades", "BonusEnemyEnrageChanceAt100Percent", 0.1f, "Chance for a Level Death-created reinforcement to be enraged at 100%, the start of Level 2.");
_enrageChanceAt1000 = Bind("Enemy Upgrades", "BonusEnemyEnrageChanceAt400Percent", 0.3f, "Chance for a Level Death-created reinforcement to be enraged at 400%, the Level Death cap.");
_radiantChanceAt400 = Bind("Enemy Upgrades", "BonusEnemyRadiantChanceAt200Percent", 0.08f, "Chance for a Level Death-created reinforcement to receive tier-1 radiant buffs at 200%, the start of Level 3.");
_radiantChanceAt1000 = Bind("Enemy Upgrades", "BonusEnemyRadiantChanceAt400Percent", 0.22f, "Chance for a Level Death-created reinforcement to receive tier-1 radiant buffs at 400%.");
_radianceTier = Bind("Enemy Upgrades", "RadianceTier", 1f, "Radiance tier assigned to radiant Level Death enemies. Default is true tier 1 radiance.");
_allowUnknownEnemyTypes = Bind("Advanced Safety", "AllowUnknownEnemyTypes", defaultValue: false, "Unknown enemy types are ignored by default because custom/scripted enemies are more likely to break when cloned.");
_sanitizeInheritedIdolState = Bind("Advanced Safety", "RemoveInheritedIdolBlessingFromClones", defaultValue: true, "When Level Death clones an enemy, clear inherited Idol/blessed runtime state so duplicates do not become invulnerable just because the source was protected by an Idol.");
}
private void OnEnabledSettingChanged(object sender, EventArgs e)
{
if (_enabled != null && !_enabled.Value)
{
_meter = 0f;
_pendingWave.Clear();
_pendingWaveDeadline = -1f;
_consecutiveDamageHits = 0;
_lastDamageTime = -999f;
_nextAllowedBonusSpawnTime = Time.time + Mathf.Max(0.5f, (_minimumSecondsBetweenBonusSpawns != null) ? _minimumSecondsBetweenBonusSpawns.Value : 1f);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)"Level Death disabled from config; meter reset and future spawns halted.");
}
}
else if (_enabled != null && _enabled.Value)
{
_sceneLoadedTime = Time.time;
_nextAllowedBonusSpawnTime = Time.time + ((_minimumSceneAgeForSpawns != null) ? Mathf.Max(0f, _minimumSceneAgeForSpawns.Value) : 3f);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)"Level Death enabled from config; spawns will resume after the scene grace period.");
}
}
}
private void PatchSafely()
{
PatchFirstMethod(GetEnemyIdentifierType(), "Start", "EnemyStartPostfix", required: true);
PatchFirstMethod(AccessTools.TypeByName("StyleHUD"), "AddPoints", "StyleAddPointsPostfix", required: false);
PatchAllNamedMethods(AccessTools.TypeByName("NewMovement"), "GetHurt", "PlayerHurtPostfix", required: false);
PatchAllNamedMethods(AccessTools.TypeByName("NewMovement"), "Respawn", "PlayerDeathOrRespawnPostfix", required: false);
PatchAllNamedMethods(AccessTools.TypeByName("NewMovement"), "Die", "PlayerDeathOrRespawnPostfix", required: false);
PatchAllNamedMethods(AccessTools.TypeByName("FinalRank"), "SetInfo", "LevelCompletePostfix", required: false);
}
private void PatchFirstMethod(Type type, string methodName, string patchMethodName, bool required)
{
//IL_008e: Unknown result type (might be due to invalid IL or missing references)
//IL_0094: Expected O, but got Unknown
if (type == null)
{
if (required)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find type for required patch method " + methodName + "."));
}
return;
}
MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null);
if (methodInfo == null)
{
if (required)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find required method " + type.Name + "." + methodName + "."));
}
}
else
{
HarmonyMethod val = new HarmonyMethod(typeof(LevelDeathPlugin).GetMethod(patchMethodName, BindingFlags.Static | BindingFlags.NonPublic));
_harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
((BaseUnityPlugin)this).Logger.LogInfo((object)("Patched " + type.Name + "." + methodName + "."));
}
}
private void PatchAllNamedMethods(Type type, string methodName, string patchMethodName, bool required)
{
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_0041: Expected O, but got Unknown
if (type == null)
{
if (required)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find type for required patch method " + methodName + "."));
}
return;
}
HarmonyMethod val = new HarmonyMethod(typeof(LevelDeathPlugin).GetMethod(patchMethodName, BindingFlags.Static | BindingFlags.NonPublic));
int num = 0;
foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type))
{
if (!(declaredMethod == null) && !(declaredMethod.Name != methodName))
{
try
{
_harmony.Patch((MethodBase)declaredMethod, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
num++;
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogDebug((object)("Skipped patch " + type.Name + "." + methodName + ": " + ex.Message));
}
}
}
if (num > 0)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Patched " + num + " overload(s) of " + type.Name + "." + methodName + "."));
}
else if (required)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not find required method " + type.Name + "." + methodName + "."));
}
else
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Optional patch not found: " + type.Name + "." + methodName + "."));
}
}
private void Update()
{
ProcessPendingWaveIfReady();
}
private void OnGUI()
{
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0033: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_0042: 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_0050: Unknown result type (might be due to invalid IL or missing references)
//IL_005a: Expected O, but got Unknown
//IL_005f: Expected O, but got Unknown
//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
if (_showHud.Value && _enabled.Value)
{
if (_hudStyle == null)
{
_hudStyle = new GUIStyle(GUI.skin.box)
{
fontSize = 18,
alignment = (TextAnchor)3,
richText = true,
padding = new RectOffset(10, 10, 8, 8)
};
}
string rankName = GetRankName();
string text = (CanSpawnInCurrentScene() ? string.Empty : "\n<size=12>spawns gated</size>");
string text2 = ((_enemyPoolMode.Value == EnemyPoolMode.GlobalEnemies) ? "Global" : "Limited");
GUI.Box(new Rect(20f, 20f, 340f, 70f), "<b>LEVEL DEATH</b> " + rankName + " <size=12>" + text2 + "</size>\n" + Mathf.RoundToInt(_meter) + "% " + MakeMeterBar(_meter) + text, _hudStyle);
}
}
private static string MakeMeterBar(float value)
{
int num = Mathf.Clamp(Mathf.RoundToInt(((value >= 400f) ? 100f : Mathf.Repeat(Mathf.Max(0f, value), 100f)) / 10f), 0, 10);
return new string('|', num) + new string('.', 10 - num);
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
_processedEnemies.Clear();
_pendingWave.Clear();
_pendingWaveDeadline = -1f;
_enemyCooldowns.Clear();
_limitedEnemyPools.Clear();
_loggedUnknownTypes.Clear();
LevelDeathSpawnedMarker.ActiveCount = 0;
_meter = Mathf.Min(_meter, Mathf.Clamp((_maxMeterCarriedBetweenScenes != null) ? _maxMeterCarriedBetweenScenes.Value : 400f, 0f, 400f));
ApplyStartingMeterFloor();
_sceneLoadedTime = Time.time;
_globalPoolScannedThisScene = false;
_currentSceneName = ((Scene)(ref scene)).name ?? string.Empty;
_levelCompletionRewardGiven = false;
_consecutiveDamageHits = 0;
_lastDamageTime = -999f;
_nextAllowedBonusSpawnTime = Time.time + _minimumSceneAgeForSpawns.Value;
((MonoBehaviour)this).StartCoroutine(PrimeGlobalEnemyPoolSoon());
}
private static void EnemyStartPostfix(object __instance)
{
try
{
Instance?.OnEnemyStarted(__instance);
}
catch (Exception ex)
{
ManualLogSource log = Log;
if (log != null)
{
log.LogError((object)("Level Death EnemyStartPostfix failed: " + ex));
}
}
}
private static void StyleAddPointsPostfix(object[] __args)
{
try
{
if (!((Object)(object)Instance == (Object)null) && __args != null && __args.Length != 0)
{
int points = ReadIntLike(__args[0]);
string text = ((__args.Length > 1) ? Convert.ToString(__args[1]) : string.Empty);
Instance.OnStylePoints(points, text ?? string.Empty);
}
}
catch (Exception ex)
{
ManualLogSource log = Log;
if (log != null)
{
log.LogError((object)("Level Death StyleAddPointsPostfix failed: " + ex));
}
}
}
private static void PlayerHurtPostfix(object[] __args)
{
try
{
if ((Object)(object)Instance == (Object)null)
{
return;
}
float num = 0f;
if (__args != null)
{
foreach (object obj in __args)
{
if (obj is int num2)
{
num = Mathf.Max(num, (float)num2);
break;
}
if (obj is float num3)
{
num = Mathf.Max(num, num3);
break;
}
}
}
Instance.OnPlayerHurt(num);
}
catch (Exception ex)
{
ManualLogSource log = Log;
if (log != null)
{
log.LogError((object)("Level Death PlayerHurtPostfix failed: " + ex));
}
}
}
private static void PlayerDeathOrRespawnPostfix()
{
try
{
Instance?.OnPlayerDeathOrRespawn();
}
catch (Exception ex)
{
ManualLogSource log = Log;
if (log != null)
{
log.LogError((object)("Level Death PlayerDeathOrRespawnPostfix failed: " + ex));
}
}
}
private static void LevelCompletePostfix()
{
try
{
Instance?.OnLevelComplete();
}
catch (Exception ex)
{
ManualLogSource log = Log;
if (log != null)
{
log.LogError((object)("Level Death LevelCompletePostfix failed: " + ex));
}
}
}
private static int ReadIntLike(object value)
{
if (value is int)
{
return (int)value;
}
if (value is float num)
{
return Mathf.RoundToInt(num);
}
if (value is double a)
{
return (int)Math.Round(a);
}
return 0;
}
private void OnStylePoints(int points, string styleKey)
{
if (_enabled.Value && points > 0)
{
string text = Normalize(styleKey);
float num = Mathf.Clamp((float)points * _styleGainScale.Value, 0.05f, 3.5f);
if (text.Contains("parry"))
{
num += 2.25f;
}
if (text.Contains("projectileboost"))
{
num += 0.75f;
}
if (text.Contains("kill"))
{
num += 0.8f;
}
if (text.Contains("doublekill") || text.Contains("triplekill") || text.Contains("multikill"))
{
num += 1.2f;
}
if (text.Contains("air") || text.Contains("arsenal") || text.Contains("ultra"))
{
num += 0.35f;
}
AddMeter(num);
}
}
private void OnPlayerHurt(float damage)
{
if (_enabled.Value)
{
float time = Time.time;
float num = Mathf.Max(0.25f, _consecutiveHitWindowSeconds.Value);
if (time - _lastDamageTime > num)
{
_consecutiveDamageHits = 0;
}
_consecutiveDamageHits++;
_lastDamageTime = time;
float num2 = _damagePenaltyFlat.Value + Mathf.Max(0f, damage) * _damagePenaltyScale.Value;
float num3 = Mathf.Clamp(_isolatedHitPenaltyMultiplier.Value, 0f, 1f);
float num4 = Mathf.Max(0f, _consecutiveHitPenaltyStep.Value);
float num5 = Mathf.Max(num3, _maxConsecutiveHitPenaltyMultiplier.Value);
float num6 = Mathf.Clamp(num3 + (float)(_consecutiveDamageHits - 1) * num4, num3, num5);
float num7 = num2 * num6 * Mathf.Max(0f, (_damagePenaltyMultiplier != null) ? _damagePenaltyMultiplier.Value : 1.3f);
AddMeter(0f - num7);
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death damage penalty: damage=" + damage + ", streak=" + _consecutiveDamageHits + ", multiplier=" + num6.ToString("0.00") + ", penalty=" + num7.ToString("0.0") + ", meter=" + Mathf.RoundToInt(_meter) + "%."));
}
}
}
private void OnPlayerDeathOrRespawn()
{
if (!_enabled.Value)
{
return;
}
float time = Time.time;
if (!(time - _lastDeathPenaltyTime < 1.25f))
{
_lastDeathPenaltyTime = time;
float num = Mathf.Clamp01((_deathMeterRetainedMultiplier != null) ? _deathMeterRetainedMultiplier.Value : 0.5f);
_meter = Mathf.Clamp(_meter * num, 0f, 400f);
_pendingWave.Clear();
_pendingWaveDeadline = -1f;
_consecutiveDamageHits = 0;
_lastDamageTime = -999f;
_nextAllowedBonusSpawnTime = Time.time + 3f;
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death death/respawn penalty applied. Retained " + Mathf.RoundToInt(num * 100f) + "%; meter=" + Mathf.RoundToInt(_meter) + "%."));
}
}
}
private void OnLevelComplete()
{
if (_enabled.Value && !_levelCompletionRewardGiven)
{
_levelCompletionRewardGiven = true;
AddMeter(Mathf.Max(0f, _levelCompletionReward.Value), applySoftCap: false);
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death awarded level completion reward. Meter=" + Mathf.RoundToInt(_meter) + "%."));
}
}
}
private void AddMeter(float delta, bool applySoftCap = true)
{
if (applySoftCap && delta > 0f && _meter >= 100f)
{
delta *= Mathf.Clamp(_softCapGainMultiplier.Value, 0.01f, 1f);
}
else if (applySoftCap && delta > 0f && _meter + delta > 100f)
{
float num = 100f - _meter;
float num2 = (_meter + delta - 100f) * Mathf.Clamp(_softCapGainMultiplier.Value, 0.01f, 1f);
delta = Mathf.Max(0f, num) + num2;
}
_meter = Mathf.Clamp(_meter + delta, 0f, 400f);
}
private bool IsHardBlockedTemplateName(string name)
{
string text = Normalize(name);
if (!text.Contains("verycancerous") && !text.Contains("cancerousrodent") && !text.Contains("rodent") && !text.Contains("idol") && !text.Contains("deathcatcher") && !text.Contains("bigjohn") && !text.Contains("jakito") && !text.Contains("somethingwicked") && !text.Contains("prime") && !text.Contains("gabriel") && !text.Contains("fleshprison") && !text.Contains("fleshpanopticon") && !text.Contains("leviathan") && !text.Contains("earthmover"))
{
return text.Contains("v2");
}
return true;
}
private Type GetEnemyIdentifierType()
{
if (_enemyIdentifierType == null)
{
_enemyIdentifierType = AccessTools.TypeByName("EnemyIdentifier");
}
return _enemyIdentifierType;
}
private bool HasEnemyIdentifierComponent(GameObject gameObject)
{
if ((Object)(object)gameObject == (Object)null)
{
return false;
}
Type enemyIdentifierType = GetEnemyIdentifierType();
if (enemyIdentifierType == null)
{
return false;
}
if (!((Object)(object)gameObject.GetComponent(enemyIdentifierType) != (Object)null))
{
return (Object)(object)gameObject.GetComponentInChildren(enemyIdentifierType, true) != (Object)null;
}
return true;
}
private bool IsTemplateSafeToSpawn(GameObject template, string key)
{
if ((Object)(object)template == (Object)null)
{
return false;
}
if ((Object)(object)template.GetComponent<LevelDeathSpawnedMarker>() != (Object)null)
{
return false;
}
if (IsHardBlockedTemplateName((key ?? string.Empty) + " " + ((Object)template).name))
{
return false;
}
if (!HasEnemyIdentifierComponent(template))
{
return false;
}
return true;
}
private bool IsSpawnedCloneValid(GameObject clone, string key)
{
if ((Object)(object)clone == (Object)null)
{
return false;
}
if (!clone.activeInHierarchy)
{
return false;
}
if (IsHardBlockedTemplateName((key ?? string.Empty) + " " + ((Object)clone).name))
{
return false;
}
if (!HasEnemyIdentifierComponent(clone))
{
return false;
}
return true;
}
private void DestroyCloneIfPresent(GameObject clone)
{
if ((Object)(object)clone != (Object)null)
{
Object.Destroy((Object)(object)clone);
}
}
[IteratorStateMachine(typeof(<DestroyOriginalAfterReplacementValid>d__131))]
private IEnumerator DestroyOriginalAfterReplacementValid(GameObject source, GameObject clone, string key)
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <DestroyOriginalAfterReplacementValid>d__131(0)
{
<>4__this = this,
source = source,
clone = clone,
key = key
};
}
private void OnEnemyStarted(object enemyObject)
{
if (_enabled == null || !_enabled.Value || enemyObject == null)
{
return;
}
Component val = (Component)((enemyObject is Component) ? enemyObject : null);
if ((Object)(object)val == (Object)null || (Object)(object)val.gameObject == (Object)null)
{
return;
}
GameObject gameObject = val.gameObject;
if (!gameObject.activeInHierarchy)
{
return;
}
int instanceID = ((Object)gameObject).GetInstanceID();
if (_processedEnemies.Contains(instanceID))
{
return;
}
_processedEnemies.Add(instanceID);
if ((Object)(object)gameObject.GetComponent<LevelDeathSpawnedMarker>() != (Object)null || ((Object)gameObject).name.IndexOf(" [Level Death]", StringComparison.OrdinalIgnoreCase) >= 0)
{
return;
}
if (IsHardBlockedTemplateName(((Object)gameObject).name))
{
LogUnknownOrBlocked(string.Empty, ((Object)gameObject).name, "hard-blocked name");
return;
}
string text = ReadEnemyTypeName(enemyObject, gameObject);
EnemyProfile profile = EnemyCatalog.GetProfile(text, ((Object)gameObject).name, _allowUnknownEnemyTypes.Value);
if (!profile.Allowed)
{
LogUnknownOrBlocked(text, ((Object)gameObject).name, profile.BlockReason);
return;
}
RegisterLimitedTemplate(profile.Key, gameObject, profile.Cost);
RegisterGlobalTemplate(profile.Key, gameObject, profile.Cost, allowInactiveTemplate: false);
if (CanSpawnInCurrentScene() && _enemyPoolMode.Value == EnemyPoolMode.GlobalEnemies)
{
ScanGlobalEnemyPoolOncePerScene();
}
QueueNaturalEnemyForWave(gameObject, profile);
}
private void QueueNaturalEnemyForWave(GameObject source, EnemyProfile profile)
{
if (!((Object)(object)source == (Object)null) && source.activeInHierarchy && profile.Allowed)
{
_pendingWave.Add(new PendingWaveEnemy(source, profile, Time.time));
RefreshPendingWaveDeadline();
}
}
private void RefreshPendingWaveDeadline()
{
if (_pendingWave.Count == 0)
{
_pendingWaveDeadline = -1f;
return;
}
bool flag = true;
for (int i = 0; i < _pendingWave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = _pendingWave[i];
if (pendingWaveEnemy == null || !IsFastFodderKey(pendingWaveEnemy.Profile.Key))
{
flag = false;
break;
}
}
float num = Mathf.Clamp((_waveBatchWindowSeconds != null) ? _waveBatchWindowSeconds.Value : 1.45f, 0.1f, 4f);
float num2 = Mathf.Clamp((_lowTierWaveReadSeconds != null) ? _lowTierWaveReadSeconds.Value : 0.45f, 0.05f, num);
float num3 = (flag ? num2 : num);
int num4 = Mathf.Max(2, (_processWaveImmediatelyAtSize != null) ? _processWaveImmediatelyAtSize.Value : 4);
if (_pendingWave.Count >= num4)
{
num3 = Mathf.Min(num3, 0.12f);
}
_pendingWaveDeadline = Time.time + num3;
}
private bool ShouldFlushPendingWaveEarly()
{
if (_pendingWave.Count == 0)
{
return false;
}
float num = Time.time;
bool flag = true;
bool flag2 = false;
for (int i = 0; i < _pendingWave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = _pendingWave[i];
if (pendingWaveEnemy != null)
{
num = Mathf.Min(num, pendingWaveEnemy.TimeStarted);
if (!IsFastFodderKey(pendingWaveEnemy.Profile.Key))
{
flag = false;
}
if ((Object)(object)pendingWaveEnemy.Source == (Object)null || !pendingWaveEnemy.Source.activeInHierarchy)
{
flag2 = true;
}
}
}
float num2 = Time.time - num;
int num3 = Mathf.Max(2, (_processWaveImmediatelyAtSize != null) ? _processWaveImmediatelyAtSize.Value : 4);
if (_pendingWave.Count >= num3 && num2 >= 0.08f)
{
return true;
}
if (flag && flag2 && num2 >= 0.08f)
{
return true;
}
return false;
}
private bool IsFastFodderKey(string key)
{
key = Normalize(key);
if (!(key == "filth") && !(key == "stray"))
{
return key == "drone";
}
return true;
}
private bool IsEntrySourceAlive(PendingWaveEnemy entry)
{
if (entry != null && (Object)(object)entry.Source != (Object)null)
{
return entry.Source.activeInHierarchy;
}
return false;
}
private void ProcessPendingWaveIfReady()
{
if (_pendingWave.Count == 0)
{
return;
}
if (!_enabled.Value)
{
_pendingWave.Clear();
_pendingWaveDeadline = -1f;
}
else
{
if (Time.time < _pendingWaveDeadline && !ShouldFlushPendingWaveEarly())
{
return;
}
if (!CanSpawnInCurrentScene())
{
_pendingWaveDeadline = Time.time + 0.25f;
return;
}
List<PendingWaveEnemy> list = new List<PendingWaveEnemy>();
for (int i = 0; i < _pendingWave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = _pendingWave[i];
if (pendingWaveEnemy != null && pendingWaveEnemy.Profile.Allowed && (!((Object)(object)pendingWaveEnemy.Source != (Object)null) || (!((Object)(object)pendingWaveEnemy.Source.GetComponent<LevelDeathSpawnedMarker>() != (Object)null) && (((Object)pendingWaveEnemy.Source).name ?? string.Empty).IndexOf(" [Level Death]", StringComparison.OrdinalIgnoreCase) < 0)))
{
list.Add(pendingWaveEnemy);
}
}
_pendingWave.Clear();
_pendingWaveDeadline = -1f;
if (list.Count != 0)
{
if (_enemyPoolMode.Value == EnemyPoolMode.GlobalEnemies)
{
ScanGlobalEnemyPoolOncePerScene();
}
int levelIndex = GetLevelIndex();
HashSet<int> replacedIds = new HashSet<int>();
_activeWaveNonFilthEnemyCount = CountNonFilthEnemies(list);
_activeWaveLargeEnemySpawned = false;
int num = TryApplyWaveReplacements(list, replacedIds, levelIndex);
int num2 = TryApplyWaveNaturalBuffs(list, replacedIds, levelIndex);
int num3 = TryApplyWaveDuplicates(list, replacedIds, levelIndex);
if (_debugLogging.Value && (num > 0 || num2 > 0 || num3 > 0))
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death processed wave: natural=" + list.Count + ", level=" + GetRankName() + ", replacements=" + num + ", naturalBuffs=" + num2 + ", duplicates=" + num3 + ", meter=" + Mathf.RoundToInt(_meter) + "%."));
}
_activeWaveLargeEnemySpawned = false;
_activeWaveNonFilthEnemyCount = 0;
}
}
}
private List<PendingWaveEnemy> GetWaveOrderedByCost(List<PendingWaveEnemy> wave, bool ascending)
{
List<PendingWaveEnemy> list = new List<PendingWaveEnemy>();
if (wave == null)
{
return list;
}
for (int i = 0; i < wave.Count; i++)
{
if (wave[i] != null)
{
list.Add(wave[i]);
}
}
list.Sort((PendingWaveEnemy a, PendingWaveEnemy b) => (!ascending) ? b.Profile.Cost.CompareTo(a.Profile.Cost) : a.Profile.Cost.CompareTo(b.Profile.Cost));
return list;
}
private bool IsMauriceProfile(EnemyProfile profile)
{
if (!(profile.Key == "maurice"))
{
return profile.Key == "maliciousface";
}
return true;
}
private int CountMaurices(List<PendingWaveEnemy> wave)
{
if (wave == null)
{
return 0;
}
int num = 0;
for (int i = 0; i < wave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = wave[i];
if (pendingWaveEnemy != null && IsMauriceProfile(pendingWaveEnemy.Profile))
{
num++;
}
}
return num;
}
private int CountNonFilthEnemies(List<PendingWaveEnemy> wave)
{
if (wave == null)
{
return 0;
}
int num = 0;
for (int i = 0; i < wave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = wave[i];
if (pendingWaveEnemy != null && pendingWaveEnemy.Profile.Key != "filth")
{
num++;
}
}
return num;
}
private int TryApplyWaveReplacements(List<PendingWaveEnemy> wave, HashSet<int> replacedIds, int level)
{
if (!_allowTypeReplacements.Value)
{
return 0;
}
if (_enemyPoolMode.Value != EnemyPoolMode.GlobalEnemies)
{
return 0;
}
if (_directorActionMode.Value == DirectorActionMode.AddReinforcementsOnly)
{
return 0;
}
if (_meter < 100f)
{
return 0;
}
if (LevelDeathSpawnedMarker.ActiveCount >= _maxActiveBonusEnemies.Value)
{
return 0;
}
int num = ApplyLargeEnemyActionPenalty(GetReplacementCapForLevel(level, wave.Count));
if (num <= 0 && IsEligibleSingleFodderReplacementWave(wave, level))
{
num = 1;
}
if (num <= 0)
{
return 0;
}
float typeReplacementChance = GetTypeReplacementChance();
bool flag = CountMaurices(wave) >= 2;
int num2 = 0;
List<PendingWaveEnemy> waveOrderedByCost = GetWaveOrderedByCost(wave, ascending: true);
for (int i = 0; i < waveOrderedByCost.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = waveOrderedByCost[i];
if (num2 >= num)
{
break;
}
bool flag2 = IsEntrySourceAlive(pendingWaveEnemy);
if ((flag && IsMauriceProfile(pendingWaveEnemy.Profile)) || (flag2 && ShouldAvoidCloningOrReplacingSource(pendingWaveEnemy.Source)) || (pendingWaveEnemy.Profile.Cost >= 10 && _meter < 300f) || Random.value > typeReplacementChance)
{
continue;
}
EnemySpawnCandidate enemySpawnCandidate = ChooseReplacementCandidate(pendingWaveEnemy.Profile, flag2 ? pendingWaveEnemy.Source : null);
if (enemySpawnCandidate == null || enemySpawnCandidate.BaseCost <= pendingWaveEnemy.Profile.Cost || !CanCandidateReplaceSource(enemySpawnCandidate, pendingWaveEnemy.Profile, flag2 ? pendingWaveEnemy.Source : null))
{
continue;
}
int sourceId = pendingWaveEnemy.SourceId;
if (SpawnReplacement(pendingWaveEnemy, enemySpawnCandidate.Pool, pendingWaveEnemy.Profile, enemySpawnCandidate))
{
replacedIds.Add(sourceId);
num2++;
if (_activeWaveLargeEnemySpawned)
{
num = ApplyLargeEnemyActionPenalty(num);
}
}
}
return num2;
}
private bool IsEligibleSingleFodderReplacementWave(List<PendingWaveEnemy> wave, int level)
{
if (_allowSingleFodderReplacement == null || !_allowSingleFodderReplacement.Value)
{
return false;
}
if (level < 2 || _meter < 100f)
{
return false;
}
if (wave == null || wave.Count != 1 || wave[0] == null)
{
return false;
}
return IsFastFodderKey(wave[0].Profile.Key);
}
private int ApplyLargeEnemyActionPenalty(int cap)
{
if (!_activeWaveLargeEnemySpawned)
{
return cap;
}
int num = Mathf.Max(0, Mathf.RoundToInt((_largeEnemyActionPenalty != null) ? _largeEnemyActionPenalty.Value : 1f));
return Mathf.Max(0, cap - num);
}
private int TryApplyWaveNaturalBuffs(List<PendingWaveEnemy> wave, HashSet<int> replacedIds, int level)
{
if (!_allowNaturalSpawnBuffs.Value)
{
return 0;
}
if (_directorActionMode.Value == DirectorActionMode.AddReinforcementsOnly)
{
return 0;
}
if (level < 2)
{
return 0;
}
int num = 0;
int num2 = Mathf.Clamp(level, 1, 4);
List<PendingWaveEnemy> waveOrderedByCost = GetWaveOrderedByCost(wave, ascending: false);
for (int i = 0; i < waveOrderedByCost.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = waveOrderedByCost[i];
if (num >= num2)
{
break;
}
if (!replacedIds.Contains(pendingWaveEnemy.SourceId) && !((Object)(object)pendingWaveEnemy.Source == (Object)null) && pendingWaveEnemy.Source.activeInHierarchy)
{
bool flag = false;
if (level >= 3 && Random.value < GetNaturalRadiantChance())
{
ApplyRadiantLikeBuff(pendingWaveEnemy.Source);
flag = true;
}
else if (Random.value < GetNaturalEnrageChance())
{
ApplyEnrage(pendingWaveEnemy.Source);
flag = true;
}
if (flag)
{
num++;
}
}
}
return num;
}
private int TryApplyWaveDuplicates(List<PendingWaveEnemy> wave, HashSet<int> replacedIds, int level)
{
if (_meter < _minimumMeterForBonusSpawns.Value)
{
return 0;
}
if (Time.time < _nextAllowedBonusSpawnTime && (level < 2 || !_guaranteeSafeDuplicateAtLevel2.Value))
{
return 0;
}
if (LevelDeathSpawnedMarker.ActiveCount >= _maxActiveBonusEnemies.Value)
{
return 0;
}
int num = ApplyLargeEnemyActionPenalty(GetDuplicateCapForLevel(level, wave.Count));
if (num <= 0)
{
return 0;
}
List<PendingWaveEnemy> list = new List<PendingWaveEnemy>();
for (int i = 0; i < wave.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = wave[i];
if (pendingWaveEnemy != null && (Object)(object)pendingWaveEnemy.Source != (Object)null && pendingWaveEnemy.Source.activeInHierarchy && !replacedIds.Contains(pendingWaveEnemy.SourceId) && !ShouldAvoidCloningOrReplacingSource(pendingWaveEnemy.Source))
{
list.Add(pendingWaveEnemy);
}
}
if (list.Count == 0)
{
return 0;
}
int j = 0;
bool flag = level >= 2 && _guaranteeSafeDuplicateAtLevel2.Value && wave.Count >= 2;
if (flag)
{
j = 1;
}
else if (Random.value < GetWaveDuplicateChance(level))
{
j = 1;
}
for (; j < num && Random.value < GetAdditionalDuplicateChance(level, j); j++)
{
}
int num2 = 0;
int num3 = Mathf.Max(1, GetBonusBudgetForMeter());
int num4 = 0;
while (num2 < j && num4 < j * 4 + 4 && LevelDeathSpawnedMarker.ActiveCount < _maxActiveBonusEnemies.Value)
{
num4++;
PendingWaveEnemy pendingWaveEnemy2 = ChooseLocalDuplicateSource(list, num3, num2 == 0 && flag);
if (pendingWaveEnemy2 == null)
{
break;
}
if (SpawnDuplicateOfNatural(pendingWaveEnemy2.Source, num2))
{
num2++;
RecordEnemySelectionCooldown(pendingWaveEnemy2.Profile.Key, pendingWaveEnemy2.Profile.Cost, pendingWaveEnemy2.Profile.IsLarge);
num3 -= Mathf.Max(1, pendingWaveEnemy2.Profile.Cost);
if (_activeWaveLargeEnemySpawned)
{
j = Mathf.Min(j, ApplyLargeEnemyActionPenalty(num));
}
}
}
if (num2 > 0)
{
_nextAllowedBonusSpawnTime = Time.time + Mathf.Max(0.05f, _minimumSecondsBetweenBonusSpawns.Value);
}
return num2;
}
private PendingWaveEnemy ChooseLocalDuplicateSource(List<PendingWaveEnemy> alive, int remainingBudget, bool safetyDuplicate)
{
if (alive == null || alive.Count == 0)
{
return null;
}
List<WeightedWaveEnemy> list = new List<WeightedWaveEnemy>();
bool flag = !safetyDuplicate && _meter >= _toughPoolStartsAtMeter.Value;
bool flag2 = false;
if (flag)
{
for (int i = 0; i < alive.Count; i++)
{
PendingWaveEnemy pendingWaveEnemy = alive[i];
if (pendingWaveEnemy != null && !ShouldSuppressLowTier(pendingWaveEnemy.Profile.Key))
{
flag2 = true;
break;
}
}
}
for (int j = 0; j < alive.Count; j++)
{
PendingWaveEnemy pendingWaveEnemy2 = alive[j];
if (pendingWaveEnemy2 != null && !((Object)(object)pendingWaveEnemy2.Source == (Object)null) && pendingWaveEnemy2.Source.activeInHierarchy && (safetyDuplicate || pendingWaveEnemy2.Profile.Cost <= remainingBudget) && (!(flag && flag2) || !ShouldSuppressLowTier(pendingWaveEnemy2.Profile.Key)))
{
float num = 1f;
if (pendingWaveEnemy2.Profile.Cost >= 5)
{
num *= 1.25f;
}
if (pendingWaveEnemy2.Profile.Key == "cerberus")
{
num *= 1.35f;
}
num *= GetEnemyVarietyCooldownWeight(pendingWaveEnemy2.Profile.Key, pendingWaveEnemy2.Profile.Cost, pendingWaveEnemy2.Profile.IsLarge);
list.Add(new WeightedWaveEnemy(pendingWaveEnemy2, num));
}
}
if (list.Count == 0 && safetyDuplicate)
{
for (int k = 0; k < alive.Count; k++)
{
PendingWaveEnemy pendingWaveEnemy3 = alive[k];
if (pendingWaveEnemy3 != null && (Object)(object)pendingWaveEnemy3.Source != (Object)null && pendingWaveEnemy3.Source.activeInHierarchy && !ShouldAvoidCloningOrReplacingSource(pendingWaveEnemy3.Source))
{
list.Add(new WeightedWaveEnemy(pendingWaveEnemy3, 1f));
}
}
}
if (list.Count == 0)
{
return null;
}
float num2 = 0f;
for (int l = 0; l < list.Count; l++)
{
num2 += list[l].Weight;
}
float num3 = Random.value * num2;
for (int m = 0; m < list.Count; m++)
{
num3 -= list[m].Weight;
if (num3 <= 0f)
{
return list[m].Enemy;
}
}
return list[list.Count - 1].Enemy;
}
private bool SpawnDuplicateOfNatural(GameObject source, int index)
{
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: 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_0056: Unknown result type (might be due to invalid IL or missing references)
//IL_0057: Unknown result type (might be due to invalid IL or missing references)
//IL_010f: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)source == (Object)null)
{
return false;
}
if (IsHardBlockedTemplateName(((Object)source).name) || !IsTemplateSafeToSpawn(source, string.Empty))
{
return false;
}
try
{
Vector3 val = PickSpawnPosition(source.transform.position, index);
Quaternion rotation = source.transform.rotation;
Transform parent = source.transform.parent;
GameObject val2 = Object.Instantiate<GameObject>(source, val, rotation, parent);
((Object)val2).name = ((Object)source).name + " [Level Death]";
val2.AddComponent<LevelDeathSpawnedMarker>();
SanitizeClonedEnemyRuntimeState(val2);
if (!val2.activeSelf)
{
val2.SetActive(true);
}
if (!IsSpawnedCloneValid(val2, string.Empty))
{
DestroyCloneIfPresent(val2);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death rejected a natural duplicate clone that did not validate: " + ((Object)source).name));
}
return false;
}
TryApplyHighMeterSpawnBuffs(val2);
if (EnemyCatalog.IsLargeKey(EnemyCatalog.GetProfile(string.Empty, ((Object)val2).name, allowUnknown: false).Key))
{
_activeWaveLargeEnemySpawned = true;
}
TrySpawnCerberusPairBonus(val2, source.transform.position, parent, null);
return true;
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Level Death refused a local duplicate because Instantiate failed: " + ex.Message));
return false;
}
}
private void TrySpawnCerberusPairBonus(GameObject spawned, Vector3 sourcePosition, Transform parent, EnemyTemplatePool preferredPool)
{
//IL_0081: Unknown result type (might be due to invalid IL or missing references)
//IL_008b: Unknown result type (might be due to invalid IL or missing references)
//IL_0090: Unknown result type (might be due to invalid IL or missing references)
//IL_0092: Unknown result type (might be due to invalid IL or missing references)
//IL_0099: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)spawned == (Object)null || LevelDeathSpawnedMarker.ActiveCount >= _maxActiveBonusEnemies.Value || Normalize(((Object)spawned).name).IndexOf("cerberus", StringComparison.OrdinalIgnoreCase) < 0 || Random.value > Mathf.Clamp01((_cerberusPairChance != null) ? _cerberusPairChance.Value : 0.55f))
{
return;
}
try
{
GameObject val = spawned;
if (preferredPool != null && preferredPool.HasUsableTemplate)
{
GameObject randomLiveTemplate = preferredPool.GetRandomLiveTemplate();
if ((Object)(object)randomLiveTemplate != (Object)null)
{
val = randomLiveTemplate;
}
}
Vector3 val2 = PickSpawnPosition(sourcePosition, 2 + Random.Range(0, 3));
GameObject val3 = Object.Instantiate<GameObject>(val, val2, spawned.transform.rotation, parent);
((Object)val3).name = ((Object)val).name + " [Level Death Cerberus Pair]";
val3.AddComponent<LevelDeathSpawnedMarker>();
SanitizeClonedEnemyRuntimeState(val3);
if (!val3.activeSelf)
{
val3.SetActive(true);
}
if (!IsSpawnedCloneValid(val3, "cerberus"))
{
DestroyCloneIfPresent(val3);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)"Level Death rejected a paired Cerberus clone that did not validate.");
}
}
else
{
TryApplyHighMeterSpawnBuffs(val3);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)"Level Death spawned a paired Cerberus.");
}
}
}
catch (Exception ex)
{
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death skipped Cerberus pair spawn safely: " + ex.Message));
}
}
}
private int GetReplacementCapForLevel(int level, int waveCount)
{
if (waveCount <= 1)
{
return 0;
}
int num = ((level <= 1) ? _maxReplacementsLevel1.Value : (level switch
{
2 => _maxReplacementsLevel2.Value,
3 => _maxReplacementsLevel3.Value,
_ => _maxReplacementsLevel4.Value,
}));
if (num <= 0)
{
return 0;
}
int num2 = Mathf.Max(2, (_fullDirectorStartsAtWaveSize != null) ? _fullDirectorStartsAtWaveSize.Value : 4);
if (waveCount < num2)
{
num = Mathf.Min(num, Mathf.Max(0, (_mediumWaveMaxReplacements == null) ? 1 : _mediumWaveMaxReplacements.Value));
}
float num3 = Mathf.Clamp(_maxReplacementFractionOfWave.Value, 0.05f, 1f);
int num4 = Mathf.Max(1, Mathf.FloorToInt((float)waveCount * num3));
if (level >= 4 && waveCount >= num2)
{
num4 = Mathf.Max(num4, Mathf.Min(num, Mathf.CeilToInt((float)waveCount * 0.45f)));
}
return Mathf.Clamp(num, 0, num4);
}
private int GetDuplicateCapForLevel(int level, int waveCount)
{
if (waveCount <= 1)
{
return 0;
}
int num = ((level <= 1) ? Mathf.Max(0, _maxDuplicatesLevel1.Value) : (level switch
{
2 => Mathf.Max(0, _maxDuplicatesLevel2.Value),
3 => Mathf.Max(0, _maxDuplicatesLevel3.Value),
_ => Mathf.Max(0, _maxDuplicatesLevel4.Value),
}));
if (num <= 0)
{
return 0;
}
int num2 = Mathf.Max(2, (_fullDirectorStartsAtWaveSize != null) ? _fullDirectorStartsAtWaveSize.Value : 4);
if (waveCount < num2)
{
num = Mathf.Min(num, Mathf.Max(0, (_mediumWaveMaxDuplicates == null) ? 1 : _mediumWaveMaxDuplicates.Value));
}
return num;
}
private float GetWaveDuplicateChance(int level)
{
if (_meter < _minimumMeterForBonusSpawns.Value)
{
return 0f;
}
if (level <= 1)
{
return Mathf.Lerp(0.08f, 0.24f, Mathf.InverseLerp(_minimumMeterForBonusSpawns.Value, 100f, _meter)) * Mathf.Max(0f, _spawnChanceMultiplier.Value);
}
return level switch
{
2 => 0.55f * Mathf.Max(0f, _spawnChanceMultiplier.Value),
3 => 0.7f * Mathf.Max(0f, _spawnChanceMultiplier.Value),
_ => 0.8f * Mathf.Max(0f, _spawnChanceMultiplier.Value),
};
}
private float GetAdditionalDuplicateChance(int level, int alreadyPlanned)
{
if (alreadyPlanned <= 0)
{
return GetWaveDuplicateChance(level);
}
if (level <= 1)
{
return 0f;
}
return level switch
{
2 => 0.28f,
3 => 0.36f,
_ => 0.42f,
};
}
private void ApplyStartingMeterFloor()
{
if (_startingLevelMode != null)
{
float num = 0f;
num = _startingLevelMode.Value switch
{
StartingLevelMode.Enraged => 100f,
StartingLevelMode.Radiant => 200f,
StartingLevelMode.LevelDeath => 300f,
StartingLevelMode.Custom => Mathf.Clamp((_startingMeterPercent != null) ? _startingMeterPercent.Value : 0f, 0f, 400f),
_ => 0f,
};
if (num > _meter)
{
_meter = num;
}
}
}
private string ReadEnemyTypeName(object enemyObject, GameObject gameObject)
{
Type type = enemyObject.GetType();
string text = ReadStringMember(enemyObject, type, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, "FullName") ?? ReadStringMember(enemyObject, type, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, "fullName") ?? ReadStringMember(enemyObject, type, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, "enemyName") ?? ReadStringMember(enemyObject, type, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, "EnemyName");
if (!string.IsNullOrEmpty(text) && !string.Equals(text, "None", StringComparison.OrdinalIgnoreCase) && !string.Equals(text, "Unknown Entity", StringComparison.OrdinalIgnoreCase))
{
return text;
}
FieldInfo fieldInfo = type.GetField("enemyType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("type", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (fieldInfo != null)
{
object value = fieldInfo.GetValue(enemyObject);
if (value != null)
{
string knownEnemyTypeAlias = EnemyCatalog.GetKnownEnemyTypeAlias(value);
if (!string.IsNullOrEmpty(knownEnemyTypeAlias))
{
return knownEnemyTypeAlias + " " + value;
}
return value.ToString();
}
}
PropertyInfo propertyInfo = type.GetProperty("enemyType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetProperty("type", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (propertyInfo != null && propertyInfo.GetIndexParameters().Length == 0)
{
object value2 = propertyInfo.GetValue(enemyObject, null);
if (value2 != null)
{
string knownEnemyTypeAlias2 = EnemyCatalog.GetKnownEnemyTypeAlias(value2);
if (!string.IsNullOrEmpty(knownEnemyTypeAlias2))
{
return knownEnemyTypeAlias2 + " " + value2;
}
return value2.ToString();
}
}
return ((Object)gameObject).name ?? "unknown";
}
private void RegisterLimitedTemplate(string key, GameObject source, int cost)
{
if (!string.IsNullOrEmpty(key) && !((Object)(object)source == (Object)null) && !IsHardBlockedTemplateName(key + " " + ((Object)source).name))
{
if (!_limitedEnemyPools.TryGetValue(key, out var value))
{
value = new EnemyTemplatePool(key, cost, requireLiveTemplate: true);
_limitedEnemyPools[key] = value;
}
value.Cost = cost;
value.IsLarge = EnemyCatalog.IsLargeKey(key);
value.IsFlying = EnemyCatalog.IsFlyingKey(key);
value.Add(source);
}
}
private void RegisterGlobalTemplate(string key, GameObject source, int cost, bool allowInactiveTemplate)
{
if (!string.IsNullOrEmpty(key) && !((Object)(object)source == (Object)null) && !IsHardBlockedTemplateName(key + " " + ((Object)source).name))
{
if (!_globalEnemyPools.TryGetValue(key, out var value))
{
value = new EnemyTemplatePool(key, cost, !allowInactiveTemplate);
_globalEnemyPools[key] = value;
}
value.Cost = cost;
value.IsLarge = EnemyCatalog.IsLargeKey(key);
value.IsFlying = EnemyCatalog.IsFlyingKey(key);
value.RequireLiveTemplate = value.RequireLiveTemplate && !allowInactiveTemplate;
value.Add(source);
}
}
[IteratorStateMachine(typeof(<PrimeGlobalEnemyPoolSoon>d__159))]
private IEnumerator PrimeGlobalEnemyPoolSoon()
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <PrimeGlobalEnemyPoolSoon>d__159(0)
{
<>4__this = this
};
}
private void ScanGlobalEnemyPoolOncePerScene()
{
if (_globalPoolScannedThisScene)
{
return;
}
_globalPoolScannedThisScene = true;
Type enemyIdentifierType = GetEnemyIdentifierType();
if (enemyIdentifierType == null)
{
return;
}
int num = 0;
try
{
Object[] array = Resources.FindObjectsOfTypeAll(enemyIdentifierType);
foreach (Object obj in array)
{
Component val = (Component)(object)((obj is Component) ? obj : null);
if ((Object)(object)val == (Object)null)
{
continue;
}
GameObject gameObject = val.gameObject;
if ((Object)(object)gameObject == (Object)null || (Object)(object)gameObject.GetComponent<LevelDeathSpawnedMarker>() != (Object)null || IsHardBlockedTemplateName(((Object)gameObject).name) || (((Object)gameObject).name ?? string.Empty).IndexOf(" [Level Death]", StringComparison.OrdinalIgnoreCase) >= 0)
{
continue;
}
string text = ReadEnemyTypeName(val, gameObject);
EnemyProfile profile = EnemyCatalog.GetProfile(text, ((Object)gameObject).name, _allowUnknownEnemyTypes.Value);
if (!profile.Allowed)
{
LogUnknownOrBlocked(text, ((Object)gameObject).name, profile.BlockReason);
}
else if (IsTemplateSafeToSpawn(gameObject, profile.Key))
{
int num2 = (_globalEnemyPools.ContainsKey(profile.Key) ? _globalEnemyPools[profile.Key].Count : 0);
bool allowInactiveTemplate = !gameObject.activeInHierarchy;
RegisterGlobalTemplate(profile.Key, gameObject, profile.Cost, allowInactiveTemplate);
if ((_globalEnemyPools.ContainsKey(profile.Key) ? _globalEnemyPools[profile.Key].Count : 0) > num2)
{
num++;
}
}
}
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death global scan added " + num + " template(s). Global enemy types available: " + _globalEnemyPools.Count + "."));
}
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Level Death global enemy scan failed safely: " + ex.GetType().Name + ": " + ex.Message));
}
}
private bool CanSpawnInCurrentScene()
{
if (Time.time - _sceneLoadedTime < _minimumSceneAgeForSpawns.Value)
{
return false;
}
if (!_cyberGrindOnly.Value)
{
return true;
}
string text = Normalize(_currentSceneName);
if (!text.Contains("cyber") && !text.Contains("endless"))
{
return text.Contains("grid");
}
return true;
}
private float GetNaturalEnrageChance()
{
if (_meter < 100f)
{
return 0f;
}
if (_meter < 200f)
{
float num = Mathf.InverseLerp(100f, 200f, _meter);
return Mathf.Lerp(Mathf.Clamp01(_naturalEnrageChanceLevel2.Value), Mathf.Clamp01(_naturalEnrageChanceLevel2.Value) * 1.35f, num);
}
if (_meter < 300f)
{
return Mathf.Clamp01(_naturalEnrageChanceLevel2.Value * 0.85f);
}
return Mathf.Clamp01(_naturalEnrageChanceLevel2.Value * 1.1f);
}
private float GetNaturalRadiantChance()
{
if (_meter < 200f)
{
return 0f;
}
if (_meter < 300f)
{
float num = Mathf.InverseLerp(200f, 400f, _meter);
return Mathf.Lerp(Mathf.Clamp01(_naturalRadiantChanceLevel3.Value), Mathf.Clamp01(_naturalRadiantChanceLevel3.Value) * 1.35f, num);
}
return Mathf.Clamp01(_naturalRadiantChanceLevel3.Value * 1.65f);
}
private float GetTypeReplacementChance()
{
if (_directorActionMode.Value == DirectorActionMode.AddReinforcementsOnly)
{
return 0f;
}
if (_meter < 100f)
{
return 0f;
}
float num = ((_meter < 200f) ? Mathf.Lerp(0.08f, Mathf.Clamp01(_replacementChanceLevel2.Value), Mathf.InverseLerp(100f, 200f, _meter)) : ((!(_meter < 300f)) ? Mathf.Lerp(Mathf.Clamp01(_replacementChanceLevel3.Value), Mathf.Clamp01(_replacementChanceLevel4.Value), Mathf.InverseLerp(300f, 400f, _meter)) : Mathf.Lerp(Mathf.Clamp01(_replacementChanceLevel2.Value), Mathf.Clamp01(_replacementChanceLevel3.Value), Mathf.InverseLerp(200f, 300f, _meter))));
if (_directorActionMode.Value == DirectorActionMode.PreferUpgrades)
{
num *= 1.2f;
}
return Mathf.Clamp01(num);
}
private EnemySpawnCandidate ChooseReplacementCandidate(EnemyProfile sourceProfile)
{
return ChooseReplacementCandidate(sourceProfile, null);
}
private EnemySpawnCandidate ChooseReplacementCandidate(EnemyProfile sourceProfile, GameObject source)
{
CleanupPools();
int pointCapForSource = GetPointCapForSource(sourceProfile.Cost);
int num = Mathf.Max(sourceProfile.Cost + 1, 3);
int num2 = Mathf.Max(num, pointCapForSource);
List<EnemySpawnCandidate> list = new List<EnemySpawnCandidate>();
HashSet<string> hashSet = new HashSet<string>(_limitedEnemyPools.Keys);
foreach (EnemyTemplatePool value in _globalEnemyPools.Values)
{
if (!value.HasUsableTemplate || value.Cost < num || (hashSet.Contains(value.Key) && value.Cost <= sourceProfile.Cost) || ShouldSuppressLowTier(value.Key))
{
continue;
}
int num3 = (hashSet.Contains(value.Key) ? value.Cost : GetGlobalEffectiveCost(value.Cost));
if (num3 <= num2)
{
EnemySpawnCandidate enemySpawnCandidate = new EnemySpawnCandidate(value, num3, !hashSet.Contains(value.Key));
if (CanCandidateReplaceSource(enemySpawnCandidate, sourceProfile, source))
{
list.Add(enemySpawnCandidate);
}
}
}
if (list.Count == 0)
{
return null;
}
List<WeightedCandidate> list2 = new List<WeightedCandidate>();
foreach (EnemySpawnCandidate item in list)
{
float num4 = Mathf.Max(1f, (float)(item.BaseCost - sourceProfile.Cost));
float num5 = 0.75f + num4 * 0.22f;
if (item.BaseCost >= 8 && _meter < 200f)
{
num5 *= 0.35f;
}
if (item.BaseCost >= 9 && _meter < 260f)
{
num5 *= 0.45f;
}
if (item.BaseCost >= 10 && _meter < 320f)
{
num5 *= 0.35f;
}
if (item.IsGlobalOnly)
{
num5 *= 0.9f;
}
if (item.IsLarge)
{
num5 *= Mathf.Clamp((_largeEnemyRarityMultiplier != null) ? _largeEnemyRarityMultiplier.Value : 0.5f, 0.05f, 1f);
}
num5 *= GetEnemyVarietyCooldownWeight(item.Pool.Key, item.BaseCost, item.IsLarge);
list2.Add(new WeightedCandidate(item, num5));
}
return PickWeightedCandidate(list2);
}
private float GetEnemyVarietyCooldownWeight(string key, int cost, bool isLarge)
{
if (_enableEnemyVarietyCooldown == null || !_enableEnemyVarietyCooldown.Value)
{
return 1f;
}
if (((_enemyVarietyCooldownStrength != null) ? _enemyVarietyCooldownStrength.Value : 1f) <= 0f)
{
return 1f;
}
if (cost <= 6)
{
return 1f;
}
key = Normalize(key);
if (!_enemyCooldowns.TryGetValue(key, out var value))
{
return 1f;
}
DecayEnemyCooldown(value);
if (value.Heat <= 0.01f)
{
return 1f;
}
float num = Mathf.Max(0f, (_enemyVarietyCooldownStrength != null) ? _enemyVarietyCooldownStrength.Value : 1f);
float num2 = 1f + (float)Mathf.Max(0, cost - 6) * 0.35f;
if (isLarge)
{
num2 += Mathf.Max(0f, (_largeEnemyCooldownBonus != null) ? _largeEnemyCooldownBonus.Value : 3.5f) * 0.35f;
}
return Mathf.Clamp(1f / (1f + value.Heat * num2 * num), 0.04f, 1f);
}
private void RecordEnemySelectionCooldown(string key, int cost, bool isLarge)
{
if (_enableEnemyVarietyCooldown == null || !_enableEnemyVarietyCooldown.Value || cost <= 6)
{
return;
}
key = Normalize(key);
if (!string.IsNullOrEmpty(key))
{
if (!_enemyCooldowns.TryGetValue(key, out var value))
{
value = new EnemyCooldownState();
value.LastUpdatedTime = Time.time;
_enemyCooldowns[key] = value;
}
DecayEnemyCooldown(value);
float num = Mathf.Max(0.5f, (float)(cost - 6) * 0.85f);
if (isLarge)
{
num += Mathf.Max(0f, (_largeEnemyCooldownBonus != null) ? _largeEnemyCooldownBonus.Value : 3.5f);
}
value.Heat = Mathf.Clamp(value.Heat + num, 0f, 20f);
value.LastUpdatedTime = Time.time;
}
}
private void DecayEnemyCooldown(EnemyCooldownState state)
{
if (state != null)
{
float time = Time.time;
float num = Mathf.Max(0f, time - state.LastUpdatedTime);
float num2 = Mathf.Max(1f, (_enemyVarietyCooldownDecaySeconds != null) ? _enemyVarietyCooldownDecaySeconds.Value : 22f);
if (num > 0f)
{
state.Heat *= Mathf.Exp((0f - num) / num2);
state.LastUpdatedTime = time;
}
}
}
private EnemySpawnCandidate PickWeightedCandidate(List<WeightedCandidate> weighted)
{
if (weighted == null || weighted.Count == 0)
{
return null;
}
float num = 0f;
for (int i = 0; i < weighted.Count; i++)
{
num += weighted[i].Weight;
}
float num2 = Random.value * num;
for (int j = 0; j < weighted.Count; j++)
{
num2 -= weighted[j].Weight;
if (num2 <= 0f)
{
return weighted[j].Candidate;
}
}
return weighted[weighted.Count - 1].Candidate;
}
private bool CanCandidateReplaceSource(EnemySpawnCandidate candidate, EnemyProfile sourceProfile, GameObject source)
{
//IL_0072: Unknown result type (might be due to invalid IL or missing references)
if (candidate == null || candidate.Pool == null)
{
return false;
}
if (!candidate.IsLarge)
{
return true;
}
if (_enableLargeEnemyPool == null || !_enableLargeEnemyPool.Value)
{
return false;
}
if (sourceProfile.IsFlying)
{
return false;
}
if ((Object)(object)source == (Object)null)
{
return false;
}
int num = Mathf.Max(1, (_largeEnemyMinNonFilthWaveSize != null) ? _largeEnemyMinNonFilthWaveSize.Value : 5);
if (_activeWaveNonFilthEnemyCount < num)
{
return false;
}
if (!HasGroundBelow(source.transform.position))
{
return false;
}
return true;
}
private bool HasGroundBelow(Vector3 position)
{
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
//IL_0030: Unknown result type (might be due to invalid IL or missing references)
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: Unknown result type (might be due to invalid IL or missing references)
float num = Mathf.Max(1f, (_largeEnemyGroundCheckDistance != null) ? _largeEnemyGroundCheckDistance.Value : 8f);
RaycastHit val = default(RaycastHit);
return Physics.Raycast(position + Vector3.up * 1.25f, Vector3.down, ref val, num + 1.25f, -1, (QueryTriggerInteraction)1);
}
private bool SpawnReplacement(PendingWaveEnemy entry, EnemyTemplatePool pool, EnemyProfile sourceProfile, EnemySpawnCandidate candidate)
{
//IL_0054: Unknown result type (might be due to invalid IL or missing references)
//IL_0059: Unknown result type (might be due to invalid IL or missing references)
//IL_0047: Unknown result type (might be due to invalid IL or missing references)
//IL_003f: Unknown result type (might be due to invalid IL or missing references)
//IL_0099: Unknown result type (might be due to invalid IL or missing references)
//IL_009e: Unknown result type (might be due to invalid IL or missing references)
//IL_008c: Unknown result type (might be due to invalid IL or missing references)
//IL_0084: Unknown result type (might be due to invalid IL or missing references)
//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
//IL_0176: Unknown result type (might be due to invalid IL or missing references)
GameObject val = entry?.Source;
bool flag = IsEntrySourceAlive(entry);
GameObject randomLiveTemplate = pool.GetRandomLiveTemplate();
if ((Object)(object)randomLiveTemplate == (Object)null)
{
return false;
}
if (!IsTemplateSafeToSpawn(randomLiveTemplate, pool.Key))
{
return false;
}
try
{
Vector3 val2 = (flag ? val.transform.position : (entry?.Position ?? Vector3.zero));
if (candidate != null && candidate.IsLarge && (!flag || !CanCandidateReplaceSource(candidate, sourceProfile, val)))
{
return false;
}
Quaternion val3 = (flag ? val.transform.rotation : (entry?.Rotation ?? Quaternion.identity));
Transform val4 = (flag ? val.transform.parent : entry?.Parent);
GameObject val5 = Object.Instantiate<GameObject>(randomLiveTemplate, val2, val3, val4);
((Object)val5).name = ((Object)randomLiveTemplate).name + " [Level Death Replacement]";
val5.AddComponent<LevelDeathSpawnedMarker>();
SanitizeClonedEnemyRuntimeState(val5);
if (!val5.activeSelf)
{
val5.SetActive(true);
}
if (!IsSpawnedCloneValid(val5, pool.Key))
{
DestroyCloneIfPresent(val5);
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death rejected a replacement clone that did not validate: " + pool.Key));
}
return false;
}
TryApplyHighMeterSpawnBuffs(val5);
RecordEnemySelectionCooldown(pool.Key, pool.Cost, pool.IsLarge);
TrySpawnCerberusPairBonus(val5, val2, val4, (candidate != null) ? candidate.Pool : pool);
if (candidate != null && candidate.IsLarge)
{
_activeWaveLargeEnemySpawned = true;
}
if (flag)
{
((MonoBehaviour)this).StartCoroutine(DestroyOriginalAfterReplacementValid(val, val5, pool.Key));
}
_nextAllowedBonusSpawnTime = Time.time + Mathf.Max(0.1f, _minimumSecondsBetweenBonusSpawns.Value * 0.75f);
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death replaced natural " + sourceProfile.Key + " with " + pool.Key + " at " + Mathf.RoundToInt(_meter) + "%, effectiveCost=" + (candidate?.EffectiveCost ?? pool.Cost) + "."));
}
return true;
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Level Death skipped a type replacement safely: " + ex.GetType().Name + ": " + ex.Message));
return false;
}
}
private int GetPointCapForSource(int baseCost)
{
return Mathf.Max(baseCost, baseCost + GetBonusBudgetForMeter());
}
private int GetBonusBudgetForMeter()
{
float num = Mathf.Clamp(_meter, 0f, 400f);
int num2 = Mathf.Max(1, _maxBonusBudget.Value);
int num3 = Mathf.Max(num2, Mathf.RoundToInt((float)num2 * Mathf.Max(1f, _deathMarkBudgetMultiplier.Value)));
int num4 = Mathf.Max(num3, _maxOvercapBonusBudget.Value);
float num5 = Mathf.InverseLerp(_minimumMeterForBonusSpawns.Value, 100f, Mathf.Min(num, 100f));
if (num < 100f)
{
return Mathf.RoundToInt(Mathf.Lerp(0f, (float)num2, num5));
}
float num6 = Mathf.InverseLerp(100f, 400f, num);
return Mathf.RoundToInt(Mathf.Lerp((float)num3, (float)num4, Mathf.Pow(Mathf.Clamp01(num6), 0.82f)));
}
private bool ShouldSuppressLowTier(string key)
{
return false;
}
private int GetGlobalEffectiveCost(int baseCost)
{
float num = Mathf.Max(1f, _globalEnemyCostMultiplier.Value);
int num2 = Mathf.Max(0, _globalEnemyFlatSurcharge.Value);
return Mathf.Max(baseCost + 1, Mathf.CeilToInt((float)baseCost * num) + num2);
}
private bool ShouldAvoidCloningOrReplacingSource(GameObject source)
{
if ((Object)(object)source == (Object)null || _sanitizeInheritedIdolState == null || !_sanitizeInheritedIdolState.Value)
{
return false;
}
try
{
foreach (Component item in FindEnemyIdentifierComponents(source))
{
if ((Object)(object)item == (Object)null)
{
continue;
}
Type type = ((object)item).GetType();
if (ReadBoolMember(type, item, "blessed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death skipped an Idol-blessed enemy as a clone/replacement source: " + ((Object)source).name));
}
return true;
}
}
}
catch (Exception ex)
{
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death could not inspect Idol/blessed state on " + ((Object)source).name + ": " + ex.Message));
}
}
return false;
}
private void SanitizeClonedEnemyRuntimeState(GameObject clone)
{
if ((Object)(object)clone == (Object)null || _sanitizeInheritedIdolState == null || !_sanitizeInheritedIdolState.Value)
{
return;
}
try
{
bool flag = false;
foreach (Component item in FindEnemyIdentifierComponents(clone))
{
if (!((Object)(object)item == (Object)null))
{
Type type = ((object)item).GetType();
if (ReadBoolMember(type, item, "blessed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
SetBoolMember(type, item, "blessed", value: false, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
flag = true;
}
InvokeMethodIfExists(type, item, "UpdateBuffs", null);
InvokeMethodIfExists(type, item, "UpdateModifiers", null);
}
}
if (flag && _debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death cleared inherited Idol/blessed state from clone: " + ((Object)clone).name));
}
}
catch (Exception ex)
{
if (_debugLogging != null && _debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death could not sanitize clone runtime state on " + ((Object)clone).name + ": " + ex.Message));
}
}
}
[IteratorStateMachine(typeof(<FindEnemyIdentifierComponents>d__180))]
private IEnumerable<Component> FindEnemyIdentifierComponents(GameObject gameObject)
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <FindEnemyIdentifierComponents>d__180(-2)
{
<>4__this = this,
<>3__gameObject = gameObject
};
}
private void TryApplyHighMeterSpawnBuffs(GameObject clone)
{
if (!_allowHighMeterEnemyBuffs.Value || (Object)(object)clone == (Object)null || _meter < 100f)
{
return;
}
try
{
bool flag = RollRadiant();
bool flag2 = RollEnrage();
if (flag)
{
ApplyRadiantLikeBuff(clone);
}
if (flag2)
{
ApplyEnrage(clone);
}
if (_debugLogging.Value && (flag || flag2))
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Level Death buffed reinforcement " + ((Object)clone).name + " radiant=" + flag + " enraged=" + flag2 + " meter=" + Mathf.RoundToInt(_meter) + "%."));
}
}
catch (Exception ex)
{
if (_debugLogging.Value)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Level Death skipped high-meter spawn buff safely: " + ex.GetType().Name + ": " + ex.Message));
}
}
}
private bool RollEnrage()
{
if (_meter < 100f)
{
return false;
}
float num = Mathf.InverseLerp(100f, 400f, _meter);
float num2 = Mathf.Lerp(Mathf.Clamp01(_enrageChanceAt300.Value), Mathf.Clamp01(_enrageChanceAt1000.Value), Mathf.Sqrt(Mathf.Clamp01(num)));
return Random.value < num2;
}
private bool RollRadiant()
{
if (_meter < 200f)
{
return false;
}
float num = Mathf.InverseLerp(200f, 400f, _meter);
float num2 = Mathf.Lerp(Mathf.Clamp01(_radiantChanceAt400.Value), Mathf.Clamp01(_radiantChanceAt1000.Value), Mathf.Sqrt(Mathf.Clamp01(num)));
return Random.value < num2;
}
private void ApplyRadiantLikeBuff(GameObject clone)
{
Component val = FindEnemyIdentifierComponent(clone);
if (!((Object)(object)val == (Object)null))
{
Type type = ((object)val).GetType();
float num = Mathf.Clamp(_radianceTier.Value, 1f, 3f);
SetFloatMember(type, val, "radianceTier", num, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
InvokeMethodIfExists(type, val, "SpeedBuff", new object[1] { num });
InvokeMethodIfExists(type, val, "HealthBuff", new object[1] { num });
InvokeMethodIfExists(type, val, "DamageBuff", null);
InvokeMethodIfExists(type, val, "UpdateBuffs", null);
InvokeMethodIfExists(type, val, "UpdateModifiers", null);
InvokeMethodIfExists(type, val, "ForceGetHealth", null);
}
}
private void ApplyEnrage(GameObject clone)
{
Component[] componentsInChildren = clone.GetComponentsInChildren<Component>(true);
foreach (Component val in componentsInChildren)
{
if (!((Object)(object)val == (Object)null))
{
Type type = ((object)val).GetType();
if (CanEnrage(type) && !ReadBoolMember(type, val, "isEnraged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
InvokeMethodIfExists(type, val, "Enrage", null);
}
}
}
}
private bool CanEnrage(Type type)
{
if (type == null)
{
return false;
}
if (AccessTools.Method(type, "Enrage", (Type[])null, (Type[])null) == null)
{
return false;
}
if (type.Name.IndexOf("enrage", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
Type[] interfaces = type.GetInterfaces();
foreach (Type type2 in interfaces)
{
if (type2.Name.IndexOf("IEnrage", StringComparison.OrdinalIgnoreCase) >= 0 || type2.Name.IndexOf("Enrage", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private Component FindEnemyIdentifierComponent(GameObject gameObject)
{
if ((Object)(object)gameObject == (Object)null)
{
return null;
}
Type enemyIdentifierType = GetEnemyIdentifierType();
if (enemyIdentifierType == null)
{
return null;
}
return gameObject.GetComponent(enemyIdentifierType) ?? gameObject.GetComponentInChildren(enemyIdentifierType, true);
}
private void SetFloatMember(Type type, object instance, string memberName, float value, BindingFlags flags)
{
FieldInfo field = type.GetField(memberName, flags);
if (field != null && field.FieldType == typeof(float))
{
field.SetValue(instance, value);
return;
}
PropertyInfo property = type.GetProperty(memberName, flags);
if (property != null && property.PropertyType == typeof(float) && property.CanWrite)
{
property.SetValue(instance, value, null);
}
}
private string ReadStringMember(object instance, Type type, BindingFlags flags, string memberName)
{
if (instance == null || type == null || string.IsNullOrEmpty(memberName))
{
return null;
}
try
{
FieldInfo field = type.GetField(memberName, flags);
if (field != null)
{
object value = field.GetValue(instance);
if (value != null)
{
return value.ToString();
}
}
PropertyInfo property = type.GetProperty(memberName, flags);
if (property != null && property.GetIndexParameters().Length == 0 && property.CanRead)
{
object value2 = property.GetValue(instance, null);
if (value2 != null)
{
return value2.ToString();
}
}
}
catch
{
}
return null;
}
private bool ReadBoolMember(Type type, object instance, string memberName, BindingFlags flags)
{
FieldInfo field = type.GetField(memberName, flags);
if (field != null && field.FieldType == typeof(bool))
{
return (bool)field.GetValue(instance);
}
PropertyInfo property = type.GetProperty(memberName, flags);
if (property != null && property.PropertyType == typeof(bool) && property.CanRead)
{
return (bool)property.GetValue(instance, null);
}
return false;
}
private void SetBoolMember(Type type, object instance, string memberName, bool value, BindingFlags flags)
{
FieldInfo field = type.GetField(memberName, flags);
if (field != null && field.FieldType == typeof(bool))
{
field.SetValue(instance, value);
return;
}
PropertyInfo property = type.GetProperty(memberName, flags);
if (property != null && property.PropertyType == typeof(bool) && property.CanWrite)
{
property.SetValue(instance, value, null);
}
}
private void InvokeMethodIfExists(Type type, object instance, string methodName, object[] args)
{
MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null);
if (!(methodInfo == null))
{
ParameterInfo[] parameters = methodInfo.GetParameters();
object[] parameters2 = args;
if (parameters.Length == 0)
{
parameters2 = null;
}
else if (args == null || args.Length != parameters.Length)
{
return;
}
methodInfo.Invoke(instance, parameters2);
}
}
private Vector3 PickSpawnPosition(Vector3 sourcePosition, int index)
{
//IL_0027: Unknown result type (might be due to invalid IL or missing references)
//IL_002c: Unknown result type (might be due to invalid IL or missing references)
//IL_0030: Unknown result type (might be due to invalid IL or missing references)
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_0065: Unknown result type (might be due to invalid IL or missing references)
//IL_0066: Unknown result type (might be due to invalid IL or missing references)
//IL_0073: Unknown result type (mig