Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- added support for all enemy variants from The Golden Mask in the base game levels and vice-versa
- changed all enemy types (except eels) to have the ability to drop items
- removed restrictions on enemy combinations when the dragon is present
- removed skidoo driver and monk limits
- removed bird monster, skidoo driver and monk limits
- improved textures and ladder face rotation in wireframe mode (#852)
- fixed a crash at the end of Diving Area in TR2R (#814)
- fixed a potential key softlock in City of Khamoon if "large" range is selected and either return paths are disabled in classic, or playing remastered (#820)
Expand All @@ -29,6 +29,7 @@
- fixed an uncollectible secret in Natla's Mines (#849)
- fixed models all using the same highlight colour in wireframe mode (#852)
- fixed the Temple of Xian Dragon Seal room (default placement) sometimes being flipped on arrival, leading to potential softlocks (OG bug)
- removed support for the dragon (TR2 Remastered only) in all levels except Dragon's Lair (may revisit)

## [V1.10.2](https://github.com/LostArtefacts/TR-Rando/compare/V1.10.1...V1.10.2) - 2024-12-06
- added support for TR1X 4.6 (#796)
Expand Down
4 changes: 2 additions & 2 deletions Resources/Documentation/RETURNPATHS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Following are details on the return paths that are available. See `Global Settin
| Dragon's Lair | Slopes adjusted in room 2 to allow exiting the main arena. |
| Home Sweet Home | _Not required_ |
| The Cold War | Link room added between rooms 37 and 64. |
| Fool's Gold | _Not required_ |
| Fool's Gold | Door 143 will remain open after Lara goes through it. |
| Furnace of the Gods | Ladder added to room 8 for room 25; drawbridge added in room 42. |
| Kingdom | _Not required_ |
| Kingdom | Doors 6 and 22 will be guaranteed to be open on arrival in room 66. |
| Nightmare in Vegas | _Not required_ |

## TR3
Expand Down
2 changes: 1 addition & 1 deletion TRDataControl/Data/Remastered/TR2RDataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected override Dictionary<TR2Type, TR2RAlias> GetMapDependencies(TR2Type key

[TR2Type.Eagle] = TR2LevelNames.TIBET,
[TR2Type.Mercenary2OG] = TR2LevelNames.TIBET,
[TR2Type.Mercenary3] = TR2LevelNames.TIBET,
[TR2Type.Mercenary3OG] = TR2LevelNames.TIBET,
[TR2Type.MercSnowmobDriverOG] = TR2LevelNames.TIBET,
[TR2Type.BlackSnowmobOG] = TR2LevelNames.TIBET,
[TR2Type.RedSnowmobile] = TR2LevelNames.TIBET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,70 @@ public class EMMergeTriggersFunction : BaseEMFunction
{
public EMLocation BaseLocation { get; set; }
public EMLocation TargetLocation { get; set; }
public List<EMLocation> TargetLocations { get; set; }

public override void ApplyToLevel(TR1Level level)
{
EMLevelData data = GetData(level);
MergeTriggers(level.GetRoomSector(data.ConvertLocation(BaseLocation)),
level.GetRoomSector(data.ConvertLocation(TargetLocation)),
level.FloorData);
var data = GetData(level);
MergeTriggers(level.FloorData, loc => level.GetRoomSector(data.ConvertLocation(loc)));
}

public override void ApplyToLevel(TR2Level level)
{
EMLevelData data = GetData(level);
MergeTriggers(level.GetRoomSector(data.ConvertLocation(BaseLocation)),
level.GetRoomSector(data.ConvertLocation(TargetLocation)),
level.FloorData);
var data = GetData(level);
MergeTriggers(level.FloorData, loc => level.GetRoomSector(data.ConvertLocation(loc)));
}

public override void ApplyToLevel(TR3Level level)
{
EMLevelData data = GetData(level);
MergeTriggers(level.GetRoomSector(data.ConvertLocation(BaseLocation)),
level.GetRoomSector(data.ConvertLocation(TargetLocation)),
level.FloorData);
var data = GetData(level);
MergeTriggers(level.FloorData, loc => level.GetRoomSector(data.ConvertLocation(loc)));
}

private static void MergeTriggers(TRRoomSector baseSector, TRRoomSector targetSector, FDControl floorData)
private void MergeTriggers(FDControl floorData, Func<EMLocation, TRRoomSector> getSector)
{
FDEntry baseEntry;
var baseSector = getSector(BaseLocation);
if (baseSector.FDIndex == 0
|| baseSector == targetSector
|| (baseEntry = floorData[baseSector.FDIndex].Find(e => e is FDTriggerEntry)) == null)
|| floorData[baseSector.FDIndex].OfType<FDTriggerEntry>().FirstOrDefault() is not FDTriggerEntry baseTrigger)
{
return;
}

if (targetSector.FDIndex == 0)
var targets = new List<EMLocation>();
if (TargetLocations != null)
{
floorData.CreateFloorData(targetSector);
targets.AddRange(TargetLocations);
}

FDEntry targetEntry = floorData[targetSector.FDIndex].Find(e => e is FDTriggerEntry);
if (targetEntry == null)
if (TargetLocation != null)
{
floorData[targetSector.FDIndex].Add(baseEntry);
targets.Add(TargetLocation); // Legacy
}
else

foreach (var location in targets)
{
FDTriggerEntry baseTrigger = baseEntry as FDTriggerEntry;
FDTriggerEntry targetTrigger = targetEntry as FDTriggerEntry;
var targetSector = getSector(location);
if (baseSector == targetSector)
{
continue;
}

targetTrigger.Actions.AddRange(baseTrigger.Actions);
if (baseTrigger.OneShot)
if (targetSector.FDIndex == 0)
{
floorData.CreateFloorData(targetSector);
}

if (floorData[targetSector.FDIndex].
OfType<FDTriggerEntry>().FirstOrDefault() is not FDTriggerEntry targetTrigger)
{
floorData[targetSector.FDIndex].Add(baseTrigger);
}
else
{
targetTrigger.OneShot = true;
targetTrigger.Actions.AddRange(baseTrigger.Actions);
targetTrigger.OneShot |= baseTrigger.OneShot;
}
}

floorData[baseSector.FDIndex].Remove(baseEntry);
floorData[baseSector.FDIndex].Remove(baseTrigger);
}
}
47 changes: 47 additions & 0 deletions TRDataControlTests/Environment/Triggers/MergeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using TRDataControl.Environment;
using TRLevelControl.Model;
using TRLevelControlTests;

namespace TRDataControlTests.Environment.Triggers;

[TestClass]
public class MergeTests : TestBase
{
[TestMethod]
public void TestMerge()
{
var level = GetTR1TestLevel();
var baseLoc = new EMLocation
{
X = 15872,
Z = 20992,
Room = 7,
};
var targetLoc = new EMLocation
{
X = 5632,
Z = 20992,
};

var sectorA = level.GetRoomSector(baseLoc);
var triggerA = level.FloorData[sectorA.FDIndex].OfType<FDTriggerEntry>().FirstOrDefault();
Assert.IsTrue(triggerA.OneShot);
Assert.HasCount(1, triggerA.Actions.Where(a => a.Parameter == 13));

var sectorB = level.GetRoomSector(targetLoc);
var triggerB = level.FloorData[sectorB.FDIndex].OfType<FDTriggerEntry>().FirstOrDefault();
Assert.IsFalse(triggerB.OneShot);
Assert.HasCount(0, triggerB.Actions.Where(a => a.Parameter == 13));

new EMMergeTriggersFunction
{
BaseLocation = baseLoc,
TargetLocation = targetLoc,
}.ApplyToLevel(level);

Assert.IsTrue(triggerB.OneShot);
Assert.HasCount(1, triggerB.Actions.Where(a => a.Parameter == 13));

Assert.HasCount(0, level.FloorData[sectorA.FDIndex].OfType<FDTriggerEntry>());
}
}
23 changes: 7 additions & 16 deletions TRLevelControl/Helpers/TR2TypeUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -717,32 +717,23 @@ public static bool IsWaterCreature(TR2Type type)
|| type == TR2Type.BarracudaXian || type == TR2Type.ScubaDiver;
}

public static List<TR2Type> WaterCreatures()
public static List<TR2Type> KillableWaterCreatures(bool remastered)
{
return new()
var types = new List<TR2Type>
{
TR2Type.SharkOG,
TR2Type.SharkGM,
TR2Type.BarracudaIce,
TR2Type.BarracudaUnwater,
TR2Type.BarracudaXian,
TR2Type.YellowMorayEel,
TR2Type.BlackMorayEel,
TR2Type.ScubaDiver
};
}

public static List<TR2Type> KillableWaterCreatures()
{
return new()
if (!remastered)
{
TR2Type.SharkOG,
TR2Type.SharkGM,
TR2Type.BarracudaIce,
TR2Type.BarracudaUnwater,
TR2Type.BarracudaXian,
TR2Type.ScubaDiver
};
types.Add(TR2Type.SharkGM);
}

return types;
}

public static bool IsStaticCreature(TR2Type type)
Expand Down
85 changes: 34 additions & 51 deletions TRRandomizerCore/Randomizers/TR2/Classic/TR2EnemyRandomizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,88 +349,71 @@ private void CloneEnemies(TR2CombinedLevel level)

private void AddUnarmedItems(TR2CombinedLevel level)
{
if (!level.Script.RemovesWeapons || !Settings.GiveUnarmedItems)
if (!level.Script.RemovesWeapons || !Settings.CrossLevelEnemies || !Settings.GiveUnarmedItems)
{
return;
}

TR2Entity weapon = level.Data.Entities.Find(e =>
(e.TypeID == TR2Type.Pistols_S_P || TR2TypeUtilities.IsGunType(e.TypeID))
var weaponTypes = TR2TypeUtilities.GetGunTypes();
var weaponItem = level.Data.Entities.Find(e =>
weaponTypes.Contains(e.TypeID)
&& _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation())));
if (weapon == null)

if (weaponItem == null)
{
return;
}

if (level.Is(TR2LevelNames.HOME) && Settings.RandomizeItems && Settings.RandoItemDifficulty == ItemDifficulty.OneLimit)
{
weapon.TypeID = TR2Type.Pistols_S_P;
weaponItem.TypeID = TR2Type.Pistols_S_P;
return;
}

List<TR2Type> replacementWeapons = TR2TypeUtilities.GetGunTypes();
replacementWeapons.Add(TR2Type.Pistols_S_P);
TR2Type weaponType = replacementWeapons[_generator.Next(0, replacementWeapons.Count)];
weapon.TypeID = weaponType;
weaponItem.TypeID = weaponTypes.RandomItem(_generator);

void AddItem(TR2Type type)
{
if (ItemFactory.CanCreateItem(level.Name, level.Data.Entities))
var difficulty = TR2EnemyUtilities.GetEnemyDifficulty(level.GetEnemyEntities());
if (difficulty > EnemyDifficulty.Easy
|| weaponItem.TypeID == TR2Type.Harpoon_S_P
|| weaponItem.TypeID == TR2Type.GrenadeLauncher_S_P
|| _generator.NextDouble() < _easyPistolChance)
{
if (!level.Data.Entities.Any(e => e.TypeID == TR2Type.Pistols_S_P))
{
TR2Entity item = ItemFactory.CreateItem(level.Name, level.Data.Entities, weapon.GetLocation());
item.TypeID = type;
var item = ItemFactory.CreateItem(level.Name, level.Data.Entities,
weaponItem.GetLocation(), allowLimitBreak: true);
item.TypeID = TR2Type.Pistols_S_P;
}
}

uint ammoCount = _unarmedAmmoCounts[weaponType];
if (Settings.CrossLevelEnemies)
if (difficulty == EnemyDifficulty.Medium || difficulty == EnemyDifficulty.Hard)
{
// Create a score based on the number and difficulty of triggered enemies.
List<TR2Entity> 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);
}
level.Script.AddStartInventoryItem(TR2Type.SmallMed_S_P);
}
else if (level.Is(TR2LevelNames.LAIR))
if (difficulty > EnemyDifficulty.Medium)
{
ammoCount *= 6;
level.Script.AddStartInventoryItem(TR2Type.LargeMed_S_P);
}
if (difficulty == EnemyDifficulty.VeryHard)
{
level.Script.AddStartInventoryItem(TR2Type.LargeMed_S_P, 2);
}

var ammoCount = _unarmedAmmoCounts[weaponItem.TypeID];
ammoCount *= (uint)difficulty;
if (ammoCount == 0)
{
return;
}

if (level.Is(TR2LevelNames.LAIR))
{
ammoCount *= 6;
}

TR2Type ammoType = TR2TypeUtilities.GetWeaponAmmo(weaponType);
var ammoType = TR2TypeUtilities.GetWeaponAmmo(weaponItem.TypeID);
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,10 @@ namespace TRRandomizerCore.Randomizers;

public class TR2REnemyRandomizer : BaseTR2RRandomizer
{
private static readonly List<string> _dragonLevels = new()
{
TR2LevelNames.GW,
TR2LevelNames.DORIA,
TR2LevelNames.DECK,
TR2LevelNames.TIBET,
TR2LevelNames.COT,
TR2LevelNames.CHICKEN,
private static readonly List<string> _dragonLevels =
[
TR2LevelNames.XIAN,
};
];

private const int _hshShellCount = 16;
private static readonly List<Location> _hshShellLocations = new()
Expand Down
Loading
Loading