diff --git a/LCLandmineOutside/Abstract/AbstractCompatibilityHandler.cs b/LCLandmineOutside/Abstract/AbstractCompatibilityHandler.cs index 4f9bbd5..daabdca 100644 --- a/LCLandmineOutside/Abstract/AbstractCompatibilityHandler.cs +++ b/LCLandmineOutside/Abstract/AbstractCompatibilityHandler.cs @@ -1,16 +1,33 @@ -namespace LCHazardsOutside.Abstract +using System; + +namespace LCHazardsOutside.Abstract { internal abstract class AbstractCompatibilityHandler { - public abstract void Apply(); + protected abstract void DoApply(); - public abstract string GetModGUID(); + protected abstract string GetModGUID(); public bool IsEnabled() { return BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey(GetModGUID()); } + public void Apply() + { + if (IsEnabled()) + { + try + { + DoApply(); + } catch (Exception e) + { + Plugin.GetLogger().LogError($"There was an error in patching {GetModGUID()}. Skipping... \n {e}\n"); + } + + } + } + public void LogApply() { Plugin.GetLogger().LogInfo($"Applying compatibility fixes for {GetModGUID()}."); diff --git a/LCLandmineOutside/Abstract/SpawnStrategy.cs b/LCLandmineOutside/Abstract/SpawnStrategy.cs index 8840c54..c99e062 100644 --- a/LCLandmineOutside/Abstract/SpawnStrategy.cs +++ b/LCLandmineOutside/Abstract/SpawnStrategy.cs @@ -1,12 +1,21 @@ -using System.Collections.Generic; +using LCHazardsOutside.Data; +using System.Collections.Generic; using UnityEngine; namespace LCHazardsOutside.Abstract { - internal abstract class SpawnStrategy + public abstract class SpawnStrategy { - public abstract void CalculateCenterPosition(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier, out Vector3 centerPosition, out float spawnRadius); + public abstract List CalculateCenterPositions(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier); - public abstract (Vector3, Quaternion) GetRandomGroundPositionAndRotation(Vector3 centerPoint, float radius = 10f, System.Random randomSeed = null, int layerMask = -1, int maxAttempts = 10); + protected SpawnPositionData CalculateCenterWithSpawnRadius(Vector3 shipLandPosition, Vector3 targetPosition, float spawnRadiusMultiplier) + { + float spawnRadius; + Vector3 centerPosition = (shipLandPosition + targetPosition) / 2; + spawnRadius = Vector3.Distance(targetPosition, centerPosition) * spawnRadiusMultiplier; + centerPosition.y = Mathf.Max(shipLandPosition.y, targetPosition.y); + + return new SpawnPositionData(centerPosition, spawnRadius); + } } } diff --git a/LCLandmineOutside/Data/EntranceContainer.cs b/LCLandmineOutside/Data/EntranceContainer.cs new file mode 100644 index 0000000..a13026d --- /dev/null +++ b/LCLandmineOutside/Data/EntranceContainer.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace LCHazardsOutside.Data +{ + public class EntranceContainer(Vector3 mainEntrancePosition, List fireExitPositions) + { + public Vector3 MainEntrancePosition { get; set; } = mainEntrancePosition; + public List FireExitPositions { get; set; } = fireExitPositions; + + public bool IsInitialized() + { + return MainEntrancePosition != Vector3.zero && FireExitPositions.All(x => x != Vector3.zero); + } + } +} diff --git a/LCLandmineOutside/HazardCalculationContainer.cs b/LCLandmineOutside/Data/HazardCalculationContainer.cs similarity index 80% rename from LCLandmineOutside/HazardCalculationContainer.cs rename to LCLandmineOutside/Data/HazardCalculationContainer.cs index 3223207..6c22c10 100644 --- a/LCLandmineOutside/HazardCalculationContainer.cs +++ b/LCLandmineOutside/Data/HazardCalculationContainer.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using UnityEngine; -namespace LCHazardsOutside +namespace LCHazardsOutside.Data { - internal class HazardCalculationContainer(System.Random random, List spawnDenialPoints, SpawnableMapObject spawnableMapObject, int minSpawnRate, int maxSpawnRate) + internal class HazardCalculationContainer(System.Random random, List spawnDenialPoints, SpawnableMapObject spawnableMapObject, int minSpawnRate, int maxSpawnRate, int layerMask) { public System.Random Random { get; set; } = random; public List SpawnDenialPoints { get; set; } = spawnDenialPoints; @@ -12,6 +12,7 @@ internal class HazardCalculationContainer(System.Random random, List public int MaxSpawnRate { get; set; } = maxSpawnRate; public bool NeedsSafetyZone { get; set; } = false; public float SpawnRatioMultiplier { get; set; } = 1.5f; + public int LayerMask { get; set; } = layerMask; -} + } } diff --git a/LCLandmineOutside/Data/HazardConfiguration.cs b/LCLandmineOutside/Data/HazardConfiguration.cs new file mode 100644 index 0000000..ae527db --- /dev/null +++ b/LCLandmineOutside/Data/HazardConfiguration.cs @@ -0,0 +1,14 @@ +using LCHazardsOutside.Abstract; +using System.Collections.Generic; + +namespace LCHazardsOutside.Data +{ + public class HazardConfiguration(bool enabled, int minSpawnRate, int maxSpawnRate, Dictionary moonMap, SpawnStrategy spawnStrategy) + { + public bool Enabled { get; set; } = enabled; + public int MinSpawnRate { get; set; } = minSpawnRate; + public int MaxSpawnRate { get; set; } = maxSpawnRate; + public Dictionary MoonMap { get; set; } = moonMap; + public SpawnStrategy SpawnStrategy { get; set; } = spawnStrategy; + } +} diff --git a/LCLandmineOutside/Data/HazardType.cs b/LCLandmineOutside/Data/HazardType.cs new file mode 100644 index 0000000..e8fb89c --- /dev/null +++ b/LCLandmineOutside/Data/HazardType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LCHazardsOutside.Data +{ + public enum HazardType + { + Landmine, + Turret, + SpikeRoofTrap, + CustomHazard + } +} diff --git a/LCLandmineOutside/Data/MoonMinMax.cs b/LCLandmineOutside/Data/MoonMinMax.cs new file mode 100644 index 0000000..bf3a2c1 --- /dev/null +++ b/LCLandmineOutside/Data/MoonMinMax.cs @@ -0,0 +1,8 @@ +namespace LCHazardsOutside.Data +{ + public class MoonMinMax(int min, int max) + { + public int Min { get; set; } = min; + public int Max { get; set; } = max; + } +} diff --git a/LCLandmineOutside/Data/SpawnPositionData.cs b/LCLandmineOutside/Data/SpawnPositionData.cs new file mode 100644 index 0000000..b688bf3 --- /dev/null +++ b/LCLandmineOutside/Data/SpawnPositionData.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace LCHazardsOutside.Data +{ + public record struct SpawnPositionData + { + public SpawnPositionData(Vector3 centerPosition, float spawnRadius) : this() + { + CenterPosition = centerPosition; + SpawnRadius = spawnRadius; + } + + public Vector3 CenterPosition { get; set; } + public float SpawnRadius{ get; set; } + } +} diff --git a/LCLandmineOutside/Data/SpawnStrategyType.cs b/LCLandmineOutside/Data/SpawnStrategyType.cs new file mode 100644 index 0000000..bae6734 --- /dev/null +++ b/LCLandmineOutside/Data/SpawnStrategyType.cs @@ -0,0 +1,9 @@ +namespace LCHazardsOutside.Data +{ + public enum SpawnStrategyType + { + MainAndFireExit, + MainEntranceOnly, + FireExitsOnly + } +} diff --git a/LCLandmineOutside/VanillaMoon.cs b/LCLandmineOutside/Data/VanillaMoon.cs similarity index 56% rename from LCLandmineOutside/VanillaMoon.cs rename to LCLandmineOutside/Data/VanillaMoon.cs index ce23042..fed8be1 100644 --- a/LCLandmineOutside/VanillaMoon.cs +++ b/LCLandmineOutside/Data/VanillaMoon.cs @@ -1,4 +1,4 @@ -namespace LCHazardsOutside +namespace LCHazardsOutside.Data { internal enum VanillaMoon { @@ -9,6 +9,10 @@ internal enum VanillaMoon march, rend, dine, - titan + titan, + adamance, + embrion, + artifice, + liquidation } } diff --git a/LCLandmineOutside/DefaultSpawnStrategy.cs b/LCLandmineOutside/DefaultSpawnStrategy.cs deleted file mode 100644 index a82b47c..0000000 --- a/LCLandmineOutside/DefaultSpawnStrategy.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.AI; - -namespace LCHazardsOutside -{ - internal class DefaultSpawnStrategy : Abstract.SpawnStrategy - { - - private static float RandomNumberInRadius(float radius, System.Random randomSeed) - { - return ((float)randomSeed.NextDouble() - 0.5f) * radius; - } - public override (Vector3, Quaternion) GetRandomGroundPositionAndRotation(Vector3 centerPoint, float radius = 10f, System.Random randomSeed = null, int layerMask = -1, int maxAttempts = 10) - { - float y = centerPoint.y; - float x, y2, z; - - Vector3 randomPosition; - - for (int i = 0; i < maxAttempts; i++) - { - x = RandomNumberInRadius(radius, randomSeed); - y2 = RandomNumberInRadius(radius, randomSeed); - z = RandomNumberInRadius(radius, randomSeed); - randomPosition = centerPoint + new Vector3(x, y2, z); - randomPosition.y = y; - - float maxDistance = Vector3.Distance(centerPoint, randomPosition) + 30f; - - if (NavMesh.SamplePosition(randomPosition, out NavMeshHit navMeshHit, maxDistance, -1)) - { - if (Physics.Raycast(navMeshHit.position + Vector3.up, Vector3.down, out RaycastHit groundHit, 50f, layerMask)) - { - return (groundHit.point + Vector3.up * 0.1f, Quaternion.FromToRotation(Vector3.up, groundHit.normal)); - } else - { - Plugin.GetLogger().LogDebug($"Nav hit at: {navMeshHit.position} but ray cast failed."); - } - } - } - - return (Vector3.zero, Quaternion.identity); - } - public override void CalculateCenterPosition(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier, out Vector3 centerPosition, out float spawnRadius) - { - centerPosition = (shipLandPosition + mainEntrancePosition) / 2; - spawnRadius = Vector3.Distance(mainEntrancePosition, centerPosition) * spawnRadiusMultiplier; - centerPosition.y = Mathf.Max(shipLandPosition.y, mainEntrancePosition.y); - } - } -} diff --git a/LCLandmineOutside/LCHazardsOutside.csproj b/LCLandmineOutside/LCHazardsOutside.csproj index 344d28d..391cf21 100644 --- a/LCLandmineOutside/LCHazardsOutside.csproj +++ b/LCLandmineOutside/LCHazardsOutside.csproj @@ -1,40 +1,17 @@  - + netstandard2.1 - Debug - AnyCPU - {37D46F81-33BF-4D3F-ADD0-6F1534843895} Library Properties LCHazardsOutside LCHazardsOutside - 512 true true latest - - true - full - true - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - false - - + + E:\SteamLibrary\steamapps\common\Lethal Company\Lethal Company_Data\Managed\Assembly-CSharp.dll @@ -50,8 +27,4 @@ - - - - \ No newline at end of file diff --git a/LCLandmineOutside/LCUtils.cs b/LCLandmineOutside/LCUtils.cs index fc5fa43..aa0bd62 100644 --- a/LCLandmineOutside/LCUtils.cs +++ b/LCLandmineOutside/LCUtils.cs @@ -1,12 +1,58 @@ using System; using System.Linq; +using UnityEngine.AI; +using UnityEngine; +using System.Collections.Generic; +using LCHazardsOutside.Data; +using LCHazardsOutside.Abstract; +using LCHazardsOutside.Strategy; +using System.Reflection; +using HarmonyLib; namespace LCHazardsOutside { public class LCUtils { + + public static readonly Dictionary CUSTOM_LAYER_MASK = new() + { + { VanillaMoon.march.ToString(), ["Room"] }, // March's water is on the Default layer instead of the Water layer.. + }; + + public static readonly Dictionary HAZARD_MAP = new(3) + { + { "Landmine", HazardType.Landmine }, + { "TurretContainer", HazardType.Turret }, + { "SpikeRoofTrapHazard", HazardType.SpikeRoofTrap } + }; + + private static readonly Dictionary STRATEGY_MAP = new(3) + { + { SpawnStrategyType.MainAndFireExit, MainAndFireExitSpawnStrategy.GetInstance() }, + { SpawnStrategyType.MainEntranceOnly, MainEntranceOnlySpawnStrategy.GetInstance() }, + { SpawnStrategyType.FireExitsOnly, FireExitsOnlySpawnStrategy.GetInstance() } + }; + + public static SpawnStrategy GetSpawnStrategy(string typeString) + { + try + { + SpawnStrategyType strategyType = (SpawnStrategyType)Enum.Parse(typeof(SpawnStrategyType), typeString); + + if (STRATEGY_MAP.TryGetValue(strategyType, out SpawnStrategy result)) + { + return result; + } + } catch (Exception) + { + Plugin.GetLogger().LogError($"Type {typeString} could not be parsed into a SpawnStrategyType. Reverting to default..."); + } + + return MainAndFireExitSpawnStrategy.GetInstance(); + } + // Same as LethalLevelLoader so people get used to the same planet names. - public static string GetNumberlessPlanetName(SelectableLevel selectableLevel) + public static string GetNumberlessMoonName(SelectableLevel selectableLevel) { if (selectableLevel != null) { @@ -25,7 +71,129 @@ public static bool IsVanillaMoon(string moonName) public static bool IsVanillaMoon(SelectableLevel selectableLevel) { - return Enum.TryParse(typeof(VanillaMoon), GetNumberlessPlanetName(selectableLevel), true, out _); + return Enum.TryParse(typeof(VanillaMoon), GetNumberlessMoonName(selectableLevel), true, out _); } + + private static float RandomNumberInRadius(float radius, System.Random randomSeed) + { + return ((float)randomSeed.NextDouble() - 0.5f) * radius; + } + + public static (Vector3, Quaternion) GetRandomGroundPositionAndRotation(Vector3 centerPoint, float radius = 10f, System.Random randomSeed = null, int layerMask = -1, int maxAttempts = 10) + { + float y = centerPoint.y; + float x, y2, z; + + Vector3 randomPosition; + + for (int i = 0; i < maxAttempts; i++) + { + try + { + x = RandomNumberInRadius(radius, randomSeed); + y2 = RandomNumberInRadius(radius, randomSeed); + z = RandomNumberInRadius(radius, randomSeed); + randomPosition = centerPoint + new Vector3(x, y2, z); + randomPosition.y = y; + + float maxDistance = Vector3.Distance(centerPoint, randomPosition) + 30f; + + if (NavMesh.SamplePosition(randomPosition, out NavMeshHit navMeshHit, maxDistance, -1)) + { + if (Physics.Raycast(navMeshHit.position + Vector3.up, Vector3.down, out RaycastHit groundHit, 50f, layerMask)) + { + return (groundHit.point + Vector3.up * 0.1f, Quaternion.FromToRotation(Vector3.up, groundHit.normal)); + } + else + { + Plugin.GetLogger().LogDebug($"Nav hit at: {navMeshHit.position} but ray cast failed."); + } + } + } + catch (Exception) + { + + } + } + + return (Vector3.zero, Quaternion.identity); + } + + public static void DetermineMinMaxSpawnRates(bool increasedMapHazardSpawnRate, int configMinSpawnRate, int configMaxSpawnRate, MoonMinMax moonMinMax, out int minSpawnRate, out int maxSpawnRate) + { + int effectiveUserMinValue = moonMinMax != null ? moonMinMax.Min : configMinSpawnRate; + int effectiveUserMaxValue = moonMinMax != null ? moonMinMax.Max : configMaxSpawnRate; + + // Range from 0 to max config. + minSpawnRate = Mathf.Min(Mathf.Max(effectiveUserMinValue, 0), effectiveUserMaxValue); + // Range from min to 100. + maxSpawnRate = Mathf.Max(Mathf.Min(effectiveUserMaxValue, 100), minSpawnRate); + + if (increasedMapHazardSpawnRate) + { + minSpawnRate = Mathf.Max(5, minSpawnRate); + maxSpawnRate = Mathf.Min(maxSpawnRate * 2, 15); + } + } + + public static EntranceContainer FindAllExitPositions() + { + EntranceTeleport[] entranceTeleports = UnityEngine.Object.FindObjectsOfType(includeInactive: false); + List fireExists = []; + Vector3 mainEntrance = Vector3.zero; + for (int i = 0; i < entranceTeleports.Length; i++) + { + EntranceTeleport entranceTeleport = entranceTeleports[i]; + + if (entranceTeleport.isEntranceToBuilding) + { + // main entrance always has entranceId of 0. + if (entranceTeleport.entranceId == 0) + { + mainEntrance = entranceTeleport.transform.position; + } else + { + fireExists.Add(entranceTeleports[i].transform.position); + } + } + } + + return new EntranceContainer(mainEntrance, fireExists); + } + + public static Dictionary ParseMoonString(string moonString) + { + if (string.IsNullOrEmpty(moonString)) + { + return []; + } + + Dictionary moonMap = []; + + string[] moonMinMaxList = moonString.Trim().ToLower().Split(','); + + foreach (string moonMinMax in moonMinMaxList) + { + try + { + string[] parts = moonMinMax.Trim().Split(':'); + + moonMap.TryAdd(parts[0], new MoonMinMax(int.Parse(parts[1]), int.Parse(parts[2]))); + } + catch (Exception) + { + Plugin.GetLogger().LogError($"There was an error while parsing the moon string {moonMinMax}. Make sure it has the format moon:min:max."); + } + } + + return moonMap; + } + + // Reflection for compatibility with old versions + public static object GetReflectionField(object obj, string fieldName) + { + return AccessTools.Field(obj.GetType(), fieldName).GetValue(obj); + } + } } diff --git a/LCLandmineOutside/ModCompatibility/BrutalCompanyMinusHandler.cs b/LCLandmineOutside/ModCompatibility/BrutalCompanyMinusHandler.cs deleted file mode 100644 index 61d937e..0000000 --- a/LCLandmineOutside/ModCompatibility/BrutalCompanyMinusHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using LCHazardsOutside.Abstract; - -namespace LCHazardsOutside.ModCompatibility -{ - internal class BrutalCompanyMinusHandler : AbstractCompatibilityHandler - { - public override void Apply() - { - if (IsEnabled()) - { - LogApply(); - Plugin.instance.spawnCompatibilityMode = true; - } - } - - public override string GetModGUID() - { - return "Drinkable.BrutalCompanyMinus"; - } - } -} diff --git a/LCLandmineOutside/ModCompatibility/LateGameUpgradesHandler.cs b/LCLandmineOutside/ModCompatibility/LateGameUpgradesHandler.cs index 7c33038..72629d6 100644 --- a/LCLandmineOutside/ModCompatibility/LateGameUpgradesHandler.cs +++ b/LCLandmineOutside/ModCompatibility/LateGameUpgradesHandler.cs @@ -1,21 +1,19 @@ using HarmonyLib; using LCHazardsOutside.Abstract; +using System.Runtime.CompilerServices; namespace LCHazardsOutside.ModCompatibility { internal class LateGameUpgradesHandler : AbstractCompatibilityHandler { - - public override void Apply() + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + protected override void DoApply() { - if (IsEnabled()) - { - LogApply(); - Plugin.instance.hazardBlockList.Add(AccessTools.TypeByName("MoreShipUpgrades.UpgradeComponents.Items.Wheelbarrow.ScrapWheelbarrow")); - } + LogApply(); + Plugin.instance.hazardBlockList.Add(AccessTools.TypeByName("MoreShipUpgrades.UpgradeComponents.Items.Wheelbarrow.ScrapWheelbarrow")); } - public override string GetModGUID() + protected override string GetModGUID() { return "com.malco.lethalcompany.moreshipupgrades"; } diff --git a/LCLandmineOutside/ModCompatibility/V49Handler.cs b/LCLandmineOutside/ModCompatibility/V49Handler.cs new file mode 100644 index 0000000..f6f1e6c --- /dev/null +++ b/LCLandmineOutside/ModCompatibility/V49Handler.cs @@ -0,0 +1,26 @@ +using HarmonyLib; +using LCHazardsOutside.Abstract; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace LCHazardsOutside.ModCompatibility +{ + internal class V49Handler : AbstractCompatibilityHandler + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + protected override void DoApply() + { + Plugin.instance.v49CompatibilityEnabled = !AccessTools.AllTypes().Any(type => type.Name == "SpikeRoofTrap"); + + if (Plugin.instance.v49CompatibilityEnabled) + { + Plugin.GetLogger().LogInfo("Running in v49 compatibility mode."); + } + } + + protected override string GetModGUID() + { + return Plugin.modGUID; + } + } +} diff --git a/LCLandmineOutside/MoonMinMax.cs b/LCLandmineOutside/MoonMinMax.cs deleted file mode 100644 index 1cc7002..0000000 --- a/LCLandmineOutside/MoonMinMax.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace LCHazardsOutside -{ - public class MoonMinMax(int min, int max) - { - public int min { get; set; } = min; - public int max { get; set; } = max; - } -} diff --git a/LCLandmineOutside/Patch/RoundManagerPatch.cs b/LCLandmineOutside/Patch/RoundManagerPatch.cs new file mode 100644 index 0000000..73b673b --- /dev/null +++ b/LCLandmineOutside/Patch/RoundManagerPatch.cs @@ -0,0 +1,275 @@ +using HarmonyLib; +using System.Collections.Generic; +using UnityEngine; +using System.Linq; +using Unity.Netcode; +using LCHazardsOutside.Abstract; +using System.Collections; +using System; +using LCHazardsOutside.Data; +using System.Runtime.CompilerServices; + +namespace LCHazardsOutside.Patches +{ + + [HarmonyPatch(typeof(RoundManager))] + internal class RoundManagerPatch { + + private const string HAZARD_LAYER_NAME = "MapHazards"; + + [HarmonyPatch(nameof(RoundManager.SpawnOutsideHazards))] + [HarmonyPrefix] + static void SpawnHazardsOutsidePatch(RoundManager __instance) { + if (!__instance.currentLevel.spawnEnemiesAndScrap || !__instance.IsServer || !__instance.IsHost) { + return; + } + + // Tie hazard spawn to seed. + Plugin.GetLogger().LogDebug("randomMapSeed: " + StartOfRound.Instance.randomMapSeed); + System.Random random = new(StartOfRound.Instance.randomMapSeed + 587); + + int noHazardSpawnChance = Plugin.instance.noHazardSpawnChance.Value; + if (noHazardSpawnChance > 0) + { + double chance = noHazardSpawnChance / 100.0; + + if (random.NextDouble() < chance) + { + Plugin.GetLogger().LogInfo("No hazards spawned outside due to global chance."); + return; + } + } + + SpawnableMapObject[] hazardObjects = __instance.currentLevel.spawnableMapObjects; + + // This map has no hazards so skip outside hazard spawning. + if (hazardObjects.Length == 0) + { + return; + } + + __instance.StartCoroutine(SpawnHazardsAfterExitSpawn(__instance, hazardObjects, random)); + } + + private static void SpawnHazardsOutside(RoundManager __instance, SpawnableMapObject[] hazardObjects, EntranceContainer entranceContainer, System.Random random) + { + string sanitizedPlanetName = LCUtils.GetNumberlessMoonName(__instance.currentLevel); + Plugin.GetLogger().LogDebug($"Planetname: {sanitizedPlanetName}"); + + LCUtils.CUSTOM_LAYER_MASK.TryGetValue(sanitizedPlanetName, out string[] layers); + layers ??= ["Room", "Default"]; + + int layerMask = LayerMask.GetMask(layers); + + List spawnDenialPoints = [.. GameObject.FindGameObjectsWithTag("SpawnDenialPoint")]; + + var hazardData = hazardObjects.Select((hazardObject, index) => new { + HazardObject = hazardObject, + IsBlacklisted = Plugin.instance.hazardBlockList.Any(type => hazardObject.prefabToSpawn.GetComponent(type) != null), + IsIncreasedSpawnRate = __instance.increasedMapHazardSpawnRateIndex == index, + }).ToArray(); + + foreach (var data in hazardData) + { + Plugin.GetLogger().LogDebug("Current spawnable object: " + data.HazardObject.prefabToSpawn.name); + + if (data.IsBlacklisted) + { + Plugin.GetLogger().LogInfo($"Hazard blocked from spawning due to blacklist: {data.HazardObject.prefabToSpawn.name}"); + continue; + } + + HazardType effectiveHazardType = HazardType.CustomHazard; + if (LCUtils.HAZARD_MAP.TryGetValue(data.HazardObject.prefabToSpawn.name, out HazardType hazardType)) + { + effectiveHazardType = hazardType; + } + + ProcessHazard(effectiveHazardType, __instance, data.IsIncreasedSpawnRate, data.HazardObject, spawnDenialPoints, sanitizedPlanetName, entranceContainer, random, layerMask); + } + + Plugin.GetLogger().LogInfo("Outside hazard spawning done."); + } + + private static void ProcessHazard(HazardType type, RoundManager __instance, bool isIncreasedSpawnRate, SpawnableMapObject hazardObj, List spawnDenialPoints, string moonName, EntranceContainer entranceContainer, System.Random random, int layerMask) + { + Plugin.instance.hazardConfigMap.TryGetValue(type, out HazardConfiguration hazardConfig); + + if (!hazardConfig.Enabled) + { + return; + } + + Plugin.GetLogger().LogInfo($"Spawning {type}s outside..."); + hazardConfig.MoonMap.TryGetValue(moonName, out MoonMinMax moonMinMax); + + LCUtils.DetermineMinMaxSpawnRates(isIncreasedSpawnRate, hazardConfig.MinSpawnRate, hazardConfig.MaxSpawnRate, moonMinMax, out int minSpawnRate, out int maxSpawnRate); + HazardCalculationContainer container = new(random, spawnDenialPoints, hazardObj, minSpawnRate, maxSpawnRate, layerMask); + + if (type == HazardType.Turret) + { + container.NeedsSafetyZone = true; + container.SpawnRatioMultiplier = 1.25f; + } + + if (type == HazardType.SpikeRoofTrap) + { + container.NeedsSafetyZone = true; + } + + CalculateHazardSpawn(__instance, container, entranceContainer, hazardConfig.SpawnStrategy); + } + + private static void CalculateHazardSpawn(RoundManager __instance, HazardCalculationContainer hazardCalculationContainer, EntranceContainer entranceContainer, SpawnStrategy spawnStrategy) + { + Vector3 mainEntrancePosition = entranceContainer.MainEntrancePosition; + List fireExitPositions = entranceContainer.FireExitPositions; + Transform[] shipSpawnPathPoints = __instance.shipSpawnPathPoints; + + int hazardCounter = 0; + + // This is where the ship actually lands. + Vector3 shipLandPosition = shipSpawnPathPoints.Last().position; + + int actualSpawnRate = hazardCalculationContainer.Random.Next(hazardCalculationContainer.MinSpawnRate, hazardCalculationContainer.MaxSpawnRate + 1); + Plugin.GetLogger().LogDebug("Actual spawn rate: " + actualSpawnRate); + + List positionDataList = spawnStrategy.CalculateCenterPositions(shipLandPosition, mainEntrancePosition, fireExitPositions, hazardCalculationContainer.SpawnRatioMultiplier); + + List gameObjects = []; + int effectiveLayerMask = hazardCalculationContainer.LayerMask; + + int spawnRatePerPosition = actualSpawnRate / positionDataList.Count; + + foreach (SpawnPositionData spawnPositionData in positionDataList) + { + for (int j = 0; j < spawnRatePerPosition; j++) + { + (Vector3 randomPosition, Quaternion quaternion) = LCUtils.GetRandomGroundPositionAndRotation(spawnPositionData.CenterPosition, spawnPositionData.SpawnRadius, hazardCalculationContainer.Random, effectiveLayerMask); + + if (randomPosition == Vector3.zero) + { + Plugin.GetLogger().LogDebug("No NavMesh hit!"); + continue; + } + + bool invalidSpawnPointFound = IsInvalidSpawnPoint(hazardCalculationContainer.SpawnDenialPoints, randomPosition, hazardCalculationContainer.NeedsSafetyZone ? 16f : 8f); + + // Do not spawn the hazard if it's too close to a spawn denial point. + if (invalidSpawnPointFound) + { + Plugin.GetLogger().LogDebug("Hazard was too close to denial or safety zone and was therefore deleted: " + randomPosition); + continue; + } + + gameObjects.Add(InstantiateHazardObject(__instance, hazardCalculationContainer.SpawnableMapObject, randomPosition, quaternion)); + + hazardCounter++; + } + } + + __instance.StartCoroutine(SpawnHazardsInBulk(gameObjects)); + + Plugin.GetLogger().LogDebug("Total hazard amount: " + hazardCounter); + } + + private static GameObject InstantiateHazardObject(RoundManager __instance, SpawnableMapObject spawnableMapObject, Vector3 position, Quaternion quaternion) + { + Plugin.GetLogger().LogDebug("Spawn hazard outside at: " + position); + GameObject gameObject = UnityEngine.Object.Instantiate(spawnableMapObject.prefabToSpawn, position, quaternion, __instance.mapPropsContainer.transform); + + if (spawnableMapObject.spawnFacingAwayFromWall) + { + gameObject.transform.eulerAngles = new Vector3(0f, __instance.YRotationThatFacesTheFarthestFromPosition(position + Vector3.up * 0.2f), 0f); + } + + if (!Plugin.instance.v49CompatibilityEnabled) + { + if ((bool) LCUtils.GetReflectionField(spawnableMapObject, "spawnFacingWall")) + { + gameObject.transform.eulerAngles = new Vector3(0f, __instance.YRotationThatFacesTheNearestFromPosition(position + Vector3.up * 0.2f), 0f); + } + + if ((bool) LCUtils.GetReflectionField(spawnableMapObject, "spawnWithBackToWall") && Physics.Raycast(gameObject.transform.position, -gameObject.transform.forward, out var hitInfo, 300f, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore)) + { + if (Physics.Raycast(hitInfo.point + Vector3.up * 0.2f, Vector3.down, out RaycastHit groundHit, 50f, StartOfRound.Instance.collidersAndRoomMaskAndDefault)) + { + gameObject.transform.position = groundHit.point; + } else + { + gameObject.transform.position = hitInfo.point; + } + + if ((bool) LCUtils.GetReflectionField(spawnableMapObject, "spawnWithBackFlushAgainstWall")) + { + gameObject.transform.forward = -hitInfo.normal; + gameObject.transform.eulerAngles = new Vector3(0f, gameObject.transform.eulerAngles.y, 0f); + } + } + } + + gameObject.SetActive(value: true); + gameObject.layer = LayerMask.NameToLayer(HAZARD_LAYER_NAME); + + return gameObject; + } + + private static bool IsInvalidSpawnPoint(List spawnDenialPoints, Vector3 randomPosition, float safetyDistance) + { + foreach (GameObject spawnDenialObject in spawnDenialPoints) + { + if (Vector3.Distance(randomPosition, spawnDenialObject.transform.position) < safetyDistance) + { + return true; + } + } + + return false; + } + + public static IEnumerator SpawnHazardsInBulk(List gameObjects) + { + yield return new WaitWhile(() => Plugin.instance.IsCoroutineRunning); + + Plugin.instance.IsCoroutineRunning = true; + Plugin.GetLogger().LogDebug($"SpawnHazardsInBulk Coroutine running."); + const int bulkSize = 10; + for (int i = 0; i < gameObjects.Count; i += bulkSize) + { + int range = Mathf.Min(bulkSize, gameObjects.Count - i); + for (int j = 0; j < range; j++) + { + GameObject objectToSpawn = gameObjects[i + j]; + try + { + objectToSpawn.GetComponent().Spawn(destroyWithScene: true); + } + catch (Exception e) + { + Plugin.GetLogger().LogError($"NetworkObject could not be spawned: {e}"); + } + } + + yield return new WaitForSeconds(0.5f); + } + + Plugin.instance.IsCoroutineRunning = false; + Plugin.GetLogger().LogDebug($"SpawnHazardsInBulk Coroutine done."); + } + + public static IEnumerator SpawnHazardsAfterExitSpawn(RoundManager __instance, SpawnableMapObject[] hazardObjects, System.Random random) + { + float startTime = Time.timeSinceLevelLoad; + EntranceContainer entranceContainer = LCUtils.FindAllExitPositions(); + Plugin.GetLogger().LogDebug($"Time since level loaded: {startTime}"); + while (!entranceContainer.IsInitialized() && Time.timeSinceLevelLoad - startTime < 15f) + { + Plugin.GetLogger().LogDebug("Waiting for main entrance to load..."); + yield return new WaitForSeconds(1f); + entranceContainer = LCUtils.FindAllExitPositions(); + } + + SpawnHazardsOutside(__instance, hazardObjects, entranceContainer, random); + } + } +} diff --git a/LCLandmineOutside/Patches/RoundManagerPatch.cs b/LCLandmineOutside/Patches/RoundManagerPatch.cs deleted file mode 100644 index 845894f..0000000 --- a/LCLandmineOutside/Patches/RoundManagerPatch.cs +++ /dev/null @@ -1,298 +0,0 @@ -using HarmonyLib; -using System.Collections.Generic; -using UnityEngine; -using System.Linq; -using Unity.Netcode; -using LCHazardsOutside.Abstract; -using System.Collections; -using System; - -namespace LCHazardsOutside.Patches { - - [HarmonyPatch(typeof(RoundManager))] - internal class RoundManagerPatch { - - private const string HAZARD_LAYER_NAME = "MapHazards"; - - [HarmonyPatch(nameof(RoundManager.SpawnOutsideHazards))] - [HarmonyPrefix] - static void SpawnHazardsOutsidePatch(RoundManager __instance) { - if (!__instance.currentLevel.spawnEnemiesAndScrap || !__instance.IsServer || !__instance.IsHost) { - return; - } - - // Tie hazard spawn to seed. - Plugin.GetLogger().LogDebug("randomMapSeed: " + StartOfRound.Instance.randomMapSeed); - System.Random random = new(StartOfRound.Instance.randomMapSeed + 587); - - int noHazardSpawnChance = Plugin.instance.noHazardSpawnChance.Value; - if (noHazardSpawnChance > 0) - { - double chance = noHazardSpawnChance / 100.0; - - if (random.NextDouble() < chance) - { - Plugin.GetLogger().LogInfo("No hazards spawned outside due to global chance."); - return; - } - } - - string sanitizedPlanetName = LCUtils.GetNumberlessPlanetName(__instance.currentLevel); - Plugin.GetLogger().LogDebug($"Planetname: {sanitizedPlanetName}"); - - List spawnDenialPoints = [.. GameObject.FindGameObjectsWithTag("SpawnDenialPoint")]; - List hazardObjects = [.. __instance.currentLevel.spawnableMapObjects]; - - if (hazardObjects.Any()) { - for (int i = 0; i < hazardObjects.Count; i++) - { - SpawnableMapObject hazardObject = hazardObjects[i]; - bool isIncreasedMapHazardSpawn = __instance.increasedMapHazardSpawnRateIndex == i; - - Plugin.GetLogger().LogDebug("Current spawnable object: " + hazardObject.prefabToSpawn.name); - - bool isTurret = hazardObject.prefabToSpawn.GetComponentInChildren() != null; - bool isLandmine = hazardObject.prefabToSpawn.GetComponentInChildren() != null; - bool isCustom = !isLandmine && !isTurret; - bool isBlacklisted = false; - - if (isCustom) - { - foreach (Type blockListEntry in Plugin.instance.hazardBlockList) - { - if (hazardObject.prefabToSpawn.GetComponent(blockListEntry) != null) - { - isBlacklisted = true; - Plugin.GetLogger().LogInfo($"{blockListEntry} is blocked from spawning outside."); - break; - } - } - } - - if (isBlacklisted) - { - continue; - } - - if (isLandmine && Plugin.instance.enableLandmine.Value) - { - Plugin.GetLogger().LogInfo("Spawning landmines outside..."); - int configMinSpawnRate = Plugin.instance.globalLandmineMinSpawnRate.Value; - int configMaxSpawnRate = Plugin.instance.globalLandmineMaxSpawnRate.Value; - Plugin.instance.landmineMoonMap.TryGetValue(sanitizedPlanetName, out MoonMinMax moonMinMax); - - DetermineMinMaxSpawnRates(isIncreasedMapHazardSpawn, configMinSpawnRate, configMaxSpawnRate, moonMinMax, out int minSpawnRate, out int maxSpawnRate); - HazardCalculationContainer landmineContainer = new(random, spawnDenialPoints, hazardObject, minSpawnRate, maxSpawnRate); - - CalculateHazardSpawn(__instance, landmineContainer, new DefaultSpawnStrategy()); - } - - if (isTurret && Plugin.instance.enableTurret.Value) - { - Plugin.GetLogger().LogInfo("Spawning turrets outside..."); - int configMinSpawnRate = Plugin.instance.globalTurretMinSpawnRate.Value; - int configMaxSpawnRate = Plugin.instance.globalTurretMaxSpawnRate.Value; - Plugin.instance.turretMoonMap.TryGetValue(sanitizedPlanetName, out MoonMinMax moonMinMax); - - DetermineMinMaxSpawnRates(false, configMinSpawnRate, configMaxSpawnRate, moonMinMax, out int minSpawnRate, out int maxSpawnRate); - HazardCalculationContainer turretContainer = new(random, spawnDenialPoints, hazardObject, minSpawnRate, maxSpawnRate) - { - NeedsSafetyZone = true, - SpawnRatioMultiplier = 2f - }; - - CalculateHazardSpawn(__instance, turretContainer, new DefaultSpawnStrategy()); - } - - // Unknown Hazard - if (isCustom && Plugin.instance.enableCustomHazard.Value) - { - Plugin.GetLogger().LogInfo("Spawning custom hazards outside..."); - int configMinSpawnRate = Plugin.instance.globalCustomHazardMinSpawnRate.Value; - int configMaxSpawnRate = Plugin.instance.globalCustomHazardMaxSpawnRate.Value; - Plugin.instance.customHazardMoonMap.TryGetValue(sanitizedPlanetName, out MoonMinMax moonMinMax); - - DetermineMinMaxSpawnRates(isIncreasedMapHazardSpawn, configMinSpawnRate, configMaxSpawnRate, moonMinMax, out int minSpawnRate, out int maxSpawnRate); - HazardCalculationContainer customContainer = new(random, spawnDenialPoints, hazardObject, minSpawnRate, maxSpawnRate); - - CalculateHazardSpawn(__instance, customContainer, new DefaultSpawnStrategy()); - } - } - } - - Plugin.GetLogger().LogInfo("Outside hazard spawning done."); - } - - private static void DetermineMinMaxSpawnRates(bool increasedMapHazardSpawnRate, int configMinSpawnRate, int configMaxSpawnRate, MoonMinMax moonMinMax, out int minSpawnRate, out int maxSpawnRate) - { - int effectiveUserMinValue = moonMinMax != null ? moonMinMax.min : configMinSpawnRate; - int effectiveUserMaxValue = moonMinMax != null ? moonMinMax.max : configMaxSpawnRate; - - // Range from 0 to max config. - minSpawnRate = Mathf.Min(Mathf.Max(effectiveUserMinValue, 0), effectiveUserMaxValue); - // Range from min to 50. - maxSpawnRate = Mathf.Max(Mathf.Min(effectiveUserMaxValue, 50), minSpawnRate); - - if (increasedMapHazardSpawnRate) - { - minSpawnRate = Mathf.Max(5, minSpawnRate); - maxSpawnRate = Mathf.Min(maxSpawnRate * 2, 15); - } - } - - private static void CalculateHazardSpawn(RoundManager __instance, HazardCalculationContainer hazardCalculationContainer, SpawnStrategy spawnStrategy) - { - Transform[] shipSpawnPathPoints = __instance.shipSpawnPathPoints; - - int hazardCounter = 0; - - // This is where the ship actually lands. - Transform shipLandingTransform = shipSpawnPathPoints.Last(); - - int actualSpawnRate = hazardCalculationContainer.Random.Next(hazardCalculationContainer.MinSpawnRate, hazardCalculationContainer.MaxSpawnRate + 1); - Plugin.GetLogger().LogDebug("Actual spawn rate: " + actualSpawnRate); - - List safetyPositions = new(2); - - Vector3 shipLandPosition = shipLandingTransform.position; - Vector3 mainEntrancePosition = RoundManager.FindMainEntrancePosition(false, true); - - safetyPositions.Add(shipLandPosition); - - spawnStrategy.CalculateCenterPosition(shipLandPosition, mainEntrancePosition, [], hazardCalculationContainer.SpawnRatioMultiplier, out Vector3 middlePoint, out float spawnRadius); - - List gameObjects = []; - - int roomLayerMask = LayerMask.GetMask("Room"); - int moddedMoonLayerMask = LayerMask.GetMask("Room", "Default"); - int effectiveLayerMask = LCUtils.IsVanillaMoon(__instance.currentLevel) ? roomLayerMask : moddedMoonLayerMask; - - for (int j = 0; j < actualSpawnRate; j++) - { - (Vector3 randomPosition, Quaternion quaternion) = spawnStrategy.GetRandomGroundPositionAndRotation(middlePoint, spawnRadius, hazardCalculationContainer.Random, effectiveLayerMask); - - if (randomPosition == Vector3.zero) - { - Plugin.GetLogger().LogDebug("No NavMesh hit!"); - continue; - } - - bool invalidSpawnPointFound = IsInvalidSpawnPointHighSafety(hazardCalculationContainer.SpawnDenialPoints, shipSpawnPathPoints, randomPosition, safetyPositions, hazardCalculationContainer.NeedsSafetyZone); - - // Do not spawn the hazard if it's too close to a spawn denial point. - if (invalidSpawnPointFound) - { - Plugin.GetLogger().LogDebug("Hazard was too close to denial or safety zone and was therefore deleted: " + randomPosition); - continue; - } - - gameObjects.Add(InstantiateHazardObject(__instance, hazardCalculationContainer.SpawnableMapObject, randomPosition, quaternion)); - - hazardCounter++; - } - - if (!Plugin.instance.spawnCompatibilityMode) - { - __instance.StartCoroutine(SpawnHazardsInBulk(gameObjects)); - } - - Plugin.GetLogger().LogDebug("Total hazard amount: " + hazardCounter); - } - - private static GameObject InstantiateHazardObject(RoundManager __instance, SpawnableMapObject spawnableMapObject, Vector3 position, Quaternion quaternion) - { - Plugin.GetLogger().LogDebug("Spawn hazard outside at: " + position); - GameObject gameObject = UnityEngine.Object.Instantiate(spawnableMapObject.prefabToSpawn, position, quaternion, __instance.mapPropsContainer.transform); - - if (spawnableMapObject.spawnFacingAwayFromWall) - { - gameObject.transform.eulerAngles = new Vector3(0f, __instance.YRotationThatFacesTheFarthestFromPosition(position + Vector3.up * 0.2f), 0f); - } - - gameObject.SetActive(value: true); - gameObject.layer = LayerMask.NameToLayer(HAZARD_LAYER_NAME); - - if (Plugin.instance.spawnCompatibilityMode) - { - gameObject.GetComponent().Spawn(destroyWithScene: true); - - } - - return gameObject; - } - - private static bool IsInvalidSpawnPoint(List spawnDenialPoints, Transform[] shipPathPoints, Vector3 randomPosition) - { - foreach (GameObject spawnDenialObject in spawnDenialPoints) - { - if (Vector3.Distance(randomPosition, spawnDenialObject.transform.position) < 4f) - { - return true; - } - } - - foreach (Transform shipPathTransform in shipPathPoints) - { - if (Vector3.Distance(shipPathTransform.position, randomPosition) < 6f) - { - return true; - } - } - - return false; - } - - private static bool IsInvalidSpawnPointHighSafety(List spawnDenialPoints, Transform[] shipPathPoints, Vector3 position, List safetyZones, bool needsSafetyZone) - { - if (IsInvalidSpawnPoint(spawnDenialPoints, shipPathPoints, position)) - { - return true; - } - - if (!needsSafetyZone) - { - return false; - } - - foreach (Vector3 safetyPosition in safetyZones) - { - if (Vector3.Distance(position, safetyPosition) <= 16f) - { - return true; - } - } - - return false; - } - - public static IEnumerator SpawnHazardsInBulk(List gameObjects) - { - yield return new WaitWhile(() => Plugin.instance.IsCoroutineRunning); - - Plugin.instance.IsCoroutineRunning = true; - Plugin.GetLogger().LogDebug($"SpawnHazardsInBulk Coroutine running."); - const int bulkSize = 10; - for (int i = 0; i < gameObjects.Count; i += bulkSize) - { - int range = Mathf.Min(bulkSize, gameObjects.Count - i); - for (int j = 0; j < range; j++) - { - GameObject objectToSpawn = gameObjects[i + j]; - try - { - objectToSpawn.GetComponent().Spawn(destroyWithScene: true); - } - catch (Exception e) - { - Plugin.GetLogger().LogError($"NetworkObject could not be spawned: {e}"); - } - } - - yield return new WaitForSeconds(0.5f); - } - - Plugin.instance.IsCoroutineRunning = false; - Plugin.GetLogger().LogDebug($"SpawnHazardsInBulk Coroutine done."); - } - } -} diff --git a/LCLandmineOutside/Plugin.cs b/LCLandmineOutside/Plugin.cs index b752807..8514b94 100644 --- a/LCLandmineOutside/Plugin.cs +++ b/LCLandmineOutside/Plugin.cs @@ -4,6 +4,8 @@ using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; +using LCHazardsOutside.Abstract; +using LCHazardsOutside.Data; using LCHazardsOutside.ModCompatibility; using LCHazardsOutside.Patches; using System; @@ -12,36 +14,24 @@ namespace LCHazardsOutside { [BepInPlugin(modGUID, modName, modVersion)] + [BepInDependency("com.malco.lethalcompany.moreshipupgrades", BepInDependency.DependencyFlags.SoftDependency)] public class Plugin : BaseUnityPlugin { - private const string modGUID = "snake.tech.LCHazardsOutside"; + public const string modGUID = "snake.tech.LCHazardsOutside"; private const string modName = "LCHazardsOutside"; - private const string modVersion = "1.1.3.0"; + private const string modVersion = "1.2.0"; private readonly Harmony harmony = new(modGUID); + private readonly AcceptableValueRange acceptableSpawnRange = new(0, 100); + private readonly AcceptableValueList acceptableSpawnStrategies = new(Enum.GetNames(typeof(SpawnStrategyType))); + // General Globals public static Plugin instance; public bool IsCoroutineRunning = false; public HashSet hazardBlockList = []; - // Experimental - public bool spawnCompatibilityMode = false; - - public ConfigEntry enableLandmine; - public ConfigEntry globalLandmineMinSpawnRate; - public ConfigEntry globalLandmineMaxSpawnRate; - public Dictionary landmineMoonMap; - - public ConfigEntry enableTurret; - public ConfigEntry globalTurretMinSpawnRate; - public ConfigEntry globalTurretMaxSpawnRate; - public ConfigEntry turretMoonString; - public Dictionary turretMoonMap; - - public ConfigEntry enableCustomHazard; - public ConfigEntry globalCustomHazardMinSpawnRate; - public ConfigEntry globalCustomHazardMaxSpawnRate; - public ConfigEntry customHazardMoonString; - public Dictionary customHazardMoonMap; + public bool v49CompatibilityEnabled = false; + // Config + public Dictionary hazardConfigMap = []; public ConfigEntry noHazardSpawnChance; @@ -56,7 +46,7 @@ void Awake() { // Compatibility new LateGameUpgradesHandler().Apply(); - new BrutalCompanyMinusHandler().Apply(); + new V49Handler().Apply(); // Patches harmony.PatchAll(typeof(RoundManagerPatch)); @@ -65,62 +55,70 @@ void Awake() { } void LoadConfig() { - noHazardSpawnChance = Config.Bind("General", "NoHazardSpawnChance", 0, "A global chance from 0 to 100 in % for NO hazards to spawn outside.\n Use a non-zero chance if you want to make hazards outside more of a surprise."); + ConfigDescription spawnStrategyDescription = new( + """ + This setting dictates how spawn positions are allocated. It has 3 possible options: "MainAndFireExit", "MainEntranceOnly" and "FireExitsOnly". + When set to "MainAndFireExit", spawn positions are determined based on both the main entrance, the fire exits and the ship. + When set to "MainEntranceOnly", spawn positions are limited strictly to the area between the ship and the main entrance of the facility, making spawn points more concentrated and fire exits safe. + When set to "FireExitsOnly", spawn positions are limited strictly to the area between the ship and the fire exits of the facility, making fire exits more punishing while leaving the main entrance hassle-free. + """, acceptableSpawnStrategies); + + ConfigDescription minDescription = new("Minimum amount to spawn outside.", acceptableSpawnRange); + ConfigDescription maxDescription = new("Maximum amount to spawn outside.", acceptableSpawnRange); + ConfigDescription moonDescription = new(""" + The moon(s) where this hazard can spawn outside in the form of a comma separated list of selectable level names with min/max values in moon:min:max format (e.g. "experimentation:5:15,rend:0:10,dine:10:15") + "NOTE: These must be the internal data names of the levels (for vanilla moons use the names you see on the terminal i.e. vow, march and for modded moons check their description or ask the author). + """); + + noHazardSpawnChance = Config.Bind("0. General", "NoHazardSpawnChance", 0, "A global chance from 0 to 100 in % for NO hazards to spawn outside.\n Use a non-zero chance if you want to make hazards outside more of a surprise."); // Landmine - enableLandmine = Config.Bind("Landmine","EnableLandmineOutside", true, "Whether or not to spawn landmines outside."); - globalLandmineMinSpawnRate = Config.Bind("Landmine", "LandmineMinSpawnRate", 5, "Minimum amount of landmines to spawn outside."); - globalLandmineMaxSpawnRate = Config.Bind("Landmine", "LandmineMaxSpawnRate", 15, "Maximum amount of landmines to spawn outside."); - ConfigEntry landmineMoonString = Config.Bind("Landmine", "LandmineMoons", "", "The moon(s) where the landmines can spawn outside on in the form of a comma separated list of selectable level names with min/max values in moon:min:max format (e.g. \"experimentation:5:15,rend:0:10,dine:10:15\")\n" + - "NOTE: These must be the internal data names of the levels (for vanilla moons use the names you see on the terminal i.e. vow, march and for modded moons you will have to find their name).\n"); - - landmineMoonMap = ParseMoonString(landmineMoonString.Value); + ConfigEntry enableLandmine = Config.Bind("1. Landmine","EnableLandmineOutside", true, "Whether or not to spawn landmines outside."); + ConfigEntry globalLandmineMinSpawnRate = Config.Bind("1. Landmine", "LandmineMinSpawnRate", 15, minDescription); + ConfigEntry globalLandmineMaxSpawnRate = Config.Bind("1. Landmine", "LandmineMaxSpawnRate", 30, maxDescription); + ConfigEntry landmineMoonString = Config.Bind("1. Landmine", "LandmineMoons", "", moonDescription); + ConfigEntry landmineSpawnStrategyString = Config.Bind("1. Landmine", "LandmineSpawnStrategy", SpawnStrategyType.MainAndFireExit.ToString(), spawnStrategyDescription); + + Dictionary landmineMoonMap = LCUtils.ParseMoonString(landmineMoonString.Value); + SpawnStrategy landmineSpawnStrategy = LCUtils.GetSpawnStrategy(landmineSpawnStrategyString.Value); + + hazardConfigMap.Add(HazardType.Landmine, new HazardConfiguration(enableLandmine.Value, globalLandmineMinSpawnRate.Value, globalLandmineMaxSpawnRate.Value, landmineMoonMap, landmineSpawnStrategy)); // Turret - enableTurret = Config.Bind("Turret", "EnableTurretOutside", true, "Whether or not to spawn turrets outside."); - globalTurretMinSpawnRate = Config.Bind("Turret", "TurretMinSpawnRate", 0, "Minimum amount of turrets to spawn outside."); - globalTurretMaxSpawnRate = Config.Bind("Turret", "TurretMaxSpawnRate", 1, "Maximum amount of turrets to spawn outside."); - ConfigEntry turretMoonString = Config.Bind("Turret", "TurretMoons", "", "The moon(s) where the landmines can spawn outside on in the form of a comma separated list of selectable level names with min/max values in moon:min:max format (e.g. \"experimentation:5:15,rend:0:10,dine:10:15\")\n" + - "NOTE: These must be the internal data names of the levels (for vanilla moons use the names you see on the terminal i.e. vow, march and for modded moons you will have to find their name).\n"); - - turretMoonMap = ParseMoonString(turretMoonString.Value); - - // Custom - enableCustomHazard = Config.Bind("Custom", "EnableCustomHazardOutside", true, "Whether or not to spawn modded hazards outside."); - globalCustomHazardMinSpawnRate = Config.Bind("Custom", "CustomHazardMinSpawnRate", 1, "Minimum amount of custom hazards to spawn outside."); - globalCustomHazardMaxSpawnRate = Config.Bind("Custom", "CustomHazardMaxSpawnRate", 1, "Maximum amount of custom hazards to spawn outside."); - ConfigEntry customHazardMoonString = Config.Bind("Custom", "CustomHazardMoons", "", "The moon(s) where the custom hazards can spawn outside on in the form of a comma separated list of selectable level names with min/max values in moon:min:max format (e.g. \"experimentation:5:15,rend:0:10,dine:10:15\")\n" + - "NOTE: These must be the internal data names of the levels (for vanilla moons use the names you see on the terminal e.g. vow, march and for modded moons you will have to find their name).\n"); - - customHazardMoonMap = ParseMoonString(customHazardMoonString.Value); - } + ConfigEntry enableTurret = Config.Bind("2. Turret", "EnableTurretOutside", false, "Whether or not to spawn turrets outside."); + ConfigEntry globalTurretMinSpawnRate = Config.Bind("2. Turret", "TurretMinSpawnRate", 0, minDescription); + ConfigEntry globalTurretMaxSpawnRate = Config.Bind("2. Turret", "TurretMaxSpawnRate", 1, maxDescription); + ConfigEntry turretMoonString = Config.Bind("2. Turret", "TurretMoons", "", moonDescription); + ConfigEntry turretSpawnStrategyString = Config.Bind("2. Turret", "TurretSpawnStrategy", SpawnStrategyType.MainAndFireExit.ToString(), spawnStrategyDescription); - private Dictionary ParseMoonString(string moonString) - { - if (string.IsNullOrEmpty(moonString)) - { - return []; - } + Dictionary turretMoonMap = LCUtils.ParseMoonString(turretMoonString.Value); + SpawnStrategy turretSpawnStrategy = LCUtils.GetSpawnStrategy(turretSpawnStrategyString.Value); - Dictionary moonMap = []; + hazardConfigMap.Add(HazardType.Turret, new HazardConfiguration(enableTurret.Value, globalTurretMinSpawnRate.Value, globalTurretMaxSpawnRate.Value, turretMoonMap, turretSpawnStrategy)); - string[] moonMinMaxList = moonString.Trim().ToLower().Split(','); + // SpikeRoofTrap + ConfigEntry enableSpikeRoofTrap = Config.Bind("3. SpikeRoofTrap", "EnableSpikeRoofTrapOutside", true, "Whether or not to spawn spike roof traps outside."); + ConfigEntry globalSpikeRoofTrapMinSpawnRate = Config.Bind("3. SpikeRoofTrap", "SpikeRoofTrapMinSpawnRate", 0, minDescription); + ConfigEntry globalSpikeRoofTrapMaxSpawnRate = Config.Bind("3. SpikeRoofTrap", "SpikeRoofTrapMaxSpawnRate", 2, maxDescription); + ConfigEntry spikeRoofTrapMoonString = Config.Bind("3. SpikeRoofTrap", "SpikeRoofTrapMoons", "", moonDescription); + ConfigEntry spikeRoofTrapSpawnStrategyString = Config.Bind("3. SpikeRoofTrap", "SpikeRoofTrapSpawnStrategy", SpawnStrategyType.MainAndFireExit.ToString(), spawnStrategyDescription); - foreach (string moonMinMax in moonMinMaxList) - { - try - { - string[] parts = moonMinMax.Trim().Split(':'); - - moonMap.TryAdd(parts[0], new MoonMinMax(int.Parse(parts[1]), int.Parse(parts[2]))); - } - catch (Exception) - { - GetLogger().LogError($"There was an error while parsing the moon string {moonMinMax}. Make sure it has the format moon:min:max."); - } - } + Dictionary spikeRoofTrapMoonMap = LCUtils.ParseMoonString(spikeRoofTrapMoonString.Value); + SpawnStrategy spikeRoofTrapSpawnStrategy = LCUtils.GetSpawnStrategy(spikeRoofTrapSpawnStrategyString.Value); + + hazardConfigMap.Add(HazardType.SpikeRoofTrap, new HazardConfiguration(enableSpikeRoofTrap.Value, globalSpikeRoofTrapMinSpawnRate.Value, globalSpikeRoofTrapMaxSpawnRate.Value, spikeRoofTrapMoonMap, spikeRoofTrapSpawnStrategy)); + + // CustomHazard + ConfigEntry enableCustomHazard = Config.Bind("99. Custom", "EnableCustomHazardOutside", true, "Whether or not to spawn modded hazards outside."); + ConfigEntry globalCustomHazardMinSpawnRate = Config.Bind("99. Custom", "CustomHazardMinSpawnRate", 15, minDescription); + ConfigEntry globalCustomHazardMaxSpawnRate = Config.Bind("99. Custom", "CustomHazardMaxSpawnRate", 30, maxDescription); + ConfigEntry customHazardMoonString = Config.Bind("99. Custom", "CustomHazardMoons", "", moonDescription); + ConfigEntry customHazardSpawnStrategyString = Config.Bind("99. Custom", "CustomHazardSpawnStrategy", SpawnStrategyType.MainAndFireExit.ToString(), spawnStrategyDescription); + + Dictionary customHazardMoonMap = LCUtils.ParseMoonString(customHazardMoonString.Value); + SpawnStrategy customHazardSpawnStrategy = LCUtils.GetSpawnStrategy(customHazardSpawnStrategyString.Value); - return moonMap; + hazardConfigMap.Add(HazardType.CustomHazard, new HazardConfiguration(enableCustomHazard.Value, globalCustomHazardMinSpawnRate.Value, globalCustomHazardMaxSpawnRate.Value, customHazardMoonMap, customHazardSpawnStrategy)); } public static ManualLogSource GetLogger() { diff --git a/LCLandmineOutside/Properties/AssemblyInfo.cs b/LCLandmineOutside/Properties/AssemblyInfo.cs deleted file mode 100644 index bc04c97..0000000 --- a/LCLandmineOutside/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// Allgemeine Informationen über eine Assembly werden über die folgenden -// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, -// die einer Assembly zugeordnet sind. -[assembly: AssemblyTitle("LCHazardsOutside")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("LCHazardsOutside")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly -// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von -// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. -[assembly: ComVisible(false)] - -// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird -[assembly: Guid("37d46f81-33bf-4d3f-add0-6f1534843895")] - -// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: -// -// Hauptversion -// Nebenversion -// Buildnummer -// Revision -// -// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, -// indem Sie "*" wie unten gezeigt eingeben: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.1.0")] -[assembly: AssemblyFileVersion("1.0.1.0")] diff --git a/LCLandmineOutside/Strategy/FireExitsOnlySpawnStrategy.cs b/LCLandmineOutside/Strategy/FireExitsOnlySpawnStrategy.cs new file mode 100644 index 0000000..1f5df4e --- /dev/null +++ b/LCLandmineOutside/Strategy/FireExitsOnlySpawnStrategy.cs @@ -0,0 +1,33 @@ +using LCHazardsOutside.Data; +using System.Collections.Generic; +using UnityEngine; + +namespace LCHazardsOutside.Strategy +{ + internal class FireExitsOnlySpawnStrategy : Abstract.SpawnStrategy + { + private static FireExitsOnlySpawnStrategy instance; + + private FireExitsOnlySpawnStrategy() { } + + public static FireExitsOnlySpawnStrategy GetInstance() + { + instance ??= new FireExitsOnlySpawnStrategy(); + return instance; + } + + public override List CalculateCenterPositions(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier) + { + List centerPositionData = []; + + foreach (Vector3 pointOfInterest in pointsOfInterest) + { + centerPositionData.Add(CalculateCenterWithSpawnRadius(shipLandPosition, pointOfInterest, spawnRadiusMultiplier)); + } + + return centerPositionData; + } + + + } +} diff --git a/LCLandmineOutside/Strategy/MainAndFireExitSpawnStrategy.cs b/LCLandmineOutside/Strategy/MainAndFireExitSpawnStrategy.cs new file mode 100644 index 0000000..fafdbee --- /dev/null +++ b/LCLandmineOutside/Strategy/MainAndFireExitSpawnStrategy.cs @@ -0,0 +1,33 @@ +using LCHazardsOutside.Data; +using System.Collections.Generic; +using UnityEngine; + +namespace LCHazardsOutside.Strategy +{ + internal class MainAndFireExitSpawnStrategy : Abstract.SpawnStrategy + { + private static MainAndFireExitSpawnStrategy instance; + + private MainAndFireExitSpawnStrategy() { } + + public static MainAndFireExitSpawnStrategy GetInstance() + { + instance ??= new MainAndFireExitSpawnStrategy(); + return instance; + } + + public override List CalculateCenterPositions(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier) + { + List centerPositionData = [CalculateCenterWithSpawnRadius(shipLandPosition, mainEntrancePosition, spawnRadiusMultiplier)]; + + foreach (Vector3 pointOfInterest in pointsOfInterest) + { + centerPositionData.Add(CalculateCenterWithSpawnRadius(shipLandPosition, pointOfInterest, spawnRadiusMultiplier)); + } + + return centerPositionData; + } + + + } +} diff --git a/LCLandmineOutside/Strategy/MainEntranceOnlySpawnStrategy.cs b/LCLandmineOutside/Strategy/MainEntranceOnlySpawnStrategy.cs new file mode 100644 index 0000000..b07329c --- /dev/null +++ b/LCLandmineOutside/Strategy/MainEntranceOnlySpawnStrategy.cs @@ -0,0 +1,26 @@ +using LCHazardsOutside.Data; +using System.Collections.Generic; +using UnityEngine; + +namespace LCHazardsOutside.Strategy +{ + internal class MainEntranceOnlySpawnStrategy : Abstract.SpawnStrategy + { + private static MainEntranceOnlySpawnStrategy instance; + + private MainEntranceOnlySpawnStrategy() { } + + public static MainEntranceOnlySpawnStrategy GetInstance() + { + instance ??= new MainEntranceOnlySpawnStrategy(); + return instance; + } + + public override List CalculateCenterPositions(Vector3 shipLandPosition, Vector3 mainEntrancePosition, List pointsOfInterest, float spawnRadiusMultiplier) + { + SpawnPositionData data = CalculateCenterWithSpawnRadius(shipLandPosition, mainEntrancePosition, spawnRadiusMultiplier); + + return [data]; + } + } +} diff --git a/LCLandmineOutside/Thunderstore/CHANGELOG.md b/LCLandmineOutside/Thunderstore/CHANGELOG.md index f198a36..0d4ed3a 100644 --- a/LCLandmineOutside/Thunderstore/CHANGELOG.md +++ b/LCLandmineOutside/Thunderstore/CHANGELOG.md @@ -1,3 +1,17 @@ +1.2.0 (v50 Update) +================== +- Updated for full support of v50. +- Added backwards compatibility for v49. +- Added the Spike Roof Trap from v50 as a new configurable hazard. +- Added configurable spawn strategies! Currently there are three options: MainAndFireExit, MainEntranceOnly and FireExitsOnly. +- Added better error handling and made spawning more reliable. +- Spike Roof Traps will spawn flush against a wall if possible. +- Increased highest max value from 50 to 100. +- Now only landmines and spike traps are enabled by default and other hazards are opt-in for higher difficulty if desired. +- Removed experimental BrutalCompanyMinus compatibility patch as it was not needed. +- Min/Max values are now sliders in the config. +- Updated README. + v1.1.3 ====== - Added compatibility patch for LategameUpgrades -> Shopping carts are now blocked from spawning outside. diff --git a/LCLandmineOutside/Thunderstore/README.md b/LCLandmineOutside/Thunderstore/README.md index 405b015..58443b7 100644 --- a/LCLandmineOutside/Thunderstore/README.md +++ b/LCLandmineOutside/Thunderstore/README.md @@ -5,19 +5,7 @@ Introducing the "Lethal Company: Outdoors Hazards Edition" mod – because why c This mod will let you spawn landmines, turrets and even modded hazards outside with spawn rates configurable to your liking. So, if you've ever thought to yourself, "Gee, I love Lethal Company, but wouldn't it be great if it tried to kill me in the great outdoors?" – well, this mod's got your back. -Literally. Because there's probably a turret back there too. Happy surviving! - -Details --------- -- Seeded hazard spawns so they can be replicated. Try it out on challenge moons! -- Configuration for each hazard: - - Min/Max spawn rates per hazard. - - Min/Max spawn rates per moon per hazard. - - Global "No Extra Hazard Spawn" chance from 0-100%. -- Modded moons are supported. Keep in mind that hazard spawn points might be unpredictable due to modders layering their moons differently than the base game. -- Modded hazards are supported (if added to the game correctly). Successfully tested with Evaisa's teleport traps from [LethalThings](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalThings/). -- "Safe" zones for turret spawns to avoid game breaking (because that's no fun). -- Fire exits should be relatively safe, although this may change in the future. +Literally. Because there's probably a turret back there too. Happy surviving! Installation ------------ @@ -29,9 +17,37 @@ Installation ### Manual - Install BepinEx. -- Place BepInEx/plugins/LCHazardsOutside.dll in your BepInEx/plugins folder. +- Place LCHazardsOutside.dll in your BepInEx/plugins folder. - That's it! Only the host needs this mod installed! Amazing! +Details +-------- +- Seeded hazard spawns so they can be replicated. Try it out on challenge moons! +- Configuration for each hazard: + - Min/Max spawn rates per hazard. + - Min/Max spawn rates per moon per hazard. + - Different spawn strategies: Cover the paths to all exits or concentrate the hazards on the main path only! + - Global "No Extra Hazard Spawn" chance from 0-100%. +- Modded moons are supported. Keep in mind that hazard spawn points might be unpredictable due to modders layering their moons differently than the base game. +- Modded hazards are supported (if added to the game correctly). Successfully tested with Evaisa's teleport traps from [LethalThings](https://thunderstore.io/c/lethal-company/p/Evaisa/LethalThings/). +- The positioning of hazards will be replicated outdoors as closely as possible to their indoor setup. For example, a turret will attempt to spawn with its rear facing a wall or an obstacle, and a spike trap will be placed flush against a wall. +This alignment might not always be perfect outdoors due to uneven terrain, but this mod aims to maintain consistent placement. +- By default, only landmines and spike traps are enabled using the `MainAndFireExit` spawn strategy. Other hazards are opt-in in the configuration for higher difficulty! + +Spawn Strategies +---------------- +### MainAndFireExit (Default) +This strategy distributes hazards along the paths from the ship to the main entrance and the fire exits. +The total number of hazards is divided equally among the exits, ensuring coverage across multiple areas. +Compared to the older `MainEntranceOnly` method, this approach covers a broader area and, as such, it is advisable to increase the number of hazards to be spawned. + +### MainEntranceOnly +Previously the default in version `1.1.3` and earlier, this method calculates the midpoint between the ship and the main entrance to concentrate hazards. +This strategy ensures that fire exits remain mostly hazard-free. + +### FireExitsOnly +Similar to the `MainEntranceOnly` strategy, but exclusively targeting all available fire exits. Opt for this method if you aim to enhance the challenge for players using the fire exits. + FAQ --- @@ -47,9 +63,9 @@ FAQ Upcoming features ----------------- -- Various configurable spawn strategies. This could include making fire exits less safe, or having a turret guard an exit. +- Currently none. Everything I envisioned for this mod has been done. Known issues ------------ -- Rotation does not work at all on Vow for some reason. \ No newline at end of file +- Minor: The rotation data on landmines is not calculated correctly on Vow. Presumably not something that I can fix. \ No newline at end of file diff --git a/LCLandmineOutside/Thunderstore/manifest.json b/LCLandmineOutside/Thunderstore/manifest.json index 486a256..c38e89b 100644 --- a/LCLandmineOutside/Thunderstore/manifest.json +++ b/LCLandmineOutside/Thunderstore/manifest.json @@ -1,8 +1,8 @@ { "name": "HazardsOutside", - "version_number": "1.1.3", + "version_number": "1.2.0", "website_url": "https://github.com/snaketech-tu/LCHazardsOutside", - "description": "[v49] This mod allows hazards such as landmines, turrets and even modded ones to also spawn outside! Spawn rates can be configured for each hazard.", + "description": "[v49/v50+] This mod allows hazards such as landmines, turrets, spike traps and even modded ones to also spawn outside! Spawn rates and strategies can be configured for each hazard.", "dependencies": [ "BepInEx-BepInExPack-5.4.2100" ] diff --git a/LCLandmineOutside/Thunderstore/plugins/LCHazardsOutside/LCHazardsOutside.dll b/LCLandmineOutside/Thunderstore/plugins/LCHazardsOutside/LCHazardsOutside.dll index b4bdf2f..a6ae58c 100644 Binary files a/LCLandmineOutside/Thunderstore/plugins/LCHazardsOutside/LCHazardsOutside.dll and b/LCLandmineOutside/Thunderstore/plugins/LCHazardsOutside/LCHazardsOutside.dll differ