diff --git a/Deps/TRGE.Core.dll b/Deps/TRGE.Core.dll index 781b77d99..c4f33fe88 100644 Binary files a/Deps/TRGE.Core.dll and b/Deps/TRGE.Core.dll differ diff --git a/TRLevelReader/Helpers/TR2EntityUtilities.cs b/TRLevelReader/Helpers/TR2EntityUtilities.cs index b2e0e11b1..6c3130aad 100644 --- a/TRLevelReader/Helpers/TR2EntityUtilities.cs +++ b/TRLevelReader/Helpers/TR2EntityUtilities.cs @@ -113,6 +113,20 @@ public static List GetEntityFamily(TR2Entities entity) return new List { entity }; } + public static List RemoveAliases(IEnumerable entities) + { + List ents = new List(); + foreach (TR2Entities ent in entities) + { + TR2Entities normalisedEnt = TranslateEntityAlias(ent); + if (!ents.Contains(normalisedEnt)) + { + ents.Add(normalisedEnt); + } + } + return ents; + } + public static List GetLaraTypes() { return new List diff --git a/TRLevelReader/Helpers/TR3EntityUtilities.cs b/TRLevelReader/Helpers/TR3EntityUtilities.cs index a97907d66..058155125 100644 --- a/TRLevelReader/Helpers/TR3EntityUtilities.cs +++ b/TRLevelReader/Helpers/TR3EntityUtilities.cs @@ -95,6 +95,20 @@ public static TR3Entities GetAliasForLevel(string lvl, TR3Entities entity) return entity; } + public static List RemoveAliases(IEnumerable entities) + { + List ents = new List(); + foreach (TR3Entities ent in entities) + { + TR3Entities normalisedEnt = TranslateEntityAlias(ent); + if (!ents.Contains(normalisedEnt)) + { + ents.Add(normalisedEnt); + } + } + return ents; + } + public static List GetLaraTypes() { return new List diff --git a/TRRandomizerCore/Editors/RandomizerSettings.cs b/TRRandomizerCore/Editors/RandomizerSettings.cs index 3210ae611..71e878c8f 100644 --- a/TRRandomizerCore/Editors/RandomizerSettings.cs +++ b/TRRandomizerCore/Editors/RandomizerSettings.cs @@ -3,6 +3,8 @@ using TRRandomizerCore.Helpers; using TRGE.Core; using System.Drawing; +using System.Collections.Generic; +using System.Linq; namespace TRRandomizerCore.Editors { @@ -54,6 +56,12 @@ public class RandomizerSettings public bool DocileBirdMonsters { get; set; } public RandoDifficulty RandoEnemyDifficulty { get; set; } public bool MaximiseDragonAppearance { get; set; } + public bool UseEnemyExclusions { get; set; } + public List ExcludedEnemies { get; set; } + public Dictionary ExcludableEnemies { get; set; } + public bool ShowExclusionWarnings { get; set; } + public List IncludedEnemies => ExcludableEnemies.Keys.Except(ExcludedEnemies).ToList(); + public bool OneEnemyMode => IncludedEnemies.Count == 1; public bool GlitchedSecrets { get; set; } public bool UseRewardRoomCameras { get; set; } public bool PersistOutfits { get; set; } @@ -124,6 +132,13 @@ public void ApplyConfig(Config config) DocileBirdMonsters = config.GetBool(nameof(DocileBirdMonsters)); RandoEnemyDifficulty = (RandoDifficulty)config.GetEnum(nameof(RandoEnemyDifficulty), typeof(RandoDifficulty), RandoDifficulty.Default); MaximiseDragonAppearance = config.GetBool(nameof(MaximiseDragonAppearance)); + UseEnemyExclusions = config.GetBool(nameof(UseEnemyExclusions)); + ShowExclusionWarnings = config.GetBool(nameof(ShowExclusionWarnings)); + ExcludedEnemies = config.GetString(nameof(ExcludedEnemies)) + .Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => short.Parse(s)) + .Where(s => ExcludableEnemies.ContainsKey(s)) + .ToList(); RandomizeTextures = config.GetBool(nameof(RandomizeTextures)); TextureSeed = config.GetInt(nameof(TextureSeed), defaultSeed); @@ -219,6 +234,9 @@ public void StoreConfig(Config config) config[nameof(DocileBirdMonsters)] = DocileBirdMonsters; config[nameof(RandoEnemyDifficulty)] = RandoEnemyDifficulty; config[nameof(MaximiseDragonAppearance)] = MaximiseDragonAppearance; + config[nameof(ExcludedEnemies)] = string.Join(",", ExcludedEnemies); + config[nameof(UseEnemyExclusions)] = UseEnemyExclusions; + config[nameof(ShowExclusionWarnings)] = ShowExclusionWarnings; config[nameof(RandomizeTextures)] = RandomizeTextures; config[nameof(TextureSeed)] = TextureSeed; diff --git a/TRRandomizerCore/Editors/TR2RandoEditor.cs b/TRRandomizerCore/Editors/TR2RandoEditor.cs index 811af026b..b2831009a 100644 --- a/TRRandomizerCore/Editors/TR2RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR2RandoEditor.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; using System.Linq; using TRGE.Coord; using TRGE.Core; +using TRLevelReader.Helpers; using TRRandomizerCore.Processors; using TRRandomizerCore.Randomizers; using TRRandomizerCore.Textures; @@ -17,7 +20,10 @@ public TR2RandoEditor(TRDirectoryIOArgs args, TREdition edition) protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings(); + Settings = new RandomizerSettings + { + ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR2\Restrictions\excludable_enemies.json")) + }; Settings.ApplyConfig(config); } @@ -48,6 +54,12 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni TR23ScriptEditor tr23ScriptEditor = scriptEditor as TR23ScriptEditor; string wipDirectory = _io.WIPOutputDirectory.FullName; + if (Settings.DevelopmentMode) + { + (tr23ScriptEditor.Script as TR23Script).LevelSelectEnabled = true; + scriptEditor.SaveScript(); + } + // Texture monitoring is needed between enemy and texture randomization // to track where imported enemies are placed. using (TR2TextureMonitorBroker textureMonitor = new TR2TextureMonitorBroker()) diff --git a/TRRandomizerCore/Editors/TR3RandoEditor.cs b/TRRandomizerCore/Editors/TR3RandoEditor.cs index 41a4a1ace..e8f94fa99 100644 --- a/TRRandomizerCore/Editors/TR3RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR3RandoEditor.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System.Drawing; +using Newtonsoft.Json; +using System.Collections.Generic; using System.IO; using System.Linq; using TRGE.Coord; @@ -20,7 +20,10 @@ public TR3RandoEditor(TRDirectoryIOArgs args, TREdition edition) protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings(); + Settings = new RandomizerSettings + { + ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR3\Restrictions\excludable_enemies.json")) + }; Settings.ApplyConfig(config); } diff --git a/TRRandomizerCore/Helpers/TRRandomizationCategory.cs b/TRRandomizerCore/Helpers/TRRandomizationCategory.cs index a911be285..212ca9c4e 100644 --- a/TRRandomizerCore/Helpers/TRRandomizationCategory.cs +++ b/TRRandomizerCore/Helpers/TRRandomizationCategory.cs @@ -7,6 +7,7 @@ public enum TRRandomizationCategory PreRandomize, Randomize, Commit, - Cancel + Cancel, + Warning } } \ No newline at end of file diff --git a/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs b/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs index e21a0bbed..3883acabf 100644 --- a/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs +++ b/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs @@ -43,6 +43,8 @@ private static TRRandomizationCategory ConvertCategory(TRSaveCategory category) return TRRandomizationCategory.Cancel; // The operation has been cancelled externally case TRSaveCategory.Commit: return TRRandomizationCategory.Commit; // TRGE is commiting the changes to the original data directory + case TRSaveCategory.Warning: + return TRRandomizationCategory.Warning; // A processor wants to send a warning message default: return TRRandomizationCategory.None; } diff --git a/TRRandomizerCore/Levels/TR2CombinedLevel.cs b/TRRandomizerCore/Levels/TR2CombinedLevel.cs index 9377779b9..55c8e6b70 100644 --- a/TRRandomizerCore/Levels/TR2CombinedLevel.cs +++ b/TRRandomizerCore/Levels/TR2CombinedLevel.cs @@ -137,5 +137,26 @@ public int GetMaximumEntityLimit() return limit; } + + public int GetActualEntityCount() + { + int count = 0; + foreach (TR2Entity entity in Data.Entities) + { + switch ((TR2Entities)entity.TypeID) + { + case TR2Entities.MercSnowmobDriver: + count += 2; + break; + case TR2Entities.MarcoBartoli: + count += 7; + break; + default: + count++; + break; + } + } + return count; + } } } \ No newline at end of file diff --git a/TRRandomizerCore/Processors/AbstractLevelProcessor.cs b/TRRandomizerCore/Processors/AbstractLevelProcessor.cs index 5686a70b2..76eb9fbf6 100644 --- a/TRRandomizerCore/Processors/AbstractLevelProcessor.cs +++ b/TRRandomizerCore/Processors/AbstractLevelProcessor.cs @@ -81,6 +81,14 @@ internal void SetMessage(string text) } } + internal void SetWarning(string text) + { + lock (_monitorLock) + { + SaveMonitor.FireSaveStateChanged(category: TRSaveCategory.Warning, customDescription: text); + } + } + public void HandleException(Exception e) { lock (_monitorLock) diff --git a/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs index dd8086e31..75b1942ab 100644 --- a/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Numerics; +using TRFDControl; +using TRFDControl.FDEntryTypes; +using TRFDControl.Utilities; using TRGE.Core; using TRLevelReader.Helpers; using TRLevelReader.Model; @@ -18,6 +23,8 @@ namespace TRRandomizerCore.Randomizers public class TR2EnemyRandomizer : BaseTR2Randomizer { private Dictionary> _gameEnemyTracker; + private List _excludedEnemies; + private ISet _resultantEnemies; internal int MaxPackingAttempts { get; set; } internal TR2TextureMonitorBroker TextureMonitor { get; set; } @@ -95,6 +102,12 @@ private void RandomizeEnemiesCrossLevel() // Track enemies whose counts across the game are restricted _gameEnemyTracker = TR2EnemyUtilities.PrepareEnemyGameTracker(Settings.DocileBirdMonsters, Settings.RandoEnemyDifficulty); + // #272 Selective enemy pool - convert the shorts in the settings to actual entity types + _excludedEnemies = Settings.UseEnemyExclusions ? + Settings.ExcludedEnemies.Select(s => (TR2Entities)s).ToList() : + new List(); + _resultantEnemies = new HashSet(); + SetMessage("Randomizing enemies - importing models"); foreach (EnemyProcessor processor in processors) { @@ -119,6 +132,28 @@ private void RandomizeEnemiesCrossLevel() { _processingException.Throw(); } + + // If any exclusions failed to be avoided, send a message + if (Settings.ShowExclusionWarnings) + { + VerifyExclusionStatus(); + } + } + + private void VerifyExclusionStatus() + { + List failedExclusions = _resultantEnemies.ToList().FindAll(_excludedEnemies.Contains); + if (failedExclusions.Count > 0) + { + // A little formatting + List failureNames = new List(); + foreach (TR2Entities entity in failedExclusions) + { + failureNames.Add(Settings.ExcludableEnemies[(short)entity]); + } + failureNames.Sort(); + SetWarning(string.Format("The following enemies could not be excluded entirely from the randomization pool.{0}{0}{1}", Environment.NewLine, string.Join(Environment.NewLine, failureNames))); + } } private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, int reduceEnemyCountBy = 0) @@ -139,6 +174,8 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, List chickenGuisers = TR2EnemyUtilities.GetEnemyGuisers(TR2Entities.BirdMonster); TR2Entities chickenGuiser = TR2Entities.BirdMonster; + RandoDifficulty difficulty = GetImpliedDifficulty(); + // #148 For HSH, we lock the enemies that are required for the kill counter to work outside // the gate, which means the game still has the correct target kill count, while allowing // us to randomize the ones inside the gate (except the final shotgun goon). @@ -172,30 +209,18 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, // Do we need at least one enemy that can drop? bool droppableEnemyRequired = TR2EnemyUtilities.IsDroppableEnemyRequired(level); - // Let's try to populate the list. Start by adding one water enemy - // and one droppable enemy if they are needed. + // Let's try to populate the list. Start by adding one water enemy and one droppable + // enemy if they are needed. If we want to exclude, try to select based on user priority. if (waterEnemyRequired) { List waterEnemies = TR2EntityUtilities.KillableWaterCreatures(); - TR2Entities entity; - do - { - entity = waterEnemies[_generator.Next(0, waterEnemies.Count)]; - } - while (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(waterEnemies, level, difficulty)); } if (droppableEnemyRequired) { List droppableEnemies = TR2EntityUtilities.GetCrossLevelDroppableEnemies(!Settings.ProtectMonks); - TR2Entities entity; - do - { - entity = droppableEnemies[_generator.Next(0, droppableEnemies.Count)]; - } - while (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(droppableEnemies, level, difficulty)); } // Are there any other types we need to retain? @@ -207,16 +232,34 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } } - // Get all other candidate enemies and fill the list - List allEnemies = TR2EntityUtilities.GetCandidateCrossLevelEnemies(); + // Get all other candidate supported enemies + List allEnemies = TR2EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => TR2EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + if (Settings.OneEnemyMode || Settings.IncludedEnemies.Count < newEntities.Capacity) + { + // Marco isn't excludable in his own right because supporting a dragon-only game is impossible + allEnemies.Remove(TR2Entities.MarcoBartoli); + } + + // Remove all exclusions from the pool, and adjust the target capacity + allEnemies.RemoveAll(e => _excludedEnemies.Contains(e)); - while (newEntities.Count < newEntities.Capacity) + IEnumerable ex = allEnemies.Where(e => !newEntities.Any(TR2EntityUtilities.GetEntityFamily(e).Contains)); + List unalisedEntities = TR2EntityUtilities.RemoveAliases(ex); + while (unalisedEntities.Count < newEntities.Capacity - newEntities.Count) + { + --newEntities.Capacity; + } + + // Fill the list from the remaining candidates. Keep track of ones tested to avoid + // looping infinitely if it's not possible to fill to capacity + ISet testedEntities = new HashSet(); + while (newEntities.Count < newEntities.Capacity && testedEntities.Count < allEnemies.Count) { TR2Entities entity; // Try to enforce Marco's appearance, but only if this isn't the final packing attempt if (Settings.MaximiseDragonAppearance && !newEntities.Contains(TR2Entities.MarcoBartoli) - && TR2EnemyUtilities.IsEnemySupported(level.Name, TR2Entities.MarcoBartoli, Settings.RandoEnemyDifficulty) + && TR2EnemyUtilities.IsEnemySupported(level.Name, TR2Entities.MarcoBartoli, difficulty) && reduceEnemyCountBy == 0) { entity = TR2Entities.MarcoBartoli; @@ -226,10 +269,12 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, entity = allEnemies[_generator.Next(0, allEnemies.Count)]; } + testedEntities.Add(entity); + int adjustmentCount = TR2EnemyUtilities.GetTargetEnemyAdjustmentCount(level.Name, entity); - if (adjustmentCount != 0) + if (!Settings.OneEnemyMode && adjustmentCount != 0) { - while (newEntities.Count >= newEntities.Capacity + adjustmentCount) + while (newEntities.Count > 0 && newEntities.Count >= newEntities.Capacity + adjustmentCount) { newEntities.RemoveAt(newEntities.Count - 1); } @@ -238,33 +283,23 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, // Check if the use of this enemy triggers an overwrite of the pool, for example // the dragon in HSH. Null means nothing special has been defined. - List> restrictedCombinations = TR2EnemyUtilities.GetPermittedCombinations(level.Name, entity, Settings.RandoEnemyDifficulty); + List> restrictedCombinations = TR2EnemyUtilities.GetPermittedCombinations(level.Name, entity, difficulty); if (restrictedCombinations != null) { do { - // Pick a combination, ensuring we honour docile bird monsters if present + // Pick a combination, ensuring we honour docile bird monsters if present, + // and try to select a group that doesn't contain an excluded enemy. newEntities.Clear(); newEntities.AddRange(restrictedCombinations[_generator.Next(0, restrictedCombinations.Count)]); } - while (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuisers.All(g => newEntities.Contains(g))); + while (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuisers.All(g => newEntities.Contains(g)) + || (newEntities.Any(_excludedEnemies.Contains) && restrictedCombinations.Any(c => !c.Any(_excludedEnemies.Contains)))); break; } - // Make sure this isn't known to be unsupported in the level - if (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)) - { - continue; - } - // If it's the chicken in HSH but we're not using docile, we don't want it ending the level - if (!Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.HOME)) - { - continue; - } - - // If it's a docile chicken in Barkhang, it won't work because we can't disguise monks in this level. - if (Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.MONASTERY)) + if (!Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.HOME) && allEnemies.Except(newEntities).Count() > 1) { continue; } @@ -281,8 +316,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } else { - // Otherwise, pick something else - continue; + // Otherwise, pick something else. If we tried to previously exclude this + // enemy and couldn't, it will slip through the net and so the appearances + // will increase. + if (allEnemies.Except(newEntities).Count() > 1) + { + continue; + } } } @@ -320,6 +360,26 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } } + // If everything we are including is restriced by room, we need to provide at least one other enemy type + Dictionary> restrictedRoomEnemies = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, difficulty); + if (restrictedRoomEnemies != null && newEntities.All(e => restrictedRoomEnemies.ContainsKey(e))) + { + List pool = TR2EntityUtilities.GetCrossLevelDroppableEnemies(!Settings.ProtectMonks); + do + { + TR2Entities fallbackEnemy; + do + { + fallbackEnemy = pool[_generator.Next(0, pool.Count)]; + } + while ((_excludedEnemies.Contains(fallbackEnemy) && pool.Any(e => !_excludedEnemies.Contains(e))) + || newEntities.Contains(fallbackEnemy) + || !TR2EnemyUtilities.IsEnemySupported(level.Name, fallbackEnemy, difficulty)); + newEntities.Add(fallbackEnemy); + } + while (newEntities.All(e => restrictedRoomEnemies.ContainsKey(e))); + } + // #144 Decide at this point who will be guising unless it has already been decided above (e.g. HSH) if (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuiser == TR2Entities.BirdMonster) { @@ -338,6 +398,46 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, }; } + private TR2Entities SelectRequiredEnemy(List pool, TR2CombinedLevel level, RandoDifficulty difficulty) + { + pool.RemoveAll(e => !TR2EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + + TR2Entities entity; + if (pool.All(_excludedEnemies.Contains)) + { + // Select the last excluded enemy (lowest priority) + entity = _excludedEnemies.Last(e => pool.Contains(e)); + } + else + { + do + { + entity = pool[_generator.Next(0, pool.Count)]; + } + while (_excludedEnemies.Contains(entity)); + } + + return entity; + } + + private RandoDifficulty GetImpliedDifficulty() + { + if (_excludedEnemies.Count > 0 && Settings.RandoEnemyDifficulty == RandoDifficulty.Default) + { + // If every enemy in the pool has room restrictions for any level, we have to imply NoRestrictions difficulty mode + List includedEnemies = Settings.ExcludableEnemies.Keys.Except(Settings.ExcludedEnemies).Select(s => (TR2Entities)s).ToList(); + foreach (TR2ScriptedLevel level in Levels) + { + IEnumerable restrictedRoomEnemies = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.LevelFileBaseName.ToUpper(), RandoDifficulty.Default).Keys; + if (includedEnemies.All(e => restrictedRoomEnemies.Contains(e))) + { + return RandoDifficulty.NoRestrictions; + } + } + } + return Settings.RandoEnemyDifficulty; + } + private void RandomizeEnemiesNatively(TR2CombinedLevel level) { // For the assault course, nothing will be changed for the time being @@ -410,6 +510,8 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // Keep track of any new entities added (e.g. Skidoo) List newEntities = new List(); + RandoDifficulty difficulty = GetImpliedDifficulty(); + // #148 If it's HSH and we have been able to import cross-level, we will add 15 // dogs outside the gate to ensure the kill counter works. Dogs, Goon1 and // StickGoons will have been excluded from the cross-level pool for simplicity @@ -435,7 +537,7 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti } // First iterate through any enemies that are restricted by room - Dictionary> enemyRooms = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, Settings.RandoEnemyDifficulty); + Dictionary> enemyRooms = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, difficulty); if (enemyRooms != null) { foreach (TR2Entities entity in enemyRooms.Keys) @@ -446,7 +548,7 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti } List rooms = enemyRooms[entity]; - int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(entity, Settings.RandoEnemyDifficulty); + int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(entity, difficulty); if (maxEntityCount == -1) { // We are allowed any number, but this can't be more than the number of unique rooms, @@ -609,7 +711,8 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // #278 Flamethrowers in room 29 after pulling the lever are too difficult, but if difficulty is set to unrestricted // and they do end up here, environment mods will change their positions. - if (level.Is(TR2LevelNames.FLOATER) && Settings.RandoEnemyDifficulty == RandoDifficulty.Default && (enemyIndex == 34 || enemyIndex == 35)) + int totalRestrictionCount = TR2EnemyUtilities.GetRestrictedEnemyTotalTypeCount(difficulty); + if (level.Is(TR2LevelNames.FLOATER) && difficulty == RandoDifficulty.Default && (enemyIndex == 34 || enemyIndex == 35) && enemyPool.Count > totalRestrictionCount) { while (newEntityType == TR2Entities.FlamethrowerGoon) { @@ -620,10 +723,10 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // If we are restricting count per level for this enemy and have reached that count, pick // something else. This applies when we are restricting by in-level count, but not by room // (e.g. Winston). - int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, Settings.RandoEnemyDifficulty); + int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, difficulty); if (maxEntityCount != -1) { - if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount) + if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount && enemyPool.Count > totalRestrictionCount) { TR2Entities tmp = newEntityType; while (newEntityType == tmp) @@ -647,6 +750,9 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // to the dragon, which will be handled above in defined rooms, but the check should be made // here in case this needs to be extended later. TR2EnemyUtilities.SetEntityTriggers(level.Data, currentEntity); + + // Track every enemy type across the game + _resultantEnemies.Add(newEntityType); } // MercSnowMobDriver relies on RedSnowmobile so it will be available in the model list @@ -702,9 +808,64 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti level.Data.NumEntities = (uint)levelEntities.Count; } + // Check in case there are too many skidoo drivers + if (difficulty == RandoDifficulty.NoRestrictions && Array.Find(level.Data.Entities, e => e.TypeID == (short)TR2Entities.MercSnowmobDriver) != null) + { + LimitSkidooEntities(level); + } + RandomizeEnemyMeshes(level, enemies); } + private void LimitSkidooEntities(TR2CombinedLevel level) + { + // Although 256 is the entity limit, any more than 250 and the level can't be saved + const int skidooLimit = 250; + if (level.GetActualEntityCount() >= skidooLimit) + { + FDControl floorData = new FDControl(); + floorData.ParseFromLevel(level.Data); + LocationGenerator locationGenerator = new LocationGenerator(); + List replacementPool = TR2EntityUtilities.GetListOfAmmoTypes(); + + TR2Entity[] skidMen; + do + { + skidMen = Array.FindAll(level.Data.Entities, e => e.TypeID == (short)TR2Entities.MercSnowmobDriver); + if (skidMen.Length == 0) + { + break; + } + + // Select a random Skidoo driver and convert him into a pickup + TR2Entity skidMan = skidMen[_generator.Next(0, skidMen.Length)]; + skidMan.TypeID = (short)replacementPool[_generator.Next(0, replacementPool.Count)]; + + // Make sure the pickup is pickupable + TRRoomSector sector = FDUtilities.GetRoomSector(skidMan.X, skidMan.Y, skidMan.Z, skidMan.Room, level.Data, floorData); + skidMan.Y = sector.Floor * 256; + if (sector.FDIndex != 0) + { + FDEntry entry = floorData.Entries[sector.FDIndex].Find(e => e is FDSlantEntry s && s.Type == FDSlantEntryType.FloorSlant); + if (entry is FDSlantEntry slant) + { + Vector4? bestMidpoint = locationGenerator.GetBestSlantMidpoint(slant); + if (bestMidpoint.HasValue) + { + skidMan.Y += (int)bestMidpoint.Value.Y; + } + } + } + + // Get rid of the enemy's triggers + FDUtilities.RemoveEntityTriggers(level.Data, Array.IndexOf(level.Data.Entities, skidMan), floorData); + } + while (level.GetActualEntityCount() >= skidooLimit); + + floorData.WriteToLevel(level.Data); + } + } + private void RandomizeEnemyMeshes(TR2CombinedLevel level, EnemyRandomizationCollection enemies) { // #314 A very primitive start to mixing-up enemy meshes - monks and yetis can take on Lara's meshes @@ -909,6 +1070,10 @@ internal void ApplyRandomization() } _outer.RandomizeEnemies(level, enemies); + if (_outer.Settings.DevelopmentMode) + { + Debug.WriteLine(level.Name + ": " + string.Join(", ", enemies.All)); + } } _outer.SaveLevel(level); diff --git a/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs index ac5868834..171ca07f1 100644 --- a/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs @@ -1,22 +1,18 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.Processors; -using TRRandomizerCore.Utilities; using TRGE.Core; using TRLevelReader.Helpers; using TRLevelReader.Model; using TRLevelReader.Model.Enums; using TRModelTransporter.Transport; -using System.Diagnostics; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Processors; using TRRandomizerCore.Textures; -using Newtonsoft.Json; -using TREnvironmentEditor; -using TRFDControl.Utilities; -using TRFDControl; -using TRFDControl.FDEntryTypes; +using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers { @@ -24,6 +20,8 @@ public class TR3EnemyRandomizer : BaseTR3Randomizer { private Dictionary> _gameEnemyTracker; private Dictionary> _pistolLocations; + private List _excludedEnemies; + private ISet _resultantEnemies; internal TR3TextureMonitorBroker TextureMonitor { get; set; } public ItemFactory ItemFactory { get; set; } @@ -93,6 +91,12 @@ private void RandomizeEnemiesCrossLevel() // Track enemies whose counts across the game are restricted _gameEnemyTracker = TR3EnemyUtilities.PrepareEnemyGameTracker(Settings.RandoEnemyDifficulty); + // #272 Selective enemy pool - convert the shorts in the settings to actual entity types + _excludedEnemies = Settings.UseEnemyExclusions ? + Settings.ExcludedEnemies.Select(s => (TR3Entities)s).ToList() : + new List(); + _resultantEnemies = new HashSet(); + SetMessage("Randomizing enemies - importing models"); foreach (EnemyProcessor processor in processors) { @@ -117,6 +121,28 @@ private void RandomizeEnemiesCrossLevel() { _processingException.Throw(); } + + // If any exclusions failed to be avoided, send a message + if (Settings.ShowExclusionWarnings) + { + VerifyExclusionStatus(); + } + } + + private void VerifyExclusionStatus() + { + List failedExclusions = _resultantEnemies.ToList().FindAll(_excludedEnemies.Contains); + if (failedExclusions.Count > 0) + { + // A little formatting + List failureNames = new List(); + foreach (TR3Entities entity in failedExclusions) + { + failureNames.Add(Settings.ExcludableEnemies[(short)entity]); + } + failureNames.Sort(); + SetWarning(string.Format("The following enemies could not be excluded entirely from the randomization pool.{0}{0}{1}", Environment.NewLine, string.Join(Environment.NewLine, failureNames))); + } } private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) @@ -131,12 +157,8 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) List oldEntities = GetCurrentEnemyEntities(level); // Get the list of canidadates - List allEnemies = TR3EntityUtilities.GetCandidateCrossLevelEnemies(); - if (!Settings.DocileBirdMonsters) - { - allEnemies.Remove(TR3Entities.Willie); - } - + List allEnemies = TR3EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => TR3EnemyUtilities.IsEnemySupported(level.Name, e, Settings.RandoEnemyDifficulty)); + // Work out how many we can support int enemyCount = oldEntities.Count + TR3EnemyUtilities.GetEnemyAdjustmentCount(level.Name); List newEntities = new List(enemyCount); @@ -151,25 +173,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) if (waterEnemyRequired) { List waterEnemies = TR3EntityUtilities.GetKillableWaterEnemies(); - TR3Entities entity; - do - { - entity = waterEnemies[_generator.Next(0, waterEnemies.Count)]; - } - while (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(waterEnemies, level, Settings.RandoEnemyDifficulty)); } if (droppableEnemyRequired) { List droppableEnemies = TR3EntityUtilities.FilterDroppableEnemies(allEnemies, Settings.ProtectMonks); - TR3Entities entity; - do - { - entity = droppableEnemies[_generator.Next(0, droppableEnemies.Count)]; - } - while (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(droppableEnemies, level, Settings.RandoEnemyDifficulty)); } // Are there any other types we need to retain? @@ -181,10 +191,29 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } } - // Fill the list from the remaining candidates - while (newEntities.Count < newEntities.Capacity) + if (!Settings.DocileBirdMonsters || Settings.OneEnemyMode || Settings.IncludedEnemies.Count < newEntities.Capacity) + { + // Willie isn't excludable in his own right because supporting a Willie-only game is impossible + allEnemies.Remove(TR3Entities.Willie); + } + + // Remove all exclusions from the pool, and adjust the target capacity + allEnemies.RemoveAll(e => _excludedEnemies.Contains(e)); + + IEnumerable ex = allEnemies.Where(e => !newEntities.Any(TR3EntityUtilities.GetEntityFamily(e).Contains)); + List unalisedEntities = TR3EntityUtilities.RemoveAliases(ex); + while (unalisedEntities.Count < newEntities.Capacity - newEntities.Count) + { + --newEntities.Capacity; + } + + // Fill the list from the remaining candidates. Keep track of ones tested to avoid + // looping infinitely if it's not possible to fill to capacity + ISet testedEntities = new HashSet(); + while (newEntities.Count < newEntities.Capacity && testedEntities.Count < allEnemies.Count) { TR3Entities entity = allEnemies[_generator.Next(0, allEnemies.Count)]; + testedEntities.Add(entity); // Make sure this isn't known to be unsupported in the level if (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)) @@ -210,8 +239,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } else { - // Otherwise, pick something else - continue; + // Otherwise, pick something else. If we tried to previously exclude this + // enemy and couldn't, it will slip through the net and so the appearances + // will increase. + if (allEnemies.Except(newEntities).Count() > 1) + { + continue; + } } } @@ -225,25 +259,26 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } } - if (newEntities.Capacity > 1 && newEntities.Any(e => TR3EnemyUtilities.IsEnemyRestricted(level.Name, e))) + if (newEntities.Capacity > 1 && newEntities.All(e => TR3EnemyUtilities.IsEnemyRestricted(level.Name, e))) { // Make sure we have an unrestricted enemy available for the individual level conditions. This will // guarantee a "safe" enemy for the level; we avoid aliases here to avoid further complication. - TR3Entities unrestrictedEnemy; - do + bool RestrictionCheck(TR3Entities e) => + (droppableEnemyRequired && !TR3EntityUtilities.CanDropPickups(e, Settings.ProtectMonks)) + || !TR3EnemyUtilities.IsEnemySupported(level.Name, e, Settings.RandoEnemyDifficulty) + || newEntities.Contains(e) + || TR3EntityUtilities.IsWaterCreature(e) + || TR3EnemyUtilities.IsEnemyRestricted(level.Name, e) + || TR3EntityUtilities.TranslateEntityAlias(e) != e; + + List unrestrictedPool = allEnemies.FindAll(e => !RestrictionCheck(e)); + if (unrestrictedPool.Count == 0) { - unrestrictedEnemy = allEnemies[_generator.Next(0, allEnemies.Count)]; + // We are going to have to pull in the full list of candiates again, so ignoring any exclusions + unrestrictedPool = TR3EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => !RestrictionCheck(e)); } - while - ( - (droppableEnemyRequired && !TR3EntityUtilities.CanDropPickups(unrestrictedEnemy, Settings.ProtectMonks)) - || newEntities.Contains(unrestrictedEnemy) - || TR3EntityUtilities.IsWaterCreature(unrestrictedEnemy) - || TR3EnemyUtilities.IsEnemyRestricted(level.Name, unrestrictedEnemy) - || TR3EntityUtilities.TranslateEntityAlias(unrestrictedEnemy) != unrestrictedEnemy - ); - newEntities.Add(unrestrictedEnemy); + newEntities.Add(unrestrictedPool[_generator.Next(0, unrestrictedPool.Count)]); } if (Settings.DevelopmentMode) @@ -267,6 +302,28 @@ private List GetCurrentEnemyEntities(TR3CombinedLevel level) return oldEntities; } + private TR3Entities SelectRequiredEnemy(List pool, TR3CombinedLevel level, RandoDifficulty difficulty) + { + pool.RemoveAll(e => !TR3EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + + TR3Entities entity; + if (pool.All(_excludedEnemies.Contains)) + { + // Select the last excluded enemy (lowest priority) + entity = _excludedEnemies.Last(e => pool.Contains(e)); + } + else + { + do + { + entity = pool[_generator.Next(0, pool.Count)]; + } + while (_excludedEnemies.Contains(entity)); + } + + return entity; + } + private void RandomizeEnemiesNatively(TR3CombinedLevel level) { // For the assault course, nothing will be changed for the time being @@ -424,7 +481,7 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti int maxEntityCount = TR3EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, Settings.RandoEnemyDifficulty); if (maxEntityCount != -1) { - if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount) + if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount && enemyPool.Count > maxEntityCount) { TR3Entities tmp = newEntityType; while (newEntityType == tmp || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) @@ -452,13 +509,18 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti } } } - else if (level.Is(TR3LevelNames.RXTECH) && level.IsWillardSequence && Settings.RandoEnemyDifficulty == RandoDifficulty.Default && (currentEntity.Room == 14 || currentEntity.Room == 45)) + else if (level.Is(TR3LevelNames.RXTECH) + && level.IsWillardSequence + && Settings.RandoEnemyDifficulty == RandoDifficulty.Default + && newEntityType == TR3Entities.RXTechFlameLad + && (currentEntity.Room == 14 || currentEntity.Room == 45)) { // #269 We don't want flamethrowers here because they're hostile, so getting off the minecart - // safely is too difficult. - while (newEntityType == TR3Entities.RXTechFlameLad || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) + // safely is too difficult. We can only change them if there is something else unrestricted available. + List safePool = enemyPool.FindAll(e => e != TR3Entities.RXTechFlameLad && !TR3EnemyUtilities.IsEnemyRestricted(level.Name, e)); + if (safePool.Count > 0) { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; + newEntityType = safePool[_generator.Next(0, safePool.Count)]; } } else if (level.Is(TR3LevelNames.HSC)) @@ -473,24 +535,26 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti newEntityType = TR3Entities.Prisoner; } } - else if (currentEntity.Room == 78) + else if (currentEntity.Room == 78 && newEntityType == TR3Entities.Monkey) { // #286 Monkeys cannot share AI Ambush spots largely, but these are needed here to ensure the enemies // come through the gate before the timer closes them again. Just ensure no monkeys are here. - while (newEntityType == TR3Entities.Monkey || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) + List safePool = enemyPool.FindAll(e => e != TR3Entities.Monkey && !TR3EnemyUtilities.IsEnemyRestricted(level.Name, e)); + if (safePool.Count > 0) { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; + newEntityType = safePool[_generator.Next(0, safePool.Count)]; + } + else + { + // Full monkey mode means we have to move them inside the gate + currentEntity.Z -= 4096; } } } - else if (level.Is(TR3LevelNames.THAMES) && (currentEntity.Room == 61 || currentEntity.Room == 62)) + else if (level.Is(TR3LevelNames.THAMES) && (currentEntity.Room == 61 || currentEntity.Room == 62) && newEntityType == TR3Entities.Monkey) { - // #286 Ban monkeys from these two rooms for now because of JP entity index differences (environment mods - // can't yet tell the difference). - while (newEntityType == TR3Entities.Monkey || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) - { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; - } + // #286 Move the monkeys away from the AI entities + currentEntity.Z -= 1024; } // Make sure to convert back to the actual type @@ -506,6 +570,9 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti { targetEntity.Invisible = false; } + + // Track every enemy type across the game + _resultantEnemies.Add(newEntityType); } // Add extra ammo based on this level's difficulty diff --git a/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json b/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json index 6422788e9..f8c32b0a7 100644 --- a/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json +++ b/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json @@ -44,6 +44,28 @@ ] }, + { + "Condition": { + "Comments": "Similary in the cage room, rotate enemy 108 so there is a bit of time to escape after triggering him.", + "ConditionType": 0, + "EntityIndex": 108, + "EntityType": 34 + }, + "OnTrue": [ + { + "EMType": 44, + "EntityIndex": 108, + "TargetLocation": { + "X": 38400, + "Y": 4608, + "Z": 81408, + "Room": 145, + "Angle": 16384 + } + } + ] + }, + { "Condition": { "Comments": "If Marco has been added, make a new room for him and move him to it.", diff --git a/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json b/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json index 177f22c4f..9bc51e4c9 100644 --- a/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json +++ b/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json @@ -2943,9 +2943,7 @@ ], "1": [ [ 40, 20, 1009, 21 ], - [ 40, 20, 1009, 36 ], - [ 40, 20, 1010, 21 ], - [ 40, 20, 1010, 36 ], + [ 40, 20, 1010, 21 ], [ 40, 20, 38, 21 ], [ 40, 20, 38, 36 ], [ 40, 20, 37, 21 ], diff --git a/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json b/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json new file mode 100644 index 000000000..aab1248ae --- /dev/null +++ b/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json @@ -0,0 +1,44 @@ +{ + "1009": "Barracuda (Tibet)", + "1008": "Barracuda (Underwater Levels)", + "1010": "Barracuda (Temple of Xian)", + "1000": "Tiger (Bengal)", + "46": "Bird Monster", + "27": "Black Moray Eel", + "38": "Crow", + "15": "Doberman", + "47": "Eagle", + "34": "Flamethrower", + "30": "Gunman 1 (Shotgun)", + "31": "Gunman 2 (Rifle)", + "19": "Knifethrower", + "16": "Masked Goon 1 (Jacket)", + "17": "Masked Goon 2 (Waistcoat)", + "18": "Masked Goon 3 (T-Shirt)", + "48": "Mercenary 1 (Grey Trousers)", + "49": "Mercenary 2 (Blue Trousers)", + "50": "Mercenary 3 (Green Trousers)", + "52": "Skidoo Driver", + "53": "Monk (Long Stick)", + "54": "Monk (Knife Stick)", + "21": "Rat", + "29": "Scuba Diver", + "25": "Shark", + "20": "Shotgun Goon", + "1001": "Snow Leopard", + "36": "Spider (Small)", + "37": "Spider (Large)", + "1003": "Stick Goon (Bandana)", + "1004": "Stick Goon (Black Jacket)", + "1005": "Stick Goon (Bodywarmer)", + "1006": "Stick Goon (Green Vest)", + "1007": "Stick Goon (White Vest)", + "33": "Stick Goon (White T-Shirt)", + "214": "T-Rex", + "1002": "Tiger (White)", + "260": "Winston", + "41": "Jade Guardian (Spear)", + "43": "Jade Guardian (Sword)", + "26": "Yellow Moray Eel", + "45": "Yeti" +} \ No newline at end of file diff --git a/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json b/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json new file mode 100644 index 000000000..4b8e9720b --- /dev/null +++ b/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json @@ -0,0 +1,41 @@ +{ + "46": "RX Tech Claw Mutant", + "1000": "Cobra (India)", + "1001": "Cobra (Nevada)", + "34": "Compsognathus", + "42": "RX Tech Crawler Mutant", + "32": "Crocodile", + "27": "Crow", + "65": "Dam Guard", + "41": "Dog (Antarctica)", + "2000": "Dog (London)", + "2001": "Dog (Nevada)", + "25": "Killer Whale", + "35": "Lizard Man", + "56": "London Guard", + "51": "Mercenary (London)", + "37": "Mercenary (South Pacific)", + "71": "Monkey", + "61": "MP (Handgun)", + "63": "MP (MP5)", + "60": "MP (Baton)", + "62": "Prisoner", + "53": "Punk", + "288": "Raptor", + "23": "Rat", + "40": "Gunman (Meteorite Cavern)", + "39": "Gunman (Antarctica)", + "50": "Flamethrower", + "26": "Scuba Diver", + "70": "Shiva", + "28": "Tiger", + "45": "Tinnos Monster", + "44": "Tinnos Wasp", + "73": "Tony", + "20": "Tribesman (Axe)", + "21": "Tribesman (Dart)", + "287": "T-Rex", + "29": "Vulture", + "360": "Winston (Regular)", + "361": "Winston (Camoflauge)" +} \ No newline at end of file diff --git a/TRRandomizerCore/TRRandomizerController.cs b/TRRandomizerCore/TRRandomizerController.cs index 789e0fdf1..1667c475d 100644 --- a/TRRandomizerCore/TRRandomizerController.cs +++ b/TRRandomizerCore/TRRandomizerController.cs @@ -461,6 +461,34 @@ public bool MaximiseDragonAppearance set => LevelRandomizer.MaximiseDragonAppearance = value; } + public bool UseEnemyExclusions + { + get => LevelRandomizer.UseEnemyExclusions; + set => LevelRandomizer.UseEnemyExclusions = value; + } + + public bool ShowExclusionWarnings + { + get => LevelRandomizer.ShowExclusionWarnings; + set => LevelRandomizer.ShowExclusionWarnings = value; + } + + public List ExcludedEnemies + { + get => LevelRandomizer.ExcludedEnemies; + set => LevelRandomizer.ExcludedEnemies = value; + } + + public Dictionary ExcludableEnemies + { + get => LevelRandomizer.ExcludableEnemies; + } + + public List IncludedEnemies + { + get => LevelRandomizer.IncludedEnemies; + } + public bool RandomizeOutfits { get => LevelRandomizer.RandomizeOutfits; diff --git a/TRRandomizerCore/Utilities/LocationGenerator.cs b/TRRandomizerCore/Utilities/LocationGenerator.cs index c06d3cfff..fe3e41624 100644 --- a/TRRandomizerCore/Utilities/LocationGenerator.cs +++ b/TRRandomizerCore/Utilities/LocationGenerator.cs @@ -296,7 +296,7 @@ private bool IsTriggerInvalid(FDTriggerEntry trigger) ); } - private Vector4? GetBestSlantMidpoint(FDSlantEntry slant) + public Vector4? GetBestSlantMidpoint(FDSlantEntry slant) { List corners = new List { 0, 0, 0, 0 }; if (slant.XSlant > 0) diff --git a/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs b/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs index cf0bfef58..04cc8bd79 100644 --- a/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs +++ b/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs @@ -177,6 +177,16 @@ public static int GetRestrictedEnemyLevelCount(TR2Entities entity, RandoDifficul return -1; } + public static int GetRestrictedEnemyTotalTypeCount(RandoDifficulty difficulty) + { + if (difficulty == RandoDifficulty.Default) + { + return _restrictedEnemyLevelCountsDefault.Count; + } + + return _restrictedEnemyLevelCountsTechnical.Count; + } + public static List> GetPermittedCombinations(string lvl, TR2Entities entity, RandoDifficulty difficulty) { if (_specialEnemyCombinations.ContainsKey(lvl) && _specialEnemyCombinations[lvl].ContainsKey(entity)) @@ -284,7 +294,6 @@ public static EnemyDifficulty GetEnemyDifficulty(List enemies) TR2Entities.Barracuda, TR2Entities.BlackMorayEel, TR2Entities.ScubaDiver, TR2Entities.Shark, TR2Entities.YellowMorayEel }, - // #192 The Barkhang/Opera House freeze appears to be caused by dead floating water creatures, so they're all banished [TR2LevelNames.MONASTERY] = new List { @@ -300,7 +309,7 @@ public static EnemyDifficulty GetEnemyDifficulty(List enemies) TR2Entities.BlackMorayEel, TR2Entities.Doberman, TR2Entities.MaskedGoon1, TR2Entities.MaskedGoon2, TR2Entities.MaskedGoon3, TR2Entities.MercSnowmobDriver, TR2Entities.MonkWithKnifeStick, TR2Entities.MonkWithLongStick, TR2Entities.StickWieldingGoon1, - TR2Entities.StickWieldingGoon2, TR2Entities.Winston, TR2Entities.YellowMorayEel + TR2Entities.StickWieldingGoon2, TR2Entities.Winston, TR2Entities.YellowMorayEel, TR2Entities.ShotgunGoon } }; diff --git a/TRRandomizerView/Model/BoolItemIDControlClass.cs b/TRRandomizerView/Model/BoolItemIDControlClass.cs new file mode 100644 index 000000000..0d7394ead --- /dev/null +++ b/TRRandomizerView/Model/BoolItemIDControlClass.cs @@ -0,0 +1,19 @@ +using System; + +namespace TRRandomizerView.Model +{ + public class BoolItemIDControlClass : BoolItemControlClass, ICloneable + { + public int ID { get; set; } + + public BoolItemIDControlClass Clone() + { + return (BoolItemIDControlClass)MemberwiseClone(); + } + + object ICloneable.Clone() + { + return Clone(); + } + } +} \ No newline at end of file diff --git a/TRRandomizerView/Model/ControllerOptions.cs b/TRRandomizerView/Model/ControllerOptions.cs index 00f945f92..200fed505 100644 --- a/TRRandomizerView/Model/ControllerOptions.cs +++ b/TRRandomizerView/Model/ControllerOptions.cs @@ -58,6 +58,8 @@ public class ControllerOptions : INotifyPropertyChanged private bool _vfxWave; private List _secretBoolItemControls, _itemBoolItemControls, _enemyBoolItemControls, _textureBoolItemControls, _audioBoolItemControls, _outfitBoolItemControls, _textBoolItemControls, _startBoolItemControls, _environmentBoolItemControls; + private List _selectableEnemies; + private bool _useEnemyExclusions, _showExclusionWarnings; private RandoDifficulty _randoEnemyDifficulty; private ItemDifficulty _randoItemDifficulty; @@ -1111,6 +1113,36 @@ public List EnemyBoolItemControls } } + public List SelectableEnemyControls + { + get => _selectableEnemies; + set + { + _selectableEnemies = value; + FirePropertyChanged(); + } + } + + public bool UseEnemyExclusions + { + get => _useEnemyExclusions; + set + { + _useEnemyExclusions = value; + FirePropertyChanged(); + } + } + + public bool ShowExclusionWarnings + { + get => _showExclusionWarnings; + set + { + _showExclusionWarnings = value; + FirePropertyChanged(); + } + } + public List TextureBoolItemControls { get => _textureBoolItemControls; @@ -1508,6 +1540,9 @@ public void Load(TRRandomizerController controller) DocileBirdMonsters.Value = _controller.DocileBirdMonsters; MaximiseDragonAppearance.Value = _controller.MaximiseDragonAppearance; RandoEnemyDifficulty = _controller.RandoEnemyDifficulty; + UseEnemyExclusions = _controller.UseEnemyExclusions; + ShowExclusionWarnings = _controller.ShowExclusionWarnings; + LoadEnemyExclusions(); RandomizeSecrets = _controller.RandomizeSecrets; SecretSeed = _controller.SecretSeed; @@ -1564,6 +1599,26 @@ public void Load(TRRandomizerController controller) FireSupportPropertiesChanged(); } + public void LoadEnemyExclusions() + { + SelectableEnemyControls = new List(); + + // Add exclusions based on priority (i.e. order) followed by the remaining included controls + _controller.ExcludedEnemies.ForEach(e => SelectableEnemyControls.Add(new BoolItemIDControlClass + { + ID = e, + Title = _controller.ExcludableEnemies[e], + Value = true + })); + + _controller.IncludedEnemies.ForEach(e => SelectableEnemyControls.Add(new BoolItemIDControlClass + { + ID = e, + Title = _controller.ExcludableEnemies[e], + Value = false + })); + } + public void RandomizeActiveSeeds() { Random rng = new Random(); @@ -1759,6 +1814,12 @@ public void Save() _controller.DocileBirdMonsters = DocileBirdMonsters.Value; _controller.MaximiseDragonAppearance = MaximiseDragonAppearance.Value; _controller.RandoEnemyDifficulty = RandoEnemyDifficulty; + _controller.UseEnemyExclusions = UseEnemyExclusions; + _controller.ShowExclusionWarnings = ShowExclusionWarnings; + + List excludedEnemies = new List(); + SelectableEnemyControls.FindAll(c => c.Value).ForEach(c => excludedEnemies.Add((short)c.ID)); + _controller.ExcludedEnemies = excludedEnemies; _controller.RandomizeSecrets = RandomizeSecrets; _controller.SecretSeed = SecretSeed; diff --git a/TRRandomizerView/TRRandomizerView.csproj b/TRRandomizerView/TRRandomizerView.csproj index 19ebf4b22..72b1afd2f 100644 --- a/TRRandomizerView/TRRandomizerView.csproj +++ b/TRRandomizerView/TRRandomizerView.csproj @@ -97,6 +97,7 @@ + @@ -107,6 +108,9 @@ AdvancedWindow.xaml + + EnemyWindow.xaml + GlobalSeedWindow.xaml @@ -165,6 +169,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/TRRandomizerView/Windows/AdvancedWindow.xaml b/TRRandomizerView/Windows/AdvancedWindow.xaml index 1f977a2bc..4e59fb15a 100644 --- a/TRRandomizerView/Windows/AdvancedWindow.xaml +++ b/TRRandomizerView/Windows/AdvancedWindow.xaml @@ -72,6 +72,8 @@ + + @@ -180,7 +182,7 @@ - @@ -239,8 +241,51 @@ + + + + + + + + + + + + + + + + + + + + + + + +