Skip to content

Commit

Permalink
Refactor item randomization (LostArtefacts#688)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahm86 authored May 26, 2024
1 parent 789f0b1 commit 6e30f2e
Show file tree
Hide file tree
Showing 24 changed files with 1,003 additions and 1,896 deletions.
14 changes: 14 additions & 0 deletions TRLevelControl/Helpers/TR2TypeUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,20 @@ public static List<TR2Type> 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 ||
Expand Down
12 changes: 2 additions & 10 deletions TRRandomizerCore/Editors/TR2ClassicEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
}
}
}
20 changes: 10 additions & 10 deletions TRRandomizerCore/Helpers/ExtRoomInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions TRRandomizerCore/Levels/TR3RCombinedLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ public class TR3RCombinedLevel
public bool IsAssault => Is(TR3LevelNames.ASSAULT);
public TRDictionary<TR3Type, TRModel> PDPData { get; set; }
public Dictionary<TR3Type, TR3RAlias> MapData { get; set; }
public bool HasExposureMeter => Sequence == 16 || Sequence == 17;
}
205 changes: 205 additions & 0 deletions TRRandomizerCore/Randomizers/Shared/ItemAllocator.cs
Original file line number Diff line number Diff line change
@@ -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<T, E>
where T : Enum
where E : TREntity<T>, new()
{
protected readonly Dictionary<string, List<Location>> _excludedLocations;
protected readonly Dictionary<string, List<Location>> _pistolLocations;
protected readonly Dictionary<string, E> _unarmedPistolCache;
protected readonly LocationPicker _picker;

protected ItemSpriteRandomizer<T> _spriteRandomizer;

public Random Generator { get; set; }
public RandomizerSettings Settings { get; set; }
public ItemFactory<E> ItemFactory { get; set; }

public ItemAllocator(TRGameVersion gameVersion)
{
_excludedLocations = JsonConvert.DeserializeObject<Dictionary<string, List<Location>>>(File.ReadAllText($@"Resources\{gameVersion}\Locations\invalid_item_locations.json"));
_pistolLocations = JsonConvert.DeserializeObject<Dictionary<string, List<Location>>>(File.ReadAllText($@"Resources\{gameVersion}\Locations\unarmed_locations.json"));
_picker = new($@"Resources\{gameVersion}\Locations\routes.json");
_unarmedPistolCache = new();
}

public E GetUnarmedLevelPistols(string levelName, List<E> items)
{
if (!_pistolLocations.ContainsKey(levelName))
{
return null;
}

if (!_unarmedPistolCache.ContainsKey(levelName))
{
List<T> weaponTypes = GetWeaponItemTypes();
T pistols = GetPistolType();
_unarmedPistolCache[levelName] = items.Find(e =>
(weaponTypes.Contains(e.TypeID) || EqualityComparer<T>.Default.Equals(e.TypeID, pistols))
&& _pistolLocations[levelName].Any(l => l.IsEquivalent(e.GetLocation())));
}

return _unarmedPistolCache[levelName];
}

public void RandomizeItemTypes(string levelName, List<E> items, bool isUnarmed)
{
if (!Settings.RandomizeItemTypes)
{
return;
}

List<T> stdItemTypes = GetStandardItemTypes();
List<T> weaponTypes = GetWeaponItemTypes();
T pistols = GetPistolType();
List<int> excludedItems = GetExcludedItems(levelName);

bool hasPistols = items.Any(e => EqualityComparer<T>.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<T>.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<T>.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<T>.Default.Equals(newType, pistols));
}
entity.TypeID = newType;
}

hasPistols = items.Any(e => EqualityComparer<T>.Default.Equals(e.TypeID, pistols));
}
}

public void RandomizeItemLocations(string levelName, List<E> items, bool isUnarmed)
{
if (!Settings.RandomizeItemPositions)
{
return;
}

List<T> stdItemTypes = GetStandardItemTypes();
List<int> 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<E> items, bool isUnarmed)
{
if (Settings.RandoItemDifficulty != ItemDifficulty.OneLimit)
{
return null;
}

List<T> stdItemTypes = GetStandardItemTypes();
List<int> excludedItems = GetExcludedItems(levelName);
HashSet<T> 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<T, TRSpriteSequence> sequences, List<T> keyItemTypes, List<T> 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<T> GetStandardItemTypes();
protected abstract List<T> GetWeaponItemTypes();
protected abstract T GetPistolType();
protected abstract List<int> GetExcludedItems(string levelName);
protected abstract bool IsCrystalPickup(T type);
protected virtual void ItemMoved(E item) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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);
}
}

Expand Down
Loading

0 comments on commit 6e30f2e

Please sign in to comment.