diff --git a/IntelOrca.Biohazard.BioRand/EnemyRandomiser.cs b/IntelOrca.Biohazard.BioRand/EnemyRandomiser.cs index d34430fc..59a2abab 100644 --- a/IntelOrca.Biohazard.BioRand/EnemyRandomiser.cs +++ b/IntelOrca.Biohazard.BioRand/EnemyRandomiser.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Threading; using IntelOrca.Biohazard.BioRand.RE1; using IntelOrca.Biohazard.BioRand.RE2; using IntelOrca.Biohazard.BioRand.RE3; +using IntelOrca.Biohazard.BioRand.RECV; using IntelOrca.Biohazard.Extensions; using IntelOrca.Biohazard.Room; using IntelOrca.Biohazard.Script; @@ -31,6 +33,7 @@ internal class EnemyRandomiser private HashSet _killIdPool = new HashSet(); private Queue _killIds = new Queue(); private Dictionary _effects = new Dictionary(); + private List _cvEnemies = new List(); public IEnemyHelper EnemyHelper => _enemyHelper; public Dictionary ChosenEnemies { get; } = new Dictionary(); @@ -81,20 +84,43 @@ private void ReadEnemyPlacements() private void GatherEsps() { - foreach (var rdt in _gameData.Rdts) + if (_version == BioVersion.BiohazardCv) + { + // HarvestEnemyAssets(RdtId.Parse("1031"), ReCvEnemyIds.Zombie, 512, 2, 0, 2); + HarvestEnemyAssets(RdtId.Parse("10D0"), ReCvEnemyIds.Bat, 3, 1, 0, 1); + HarvestEnemyAssets(RdtId.Parse("10F0"), ReCvEnemyIds.Zombie, 256, 1, 0, 1); + HarvestEnemyAssets(RdtId.Parse("2000"), ReCvEnemyIds.ZombieDog, 0, 1, 0, 1); + HarvestEnemyAssets(RdtId.Parse("2060"), ReCvEnemyIds.Bandersnatch, 0, 1, 0, 1); + HarvestEnemyAssets(RdtId.Parse("20C2"), ReCvEnemyIds.Tyrant, 0, 2, 0, 2); + HarvestEnemyAssets(RdtId.Parse("80D0"), ReCvEnemyIds.Hunter, 0, 1, 0, 1); + HarvestEnemyAssets(RdtId.Parse("80D0"), ReCvEnemyIds.Hunter, 256, 3, 0, 2); + } + else { - var embeddedEffects = GetEmbeddedEffects(rdt.RdtFile); - for (var i = 0; i < embeddedEffects.Count; i++) + foreach (var rdt in _gameData.Rdts) { - var ee = embeddedEffects[i]; - if (ee.Id != 0xFF && !_effects.ContainsKey(ee.Id)) + var embeddedEffects = GetEmbeddedEffects(rdt.RdtFile); + for (var i = 0; i < embeddedEffects.Count; i++) { - _effects[ee.Id] = ee; + var ee = embeddedEffects[i]; + if (ee.Id != 0xFF && !_effects.ContainsKey(ee.Id)) + { + _effects[ee.Id] = ee; + } } } } } + private void HarvestEnemyAssets(RdtId rdtId, short enemyType, short variant, int modelIndex, int motionIndex, int textureIndex) + { + var rdt = (RdtCv)_gameData.GetRdt(rdtId)!.RdtFile; + var model = rdt.Models.Pages[modelIndex]; + var motion = rdt.Motions; + var texture = rdt.Textures.Groups[textureIndex]; + _cvEnemies.Add(new CvEnemyAssets(enemyType, variant, model, motion, texture)); + } + public byte GetNextKillId() { if (_killIds.Count == 0) @@ -400,7 +426,7 @@ private void RandomizeRoomWithEnemy(RandomizedRdt rdt, SelectableEnemy targetEne g_stickyEnemies.Add(rdt.RdtId, targetEnemy); } } - else + else if (rdt.Version != BioVersion.BiohazardCv) { // Mute all NPCs in the room so that we can hear enemies foreach (var em in rdt.Enemies) @@ -579,89 +605,134 @@ private bool RemoveAllEnemiesFromRoom(RandomizedRdt rdt) private void RandomiseRoom(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SelectableEnemy targetEnemy) { - var enemiesToChange = GetEnemiesToReplace(rdt, enemySpec); - var possibleTypes = targetEnemy.Types.Shuffle(_rng); - if (enemySpec.IncludeTypes != null) - { - var includeTypes = enemySpec.IncludeTypes.Select(x => (byte)x).ToHashSet(); - possibleTypes = possibleTypes.Intersect(includeTypes).ToArray(); - } - else if (enemySpec.ExcludeTypes != null) - { - var excludeTypes = enemySpec.ExcludeTypes.Select(x => (byte)x).ToHashSet(); - possibleTypes = possibleTypes.Except(excludeTypes).ToArray(); - } + var possibleTypes = GetPossibleEnemyTypes(enemySpec, targetEnemy); if (possibleTypes.Length == 0) return; - var randomEnemiesToChange = new SceEmSetOpcode[0]; - if (_config.RandomEnemyPlacement && !enemySpec.KeepPositions) + if (_version == BioVersion.BiohazardCv) { - randomEnemiesToChange = GenerateRandomEnemies(rng, rdt, enemySpec, enemiesToChange, possibleTypes[0]); - } - if (randomEnemiesToChange.Length != 0) - { - enemiesToChange = randomEnemiesToChange; + var enemyType = _rng.NextOf(possibleTypes); + var assets = _cvEnemies.FirstOrDefault(x => x.EnemyType == enemyType); + var placements = GetRandomPlacements(rdt.RdtId, rng, enemySpec, enemyType); + + rdt.PostModifications.Add(() => + { + var rdtb = ((RdtCv)rdt.RdtFile).ToBuilder(); + + var models = rdtb.Models.ToBuilder(); + for (var i = 0; i < rdtb.Enemies.Count; i++) + { + models.Pages.RemoveAt(1 + i); + } + for (var i = 0; i < placements.Length; i++) + { + models.Pages.Insert(1, assets.Model); + } + rdtb.Models = models.ToCvModelList(); + + rdtb.Motions = assets.Motion; + + var textures = rdtb.Textures.ToBuilder(); + textures.Groups.RemoveAt(1); + textures.Groups.Insert(1, assets.Texture); + rdtb.Textures = textures.ToTextureList(); + + rdtb.Enemies.Clear(); + foreach (var ep in placements) + { + rdtb.Enemies.Add(new RdtCv.Enemy() + { + Header = 1, + Type = enemyType, + Effect = 0, + Variant = assets.Variant, + Index = (short)rdtb.Enemies.Count, + Position = new RdtCv.VectorF(ep.X, ep.Y, ep.Z), + Rotation = new RdtCv.Vector32(0, ep.D, 0), + }); + } + + rdt.RdtFile = rdtb.ToRdt(); + }); } else { - var quantity = enemiesToChange.DistinctBy(x => x.Id).Count(); - possibleTypes = possibleTypes.Where(type => + var enemiesToChange = GetEnemiesToReplace(rdt, enemySpec); + var randomEnemiesToChange = new SceEmSetOpcode[0]; + if (_config.RandomEnemyPlacement && !enemySpec.KeepPositions) { - var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty); - var maxQuantity = _enemyHelper.GetEnemyTypeLimit(_config, enemySpec.MaxDifficulty ?? _config.EnemyDifficulty, type); - return maxQuantity >= quantity; - }).ToArray(); - } - - if (possibleTypes.Length == 0) - return; + randomEnemiesToChange = GenerateRandomEnemies(rng, rdt, enemySpec, enemiesToChange, possibleTypes[0]); + } + if (randomEnemiesToChange.Length != 0) + { + enemiesToChange = randomEnemiesToChange; + } + else + { + var quantity = enemiesToChange.DistinctBy(x => x.Id).Count(); + possibleTypes = possibleTypes.Where(type => + { + var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty); + var maxQuantity = _enemyHelper.GetEnemyTypeLimit(_config, enemySpec.MaxDifficulty ?? _config.EnemyDifficulty, type); + return maxQuantity >= quantity; + }).ToArray(); + } - var randomEnemyType = possibleTypes[0]; - var ids = rdt.Enemies.Select(x => x.Id).Distinct().ToArray(); - var enemyTypesId = ids.Select(x => randomEnemyType).ToArray(); + if (possibleTypes.Length == 0) + return; - _enemyHelper.BeginRoom(rdt); + var randomEnemyType = possibleTypes[0]; + var ids = rdt.Enemies.Select(x => x.Id).Distinct().ToArray(); + var enemyTypesId = ids.Select(x => randomEnemyType).ToArray(); - for (var i = 0; i < enemiesToChange.Length; i++) - { - var enemy = enemiesToChange[i]; - var index = Array.IndexOf(ids, enemy.Id); - var enemyType = enemyTypesId[index]; - enemy.Type = enemyType; - if (!enemySpec.KeepState) - enemy.State = 0; - if (!enemySpec.KeepAi) - enemy.Ai = 0; - enemy.Texture = 0; - if (enemySpec.Y != null) - enemy.Y = enemySpec.Y.Value; - _enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType); + _enemyHelper.BeginRoom(rdt); - foreach (var dependencyType in _enemyHelper.GetEnemyDependencies(enemyType)) + for (var i = 0; i < enemiesToChange.Length; i++) { - i++; - enemy = enemiesToChange[i]; - enemy.Type = dependencyType; + var enemy = enemiesToChange[i]; + var index = Array.IndexOf(ids, enemy.Id); + var enemyType = enemyTypesId[index]; + enemy.Type = enemyType; + if (!enemySpec.KeepState) + enemy.State = 0; + if (!enemySpec.KeepAi) + enemy.Ai = 0; + enemy.Texture = 0; + if (enemySpec.Y != null) + enemy.Y = enemySpec.Y.Value; _enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType); + + foreach (var dependencyType in _enemyHelper.GetEnemyDependencies(enemyType)) + { + i++; + enemy = enemiesToChange[i]; + enemy.Type = dependencyType; + _enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType); + } } } } - private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SceEmSetOpcode[] currentEnemies, byte enemyType) + private byte[] GetPossibleEnemyTypes(MapRoomEnemies enemySpec, SelectableEnemy targetEnemy) { - var relevantPlacements = _enemyPositions - .Where(x => x.RdtId == rdt.RdtId) - .ToEndlessBag(_rng); + var possibleTypes = targetEnemy.Types.Shuffle(_rng); + if (enemySpec.IncludeTypes != null) + { + var includeTypes = enemySpec.IncludeTypes.Select(x => (byte)x).ToHashSet(); + possibleTypes = possibleTypes.Intersect(includeTypes).ToArray(); + } + else if (enemySpec.ExcludeTypes != null) + { + var excludeTypes = enemySpec.ExcludeTypes.Select(x => (byte)x).ToHashSet(); + possibleTypes = possibleTypes.Except(excludeTypes).ToArray(); + } - if (relevantPlacements.Count == 0) - return new SceEmSetOpcode[0]; + return possibleTypes; + } - var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty); - var enemyTypeLimit = _enemyHelper.GetEnemyTypeLimit(_config, difficulty, enemyType); - var avg = 1 + _config.EnemyQuantity; - var quantity = rng.Next(1, avg * 2); - quantity = Math.Min(quantity, Math.Min(enemyTypeLimit, relevantPlacements.Count * 3)); + private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SceEmSetOpcode[] currentEnemies, byte enemyType) + { + var placements = GetRandomPlacements(rdt.RdtId, rng, enemySpec, enemyType); foreach (var enemy in currentEnemies) rdt.Nop(enemy.Offset); @@ -673,9 +744,8 @@ private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRo var enemyOpcodes = new List(); var firstEnemyOpcodeIndex = rdt.AdditionalOpcodes.Count; - for (var i = 0; i < quantity; i++) + foreach (var ep in placements) { - var ep = relevantPlacements.Next(); while (usedIds.Contains(enemyId)) { enemyId++; @@ -701,6 +771,23 @@ private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRo return enemies.ToArray(); } + private EnemyPosition[] GetRandomPlacements(RdtId rdtId, Rng rng, MapRoomEnemies enemySpec, byte enemyType) + { + var relevantPlacements = _enemyPositions + .Where(x => x.RdtId == rdtId) + .ToEndlessBag(_rng); + + if (relevantPlacements.Count == 0) + return new EnemyPosition[0]; + + var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty); + var enemyTypeLimit = _enemyHelper.GetEnemyTypeLimit(_config, difficulty, enemyType); + var avg = 1 + _config.EnemyQuantity; + var quantity = rng.Next(1, avg * 2); + quantity = Math.Min(quantity, Math.Min(enemyTypeLimit, relevantPlacements.Count * 3)); + return relevantPlacements.Next(quantity); + } + private SceEmSetOpcode CreateEnemy(byte id, byte killId, EnemyPosition ep) { if (_version == BioVersion.Biohazard1) @@ -866,5 +953,24 @@ public override int GetHashCode() return (Room?.GetHashCode() ?? 0) ^ X ^ Y ^ Z ^ D ^ F; } } + + [DebuggerDisplay("EnemyType = {EnemyType} Variant = {Variant}")] + private readonly struct CvEnemyAssets + { + public short EnemyType { get; } + public short Variant { get; } + public CvModelListPage Model { get; } + public CvMotionList Motion { get; } + public CvTextureEntryGroup Texture { get; } + + public CvEnemyAssets(short enemyType, short variant, CvModelListPage model, CvMotionList motion, CvTextureEntryGroup texture) + { + EnemyType = enemyType; + Variant = variant; + Model = model; + Motion = motion; + Texture = texture; + } + } } } diff --git a/IntelOrca.Biohazard.BioRand/RECV/ReCvEnemyHelper.cs b/IntelOrca.Biohazard.BioRand/RECV/ReCvEnemyHelper.cs index 1db95c1b..c1b095aa 100644 --- a/IntelOrca.Biohazard.BioRand/RECV/ReCvEnemyHelper.cs +++ b/IntelOrca.Biohazard.BioRand/RECV/ReCvEnemyHelper.cs @@ -22,7 +22,12 @@ public string GetEnemyName(byte type) public int GetEnemyTypeLimit(RandoConfig config, int difficulty, byte type) { - var limit = new byte[] { 4, 8, 12, 16 }; + if (type == RECV.ReCvEnemyIds.Tyrant) + return 1; + if (type == RECV.ReCvEnemyIds.Zombie) + return 1; + + var limit = new byte[] { 2, 4, 7, 10 }; var index = Math.Min(limit.Length - 1, difficulty); return limit[index]; } @@ -33,7 +38,8 @@ public SelectableEnemy[] GetSelectableEnemies() => new[] new SelectableEnemy("Zombie", "LightGray", ReCvEnemyIds.Zombie), new SelectableEnemy("Hunter", "IndianRed", ReCvEnemyIds.Hunter), new SelectableEnemy("Bandersnatch", "Cyan", ReCvEnemyIds.Bandersnatch), - new SelectableEnemy("Zombie Dog", "Black", ReCvEnemyIds.ZombieDog) + new SelectableEnemy("Zombie Dog", "Black", ReCvEnemyIds.ZombieDog), + new SelectableEnemy("Tyrant", "Gray", ReCvEnemyIds.Tyrant) }; public bool IsEnemy(byte type) @@ -76,9 +82,6 @@ public bool SupportsEnemyType(RandoConfig config, RandomizedRdt rdt, bool hasEne public byte[] GetEnemyDependencies(byte enemyType) => new byte[0]; - public byte[] GetRequiredEsps(byte enemyType) - { - throw new NotImplementedException(); - } + public byte[] GetRequiredEsps(byte enemyType) => new byte[0]; } } diff --git a/IntelOrca.Biohazard.BioRand/RECV/ReCvRandomiser.cs b/IntelOrca.Biohazard.BioRand/RECV/ReCvRandomiser.cs index 3af0f759..0f2db56e 100644 --- a/IntelOrca.Biohazard.BioRand/RECV/ReCvRandomiser.cs +++ b/IntelOrca.Biohazard.BioRand/RECV/ReCvRandomiser.cs @@ -134,8 +134,6 @@ public override void Generate(RandoConfig config, IRandoProgress progress, FileR config.IncludeDocuments = false; config.SwapCharacters = false; config.RandomNPCs = false; - config.RandomEnemies = false; - config.RandomEnemyPlacement = false; config.RandomCutscenes = false; config.RandomEvents = false; config.RandomBgm = false; diff --git a/IntelOrca.Biohazard.BioRand/data/recv/enemy.json b/IntelOrca.Biohazard.BioRand/data/recv/enemy.json new file mode 100644 index 00000000..82c6b4d9 --- /dev/null +++ b/IntelOrca.Biohazard.BioRand/data/recv/enemy.json @@ -0,0 +1,6 @@ +[ + { "room": "1010", "x": 8, "y": 0, "z": -11, "d": 0 }, + { "room": "1010", "x": 8, "y": 0, "z": -11, "d": 0 }, + { "room": "1010", "x": 8, "y": 0, "z": 24, "d": 0 }, + { "room": "1010", "x": 8, "y": 0, "z": 50, "d": 0 } +] diff --git a/IntelOrca.Biohazard.BioRand/data/recv/rdt.json b/IntelOrca.Biohazard.BioRand/data/recv/rdt.json index 1db6c43f..3bb3ef0c 100644 --- a/IntelOrca.Biohazard.BioRand/data/recv/rdt.json +++ b/IntelOrca.Biohazard.BioRand/data/recv/rdt.json @@ -85,6 +85,11 @@ "type": 12, "amount": 15 } + ], + "enemies": [ + { + "nop": [ "0x3EF10-0x3EF28" ] + } ] }, "1020": { diff --git a/biohazard-utils b/biohazard-utils index 94d14a87..ead53e60 160000 --- a/biohazard-utils +++ b/biohazard-utils @@ -1 +1 @@ -Subproject commit 94d14a870b08c61250481a05a15856909cc033c8 +Subproject commit ead53e60a02e30ce61a23e2b07da001c1e22e6a1 diff --git a/biorand/MainWindow.xaml.cs b/biorand/MainWindow.xaml.cs index 7d936e02..0c33aa6a 100644 --- a/biorand/MainWindow.xaml.cs +++ b/biorand/MainWindow.xaml.cs @@ -1193,14 +1193,7 @@ private void UpdateEnemies() items.Add(new SliderListItem(enemy.Name, 4, 7)); } listEnemies.ItemsSource = items; - if (SelectedGame == 3) - { - chkRandomEnemyPlacements.Visibility = Visibility.Hidden; - } - else - { - chkRandomEnemyPlacements.Visibility = Visibility.Visible; - } + chkRandomEnemyPlacements.Visibility = Visibility.Visible; } private void UpdateEnemySkinList()