From 6e30f2e7f225bc98133f53a4e13a417fe4f0a3fd Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sun, 26 May 2024 21:31:09 +0100 Subject: [PATCH] Refactor item randomization (#688) Part of #614. --- TRLevelControl/Helpers/TR2TypeUtilities.cs | 14 + TRRandomizerCore/Editors/TR2ClassicEditor.cs | 12 +- TRRandomizerCore/Helpers/ExtRoomInfo.cs | 20 +- TRRandomizerCore/Levels/TR3RCombinedLevel.cs | 1 + .../Randomizers/Shared/ItemAllocator.cs | 205 ++++++ .../TR1/Classic/TR1EnemyRandomizer.cs | 8 +- .../TR1/Classic/TR1ItemRandomizer.cs | 480 ++----------- .../TR1/Classic/TR1SecretRandomizer.cs | 4 +- .../TR1/Remastered/TR1REnemyRandomizer.cs | 7 +- .../TR1/Remastered/TR1RItemRandomizer.cs | 219 +----- .../TR1/Remastered/TR1RSecretRandomizer.cs | 4 +- .../TR1/Shared/TR1ItemAllocator.cs | 230 +++++++ .../TR2/Classic/TR2EnemyRandomizer.cs | 111 +++- .../TR2/Classic/TR2ItemRandomizer.cs | 628 ++---------------- .../TR2/Remastered/TR2RItemRandomizer.cs | 194 +----- .../TR2/Shared/TR2ItemAllocator.cs | 103 +++ .../TR2/Shared/TR2SecretAllocator.cs | 4 +- .../TR3/Classic/TR3ItemRandomizer.cs | 262 +------- .../TR3/Classic/TR3SecretRandomizer.cs | 4 +- .../TR3/Remastered/TR3RItemRandomizer.cs | 243 +------ .../TR3/Remastered/TR3RSecretRandomizer.cs | 4 +- .../TR3/Shared/TR3ItemAllocator.cs | 129 ++++ .../TR3/Locations/unarmed_locations.json | 8 + .../Utilities/VehicleUtilities.cs | 5 +- 24 files changed, 1003 insertions(+), 1896 deletions(-) create mode 100644 TRRandomizerCore/Randomizers/Shared/ItemAllocator.cs create mode 100644 TRRandomizerCore/Randomizers/TR1/Shared/TR1ItemAllocator.cs create mode 100644 TRRandomizerCore/Randomizers/TR2/Shared/TR2ItemAllocator.cs create mode 100644 TRRandomizerCore/Randomizers/TR3/Shared/TR3ItemAllocator.cs diff --git a/TRLevelControl/Helpers/TR2TypeUtilities.cs b/TRLevelControl/Helpers/TR2TypeUtilities.cs index c36b19f0e..370b4d9fa 100644 --- a/TRLevelControl/Helpers/TR2TypeUtilities.cs +++ b/TRLevelControl/Helpers/TR2TypeUtilities.cs @@ -461,6 +461,20 @@ public static List GetAmmoTypes() }; } + public static TR2Type GetWeaponAmmo(TR2Type weapon) + { + return weapon switch + { + TR2Type.Shotgun_S_P => TR2Type.ShotgunAmmo_S_P, + TR2Type.Automags_S_P => TR2Type.AutoAmmo_S_P, + TR2Type.Uzi_S_P => TR2Type.UziAmmo_S_P, + TR2Type.Harpoon_S_P => TR2Type.HarpoonAmmo_S_P, + TR2Type.M16_S_P => TR2Type.M16Ammo_S_P, + TR2Type.GrenadeLauncher_S_P => TR2Type.Grenades_S_P, + _ => TR2Type.PistolAmmo_S_P, + }; + } + public static bool IsUtilityType(TR2Type type) { return (type == TR2Type.ShotgunAmmo_S_P || diff --git a/TRRandomizerCore/Editors/TR2ClassicEditor.cs b/TRRandomizerCore/Editors/TR2ClassicEditor.cs index 3b028ddaf..5d4dd4434 100644 --- a/TRRandomizerCore/Editors/TR2ClassicEditor.cs +++ b/TRRandomizerCore/Editors/TR2ClassicEditor.cs @@ -61,8 +61,7 @@ protected override int GetSaveTarget(int numLevels) if (Settings.RandomizeItems) { - // Standard/key item rando followed by unarmed logic after enemy rando - target += numLevels * 2; + target += numLevels; if (Settings.RandomizeItemSprites) { target += numLevels; @@ -264,13 +263,6 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni }.Randomize(Settings.EnemySeed); } - // Randomize ammo/weapon in unarmed levels post enemy randomization - if (!monitor.IsCancelled && Settings.RandomizeItems) - { - monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing unarmed level items"); - itemRandomizer.RandomizeAmmo(); - } - if (!monitor.IsCancelled && Settings.RandomizeStartPosition) { monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing start positions"); @@ -382,7 +374,7 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni if (!monitor.IsCancelled && Settings.RandomizeItems && Settings.RandomizeItemSprites) { monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing Sprites"); - itemRandomizer.RandomizeLevelsSprites(); + itemRandomizer.RandomizeSprites(); } } } diff --git a/TRRandomizerCore/Helpers/ExtRoomInfo.cs b/TRRandomizerCore/Helpers/ExtRoomInfo.cs index 6d1de44c2..83c87d228 100644 --- a/TRRandomizerCore/Helpers/ExtRoomInfo.cs +++ b/TRRandomizerCore/Helpers/ExtRoomInfo.cs @@ -18,17 +18,17 @@ public class ExtRoomInfo public int Size { get; private set; } - public ExtRoomInfo(TRRoomInfo info, int numXSectors, int numZSectors) + public ExtRoomInfo(TRRoom room) { - MinX = info.X + TRConsts.Step4; - MaxX = info.X + TRConsts.Step4 * (numXSectors - 1); - MinZ = info.Z + TRConsts.Step4; - MaxZ = info.Z + TRConsts.Step4 * (numZSectors - 1); - MinY = info.YTop; - MaxY = info.YBottom; - - Width = numXSectors - 2; - Depth = numZSectors - 2; + MinX = room.Info.X + TRConsts.Step4; + MaxX = room.Info.X + TRConsts.Step4 * (room.NumXSectors - 1); + MinZ = room.Info.Z + TRConsts.Step4; + MaxZ = room.Info.Z + TRConsts.Step4 * (room.NumZSectors - 1); + MinY = room.Info.YTop; + MaxY = room.Info.YBottom; + + Width = room.NumXSectors - 2; + Depth = room.NumZSectors - 2; Height = Math.Abs(MaxY - MinY) / TRConsts.Step1; Size = Width * Depth * Height; diff --git a/TRRandomizerCore/Levels/TR3RCombinedLevel.cs b/TRRandomizerCore/Levels/TR3RCombinedLevel.cs index 0dc5b9a43..3b7788b10 100644 --- a/TRRandomizerCore/Levels/TR3RCombinedLevel.cs +++ b/TRRandomizerCore/Levels/TR3RCombinedLevel.cs @@ -19,4 +19,5 @@ public class TR3RCombinedLevel public bool IsAssault => Is(TR3LevelNames.ASSAULT); public TRDictionary PDPData { get; set; } public Dictionary MapData { get; set; } + public bool HasExposureMeter => Sequence == 16 || Sequence == 17; } diff --git a/TRRandomizerCore/Randomizers/Shared/ItemAllocator.cs b/TRRandomizerCore/Randomizers/Shared/ItemAllocator.cs new file mode 100644 index 000000000..a71b1f12a --- /dev/null +++ b/TRRandomizerCore/Randomizers/Shared/ItemAllocator.cs @@ -0,0 +1,205 @@ +using Newtonsoft.Json; +using TRLevelControl.Model; +using TRRandomizerCore.Editors; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public abstract class ItemAllocator + where T : Enum + where E : TREntity, new() +{ + protected readonly Dictionary> _excludedLocations; + protected readonly Dictionary> _pistolLocations; + protected readonly Dictionary _unarmedPistolCache; + protected readonly LocationPicker _picker; + + protected ItemSpriteRandomizer _spriteRandomizer; + + public Random Generator { get; set; } + public RandomizerSettings Settings { get; set; } + public ItemFactory ItemFactory { get; set; } + + public ItemAllocator(TRGameVersion gameVersion) + { + _excludedLocations = JsonConvert.DeserializeObject>>(File.ReadAllText($@"Resources\{gameVersion}\Locations\invalid_item_locations.json")); + _pistolLocations = JsonConvert.DeserializeObject>>(File.ReadAllText($@"Resources\{gameVersion}\Locations\unarmed_locations.json")); + _picker = new($@"Resources\{gameVersion}\Locations\routes.json"); + _unarmedPistolCache = new(); + } + + public E GetUnarmedLevelPistols(string levelName, List items) + { + if (!_pistolLocations.ContainsKey(levelName)) + { + return null; + } + + if (!_unarmedPistolCache.ContainsKey(levelName)) + { + List weaponTypes = GetWeaponItemTypes(); + T pistols = GetPistolType(); + _unarmedPistolCache[levelName] = items.Find(e => + (weaponTypes.Contains(e.TypeID) || EqualityComparer.Default.Equals(e.TypeID, pistols)) + && _pistolLocations[levelName].Any(l => l.IsEquivalent(e.GetLocation()))); + } + + return _unarmedPistolCache[levelName]; + } + + public void RandomizeItemTypes(string levelName, List items, bool isUnarmed) + { + if (!Settings.RandomizeItemTypes) + { + return; + } + + List stdItemTypes = GetStandardItemTypes(); + List weaponTypes = GetWeaponItemTypes(); + T pistols = GetPistolType(); + List excludedItems = GetExcludedItems(levelName); + + bool hasPistols = items.Any(e => EqualityComparer.Default.Equals(e.TypeID, pistols)); + E unarmedPistols = isUnarmed ? GetUnarmedLevelPistols(levelName, items) : null; + + for (int i = 0; i < items.Count; i++) + { + if (excludedItems.Contains(i)) + { + continue; + } + + E entity = items[i]; + T entityType = entity.TypeID; + + if (isUnarmed && entity == unarmedPistols) + { + // Enemy rando may have changed this already to something else and allocated + // ammo to the inventory, so only change pistols. + if (EqualityComparer.Default.Equals(entityType, pistols) && Settings.GiveUnarmedItems) + { + do + { + entityType = stdItemTypes[Generator.Next(0, stdItemTypes.Count)]; + } + while (!weaponTypes.Contains(entityType)); + entity.TypeID = entityType; + } + } + else if (stdItemTypes.Contains(entityType)) + { + T newType = stdItemTypes[Generator.Next(0, stdItemTypes.Count)]; + if (EqualityComparer.Default.Equals(newType, pistols) && (hasPistols || !isUnarmed)) + { + // Only one pistol pickup per level, and only if it's unarmed + do + { + newType = stdItemTypes[Generator.Next(0, stdItemTypes.Count)]; + } + while (!weaponTypes.Contains(newType) || EqualityComparer.Default.Equals(newType, pistols)); + } + entity.TypeID = newType; + } + + hasPistols = items.Any(e => EqualityComparer.Default.Equals(e.TypeID, pistols)); + } + } + + public void RandomizeItemLocations(string levelName, List items, bool isUnarmed) + { + if (!Settings.RandomizeItemPositions) + { + return; + } + + List stdItemTypes = GetStandardItemTypes(); + List excludedItems = GetExcludedItems(levelName); + E unarmedPistols = isUnarmed ? GetUnarmedLevelPistols(levelName, items) : null; + + for (int i = 0; i < items.Count; i++) + { + E entity = items[i]; + if (excludedItems.Contains(i) + || !stdItemTypes.Contains(entity.TypeID) + || entity == unarmedPistols + || ItemFactory.IsItemLocked(levelName, i)) + { + continue; + } + + _picker.RandomizePickupLocation(entity); + ItemMoved(entity); + } + } + + public int? EnforceOneLimit(string levelName, List items, bool isUnarmed) + { + if (Settings.RandoItemDifficulty != ItemDifficulty.OneLimit) + { + return null; + } + + List stdItemTypes = GetStandardItemTypes(); + List excludedItems = GetExcludedItems(levelName); + HashSet uniqueTypes = new(); + if (isUnarmed) + { + // These will be excluded, but track their type before looking at other items. + uniqueTypes.Add(GetPistolType()); + } + + // Look for extra utility/ammo items and hide them + int hiddenCount = 0; + E unarmedPistols = isUnarmed ? GetUnarmedLevelPistols(levelName, items) : null; + + for (int i = 0; i < items.Count; i++) + { + E entity = items[i]; + if (excludedItems.Contains(i) + || entity == unarmedPistols + || ItemFactory.IsItemLocked(levelName, i)) + { + continue; + } + + if ((stdItemTypes.Contains(entity.TypeID) || IsCrystalPickup(entity.TypeID)) + && !uniqueTypes.Add(entity.TypeID)) + { + ItemUtilities.HideEntity(entity); + ItemFactory.FreeItem(levelName, i); + hiddenCount++; + } + } + + return hiddenCount; + } + + public void RandomizeSprites(TRDictionary sequences, List keyItemTypes, List secretTypes) + { + if (!Settings.RandomizeItemSprites) + { + return; + } + + _spriteRandomizer ??= new() + { + StandardItemTypes = GetStandardItemTypes(), + KeyItemTypes = keyItemTypes, + SecretItemTypes = secretTypes, + RandomizeKeyItemSprites = Settings.RandomizeKeyItemSprites, + RandomizeSecretSprites = Settings.RandomizeSecretSprites, + Mode = Settings.SpriteRandoMode + }; + + _spriteRandomizer.Sequences = sequences; + _spriteRandomizer.Randomize(Generator); + } + + protected abstract List GetStandardItemTypes(); + protected abstract List GetWeaponItemTypes(); + protected abstract T GetPistolType(); + protected abstract List GetExcludedItems(string levelName); + protected abstract bool IsCrystalPickup(T type); + protected virtual void ItemMoved(E item) { } +} diff --git a/TRRandomizerCore/Randomizers/TR1/Classic/TR1EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Classic/TR1EnemyRandomizer.cs index cb5a7bc73..765d6321e 100644 --- a/TRRandomizerCore/Randomizers/TR1/Classic/TR1EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Classic/TR1EnemyRandomizer.cs @@ -164,14 +164,14 @@ private void AdjustTihocanEnding(TR1CombinedLevel level) return; } - TR1Entity pierreReplacement = level.Data.Entities[TR1ItemRandomizer.TihocanPierreIndex]; + TR1Entity pierreReplacement = level.Data.Entities[TR1ItemAllocator.TihocanPierreIndex]; if (Settings.AllowEnemyKeyDrops && TR1EnemyUtilities.CanDropItems(pierreReplacement, level)) { // Whichever enemy has taken Pierre's place will drop the items. Move the pickups to the enemy for trview lookup. - level.Script.AddItemDrops(TR1ItemRandomizer.TihocanPierreIndex, TR1ItemRandomizer.TihocanEndItems + level.Script.AddItemDrops(TR1ItemAllocator.TihocanPierreIndex, TR1ItemAllocator.TihocanEndItems .Select(e => ItemUtilities.ConvertToScriptItem(e.TypeID))); - foreach (TR1Entity drop in TR1ItemRandomizer.TihocanEndItems) + foreach (TR1Entity drop in TR1ItemAllocator.TihocanEndItems) { level.Data.Entities.Add(new() { @@ -187,7 +187,7 @@ private void AdjustTihocanEnding(TR1CombinedLevel level) else { // Add Pierre's pickups in a default place. Allows pacifist runs effectively. - level.Data.Entities.AddRange(TR1ItemRandomizer.TihocanEndItems); + level.Data.Entities.AddRange(TR1ItemAllocator.TihocanEndItems); } } diff --git a/TRRandomizerCore/Randomizers/TR1/Classic/TR1ItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Classic/TR1ItemRandomizer.cs index 7e59bf22f..535d1e6e2 100644 --- a/TRRandomizerCore/Randomizers/TR1/Classic/TR1ItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Classic/TR1ItemRandomizer.cs @@ -1,116 +1,50 @@ -using Newtonsoft.Json; -using TRGE.Core; +using TRGE.Core; using TRLevelControl.Helpers; using TRLevelControl.Model; using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; -using TRRandomizerCore.Secrets; using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers; public class TR1ItemRandomizer : BaseTR1Randomizer { - public const int TihocanPierreIndex = 82; - - public static readonly List TihocanEndItems = new() - { - new() - { - TypeID = TR1Type.Key1_S_P, - X = 30208, - Y = 2560, - Z = 91648, - Room = 110, - Intensity = 6144 - }, - new () - { - TypeID = TR1Type.ScionPiece2_S_P, - X = 34304, - Y = 2560, - Z = 91648, - Room = 110, - Intensity = 6144 - } - }; - - // The number of extra pickups to add per level - private static readonly Dictionary _extraItemCounts = new() - { - [TR1LevelNames.CAVES] - = 10, // Default = 4 - [TR1LevelNames.VILCABAMBA] - = 9, // Default = 7 - [TR1LevelNames.VALLEY] - = 15, // Default = 2 - [TR1LevelNames.QUALOPEC] - = 6, // Default = 5 - [TR1LevelNames.FOLLY] - = 8, // Default = 8 - [TR1LevelNames.COLOSSEUM] - = 11, // Default = 7 - [TR1LevelNames.MIDAS] - = 4, // Default = 12 - }; - - private readonly Dictionary> _excludedLocations; - private readonly Dictionary> _pistolLocations; - - // Track the pistols so they remain a weapon type and aren't moved - private TR1Entity _unarmedLevelPistols; - - // Secret reward items handled in separate class, so track the reward entities - private TRSecretMapping _secretMapping; - - private readonly LocationPicker _picker; - private ItemSpriteRandomizer _spriteRandomizer; + private TR1ItemAllocator _allocator; public ItemFactory ItemFactory { get; set; } - public TR1ItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR1\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); + _allocator = new() + { + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; foreach (TR1ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedLevelPistols(_levelInstance); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - _secretMapping = TRSecretMapping.Get(GetResourcePath($@"TR1\SecretMapping\{_levelInstance.Name}-SecretMapping.json")); - - if (Settings.IncludeExtraPickups) - { - AddExtraPickups(_levelInstance); - } - if (Settings.RandomizeItemTypes) { - RandomizeItemTypes(_levelInstance); + RandomizeDefaultItemDrops(_levelInstance); } - if (Settings.RandomizeItemPositions) - { - RandomizeItemLocations(_levelInstance); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.RemovesWeapons); - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) + int? hiddenCount = _allocator.EnforceOneLimit(_levelInstance.Name, _levelInstance.Data.Entities, _levelInstance.Script.RemovesWeapons); + if (hiddenCount.HasValue) { - EnforceOneLimit(_levelInstance); - } + _levelInstance.Script.UnobtainablePickups ??= 0; + _levelInstance.Script.UnobtainablePickups += hiddenCount; + } - if (Settings.RandomizeItemSprites) + if (Settings.RandomizeItemPositions) { - RandomizeSprites(); + UpdateEnemyItemDrops(_levelInstance, _levelInstance.Data.Entities + .Where(e => TR1TypeUtilities.IsStandardPickupType(e.TypeID))); } SaveLevelInstance(); @@ -123,7 +57,6 @@ public override void Randomize(int seed) if (Settings.UseRecommendedCommunitySettings) { TR1Script script = ScriptEditor.Script as TR1Script; - script.Enable3dPickups = false; script.ConvertDroppedGuns = true; ScriptEditor.SaveScript(); } @@ -134,7 +67,12 @@ public void RandomizeKeyItems() foreach (TR1ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); + + CheckTihocanPierre(_levelInstance); + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.OriginalSequence); + + UpdateEnemyItemDrops(_levelInstance, _levelInstance.Data.Entities + .Where(e => TR1TypeUtilities.IsKeyItemType(e.TypeID))); SaveLevelInstance(); if (!TriggerProgress()) @@ -144,69 +82,28 @@ public void RandomizeKeyItems() } } - private void FindUnarmedLevelPistols(TR1CombinedLevel level) - { - if (level.Script.RemovesWeapons) - { - List pistolEntities = level.Data.Entities.FindAll(e => TR1TypeUtilities.IsWeaponPickup(e.TypeID)); - foreach (TR1Entity pistols in pistolEntities) - { - int match = _pistolLocations[level.Name].FindIndex - ( - location => - location.X == pistols.X && - location.Y == pistols.Y && - location.Z == pistols.Z && - location.Room == pistols.Room - ); - if (match != -1) - { - _unarmedLevelPistols = pistols; - break; - } - } - } - else - { - _unarmedLevelPistols = null; - } - } - - private void AddExtraPickups(TR1CombinedLevel level) + private static void CheckTihocanPierre(TR1CombinedLevel level) { - if (!_extraItemCounts.ContainsKey(level.Name)) + if (!level.Is(TR1LevelNames.TIHOCAN)) { return; } - List stdItemTypes = TR1TypeUtilities.GetStandardPickupTypes(); - stdItemTypes.Remove(TR1Type.Pistols_S_P); - stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); - - // Add what we can to the level. The locations and types may be further randomized depending on the selected options. - for (int i = 0; i < _extraItemCounts[level.Name]; i++) + // Enemy rando may not be selected or Pierre may have ended up at the end as usual. + // Remove his key and scion drops and place them as items so they can be randomized. + if (level.Data.Entities[TR1ItemAllocator.TihocanPierreIndex].TypeID == TR1Type.Pierre) { - if (!ItemFactory.CanCreateItem(level.Name, level.Data.Entities)) - { - break; - } - - TR1Entity newItem = ItemFactory.CreateItem(level.Name, level.Data.Entities, _picker.GetRandomLocation()); - newItem.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + level.Script.ItemDrops.Find(d => d.EnemyNum == TR1ItemAllocator.TihocanPierreIndex)?.ObjectIds + .RemoveAll(e => TR1ItemAllocator.TihocanEndItems.Select(i => ItemUtilities.ConvertToScriptItem(i.TypeID)).Contains(e)); } + level.Data.Entities.AddRange(TR1ItemAllocator.TihocanEndItems); } - public void RandomizeItemTypes(TR1CombinedLevel level) + private void RandomizeDefaultItemDrops(TR1CombinedLevel level) { - if (level.IsAssault) - { - return; - } - List stdItemTypes = TR1TypeUtilities.GetStandardPickupTypes(); - stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); // Sprite/model not available + stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); - // Randomize scripted item drops if we have default enemies. foreach (TR1Entity enemy in level.Data.Entities.Where(e => TR1TypeUtilities.IsEnemyType(e.TypeID))) { int enemyIndex = level.Data.Entities.IndexOf(enemy); @@ -220,309 +117,38 @@ public void RandomizeItemTypes(TR1CombinedLevel level) } } } - - bool hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - if (_secretMapping.RewardEntities.Contains(i)) - { - // Leave default secret rewards as they are - continue; - } - - TR1Entity entity = level.Data.Entities[i]; - TR1Type entityType = entity.TypeID; - - if (entity == _unarmedLevelPistols) - { - // Enemy rando may have changed this already to something else and allocated - // ammo to the inventory, so only change pistols. - if (entityType == TR1Type.Pistols_S_P && Settings.GiveUnarmedItems) - { - do - { - entityType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR1TypeUtilities.IsWeaponPickup(entityType)); - entity.TypeID = entityType; - } - } - else if (TR1TypeUtilities.IsStandardPickupType(entityType)) - { - TR1Type newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - if (newType == TR1Type.Pistols_S_P && (hasPistols || !level.Script.RemovesWeapons)) - { - // Only one pistol pickup per level, and only if it's unarmed - do - { - newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR1TypeUtilities.IsWeaponPickup(newType) || newType == TR1Type.Pistols_S_P); - } - entity.TypeID = newType; - } - - hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); - } - } - - public void EnforceOneLimit(TR1CombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - HashSet uniqueTypes = new(); - if (_unarmedLevelPistols != null) - { - // These will be excluded, but track their type before looking at other items. - uniqueTypes.Add(_unarmedLevelPistols.TypeID); - } - - // Look for extra utility/ammo items and hide them - level.Script.UnobtainablePickups ??= 0; - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - if (_secretMapping.RewardEntities.Contains(i) || entity == _unarmedLevelPistols) - { - // Rewards and unarmed level weapons excluded - continue; - } - - if ((TR1TypeUtilities.IsStandardPickupType(entity.TypeID) || TR1TypeUtilities.IsCrystalPickup(entity.TypeID)) - && !uniqueTypes.Add(entity.TypeID)) - { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); - level.Script.UnobtainablePickups++; - } - } } - public void RandomizeItemLocations(TR1CombinedLevel level) + private void UpdateEnemyItemDrops(TR1CombinedLevel level, IEnumerable pickups) { - if (level.IsAssault) - { - return; - } - - // TR1X allows us to keep the end-level stats accurate. All generated locations - // should be reachable, but this may be modifed in TestEnemyItemDrop where items - // are hidden. - level.Script.UnobtainablePickups = null; - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - if (_secretMapping.RewardEntities.Contains(i) - || ItemFactory.IsItemLocked(_levelInstance.Name, i)) - { - continue; - } - - TR1Entity entity = level.Data.Entities[i]; - // Move standard items only, excluding any unarmed level pistols, and reward items - if (TR1TypeUtilities.IsStandardPickupType(entity.TypeID) && entity != _unarmedLevelPistols) - { - _picker.RandomizePickupLocation(entity); - entity.Intensity = 0; - TestEnemyItemDrop(level, entity); - } - } - } - - private List GetItemLocationPool(TR1CombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR1Entity entity in level.Data.Entities) - { - if (!TR1TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - if (Settings.RandomizeSecrets) - { - //Make sure to exclude the reward room - exclusions.Add(new() - { - Room = RoomWaterUtilities.DefaultRoomCountDictionary[level.Name], - InvalidatesRoom = true - }); - } - - TR1LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - private void RandomizeKeyItems(TR1CombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - if (level.Is(TR1LevelNames.TIHOCAN)) + TRRoomSector sectorFunc(Location loc) => level.Data.GetRoomSector(loc); + foreach (TR1Entity pickup in pickups) { - // Enemy rando may not be selected or Pierre may have ended up at the - // end as usual. Remove his key and scion drops and place them as items. - if (level.Data.Entities[TihocanPierreIndex].TypeID == TR1Type.Pierre) - { - level.Script.ItemDrops.Find(d => d.EnemyNum == TihocanPierreIndex)?.ObjectIds - .RemoveAll(e => TihocanEndItems.Select(i => ItemUtilities.ConvertToScriptItem(i.TypeID)).Contains(e)); - } - level.Data.Entities.AddRange(TihocanEndItems); - } + // There may be several enemies in one spot e.g. in cloned enemy mode. Pick one + // at random for each call. Always exclude empty eggs. + Location floor = pickup.GetFloorLocation(sectorFunc); + List enemies = level.Data.Entities + .FindAll(e => TR1TypeUtilities.IsEnemyType(e.TypeID) || e.TypeID == TR1Type.AdamEgg) + .FindAll(e => e.GetFloorLocation(sectorFunc).IsEquivalent(floor)); - int sequence = GetKeyItemLevelSequence(level); - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - if (!IsMovableKeyItem(level, entity) - || ItemFactory.IsItemLocked(level.Name, i)) + if (enemies.Count == 0 || enemies.All(e => !TR1EnemyUtilities.CanDropItems(e, level))) { continue; } - bool hasPickupTrigger = LocationUtilities.HasPickupTriger(entity, i, level.Data); - _picker.RandomizeKeyItemLocation(entity, hasPickupTrigger, - sequence, level.Data.Rooms[entity.Room].Info); - - if (Settings.AllowEnemyKeyDrops && !hasPickupTrigger) + TR1Entity enemy; + do { - TestEnemyItemDrop(level, entity); + enemy = enemies[_generator.Next(0, enemies.Count)]; } - } - } + while (!TR1EnemyUtilities.CanDropItems(enemy, level)); - private int GetKeyItemLevelSequence(TR1CombinedLevel level) - { - int sequence = level.Script.OriginalSequence; - if (Settings.GameMode != GameMode.Normal && level.IsExpansion) - { - // The original sequence is always 1-based regardless of how we have - // combined, so we need to manually shift. This ensures there are no - // clashes in TR1Type between regular and expansion levels. - sequence += TR1LevelNames.AsList.Count; - } - return sequence; - } - - private void TestEnemyItemDrop(TR1CombinedLevel level, TR1Entity entity) - { - TRRoomSector sectorFunc(Location loc) => level.Data.GetRoomSector(loc); + level.Script.AddItemDrops(level.Data.Entities.IndexOf(enemy), ItemUtilities.ConvertToScriptItem(pickup.TypeID)); + ItemUtilities.HideEntity(pickup); - // There may be several enemies in one spot e.g. in cloned enemy mode. Pick one - // at random for each call of this method. Always exclude empty eggs. - - Location floor = entity.GetFloorLocation(sectorFunc); - List enemies = level.Data.Entities - .FindAll(e => TR1TypeUtilities.IsEnemyType(e.TypeID) || e.TypeID == TR1Type.AdamEgg) - .FindAll(e => e.GetFloorLocation(sectorFunc).IsEquivalent(floor)); - - if (enemies.Count == 0 || enemies.All(e => !TR1EnemyUtilities.CanDropItems(e, level))) - { - return; - } - - TR1Entity enemy; - do - { - enemy = enemies[_generator.Next(0, enemies.Count)]; - } - while (!TR1EnemyUtilities.CanDropItems(enemy, level)); - - level.Script.AddItemDrops(level.Data.Entities.IndexOf(enemy), ItemUtilities.ConvertToScriptItem(entity.TypeID)); - ItemUtilities.HideEntity(entity); - - // Retain the type for quick lookup in trview, but mark it as OOB for the stats. - if (!level.Script.UnobtainablePickups.HasValue) - { - level.Script.UnobtainablePickups = 0; + // Retain the type for quick lookup in trview, but mark it as OOB for the stats. + level.Script.UnobtainablePickups ??= 0; + level.Script.UnobtainablePickups++; } - level.Script.UnobtainablePickups++; - } - - private static bool IsMovableKeyItem(TR1CombinedLevel level, TR1Entity entity) - { - return TR1TypeUtilities.IsKeyItemType(entity.TypeID) - || (level.Is(TR1LevelNames.TIHOCAN) && entity.TypeID == TR1Type.ScionPiece2_S_P); - } - - private void RandomizeSprites() - { - if (!Settings.UseRecommendedCommunitySettings - && (ScriptEditor.Script as TR1Script).Enable3dPickups) - { - // With 3D pickups enabled, sprite randomization is meaningless - return; - } - - if (_spriteRandomizer == null) - { - _spriteRandomizer = new ItemSpriteRandomizer - { - StandardItemTypes = TR1TypeUtilities.GetStandardPickupTypes(), - RandomizeKeyItemSprites = Settings.RandomizeKeyItemSprites, - RandomizeSecretSprites = Settings.RandomizeSecretSprites, - Mode = Settings.SpriteRandoMode - }; - - // Pistol ammo sprite is not available - _spriteRandomizer.StandardItemTypes.Remove(TR1Type.PistolAmmo_S_P); -#if DEBUG - _spriteRandomizer.TextureChanged += (object sender, SpriteEventArgs e) => - { - System.Diagnostics.Debug.WriteLine(string.Format("{0}: {1} => {2}", _levelInstance.Name, e.OldSprite, e.NewSprite)); - }; -#endif - } - - // For key items, some may be used as secrets so look for entity instances of each to determine what's what - _spriteRandomizer.SecretItemTypes = new List(); - _spriteRandomizer.KeyItemTypes = new List(); - - foreach (TR1Type type in TR1TypeUtilities.GetKeyItemTypes()) - { - int typeInstanceIndex = _levelInstance.Data.Entities.FindIndex(e => e.TypeID == type); - if (typeInstanceIndex != -1) - { - if (IsSecretItem(_levelInstance.Data.Entities[typeInstanceIndex], typeInstanceIndex, _levelInstance.Data)) - { - _spriteRandomizer.SecretItemTypes.Add(type); - } - else - { - _spriteRandomizer.KeyItemTypes.Add(type); - } - } - } - - _spriteRandomizer.Sequences = _levelInstance.Data.Sprites; - _spriteRandomizer.Randomize(_generator); - } - - private static bool IsSecretItem(TR1Entity entity, int entityIndex, TR1Level level) - { - TRRoomSector sector = level.GetRoomSector(entity); - if (sector.FDIndex != 0) - { - return level.FloorData[sector.FDIndex].Find(e => e is FDTriggerEntry) is FDTriggerEntry trigger - && trigger.TrigType == FDTrigType.Pickup - && trigger.Actions[0].Parameter == entityIndex - && trigger.Actions.Find(a => a.Action == FDTrigAction.SecretFound) != null; - } - - return false; } } diff --git a/TRRandomizerCore/Randomizers/TR1/Classic/TR1SecretRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Classic/TR1SecretRandomizer.cs index c19c521d3..935624342 100644 --- a/TRRandomizerCore/Randomizers/TR1/Classic/TR1SecretRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Classic/TR1SecretRandomizer.cs @@ -387,9 +387,7 @@ private void RandomizeSecrets(TR1CombinedLevel level, List pickupTypes, _secretPicker.SectorAction = loc => level.Data.GetRoomSector(loc); _secretPicker.PlacementTestAction = loc => _placer.TestSecretPlacement(loc); - _routePicker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); + _routePicker.RoomInfos = new(level.Data.Rooms.Select(r => new ExtRoomInfo(r))); _routePicker.Initialise(level.Name, locations, Settings, _generator); List pickedLocations = _secretPicker.GetLocations(locations, Mirrorer.IsMirrored(level.Name), level.Script.NumSecrets); diff --git a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1REnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1REnemyRandomizer.cs index 96a6c1a82..cc965e3ba 100644 --- a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1REnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1REnemyRandomizer.cs @@ -126,16 +126,17 @@ private void UpdateAtlanteanPDP(TR1RCombinedLevel level, EnemyRandomizationColle DataCache.SetPDPData(level.PDPData, TR1Type.ShootingAtlantean_N, TR1Type.ShootingAtlantean_N); } - private static void AdjustTihocanEnding(TR1RCombinedLevel level) + private void AdjustTihocanEnding(TR1RCombinedLevel level) { if (!level.Is(TR1LevelNames.TIHOCAN) - || _tihocanEndEnemies.Any(e => level.Data.Entities[e].TypeID == TR1Type.Pierre)) + || _tihocanEndEnemies.Any(e => level.Data.Entities[e].TypeID == TR1Type.Pierre) + || (Settings.RandomizeItems && Settings.IncludeKeyItems)) { return; } // Add Pierre's pickups in a default place. Allows pacifist runs effectively. - level.Data.Entities.AddRange(TR1ItemRandomizer.TihocanEndItems); + level.Data.Entities.AddRange(TR1ItemAllocator.TihocanEndItems); } private void AddUnarmedLevelAmmo(TR1RCombinedLevel level) diff --git a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RItemRandomizer.cs index c9cf618bd..c0bd794c2 100644 --- a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RItemRandomizer.cs @@ -1,57 +1,33 @@ -using Newtonsoft.Json; -using TRGE.Core; +using TRGE.Core; using TRLevelControl.Helpers; using TRLevelControl.Model; using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; -using TRRandomizerCore.Secrets; -using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers; public class TR1RItemRandomizer : BaseTR1RRandomizer { - private readonly Dictionary> _excludedLocations, _pistolLocations; - private readonly LocationPicker _picker; - - private TRSecretMapping _secretMapping; - private TR1Entity _unarmedLevelPistols; + private TR1ItemAllocator _allocator; public ItemFactory ItemFactory { get; set; } - public TR1RItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR1\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); + _allocator = new(true) + { + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedLevelPistols(_levelInstance); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - _secretMapping = TRSecretMapping.Get(GetResourcePath($@"TR1\SecretMapping\{_levelInstance.Name}-SecretMapping.json")); - if (Settings.RandomizeItemTypes) - { - RandomizeItemTypes(_levelInstance); - } - - if (Settings.RandomizeItemPositions) - { - RandomizeItemLocations(_levelInstance); - } - - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - EnforceOneLimit(_levelInstance); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.RemovesWeapons); + _allocator.EnforceOneLimit(_levelInstance.Name, _levelInstance.Data.Entities, _levelInstance.Script.RemovesWeapons); SaveLevelInstance(); if (!TriggerProgress()) @@ -66,7 +42,9 @@ public void RandomizeKeyItems() foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); + + CheckTihocanPierre(_levelInstance); + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.OriginalSequence); SaveLevelInstance(); if (!TriggerProgress()) @@ -76,177 +54,14 @@ public void RandomizeKeyItems() } } - private void FindUnarmedLevelPistols(TR1RCombinedLevel level) - { - if (level.Script.RemovesWeapons) - { - _unarmedLevelPistols = level.Data.Entities.Find( - e => TR1TypeUtilities.IsWeaponPickup(e.TypeID) - && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); - } - else - { - _unarmedLevelPistols = null; - } - } - - private List GetItemLocationPool(TR1RCombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR1Entity entity in level.Data.Entities) - { - if (!TR1TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - TR1LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - public void RandomizeItemTypes(TR1RCombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - List stdItemTypes = TR1TypeUtilities.GetStandardPickupTypes(); - stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); - - bool hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - TR1Type entityType = entity.TypeID; - if (!TR1TypeUtilities.IsStandardPickupType(entityType) || _secretMapping.RewardEntities.Contains(i)) - { - continue; - } - - if (entity == _unarmedLevelPistols) - { - if (entityType == TR1Type.Pistols_S_P && Settings.GiveUnarmedItems) - { - do - { - entityType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR1TypeUtilities.IsWeaponPickup(entityType)); - entity.TypeID = entityType; - } - } - else if (TR1TypeUtilities.IsStandardPickupType(entityType)) - { - TR1Type newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - if (newType == TR1Type.Pistols_S_P && (hasPistols || !level.Script.RemovesWeapons)) - { - do - { - newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR1TypeUtilities.IsWeaponPickup(newType) || newType == TR1Type.Pistols_S_P); - } - entity.TypeID = newType; - } - - hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); - } - } - - public void RandomizeItemLocations(TR1RCombinedLevel level) + private static void CheckTihocanPierre(TR1RCombinedLevel level) { - if (level.IsAssault) + if (!level.Is(TR1LevelNames.TIHOCAN) + || level.Data.Entities[TR1ItemAllocator.TihocanPierreIndex].TypeID == TR1Type.Pierre) { return; } - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - if (!TR1TypeUtilities.IsStandardPickupType(entity.TypeID) - || _secretMapping.RewardEntities.Contains(i) - || ItemFactory.IsItemLocked(level.Name, i) - || entity == _unarmedLevelPistols) - { - continue; - } - - _picker.RandomizePickupLocation(entity); - entity.Intensity = 0; - } - } - - public void EnforceOneLimit(TR1RCombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - HashSet uniqueTypes = new(); - if (_unarmedLevelPistols != null) - { - uniqueTypes.Add(_unarmedLevelPistols.TypeID); - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - if (_secretMapping.RewardEntities.Contains(i) || entity == _unarmedLevelPistols) - { - continue; - } - - if ((TR1TypeUtilities.IsStandardPickupType(entity.TypeID) || TR1TypeUtilities.IsCrystalPickup(entity.TypeID)) - && !uniqueTypes.Add(entity.TypeID)) - { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); - } - } - } - - private void RandomizeKeyItems(TR1RCombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - if (level.Is(TR1LevelNames.TIHOCAN) - && level.Data.Entities[TR1ItemRandomizer.TihocanPierreIndex].TypeID != TR1Type.Pierre) - { - level.Data.Entities.AddRange(TR1ItemRandomizer.TihocanEndItems); - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR1Entity entity = level.Data.Entities[i]; - if (!IsMovableKeyItem(level, entity) - || ItemFactory.IsItemLocked(level.Name, i)) - { - continue; - } - - bool hasPickupTrigger = LocationUtilities.HasPickupTriger(entity, i, level.Data); - _picker.RandomizeKeyItemLocation(entity, hasPickupTrigger, - level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); - } - } - - private static bool IsMovableKeyItem(TR1RCombinedLevel level, TR1Entity entity) - { - return TR1TypeUtilities.IsKeyItemType(entity.TypeID) - || (level.Is(TR1LevelNames.TIHOCAN) && entity.TypeID == TR1Type.ScionPiece2_S_P); + level.Data.Entities.AddRange(TR1ItemAllocator.TihocanEndItems); } } diff --git a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RSecretRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RSecretRandomizer.cs index 86b0095bd..5565b86d4 100644 --- a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RSecretRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RSecretRandomizer.cs @@ -149,9 +149,7 @@ private void RandomizeSecrets(TR1RCombinedLevel level, List pickupTypes _secretPicker.SectorAction = loc => level.Data.GetRoomSector(loc); _secretPicker.PlacementTestAction = loc => _placer.TestSecretPlacement(loc); - _routePicker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); + _routePicker.RoomInfos = new(level.Data.Rooms.Select(r => new ExtRoomInfo(r))); _routePicker.Initialise(level.Name, locations, Settings, _generator); List pickedLocations = _secretPicker.GetLocations(locations, false, level.Script.NumSecrets); diff --git a/TRRandomizerCore/Randomizers/TR1/Shared/TR1ItemAllocator.cs b/TRRandomizerCore/Randomizers/TR1/Shared/TR1ItemAllocator.cs new file mode 100644 index 000000000..2868d40b8 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1/Shared/TR1ItemAllocator.cs @@ -0,0 +1,230 @@ +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Secrets; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR1ItemAllocator : ItemAllocator +{ + public const int TihocanPierreIndex = 82; + + public static readonly List TihocanEndItems = new() + { + new() + { + TypeID = TR1Type.Key1_S_P, + X = 30208, + Y = 2560, + Z = 91648, + Room = 110, + Intensity = 6144 + }, + new () + { + TypeID = TR1Type.ScionPiece2_S_P, + X = 34304, + Y = 2560, + Z = 91648, + Room = 110, + Intensity = 6144 + } + }; + + private static readonly Dictionary _extraItemCounts = new() + { + [TR1LevelNames.CAVES] + = 10, // Default = 4 + [TR1LevelNames.VILCABAMBA] + = 9, // Default = 7 + [TR1LevelNames.VALLEY] + = 15, // Default = 2 + [TR1LevelNames.QUALOPEC] + = 6, // Default = 5 + [TR1LevelNames.FOLLY] + = 8, // Default = 8 + [TR1LevelNames.COLOSSEUM] + = 11, // Default = 7 + [TR1LevelNames.MIDAS] + = 4, // Default = 12 + }; + + private readonly bool _remaster; + + public TR1ItemAllocator(bool remaster = false) + : base(TRGameVersion.TR1) + { + _remaster = remaster; + } + + protected override List GetExcludedItems(string levelName) + { + TRSecretMapping mapping = TRSecretMapping.Get($@"Resources\TR1\SecretMapping\{levelName}-SecretMapping.json"); + return mapping?.RewardEntities ?? new(); + } + + protected override TR1Type GetPistolType() + => TR1Type.Pistols_S_P; + + protected override List GetStandardItemTypes() + { + List stdItemTypes = TR1TypeUtilities.GetStandardPickupTypes(); + stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); + return stdItemTypes; + } + + protected override List GetWeaponItemTypes() + => TR1TypeUtilities.GetWeaponPickups(); + + protected override bool IsCrystalPickup(TR1Type type) + => type == TR1Type.SavegameCrystal_P; + + protected override void ItemMoved(TR1Entity item) + => item.Intensity = 0; + + public void RandomizeItems(string levelName, TR1Level level, bool isUnarmed) + { + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, false), Settings, Generator); + + AddExtraPickups(levelName, level.Entities); + RandomizeItemTypes(levelName, level.Entities, isUnarmed); + RandomizeItemLocations(levelName, level.Entities, isUnarmed); + RandomizeSprites(level); + } + + public void RandomizeKeyItems(string levelName, TR1Level level, int originalSequence) + { + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level); + _picker.RoomInfos = new(level.Rooms.Select(r => new ExtRoomInfo(r))); + + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, true), Settings, Generator); + + int sequence = GetKeyItemLevelSequence(levelName, originalSequence); + for (int i = 0; i < level.Entities.Count; i++) + { + TR1Entity entity = level.Entities[i]; + if (!IsMovableKeyItem(levelName, entity) + || ItemFactory.IsItemLocked(levelName, i)) + { + continue; + } + + bool hasPickupTrigger = LocationUtilities.HasPickupTriger(entity, i, level); + _picker.RandomizeKeyItemLocation(entity, hasPickupTrigger, + sequence, level.Rooms[entity.Room].Info); + } + } + + private int GetKeyItemLevelSequence(string levelName, int originalSequence) + { + if (Settings.GameMode != GameMode.Normal && TR1LevelNames.AsListGold.Contains(levelName)) + { + // The original sequence is always 1-based regardless of how we have + // combined, so we need to manually shift. This ensures there are no + // clashes in TR1Type between regular and expansion levels. + originalSequence += TR1LevelNames.AsList.Count; + } + return originalSequence; + } + + private static bool IsMovableKeyItem(string levelName, TR1Entity entity) + { + return TR1TypeUtilities.IsKeyItemType(entity.TypeID) + || (levelName == TR1LevelNames.TIHOCAN && entity.TypeID == TR1Type.ScionPiece2_S_P); + } + + private List GetItemLocationPool(string levelName, TR1Level level, bool keyItemMode) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(levelName)) + { + exclusions.AddRange(_excludedLocations[levelName]); + } + + exclusions.AddRange(level.Entities + .Where(e => !TR1TypeUtilities.CanSharePickupSpace(e.TypeID)) + .Select(e => e.GetFloorLocation(loc => level.GetRoomSector(loc)))); + + if (Settings.RandomizeSecrets + && !_remaster // Eliminate and make UseRewardRooms setting + && level.FloorData.GetActionItems(FDTrigAction.SecretFound).Any()) + { + // Make sure to exclude the reward room + exclusions.Add(new() + { + Room = RoomWaterUtilities.DefaultRoomCountDictionary[levelName], + InvalidatesRoom = true + }); + } + + TR1LocationGenerator generator = new(); + return generator.Generate(level, exclusions, keyItemMode); + } + + private void AddExtraPickups(string levelName, List allItems) + { + if (!Settings.IncludeExtraPickups || !_extraItemCounts.ContainsKey(levelName)) + { + return; + } + + List stdItemTypes = GetStandardItemTypes(); + + // Add what we can to the level. The locations and types may be further randomized depending on the selected options. + for (int i = 0; i < _extraItemCounts[levelName]; i++) + { + if (!ItemFactory.CanCreateItem(levelName, allItems)) + { + break; + } + + TR1Entity newItem = ItemFactory.CreateItem(levelName, allItems, _picker.GetRandomLocation()); + newItem.TypeID = stdItemTypes[Generator.Next(0, stdItemTypes.Count)]; + } + } + + public void RandomizeSprites(TR1Level level) + { + if (!Settings.RandomizeItemSprites) + { + return; + } + + List secretTypes = new(); + List keyItemTypes = new(); + + foreach (TR1Type type in TR1TypeUtilities.GetKeyItemTypes()) + { + int instanceIndex = level.Entities.FindIndex(e => e.TypeID == type); + if (instanceIndex != -1) + { + if (IsSecretItem(level.Entities[instanceIndex], instanceIndex, level)) + { + secretTypes.Add(type); + } + else + { + keyItemTypes.Add(type); + } + } + } + + RandomizeSprites(level.Sprites, keyItemTypes, secretTypes); + } + + private static bool IsSecretItem(TR1Entity entity, int entityIndex, TR1Level level) + { + TRRoomSector sector = level.GetRoomSector(entity); + if (sector.FDIndex != 0) + { + return level.FloorData[sector.FDIndex].Find(e => e is FDTriggerEntry) is FDTriggerEntry trigger + && trigger.TrigType == FDTrigType.Pickup + && trigger.Actions[0].Parameter == entityIndex + && trigger.Actions.Any(a => a.Action == FDTrigAction.SecretFound); + } + + return false; + } +} diff --git a/TRRandomizerCore/Randomizers/TR2/Classic/TR2EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/Classic/TR2EnemyRandomizer.cs index 7d431d7cb..fea3e189f 100644 --- a/TRRandomizerCore/Randomizers/TR2/Classic/TR2EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/Classic/TR2EnemyRandomizer.cs @@ -1,4 +1,5 @@ -using TRDataControl; +using Newtonsoft.Json; +using TRDataControl; using TRGE.Core; using TRImageControl.Packing; using TRLevelControl.Helpers; @@ -13,8 +14,21 @@ namespace TRRandomizerCore.Randomizers; public class TR2EnemyRandomizer : BaseTR2Randomizer { + private readonly Dictionary _unarmedAmmoCounts = new() + { + [TR2Type.Pistols_S_P] = 0, + [TR2Type.Shotgun_S_P] = 8, + [TR2Type.Automags_S_P] = 4, + [TR2Type.Uzi_S_P] = 4, + [TR2Type.Harpoon_S_P] = 4, + [TR2Type.M16_S_P] = 2, + [TR2Type.GrenadeLauncher_S_P] = 4, + }; + private static readonly double _cloneChance = 0.5; + private static readonly double _easyPistolChance = 0.2; + private Dictionary> _pistolLocations; private TR2EnemyAllocator _allocator; internal TR2TextureMonitorBroker TextureMonitor { get; set; } @@ -33,6 +47,7 @@ public override void Randomize(int seed) }; _allocator.Initialise(); + _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\unarmed_locations.json")); if (Settings.CrossLevelEnemies) { RandomizeEnemiesCrossLevel(); @@ -123,6 +138,7 @@ private void ApplyPostRandomization(TR2CombinedLevel level, EnemyRandomizationCo { MakeChickensUnconditional(level.Data); RandomizeEnemyMeshes(level, enemies); + AddUnarmedItems(level); } private void MakeChickensUnconditional(TR2Level level) @@ -205,6 +221,99 @@ private void AddRandomLaraClone(EnemyRandomizationCollection enemies, T } } + private void AddUnarmedItems(TR2CombinedLevel level) + { + if (!level.Script.RemovesWeapons || !Settings.GiveUnarmedItems) + { + return; + } + + TR2Entity weapon = level.Data.Entities.Find(e => + (e.TypeID == TR2Type.Pistols_S_P || TR2TypeUtilities.IsGunType(e.TypeID)) + && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); + if (weapon == null) + { + return; + } + + if (level.Is(TR2LevelNames.HOME) && Settings.RandomizeItems && Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) + { + weapon.TypeID = TR2Type.Pistols_S_P; + return; + } + + List replacementWeapons = TR2TypeUtilities.GetGunTypes(); + replacementWeapons.Add(TR2Type.Pistols_S_P); + TR2Type weaponType = replacementWeapons[_generator.Next(0, replacementWeapons.Count)]; + weapon.TypeID = weaponType; + + void AddItem(TR2Type type) + { + if (ItemFactory.CanCreateItem(level.Name, level.Data.Entities)) + { + TR2Entity item = ItemFactory.CreateItem(level.Name, level.Data.Entities, weapon.GetLocation()); + item.TypeID = type; + } + } + + uint ammoCount = _unarmedAmmoCounts[weaponType]; + if (Settings.CrossLevelEnemies) + { + // Create a score based on the number and difficulty of triggered enemies. + List enemies = level.Data.Entities.FindAll(e => TR2TypeUtilities.IsEnemyType(e.TypeID)); + enemies.RemoveAll(e => !level.Data.FloorData.GetEntityTriggers(level.Data.Entities.IndexOf(e)).Any()); + if (level.Is(TR2LevelNames.HOME)) + { + enemies.Add(new() { TypeID = TR2Type.ShotgunGoon }); + } + + EnemyDifficulty difficulty = TR2EnemyUtilities.GetEnemyDifficulty(level.GetEnemyEntities()); + ammoCount *= (uint)difficulty; + + if (difficulty > EnemyDifficulty.Easy + || weaponType == TR2Type.Harpoon_S_P + || (weaponType == TR2Type.GrenadeLauncher_S_P && (level.Is(TR2LevelNames.CHICKEN) || level.Is(TR2LevelNames.HOME))) + || _generator.NextDouble() < _easyPistolChance) + { + AddItem(TR2Type.Pistols_S_P); + } + + if (difficulty == EnemyDifficulty.Medium || difficulty == EnemyDifficulty.Hard) + { + AddItem(TR2Type.SmallMed_S_P); + } + if (difficulty > EnemyDifficulty.Medium) + { + AddItem(TR2Type.LargeMed_S_P); + } + if (difficulty == EnemyDifficulty.VeryHard) + { + AddItem(TR2Type.LargeMed_S_P); + } + } + else if (level.Is(TR2LevelNames.LAIR)) + { + ammoCount *= 6; + } + + if (ammoCount == 0) + { + return; + } + + TR2Type ammoType = TR2TypeUtilities.GetWeaponAmmo(weaponType); + if (level.Is(TR2LevelNames.HOME)) + { + // Just convert every ammo pickup to match the gun, no need for script extras + level.Data.Entities.FindAll(e => TR2TypeUtilities.IsAmmoType(e.TypeID)) + .ForEach(e => e.TypeID = ammoType); + } + else + { + level.Script.AddStartInventoryItem(ItemUtilities.ConvertToScriptItem(ammoType), ammoCount); + } + } + internal class EnemyProcessor : AbstractProcessorThread { private const int _maxPackingAttempts = 5; diff --git a/TRRandomizerCore/Randomizers/TR2/Classic/TR2ItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/Classic/TR2ItemRandomizer.cs index a1b2a1b5c..d810172ac 100644 --- a/TRRandomizerCore/Randomizers/TR2/Classic/TR2ItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/Classic/TR2ItemRandomizer.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using TRDataControl; +using TRDataControl; using TRGE.Core; using TRGE.Core.Item.Enums; using TRImageControl.Packing; @@ -22,80 +21,27 @@ public class TR2ItemRandomizer : BaseTR2Randomizer Room = 43 }; + private TR2ItemAllocator _allocator; + internal TR2TextureMonitorBroker TextureMonitor { get; set; } public ItemFactory ItemFactory { get; set; } - // This replaces plane cargo index as TRGE may have randomized the weaponless level(s), but will also have injected pistols - // into predefined locations. See FindUnarmedPistolsLocation below. - private int _unarmedLevelPistolIndex; - private readonly Dictionary> _excludedLocations; - private readonly Dictionary> _pistolLocations; - - private readonly LocationPicker _picker; - private ItemSpriteRandomizer _spriteRandomizer; - - public TR2ItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR2\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); - - foreach (TR2ScriptedLevel lvl in Levels) + _allocator = new() { - LoadLevelInstance(lvl); - - FindUnarmedPistolsLocation(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - - if (Settings.RandomizeItemPositions) - { - RandomizeItemLocations(_levelInstance); - } - - if (Settings.RandomizeItemTypes) - { - RandomizeItemTypes(_levelInstance); - } - - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - EnforceOneLimit(_levelInstance); - } - - RandomizeVehicles(); - - SaveLevelInstance(); - if (!TriggerProgress()) - { - break; - } - } - } + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; - // Called post enemy randomization if used to allow accurate enemy scoring - public void RandomizeAmmo() - { foreach (TR2ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedPistolsLocation(); - - if (lvl.RemovesWeapons) - { - RandomizeUnarmedLevelWeapon(); - } - - if (lvl.Is(TR2LevelNames.HOME)) - { - PopulateHSHCloset(); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script); + RandomizeVehicles(_levelInstance); SaveLevelInstance(); if (!TriggerProgress()) @@ -110,7 +56,8 @@ public void RandomizeKeyItems() foreach (TR2ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); + AdjustSeraphContinuity(_levelInstance); + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.OriginalSequence); SaveLevelInstance(); if (!TriggerProgress()) @@ -120,12 +67,14 @@ public void RandomizeKeyItems() } } - public void RandomizeLevelsSprites() + public void RandomizeSprites() { + // This remains separate as it must be performed following all other texture work due to + // TR2 texture deduplication. foreach (TR2ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeSprites(); + _allocator.RandomizeSprites(_levelInstance.Data.Sprites, TR2TypeUtilities.GetKeyItemTypes(), TR2TypeUtilities.GetSecretTypes()); SaveLevelInstance(); if (!TriggerProgress()) @@ -135,104 +84,6 @@ public void RandomizeLevelsSprites() } } - private List GetItemLocationPool(TR2CombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR2Entity entity in level.Data.Entities) - { - if (!TR2TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - TR2LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - private void RandomizeSprites() - { - // If the _spriteRandomizer doesn't exists it gets fed all the settings of the rando and Lists of the game once. - if (_spriteRandomizer == null) - { - _spriteRandomizer = new ItemSpriteRandomizer - { - StandardItemTypes = TR2TypeUtilities.GetGunTypes().Concat(TR2TypeUtilities.GetAmmoTypes()).ToList(), - KeyItemTypes = TR2TypeUtilities.GetKeyItemTypes(), - SecretItemTypes = TR2TypeUtilities.GetSecretTypes(), - RandomizeKeyItemSprites = Settings.RandomizeKeyItemSprites, - RandomizeSecretSprites = Settings.RandomizeSecretSprites, - Mode = Settings.SpriteRandoMode - }; -#if DEBUG - _spriteRandomizer.TextureChanged += (object sender, SpriteEventArgs e) => - { - System.Diagnostics.Debug.WriteLine(string.Format("{0}: {1} => {2}", _levelInstance.Name, e.OldSprite, e.NewSprite)); - }; -#endif - } - - _spriteRandomizer.Sequences = _levelInstance.Data.Sprites; - _spriteRandomizer.Randomize(_generator); - } - - public void RandomizeItemLocations(TR2CombinedLevel level) - { - if (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo)) - { - return; - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (!TR2TypeUtilities.IsStandardPickupType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i) - || i == _unarmedLevelPistolIndex) - { - continue; - } - - _picker.RandomizePickupLocation(entity); - entity.Intensity1 = entity.Intensity2 = -1; - } - } - - private void RandomizeKeyItems(TR2CombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - // The Seraph may be removed from The Deck and added to Barkhang. Do that first to allow - // its location to be changed. - AdjustSeraphContinuity(level); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (!TR2TypeUtilities.IsKeyItemType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i)) - { - continue; - } - - _picker.RandomizeKeyItemLocation( - entity, LocationUtilities.HasPickupTriger(entity, i, level.Data), - level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); - entity.Intensity1 = entity.Intensity2 = -1; - } - } - private void AdjustSeraphContinuity(TR2CombinedLevel level) { if (!Settings.MaintainKeyContinuity) @@ -284,448 +135,73 @@ private void AdjustSeraphContinuity(TR2CombinedLevel level) } } - private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR2CombinedLevel level) - { - // Make sure if we're placing on the same tile as an enemy, that the - // enemy can drop the item. - TR2Entity enemy = level.Data.Entities - .FindAll(e => TR2TypeUtilities.IsEnemyType(e.TypeID)) - .Find(e => e.GetLocation().IsEquivalent(location)); - - return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR2TypeUtilities.CanDropPickups - ( - TR2TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), - Settings.RandomizeEnemies && !Settings.ProtectMonks, - Settings.RandomizeEnemies && Settings.UnconditionalChickens - )); - } - - private void RandomizeItemTypes(TR2CombinedLevel level) - { - if (level.IsAssault - || (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo))) - { - return; - } - - List stdItemTypes = TR2TypeUtilities.GetStandardPickupTypes(); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (i == _unarmedLevelPistolIndex) - { - // Handled separately in RandomizeAmmo - continue; - } - else if (stdItemTypes.Contains(entity.TypeID)) - { - entity.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - } - } - - private void EnforceOneLimit(TR2CombinedLevel level) + private void RandomizeVehicles(TR2CombinedLevel level) { - HashSet uniqueTypes = new(); - - // Look for extra utility/ammo items and hide them - for (int i = 0; i < level.Data.Entities.Count; i++) + Dictionary> vehicleLocations = new(); + if (VehicleUtilities.HasLocations(level.Name, TR2Type.Boat)) { - TR2Entity entity = level.Data.Entities[i]; - if (TR2TypeUtilities.IsStandardPickupType(entity.TypeID) - && !uniqueTypes.Add(entity.TypeID)) + vehicleLocations[TR2Type.Boat] = new(); + int boatCount = Math.Max(1, level.Data.Entities.Count(e => e.TypeID == TR2Type.Boat)); + for (int i = 0; i < boatCount; i++) { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); + vehicleLocations[TR2Type.Boat].Enqueue(VehicleUtilities.GetRandomLocation(level.Name, level.Data, TR2Type.Boat, _generator)); } } - } - private void FindUnarmedPistolsLocation() - { - // #66 - checks were previously performed to clean locations from previous - // randomization sessions to avoid item pollution. This is no longer required - // as randomization is now always performed on the original level files. - - // #124 Default pistol locations are no longer limited to one per level. - - _unarmedLevelPistolIndex = -1; - - if (_levelInstance.Script.RemovesWeapons && _pistolLocations.ContainsKey(_levelInstance.Name)) + if (level.IsAssault) { - int pistolIndex = _levelInstance.Data.Entities.FindIndex(e => e.TypeID == TR2Type.Pistols_S_P); - if (pistolIndex != -1) - { - // Sanity check that the location is one that we expect - TR2Entity pistols = _levelInstance.Data.Entities[pistolIndex]; - Location pistolLocation = new() - { - X = pistols.X, - Y = pistols.Y, - Z = pistols.Z, - Room = pistols.Room - }; - - int match = _pistolLocations[_levelInstance.Name].FindIndex - ( - location => - location.X == pistolLocation.X && - location.Y == pistolLocation.Y && - location.Z == pistolLocation.Z && - location.Room == pistolLocation.Room - ); - - if (match != -1) - { - _unarmedLevelPistolIndex = pistolIndex; - } - } + // Regular skidoo rando comes with enemy rando currently + vehicleLocations[TR2Type.RedSnowmobile] = new(); + vehicleLocations[TR2Type.RedSnowmobile].Enqueue(VehicleUtilities.GetRandomLocation(level.Name, level.Data, TR2Type.RedSnowmobile, _generator)); } - } - - private readonly Dictionary _startingAmmoToGive = new() - { - {TR2Type.Shotgun_S_P, 8}, - {TR2Type.Automags_S_P, 4}, - {TR2Type.Uzi_S_P, 4}, - {TR2Type.Harpoon_S_P, 4}, // #149 Agreed that a low number of harpoons will be given for unarmed levels, but pistols will also be included - {TR2Type.M16_S_P, 2}, - {TR2Type.GrenadeLauncher_S_P, 4}, - }; - private void RandomizeUnarmedLevelWeapon() - { - if (!Settings.GiveUnarmedItems) + if (vehicleLocations.Count == 0) { return; } - //Is there something in the unarmed level pistol location? - if (_unarmedLevelPistolIndex != -1) + try { - List replacementWeapons = TR2TypeUtilities.GetGunTypes(); - replacementWeapons.Add(TR2Type.Pistols_S_P); - TR2Type weaponType = replacementWeapons[_generator.Next(0, replacementWeapons.Count)]; - - // force pistols for OneLimit and then we're done - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - return; - } - - if (_levelInstance.Is(TR2LevelNames.CHICKEN)) - { - // Grenade Launcher and Harpoon cannot trigger the bells in Ice Palace - while (weaponType.Equals(TR2Type.GrenadeLauncher_S_P) || weaponType.Equals(TR2Type.Harpoon_S_P)) - { - weaponType = replacementWeapons[_generator.Next(0, replacementWeapons.Count)]; - } - } - - uint ammoToGive = 0; - bool addPistols = false; - uint smallMediToGive = 0; - uint largeMediToGive = 0; - - if (_startingAmmoToGive.ContainsKey(weaponType)) - { - ammoToGive = _startingAmmoToGive[weaponType]; - if (Settings.RandomizeEnemies && Settings.CrossLevelEnemies) - { - // Create a score based on each type of enemy in this level and increase the ammo count based on this - EnemyDifficulty difficulty = TR2EnemyUtilities.GetEnemyDifficulty(_levelInstance.GetEnemyEntities()); - ammoToGive *= (uint)difficulty; - - // Depending on how difficult the enemy combination is, allocate some extra helpers. - addPistols = difficulty > EnemyDifficulty.Easy; - - if (difficulty == EnemyDifficulty.Medium || difficulty == EnemyDifficulty.Hard) - { - smallMediToGive++; - } - if (difficulty > EnemyDifficulty.Medium) - { - largeMediToGive++; - } - if (difficulty == EnemyDifficulty.VeryHard) - { - largeMediToGive++; - } - } - else if (_levelInstance.Is(TR2LevelNames.LAIR)) - { - ammoToGive *= 6; - } - } - - TR2Entity unarmedLevelWeapons = _levelInstance.Data.Entities[_unarmedLevelPistolIndex]; - unarmedLevelWeapons.TypeID = weaponType; - - if (weaponType != TR2Type.Pistols_S_P) - { - //#68 - Provide some additional ammo for a weapon if not pistols - AddUnarmedLevelAmmo(GetWeaponAmmo(weaponType), ammoToGive); - - // If we haven't decided to add the pistols (i.e. for enemy difficulty) - // add a 1/3 chance of getting them anyway. #149 If the harpoon is being - // given, the pistols will be included. - if (addPistols || weaponType == TR2Type.Harpoon_S_P || _generator.Next(0, 3) == 0) - { - CopyEntity(unarmedLevelWeapons, TR2Type.Pistols_S_P); - } - } - - for (int i = 0; i < smallMediToGive; i++) - { - CopyEntity(unarmedLevelWeapons, TR2Type.SmallMed_S_P); - } - for (int i = 0; i < largeMediToGive; i++) + TR2DataImporter importer = new() { - CopyEntity(unarmedLevelWeapons, TR2Type.LargeMed_S_P); - } - } - } - - private void CopyEntity(TR2Entity entity, TR2Type newType) - { - if (_levelInstance.Data.Entities.Count < _levelInstance.GetMaximumEntityLimit()) - { - TR2Entity copy = (TR2Entity)entity.Clone(); - copy.TypeID = newType; - _levelInstance.Data.Entities.Add(copy); + Level = level.Data, + LevelName = level.Name, + TypesToImport = new(vehicleLocations.Keys), + DataFolder = GetResourcePath(@"TR2\Objects"), + TextureMonitor = TextureMonitor.CreateMonitor(level.Name, new(vehicleLocations.Keys)) + }; + importer.Import(); } - } - - private static TR2Type GetWeaponAmmo(TR2Type weapon) - { - return weapon switch - { - TR2Type.Shotgun_S_P => TR2Type.ShotgunAmmo_S_P, - TR2Type.Automags_S_P => TR2Type.AutoAmmo_S_P, - TR2Type.Uzi_S_P => TR2Type.UziAmmo_S_P, - TR2Type.Harpoon_S_P => TR2Type.HarpoonAmmo_S_P, - TR2Type.M16_S_P => TR2Type.M16Ammo_S_P, - TR2Type.GrenadeLauncher_S_P => TR2Type.Grenades_S_P, - _ => TR2Type.PistolAmmo_S_P, - }; - } - - private void AddUnarmedLevelAmmo(TR2Type ammoType, uint count) - { - // #216 - Avoid bloating the entity list by creating additional pickups - // and instead add the extra ammo directly to the inventory. - _levelInstance.Script.AddStartInventoryItem(ItemUtilities.ConvertToScriptItem(ammoType), count); - } - - private void PopulateHSHCloset() - { - // Special handling for HSH to keep everything in the closet, but only if Lara loses guns or ammo. - if (!_levelInstance.Script.RemovesAmmo && !_levelInstance.Script.RemovesWeapons) + catch (PackingException) { + // Silently ignore failed imports for now as these are nice-to-have only return; } - List replacementWeapons = TR2TypeUtilities.GetGunTypes(); - if (_levelInstance.Script.RemovesWeapons) - { - replacementWeapons.Add(TR2Type.Pistols_S_P); - } - - // Pick a new weapon, but exclude the grenade launcher because it affects the kill count - TR2Type replacementWeapon; - do - { - replacementWeapon = replacementWeapons[_generator.Next(0, replacementWeapons.Count)]; - } - while (replacementWeapon == TR2Type.GrenadeLauncher_S_P); - - TR2Type replacementAmmo = GetWeaponAmmo(replacementWeapon); - - TR2Entity harpoonWeapon = null; - List oneOfEachType = new(); - foreach (TR2Entity entity in _levelInstance.Data.Entities) + foreach (var (type, locations) in vehicleLocations) { - if (!TR2TypeUtilities.IsAnyPickupType(entity.TypeID)) + List existingVehicles = level.Data.Entities.FindAll(e => e.TypeID == type); + while (existingVehicles.Count < locations.Count) { - continue; - } - - TR2Type entityType = entity.TypeID; - if (TR2TypeUtilities.IsGunType(entityType)) - { - entity.TypeID = replacementWeapon; - - if (replacementWeapon == TR2Type.Harpoon_S_P || (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit && replacementWeapon != TR2Type.Pistols_S_P)) + if (!ItemFactory.CanCreateItem(level.Name, level.Data.Entities)) { - harpoonWeapon = entity; + break; } - } - else if (TR2TypeUtilities.IsAmmoType(entityType) && replacementWeapon != TR2Type.Pistols_S_P) - { - entity.TypeID = replacementAmmo; - } - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - // look for extra utility/ammo items and hide them - TR2Type eType = entity.TypeID; - if (TR2TypeUtilities.IsUtilityType(eType) || - TR2TypeUtilities.IsGunType(eType)) - { - if (oneOfEachType.Contains(eType)) - { - ItemUtilities.HideEntity(entity); - } - else - oneOfEachType.Add(entity.TypeID); - } + TR2Entity vehicle = ItemFactory.CreateItem(level.Name, level.Data.Entities); + vehicle.TypeID = type; + existingVehicles.Add(vehicle); } - } - - // if weapon is harpoon OR difficulty is OneLimit, spawn pistols as well (see #149) - if (harpoonWeapon != null) - CopyEntity(harpoonWeapon, TR2Type.Pistols_S_P); - } - - private void RandomizeVehicles() - { - // For now, we only add the boat if it has a location defined for a level. The skidoo is added - // to levels that have MercSnowMobDriver present (see EnemyRandomizer) but we could alter this - // to include it potentially in any level. - // This perhaps needs better tracking, for example if every level has a vehicle location defined - // we might not necessarily want to include it in every level. - Dictionary vehicles = new(); - PopulateVehicleLocation(TR2Type.Boat, vehicles); - if (_levelInstance.IsAssault) - { - // The assault course doesn't have enemies i.e. MercSnowMobDriver, so just add the skidoo too - PopulateVehicleLocation(TR2Type.RedSnowmobile, vehicles); - } - - int entityLimit = _levelInstance.GetMaximumEntityLimit(); - - List boatToMove = _levelInstance.Data.Entities.FindAll(e => e.TypeID == TR2Type.Boat); - - if (vehicles.Count == 0 || vehicles.Count - boatToMove.Count + _levelInstance.Data.Entities.Count > entityLimit) - { - return; - } - - TR2DataImporter importer = new() - { - Level = _levelInstance.Data, - LevelName = _levelInstance.Name, - ClearUnusedSprites = false, - TypesToImport = new(vehicles.Keys), - DataFolder = GetResourcePath(@"TR2\Objects"), - TextureMonitor = TextureMonitor.CreateMonitor(_levelInstance.Name, vehicles.Keys.ToList()) - }; - - - try - { - importer.Import(); - // looping on boats and or skidoo - foreach (TR2Type entity in vehicles.Keys) + foreach (TR2Entity vehicle in existingVehicles) { - if (_levelInstance.Data.Entities.Count == entityLimit) - { - break; - } - - Location location = vehicles[entity]; - - if (entity == TR2Type.Boat) + Location location = locations.Dequeue(); + if (type == TR2Type.Boat) { - location = RoomWaterUtilities.MoveToTheSurface(location, _levelInstance.Data); - } - - if (boatToMove.Count == 0) - { - //Creation new entity - _levelInstance.Data.Entities.Add(new() - { - TypeID = entity, - Room = location.Room, - X = location.X, - Y = location.Y, - Z = location.Z, - Angle = location.Angle, - Flags = 0, - Intensity1 = -1, - Intensity2 = -1 - }); - } - else - { - //I am in a level with 1 or 2 boat(s) to move - for (int i = 0; i < boatToMove.Count; i++) - { - if (i == 0) // for the first one i take the vehicle value - { - TR2Entity boat = boatToMove[i]; - - boat.Room = location.Room; - boat.X = location.X; - boat.Y = location.Y; - boat.Z = location.Z; - boat.Angle = location.Angle; - boat.Flags = 0; - boat.Intensity1 = -1; - boat.Intensity2 = -1; - - } - else // I have to find another location that is different - { - Location location2ndBoat = vehicles[entity]; - int checkCount = 0; - while (location2ndBoat.IsEquivalent(vehicles[entity]) && checkCount < 5)//compare locations in bottom of water ( authorize 5 round max in case there is only 1 valid location) - { - location2ndBoat = VehicleUtilities.GetRandomLocation(_levelInstance.Name, _levelInstance.Data, TR2Type.Boat, _generator, false); - checkCount++; - } - - if (checkCount < 5)// If i actually found a different location I proceed (if not vanilla location it is) - { - location2ndBoat = RoomWaterUtilities.MoveToTheSurface(location2ndBoat, _levelInstance.Data); - - TR2Entity boat2 = boatToMove[i]; - - boat2.Room = location2ndBoat.Room; - boat2.X = location2ndBoat.X; - boat2.Y = location2ndBoat.Y; - boat2.Z = location2ndBoat.Z; - boat2.Angle = location2ndBoat.Angle; - boat2.Flags = 0; - boat2.Intensity1 = -1; - boat2.Intensity2 = -1; - } - - } - - } + location = RoomWaterUtilities.MoveToTheSurface(location, level.Data); } + vehicle.SetLocation(location); } } - catch (PackingException) - { - // Silently ignore failed imports for now as these are nice-to-have only - } - } - - /// - /// Populate (or add in) the locationMap with a random location designed for the specific entity type in parameter - /// - /// Type of the entity - /// Dictionnary EntityType/location - private void PopulateVehicleLocation(TR2Type entity, Dictionary locationMap) - { - Location location = VehicleUtilities.GetRandomLocation(_levelInstance.Name, _levelInstance.Data, entity, _generator); - if (location != null) - { - locationMap[entity] = location; - } } } diff --git a/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RItemRandomizer.cs index cab3daa10..b32a6e87e 100644 --- a/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RItemRandomizer.cs @@ -1,54 +1,29 @@ -using Newtonsoft.Json; -using TRGE.Core; -using TRLevelControl.Helpers; +using TRGE.Core; using TRLevelControl.Model; using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers; public class TR2RItemRandomizer : BaseTR2RRandomizer { - private readonly Dictionary> _excludedLocations, _pistolLocations; - private readonly LocationPicker _picker; - - private TR2Entity _unarmedLevelPistols; + private TR2ItemAllocator _allocator; public ItemFactory ItemFactory { get; set; } - public TR2RItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR2\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); + _allocator = new() + { + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedLevelPistols(_levelInstance); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - - if (Settings.RandomizeItemTypes) - { - RandomizeItemTypes(_levelInstance); - } - - if (Settings.RandomizeItemPositions) - { - RandomizeItemLocations(_levelInstance); - } - - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - EnforceOneLimit(_levelInstance); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script); SaveLevelInstance(); if (!TriggerProgress()) @@ -63,158 +38,21 @@ public void RandomizeKeyItems() foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); - - SaveLevelInstance(); - if (!TriggerProgress()) - { - break; - } - } - } - - private void FindUnarmedLevelPistols(TR2RCombinedLevel level) - { - if (level.Script.RemovesWeapons) - { - _unarmedLevelPistols = level.Data.Entities.Find( - e => (TR2TypeUtilities.IsGunType(e.TypeID) || e.TypeID == TR2Type.Pistols_S_P) - && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); - } - else - { - _unarmedLevelPistols = null; - } - } - - private List GetItemLocationPool(TR2RCombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR2Entity entity in level.Data.Entities) - { - if (!TR2TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - TR2LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - private void RandomizeItemTypes(TR2RCombinedLevel level) - { - if (level.IsAssault - || (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo))) - { - return; - } - - List stdItemTypes = TR2TypeUtilities.GetStandardPickupTypes(); - IEnumerable pickups = level.Data.Entities.Where(e => e != _unarmedLevelPistols && stdItemTypes.Contains(e.TypeID)); - - foreach (TR2Entity entity in pickups) - { - entity.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - } - - public void RandomizeItemLocations(TR2RCombinedLevel level) - { - if (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo)) - { - return; - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (!TR2TypeUtilities.IsStandardPickupType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i) - || entity == _unarmedLevelPistols) - { - continue; - } - - _picker.RandomizePickupLocation(entity); - entity.Intensity1 = entity.Intensity2 = -1; - } - } - - private void EnforceOneLimit(TR2RCombinedLevel level) - { - HashSet uniqueTypes = new(); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (TR2TypeUtilities.IsStandardPickupType(entity.TypeID) - && !uniqueTypes.Add(entity.TypeID)) - { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); - } - } - } - - private void RandomizeKeyItems(TR2RCombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR2Entity entity = level.Data.Entities[i]; - if (!TR2TypeUtilities.IsKeyItemType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i)) - { - continue; - } // In OG, all puzzle2 items are switched to puzzle3 to allow the dragon to be imported everywhere. // This means routes have been defined to look for these types, so we need to flip them temporarily. // See TR2ModelAdjuster and LocationPicker.GetKeyItemID. - bool flipPuzzle2 = entity.TypeID == TR2Type.Puzzle2_S_P; - if (flipPuzzle2) - { - entity.TypeID = TR2Type.Puzzle3_S_P; - } + List puzzle2Items = _levelInstance.Data.Entities.FindAll(e => e.TypeID == TR2Type.Puzzle2_S_P); + puzzle2Items.ForEach(e => e.TypeID = TR2Type.Puzzle3_S_P); - _picker.RandomizeKeyItemLocation( - entity, LocationUtilities.HasPickupTriger(entity, i, level.Data), - level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); - entity.Intensity1 = entity.Intensity2 = -1; + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, _levelInstance.Script.OriginalSequence); + puzzle2Items.ForEach(e => e.TypeID = TR2Type.Puzzle2_S_P); - if (flipPuzzle2) + SaveLevelInstance(); + if (!TriggerProgress()) { - entity.TypeID = TR2Type.Puzzle2_S_P; + break; } } } - - private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR2RCombinedLevel level) - { - // Make sure if we're placing on the same tile as an enemy, that the - // enemy can drop the item. - TR2Entity enemy = level.Data.Entities - .FindAll(e => TR2TypeUtilities.IsEnemyType(e.TypeID)) - .Find(e => e.GetLocation().IsEquivalent(location)); - - return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR2TypeUtilities.CanDropPickups - ( - TR2TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), - Settings.RandomizeEnemies && !Settings.ProtectMonks, - Settings.RandomizeEnemies && Settings.UnconditionalChickens - )); - } } diff --git a/TRRandomizerCore/Randomizers/TR2/Shared/TR2ItemAllocator.cs b/TRRandomizerCore/Randomizers/TR2/Shared/TR2ItemAllocator.cs new file mode 100644 index 000000000..58196a3ca --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2/Shared/TR2ItemAllocator.cs @@ -0,0 +1,103 @@ +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR2ItemAllocator : ItemAllocator +{ + public TR2ItemAllocator() + : base(TRGameVersion.TR2) { } + + protected override List GetExcludedItems(string levelName) + => new(); + + protected override TR2Type GetPistolType() + => TR2Type.Pistols_S_P; + + protected override List GetStandardItemTypes() + => TR2TypeUtilities.GetStandardPickupTypes(); + + protected override List GetWeaponItemTypes() + => TR2TypeUtilities.GetGunTypes(); + + protected override bool IsCrystalPickup(TR2Type type) + => false; + + protected override void ItemMoved(TR2Entity item) + => item.Intensity1 = item.Intensity2 = -1; + + public void RandomizeItems(string levelName, TR2Level level, AbstractTRScriptedLevel scriptedLevel) + { + if (levelName == TR2LevelNames.HOME && (scriptedLevel.RemovesWeapons || scriptedLevel.RemovesAmmo)) + { + return; + } + + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, false), Settings, Generator); + + if (levelName != TR2LevelNames.ASSAULT) + { + RandomizeItemTypes(levelName, level.Entities, scriptedLevel.RemovesWeapons); + } + RandomizeItemLocations(levelName, level.Entities, scriptedLevel.RemovesWeapons); + EnforceOneLimit(levelName, level.Entities, scriptedLevel.RemovesWeapons); + } + + public void RandomizeKeyItems(string levelName, TR2Level level, int originalSequence) + { + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level); + _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, levelName, level); + _picker.RoomInfos = new(level.Rooms.Select(r => new ExtRoomInfo(r))); + + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, true), Settings, Generator); + + for (int i = 0; i < level.Entities.Count; i++) + { + TR2Entity entity = level.Entities[i]; + if (!TR2TypeUtilities.IsKeyItemType(entity.TypeID) + || ItemFactory.IsItemLocked(levelName, i)) + { + continue; + } + + _picker.RandomizeKeyItemLocation( + entity, LocationUtilities.HasPickupTriger(entity, i, level), + originalSequence, level.Rooms[entity.Room].Info); + ItemMoved(entity); + } + } + + private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, string levelName, TR2Level level) + { + // Make sure if we're placing on the same tile as an enemy, that the enemy can drop the item. + TR2Entity enemy = level.Entities + .FindAll(e => TR2TypeUtilities.IsEnemyType(e.TypeID)) + .Find(e => e.GetLocation().IsEquivalent(location)); + + return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR2TypeUtilities.CanDropPickups + ( + TR2TypeUtilities.GetAliasForLevel(levelName, enemy.TypeID), + Settings.RandomizeEnemies && !Settings.ProtectMonks, + Settings.RandomizeEnemies && Settings.UnconditionalChickens + )); + } + + private List GetItemLocationPool(string levelName, TR2Level level, bool keyItemMode) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(levelName)) + { + exclusions.AddRange(_excludedLocations[levelName]); + } + + exclusions.AddRange(level.Entities + .Where(e => !TR2TypeUtilities.CanSharePickupSpace(e.TypeID)) + .Select(e => e.GetFloorLocation(loc => level.GetRoomSector(loc)))); + + TR2LocationGenerator generator = new(); + return generator.Generate(level, exclusions, keyItemMode); + } +} diff --git a/TRRandomizerCore/Randomizers/TR2/Shared/TR2SecretAllocator.cs b/TRRandomizerCore/Randomizers/TR2/Shared/TR2SecretAllocator.cs index ac6d84d3d..f02580e9a 100644 --- a/TRRandomizerCore/Randomizers/TR2/Shared/TR2SecretAllocator.cs +++ b/TRRandomizerCore/Randomizers/TR2/Shared/TR2SecretAllocator.cs @@ -99,9 +99,7 @@ public List RandomizeSecrets(string levelName, TR2Level level) locations.Shuffle(Generator); _secretPicker.SectorAction = loc => level.GetRoomSector(loc); - _routePicker.RoomInfos = level.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); + _routePicker.RoomInfos = new(level.Rooms.Select(r => new ExtRoomInfo(r))); _routePicker.Initialise(levelName, locations, Settings, Generator); diff --git a/TRRandomizerCore/Randomizers/TR3/Classic/TR3ItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Classic/TR3ItemRandomizer.cs index 0be9fc0d2..16abf0b2b 100644 --- a/TRRandomizerCore/Randomizers/TR3/Classic/TR3ItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Classic/TR3ItemRandomizer.cs @@ -1,12 +1,10 @@ -using Newtonsoft.Json; -using TRDataControl; +using TRDataControl; using TRGE.Core; using TRImageControl.Packing; using TRLevelControl.Helpers; using TRLevelControl.Model; using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; -using TRRandomizerCore.Secrets; using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers; @@ -29,58 +27,36 @@ public class TR3ItemRandomizer : BaseTR3Randomizer TR3Type.LaraUziAnimation_H_Nevada, }; - private readonly Dictionary> _excludedLocations; - private readonly Dictionary> _pistolLocations; - - // Track the pistols so they remain a weapon type and aren't moved - private TR3Entity _unarmedLevelPistols; - - // Secret reward items handled in separate class, so track the reward entities - private TR3SecretMapping _secretMapping; - - private readonly LocationPicker _picker; + private TR3ItemAllocator _allocator; public ItemFactory ItemFactory { get; set; } - public TR3ItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR3\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); - + _allocator = new() + { + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; + foreach (TR3ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedLevelPistols(_levelInstance); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - _secretMapping = TR3SecretMapping.Get(_levelInstance); - - // #312 If this is the assault course, import required models. On failure, don't perform any item rando. if (_levelInstance.IsAssault && !ImportAssaultModels(_levelInstance)) { + TriggerProgress(); continue; } - if (Settings.RandomizeItemTypes) - { - RandomizeItemTypes(_levelInstance); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, + _levelInstance.Script.RemovesWeapons || _levelInstance.IsAssault, _levelInstance.HasExposureMeter); - if (Settings.RandomizeItemPositions) + if (_levelInstance.IsAssault) { - RandomizeItemLocations(_levelInstance); - } - - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - EnforceOneLimit(_levelInstance); + AddAssaultCourseAmmo(_levelInstance); } SaveLevelInstance(); @@ -96,7 +72,8 @@ public void RandomizeKeyItems() foreach (TR3ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, + _levelInstance.Script.OriginalSequence, _levelInstance.HasExposureMeter); SaveLevelInstance(); if (!TriggerProgress()) @@ -116,7 +93,7 @@ private bool ImportAssaultModels(TR3CombinedLevel level) DataFolder = GetResourcePath(@"TR3\Objects") }; - string remapPath = @"TR3\Textures\Deduplication\" + level.Name + "-TextureRemap.json"; + string remapPath = $@"TR3\Textures\Deduplication\{level.Name}-TextureRemap.json"; if (ResourceExists(remapPath)) { importer.TextureRemapPath = GetResourcePath(remapPath); @@ -154,211 +131,14 @@ static void CopyFaces(TRMesh baseMesh, TRMesh targetMesh) } } - private void FindUnarmedLevelPistols(TR3CombinedLevel level) + private void AddAssaultCourseAmmo(TR3CombinedLevel level) { - if (level.Script.RemovesWeapons) - { - List pistolEntities = level.Data.Entities.FindAll(e => e.TypeID == TR3Type.Pistols_P); - foreach (TR3Entity pistols in pistolEntities) - { - int match = _pistolLocations[level.Name].FindIndex - ( - location => - location.X == pistols.X && - location.Y == pistols.Y && - location.Z == pistols.Z && - location.Room == pistols.Room - ); - if (match != -1) - { - _unarmedLevelPistols = pistols; - break; - } - } - } - else - { - _unarmedLevelPistols = null; - } - } - - public void RandomizeItemTypes(TR3CombinedLevel level) - { - List stdItemTypes = TR3TypeUtilities.GetStandardPickupTypes(); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - if (_secretMapping != null && _secretMapping.RewardEntities.Contains(i)) - { - // Leave default secret rewards as they are - handled separately - continue; - } - - TR3Entity ent = level.Data.Entities[i]; - TR3Type currentType = ent.TypeID; - // If this is an unarmed level's pistols, make sure they're replaced with another weapon. - // Similar case for the assault course, so that Lara can still shoot Winnie. - if ((ent == _unarmedLevelPistols && Settings.GiveUnarmedItems) - || (level.IsAssault && TR3TypeUtilities.IsWeaponPickup(currentType))) - { - do - { - ent.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR3TypeUtilities.IsWeaponPickup(ent.TypeID)); - - if (level.IsAssault) - { - // Add some extra ammo too - level.Script.AddStartInventoryItem(ItemUtilities.ConvertToScriptItem(TR3TypeUtilities.GetWeaponAmmo(ent.TypeID)), 20); - } - } - else if (TR3TypeUtilities.IsStandardPickupType(currentType)) - { - ent.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - } - } - - public void EnforceOneLimit(TR3CombinedLevel level) - { - HashSet uniqueTypes = new(); - if (_unarmedLevelPistols != null) - { - // These will be excluded, but track their type before looking at other items. - uniqueTypes.Add(_unarmedLevelPistols.TypeID); - } - - // Look for extra utility/ammo items and hide them - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - if ((_secretMapping != null && _secretMapping.RewardEntities.Contains(i)) || entity == _unarmedLevelPistols) - { - // Rewards and unarmed level weapons excluded - continue; - } - - if ((TR3TypeUtilities.IsStandardPickupType(entity.TypeID) || TR3TypeUtilities.IsCrystalPickup(entity.TypeID)) - && !uniqueTypes.Add(entity.TypeID)) - { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); - if (TR3TypeUtilities.IsCrystalPickup(entity.TypeID)) - { - level.Data.FloorData.RemoveEntityTriggers(i); - } - } - } - } - - public void RandomizeItemLocations(TR3CombinedLevel level) - { - if (level.IsAssault) + TR3Entity weapon = _allocator.GetUnarmedLevelPistols(_levelInstance.Name, _levelInstance.Data.Entities); + if (weapon == null) { return; } - for (int i = 0; i < level.Data.Entities.Count; i++) - { - if (_secretMapping.RewardEntities.Contains(i) - || ItemFactory.IsItemLocked(_levelInstance.Name, i)) - { - continue; - } - - TR3Entity entity = level.Data.Entities[i]; - // Move standard items only, excluding any unarmed level pistols, and reward items - if (TR3TypeUtilities.IsStandardPickupType(entity.TypeID) && entity != _unarmedLevelPistols) - { - _picker.RandomizePickupLocation(entity); - } - } - } - - private List GetItemLocationPool(TR3CombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR3Entity entity in level.Data.Entities) - { - if (!TR3TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - if (Settings.RandomizeSecrets && level.HasSecrets) - { - // Make sure to exclude the reward room - exclusions.Add(new() - { - Room = RoomWaterUtilities.DefaultRoomCountDictionary[level.Name], - InvalidatesRoom = true - }); - } - - if (level.HasExposureMeter) - { - // Don't put items underwater if it's too cold - for (short i = 0; i < level.Data.Rooms.Count; i++) - { - if (level.Data.Rooms[i].ContainsWater) - { - exclusions.Add(new() - { - Room = i, - InvalidatesRoom = true - }); - } - } - } - - TR3LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - public void RandomizeKeyItems(TR3CombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - if (!TR3TypeUtilities.IsKeyItemType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i)) - { - continue; - } - - _picker.RandomizeKeyItemLocation( - entity, LocationUtilities.HasPickupTriger(entity, i, level.Data), - level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); - } - } - - private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR3CombinedLevel level) - { - // Make sure if we're placing on the same tile as an enemy, that the - // enemy can drop the item. - TR3Entity enemy = level.Data.Entities - .FindAll(e => TR3TypeUtilities.IsEnemyType(e.TypeID)) - .Find(e => e.GetLocation().IsEquivalent(location)); - - return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR3TypeUtilities.CanDropPickups - ( - TR3TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), - !Settings.RandomizeEnemies || Settings.ProtectMonks - )); + level.Script.AddStartInventoryItem(ItemUtilities.ConvertToScriptItem(TR3TypeUtilities.GetWeaponAmmo(weapon.TypeID)), 20); } } diff --git a/TRRandomizerCore/Randomizers/TR3/Classic/TR3SecretRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Classic/TR3SecretRandomizer.cs index a217b7670..2891b3f60 100644 --- a/TRRandomizerCore/Randomizers/TR3/Classic/TR3SecretRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Classic/TR3SecretRandomizer.cs @@ -306,9 +306,7 @@ private void RandomizeSecrets(TR3CombinedLevel level, List pickupTypes, _secretPicker.PlacementTestAction = loc => _placer.TestSecretPlacement(loc); - _routePicker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); + _routePicker.RoomInfos = new(level.Data.Rooms.Select(r => new ExtRoomInfo(r))); _routePicker.Initialise(level.Name, locations, Settings, _generator); List pickedLocations = _secretPicker.GetLocations(locations, Mirrorer.IsMirrored(level.Name), level.Script.NumSecrets); diff --git a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RItemRandomizer.cs index 946132531..cc53e8bb9 100644 --- a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RItemRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RItemRandomizer.cs @@ -1,57 +1,36 @@ -using Newtonsoft.Json; -using TRGE.Core; -using TRLevelControl.Helpers; +using TRGE.Core; using TRLevelControl.Model; using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.Secrets; -using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers; public class TR3RItemRandomizer : BaseTR3RRandomizer { - private readonly Dictionary> _excludedLocations, _pistolLocations; - private readonly LocationPicker _picker; - - private TRSecretMapping _secretMapping; - private TR3Entity _unarmedLevelPistols; + private TR3ItemAllocator _allocator; public ItemFactory ItemFactory { get; set; } - public TR3RItemRandomizer() - { - _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\invalid_item_locations.json")); - _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\unarmed_locations.json")); - _picker = new(GetResourcePath(@"TR3\Locations\routes.json")); - } - public override void Randomize(int seed) { _generator = new(seed); + _allocator = new(true) + { + Generator = _generator, + Settings = Settings, + ItemFactory = ItemFactory, + }; foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - FindUnarmedLevelPistols(_levelInstance); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); - _secretMapping = TRSecretMapping.Get(GetResourcePath($@"TR3\SecretMapping\{_levelInstance.Name}-SecretMapping.json")); - - if (Settings.RandomizeItemTypes) + if (_levelInstance.IsAssault) { - RandomizeItemTypes(_levelInstance); - } - - if (Settings.RandomizeItemPositions) - { - RandomizeItemLocations(_levelInstance); + TriggerProgress(); + continue; } - if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) - { - EnforceOneLimit(_levelInstance); - } + _allocator.RandomizeItems(_levelInstance.Name, _levelInstance.Data, + _levelInstance.Script.RemovesWeapons, _levelInstance.HasExposureMeter); SaveLevelInstance(); if (!TriggerProgress()) @@ -66,7 +45,8 @@ public void RandomizeKeyItems() foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeKeyItems(_levelInstance); + _allocator.RandomizeKeyItems(_levelInstance.Name, _levelInstance.Data, + _levelInstance.Script.OriginalSequence, _levelInstance.HasExposureMeter); SaveLevelInstance(); if (!TriggerProgress()) @@ -75,197 +55,4 @@ public void RandomizeKeyItems() } } } - - private void FindUnarmedLevelPistols(TR3RCombinedLevel level) - { - if (level.Script.RemovesWeapons) - { - _unarmedLevelPistols = level.Data.Entities.Find( - e => e.TypeID == TR3Type.Pistols_P - && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); - } - else - { - _unarmedLevelPistols = null; - } - } - - private List GetItemLocationPool(TR3RCombinedLevel level, bool keyItemMode) - { - List exclusions = new(); - if (_excludedLocations.ContainsKey(level.Name)) - { - exclusions.AddRange(_excludedLocations[level.Name]); - } - - foreach (TR3Entity entity in level.Data.Entities) - { - if (!TR3TypeUtilities.CanSharePickupSpace(entity.TypeID)) - { - exclusions.Add(entity.GetFloorLocation(loc => level.Data.GetRoomSector(loc))); - } - } - - if (level.Script.HasColdWater) - { - // Don't put items underwater if it's too cold - for (short i = 0; i < level.Data.Rooms.Count; i++) - { - if (level.Data.Rooms[i].ContainsWater) - { - exclusions.Add(new() - { - Room = i, - InvalidatesRoom = true - }); - } - } - } - - TR3LocationGenerator generator = new(); - return generator.Generate(level.Data, exclusions, keyItemMode); - } - - public void RandomizeItemTypes(TR3RCombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - List stdItemTypes = TR3TypeUtilities.GetStandardPickupTypes(); - stdItemTypes.Remove(TR3Type.PistolAmmo_P); - - bool hasPistols = level.Data.Entities.Any(e => e.TypeID == TR3Type.Pistols_P); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - TR3Type entityType = entity.TypeID; - if (!TR3TypeUtilities.IsStandardPickupType(entityType) || _secretMapping.RewardEntities.Contains(i)) - { - continue; - } - - if (entity == _unarmedLevelPistols) - { - if (entityType == TR3Type.Pistols_P && Settings.GiveUnarmedItems) - { - do - { - entityType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR3TypeUtilities.IsWeaponPickup(entityType)); - entity.TypeID = entityType; - } - } - else if (TR3TypeUtilities.IsStandardPickupType(entityType)) - { - TR3Type newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - if (newType == TR3Type.Pistols_P && (hasPistols || !level.Script.RemovesWeapons)) - { - do - { - newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; - } - while (!TR3TypeUtilities.IsWeaponPickup(newType) || newType == TR3Type.Pistols_P); - } - entity.TypeID = newType; - } - - hasPistols = level.Data.Entities.Any(e => e.TypeID == TR3Type.Pistols_P); - } - } - - public void RandomizeItemLocations(TR3RCombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - if (!TR3TypeUtilities.IsStandardPickupType(entity.TypeID) - || _secretMapping.RewardEntities.Contains(i) - || ItemFactory.IsItemLocked(level.Name, i) - || entity == _unarmedLevelPistols) - { - continue; - } - - _picker.RandomizePickupLocation(entity); - } - } - - public void EnforceOneLimit(TR3RCombinedLevel level) - { - if (level.IsAssault) - { - return; - } - - HashSet uniqueTypes = new(); - if (_unarmedLevelPistols != null) - { - uniqueTypes.Add(_unarmedLevelPistols.TypeID); - } - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - if (_secretMapping.RewardEntities.Contains(i) || entity == _unarmedLevelPistols) - { - continue; - } - - if ((TR3TypeUtilities.IsStandardPickupType(entity.TypeID) || TR3TypeUtilities.IsCrystalPickup(entity.TypeID)) - && !uniqueTypes.Add(entity.TypeID)) - { - ItemUtilities.HideEntity(entity); - ItemFactory.FreeItem(level.Name, i); - } - } - } - - private void RandomizeKeyItems(TR3RCombinedLevel level) - { - _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data); - _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); - _picker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); - - _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); - - for (int i = 0; i < level.Data.Entities.Count; i++) - { - TR3Entity entity = level.Data.Entities[i]; - if (!TR3TypeUtilities.IsKeyItemType(entity.TypeID) - || ItemFactory.IsItemLocked(level.Name, i)) - { - continue; - } - - _picker.RandomizeKeyItemLocation( - entity, LocationUtilities.HasPickupTriger(entity, i, level.Data), - level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); - } - } - - private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR3RCombinedLevel level) - { - // Make sure if we're placing on the same tile as an enemy, that the - // enemy can drop the item. - TR3Entity enemy = level.Data.Entities - .FindAll(e => TR3TypeUtilities.IsEnemyType(e.TypeID)) - .Find(e => e.GetLocation().IsEquivalent(location)); - - return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR3TypeUtilities.CanDropPickups - ( - TR3TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), - !Settings.RandomizeEnemies || Settings.ProtectMonks - )); - } } diff --git a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RSecretRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RSecretRandomizer.cs index 932613eb8..dc18c113f 100644 --- a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RSecretRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RSecretRandomizer.cs @@ -154,9 +154,7 @@ private void RandomizeSecrets(TR3RCombinedLevel level, List pickupTypes _secretPicker.PlacementTestAction = loc => _placer.TestSecretPlacement(loc); - _routePicker.RoomInfos = level.Data.Rooms - .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) - .ToList(); + _routePicker.RoomInfos = new(level.Data.Rooms.Select(r => new ExtRoomInfo(r))); _routePicker.Initialise(level.Name, locations, Settings, _generator); List pickedLocations = _secretPicker.GetLocations(locations, false, level.Script.NumSecrets); diff --git a/TRRandomizerCore/Randomizers/TR3/Shared/TR3ItemAllocator.cs b/TRRandomizerCore/Randomizers/TR3/Shared/TR3ItemAllocator.cs new file mode 100644 index 000000000..026363307 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3/Shared/TR3ItemAllocator.cs @@ -0,0 +1,129 @@ +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Secrets; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR3ItemAllocator : ItemAllocator +{ + private readonly bool _remaster; + + public TR3ItemAllocator(bool remaster = false) + : base(TRGameVersion.TR3) + { + _remaster = remaster; + } + + protected override List GetExcludedItems(string levelName) + { + TRSecretMapping mapping = TRSecretMapping.Get($@"Resources\TR3\SecretMapping\{levelName}-SecretMapping.json"); + return mapping?.RewardEntities ?? new(); + } + + protected override TR3Type GetPistolType() + => TR3Type.Pistols_P; + + protected override List GetStandardItemTypes() + { + List stdItemTypes = TR3TypeUtilities.GetStandardPickupTypes(); + stdItemTypes.Remove(TR3Type.PistolAmmo_P); + return stdItemTypes; + } + + protected override List GetWeaponItemTypes() + => TR3TypeUtilities.GetWeaponPickups(); + + protected override bool IsCrystalPickup(TR3Type type) + => type == TR3Type.SaveCrystal_P; + + public void RandomizeItems(string levelName, TR3Level level, bool isUnarmed, bool isCold) + { + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, false, isCold), Settings, Generator); + + RandomizeItemTypes(levelName, level.Entities, isUnarmed); + RandomizeItemLocations(levelName, level.Entities, isUnarmed); + } + + public void RandomizeKeyItems(string levelName, TR3Level level, int originalSequence, bool isCold) + { + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level); + _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, levelName, level); + _picker.RoomInfos = new(level.Rooms.Select(r => new ExtRoomInfo(r))); + + _picker.Initialise(levelName, GetItemLocationPool(levelName, level, true, isCold), Settings, Generator); + + for (int i = 0; i < level.Entities.Count; i++) + { + TR3Entity entity = level.Entities[i]; + if (!TR3TypeUtilities.IsKeyItemType(entity.TypeID) + || ItemFactory.IsItemLocked(levelName, i)) + { + continue; + } + + _picker.RandomizeKeyItemLocation( + entity, LocationUtilities.HasPickupTriger(entity, i, level), + originalSequence, level.Rooms[entity.Room].Info); + } + } + + private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, string levelName, TR3Level level) + { + // Make sure if we're placing on the same tile as an enemy, that the enemy can drop the item. + TR3Entity enemy = level.Entities + .FindAll(e => TR3TypeUtilities.IsEnemyType(e.TypeID)) + .Find(e => e.GetLocation().IsEquivalent(location)); + + return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR3TypeUtilities.CanDropPickups + ( + TR3TypeUtilities.GetAliasForLevel(levelName, enemy.TypeID), + !Settings.RandomizeEnemies || Settings.ProtectMonks + )); + } + + private List GetItemLocationPool(string levelName, TR3Level level, bool keyItemMode, bool isCold) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(levelName)) + { + exclusions.AddRange(_excludedLocations[levelName]); + } + + exclusions.AddRange(level.Entities + .Where(e => !TR3TypeUtilities.CanSharePickupSpace(e.TypeID)) + .Select(e => e.GetFloorLocation(loc => level.GetRoomSector(loc)))); + + if (Settings.RandomizeSecrets + && !_remaster // Eliminate and make UseRewardRooms setting + && level.FloorData.GetActionItems(FDTrigAction.SecretFound).Any()) + { + // Make sure to exclude the reward room + exclusions.Add(new() + { + Room = RoomWaterUtilities.DefaultRoomCountDictionary[levelName], + InvalidatesRoom = true + }); + } + + if (isCold) + { + // Don't put items underwater if it's too cold + for (short i = 0; i < level.Rooms.Count; i++) + { + if (level.Rooms[i].ContainsWater) + { + exclusions.Add(new() + { + Room = i, + InvalidatesRoom = true + }); + } + } + } + + TR3LocationGenerator generator = new(); + return generator.Generate(level, exclusions, keyItemMode); + } +} diff --git a/TRRandomizerCore/Resources/TR3/Locations/unarmed_locations.json b/TRRandomizerCore/Resources/TR3/Locations/unarmed_locations.json index 24b9faab7..54e13e119 100644 --- a/TRRandomizerCore/Resources/TR3/Locations/unarmed_locations.json +++ b/TRRandomizerCore/Resources/TR3/Locations/unarmed_locations.json @@ -1238,5 +1238,13 @@ "Z": 58895, "Room": 45 } + ], + "HOUSE.TR2": [ + { + "X": 40448, + "Y": 3328, + "Z": 15872, + "Room": 84 + } ] } \ No newline at end of file diff --git a/TRRandomizerCore/Utilities/VehicleUtilities.cs b/TRRandomizerCore/Utilities/VehicleUtilities.cs index 3efb37849..1f9342f5b 100644 --- a/TRRandomizerCore/Utilities/VehicleUtilities.cs +++ b/TRRandomizerCore/Utilities/VehicleUtilities.cs @@ -16,6 +16,9 @@ static VehicleUtilities() _secretLocations = JsonConvert.DeserializeObject>>(File.ReadAllText(@"Resources\TR2\Locations\locations.json")); } + public static bool HasLocations(string levelName, TR2Type vehicle) + => _vehicleLocations.ContainsKey(levelName) && _vehicleLocations[levelName].Any(l => l.TargetType == (short)vehicle); + public static Location GetRandomLocation(string levelName, TR2Level level, TR2Type vehicle, Random random, bool testSecrets = true) { if (_vehicleLocations.ContainsKey(levelName)) @@ -53,6 +56,6 @@ public static IEnumerable GetDependentLocations(string levelName, TR2L IEnumerable secrets = level.Entities.Where(e => TR2TypeUtilities.IsSecretType(e.TypeID)); return levelLocations - .Where(l => secrets.Any(s => l.X == s.X && l.Y == s.Y && l.Z == s.Z && l.Room == s.Room)); + .Where(l => secrets.Any(s => s.GetLocation().IsEquivalent(l))); } }