diff --git a/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs b/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs index 79ee5042..aa6d79ea 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/GameModeOptions.cs @@ -29,10 +29,10 @@ public class GameModeOptions public int MinGanonsTowerCrystalCount { get; set; } = 7; public int MaxGanonsTowerCrystalCount { get; set; } = 7; public KeysanityMode KeysanityMode { get; set; } - public bool SkipTourianBossDoor { get; set; } - public bool PlaceGTBigKeyInGT { get; set; } + public TourianBossDoor TourianBossDoor { get; set; } + public KeysanityGanonsTowerBigKeyLocation KeysanityGanonsTowerBigKeyLocation { get; set; } public bool ShuffleMetroidBossTokens { get; set; } - public bool OpenPyramid { get; set; } + public PyramidHole PyramidHole { get; set; } public GameModeOptions Clone() { diff --git a/src/TrackerCouncil.Smz3.Data/Options/PlandoConfig.cs b/src/TrackerCouncil.Smz3.Data/Options/PlandoConfig.cs index 23fc028f..a1cd42d4 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/PlandoConfig.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/PlandoConfig.cs @@ -47,8 +47,8 @@ public PlandoConfig(World world) LiftOffOnGoalCompletion = world.Config.GameModeOptions.LiftOffOnGoalCompletion; KeysanityMode = world.Config.GameModeOptions.KeysanityMode; - SkipTourianBossDoor = world.Config.GameModeOptions.SkipTourianBossDoor; - OpenPyramid = world.Config.GameModeOptions.OpenPyramid; + SkipTourianBossDoor = world.Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Open; + OpenPyramid = world.Config.GameModeOptions.PyramidHole == PyramidHole.Open; Items = world.Locations .ToDictionary(x => x.ToString(), x => x.Item.Type); diff --git a/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs b/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs index 1165e638..5fb72e28 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs @@ -160,10 +160,16 @@ public static RandomizerOptions Load(string loadPath, string savePath, bool isYa options.SeedOptions.GameModeOptions.GanonCrystalCount = options.SeedOptions.GanonCrystalCount.Value; options.SeedOptions.GameModeOptions.GanonsTowerCrystalCount = options.SeedOptions.GanonsTowerCrystalCount!.Value; options.SeedOptions.GameModeOptions.TourianBossCount = options.SeedOptions.TourianBossCount!.Value; - options.SeedOptions.GameModeOptions.SkipTourianBossDoor = options.SeedOptions.SkipTourianBossDoor!.Value; + options.SeedOptions.GameModeOptions.TourianBossDoor = options.SeedOptions.SkipTourianBossDoor!.Value + ? TourianBossDoor.Open + : TourianBossDoor.Closed; options.SeedOptions.GameModeOptions.KeysanityMode = options.SeedOptions.KeysanityMode!.Value; - options.SeedOptions.GameModeOptions.PlaceGTBigKeyInGT = options.SeedOptions.PlaceGTBigKeyInGT!.Value; - options.SeedOptions.GameModeOptions.OpenPyramid = options.SeedOptions.OpenPyramid!.Value; + options.SeedOptions.GameModeOptions.KeysanityGanonsTowerBigKeyLocation = + options.SeedOptions.PlaceGTBigKeyInGT!.Value + ? KeysanityGanonsTowerBigKeyLocation.GanonsTower + : KeysanityGanonsTowerBigKeyLocation.Anywhere; + options.SeedOptions.GameModeOptions.PyramidHole = + options.SeedOptions.OpenPyramid!.Value ? PyramidHole.Open : PyramidHole.Closed; options.SeedOptions.GanonCrystalCount = null; options.SeedOptions.GanonsTowerCrystalCount = null; options.SeedOptions.TourianBossCount = null; diff --git a/src/TrackerCouncil.Smz3.Data/Options/RandomizerPreset.cs b/src/TrackerCouncil.Smz3.Data/Options/RandomizerPreset.cs index 731cf0f8..2c151a9b 100644 --- a/src/TrackerCouncil.Smz3.Data/Options/RandomizerPreset.cs +++ b/src/TrackerCouncil.Smz3.Data/Options/RandomizerPreset.cs @@ -63,7 +63,7 @@ public static List GetDefaultPresets() GanonCrystalCount = 0, GanonsTowerCrystalCount = 0, TourianBossCount = 0, - OpenPyramid = true + PyramidHole = PyramidHole.Open }, } }, diff --git a/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs b/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs index c6b4011f..1ad81cb9 100644 --- a/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs +++ b/src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs @@ -16,7 +16,7 @@ public class ParsedRomDetails public required int GanonsTowerCrystalCount { get; set; } public required int GanonCrystalCount { get; set; } public required int TourianBossCount { get; set; } - public required bool OpenPyarmid { get; set; } + public required bool OpenPyramid { get; set; } public required bool SkipTourianBossDoor { get; set; } public required bool SpinJumpAnimations { get; set; } public required GameModeType GameModeType { get; set; } diff --git a/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs b/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs index e471641c..51061d18 100644 --- a/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs +++ b/src/TrackerCouncil.Smz3.Data/Services/GenerationSettingsWindowService.cs @@ -260,8 +260,8 @@ public void LoadPlando(PlandoConfig config) _model.GameSettings.AdditionalSettings.GanonsTowerCrystalCount = _model.PlandoConfig.GanonsTowerCrystalCount; _model.GameSettings.AdditionalSettings.KeysanityMode = _model.PlandoConfig.KeysanityMode; - _model.GameSettings.AdditionalSettings.SkipTourianBossDoor = _model.PlandoConfig.SkipTourianBossDoor; - _model.GameSettings.AdditionalSettings.OpenPyramid = _model.PlandoConfig.OpenPyramid; + _model.GameSettings.AdditionalSettings.TourianBossDoor = _model.PlandoConfig.SkipTourianBossDoor ? TourianBossDoor.Open : TourianBossDoor.Closed; + _model.GameSettings.AdditionalSettings.PyramidHole = _model.PlandoConfig.OpenPyramid ? PyramidHole.Open : PyramidHole.Closed; var hasShuffleMetroidBossTokens = false; foreach (var reward in _model.PlandoConfig.Rewards) @@ -532,7 +532,7 @@ public void UpdateSummaryText() sb.AppendLine($" - GT Crystal Count: {details.GanonsTowerCrystalCount}"); sb.AppendLine($" - Ganon Crystal Count: {details.GanonCrystalCount}"); sb.AppendLine($" - Tourian Boss Count: {details.TourianBossCount}"); - sb.AppendLine($" - Open Pyramid: {details.OpenPyarmid}"); + sb.AppendLine($" - Open Pyramid: {details.OpenPyramid}"); } sb.AppendLine($" - IsMultiworld: {ynResponses[details.IsMultiworld]}"); @@ -552,7 +552,7 @@ public void UpdateSummaryText() if (_model.IsPlando) { _model.GameSettings.GoalSettings.HideCrystalBossCount = _model.PlandoConfig?.HideGoalCount == true; - hiddenProperties.Add("PlaceGTBigKeyInGT");; + hiddenProperties.Add("KeysanityGanonsTowerBigKeyLocation");; hiddenProperties.Add("MinGanonsTowerCrystalCount"); hiddenProperties.Add("MaxGanonsTowerCrystalCount"); hiddenProperties.Add("MinGanonCrystalCount"); @@ -647,7 +647,7 @@ public void UpdateSummaryText() if (_model.GameSettings.AdditionalSettings.KeysanityMode is KeysanityMode.None or KeysanityMode.SuperMetroid) { - hiddenProperties.Add("PlaceGTBigKeyInGT"); + hiddenProperties.Add("KeysanityGanonsTowerBigKeyLocation"); } sb.AppendLine("Game Mode"); diff --git a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowAdditionalSettingsViewModel.cs b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowAdditionalSettingsViewModel.cs index f5001b1c..e5708b4d 100644 --- a/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowAdditionalSettingsViewModel.cs +++ b/src/TrackerCouncil.Smz3.Data/ViewModels/GenerationWindowAdditionalSettingsViewModel.cs @@ -41,8 +41,8 @@ public int MaxGanonsTowerCrystalCount } } - [DynamicFormFieldBoolComboBox("Pyramid access:", falseText: "Opens after Ganon's Tower is completed", trueText: "Open by default", trueFirst: false)] - public bool OpenPyramid { get; set; } + [DynamicFormFieldComboBox(label: "Pyramid access:")] + public PyramidHole PyramidHole { get; set; } [DynamicFormFieldBoolComboBox("Metroid boss tokens:", falseText: "Disabled", trueText: "Shuffled with dungeon rewards", trueFirst: false)] public bool ShuffleMetroidBossTokens { get; set; } @@ -59,15 +59,15 @@ public KeysanityMode KeysanityMode } } - [DynamicFormFieldBoolComboBox("Metroid statue room access:", falseText: "Require Crateria boss keycard", trueText: "Open by default", visibleWhenTrue: nameof(IsMetroidKeysanity))] - public bool SkipTourianBossDoor { get; set; } + [DynamicFormFieldComboBox(label: "Metroid statue room access:")] + public TourianBossDoor TourianBossDoor { get; set; } - [DynamicFormFieldBoolComboBox("Ganon's Tower big key location:", falseText: "Fully randomized", trueText: "In Ganon's Tower", trueFirst: false, visibleWhenTrue: nameof(IsZeldaKeysanity))] - public bool PlaceGTBigKeyInGT { get; set; } + [DynamicFormFieldComboBox(label: "Ganon's Tower big key location:", visibleWhenTrue: nameof(IsZeldaKeysanity))] + public KeysanityGanonsTowerBigKeyLocation KeysanityGanonsTowerBigKeyLocation { get; set; } - public bool IsMetroidKeysanity => KeysanityMode is KeysanityMode.Both or KeysanityMode.SuperMetroid; + public bool IsMetroidKeysanity => KeysanityMode is KeysanityMode.Both or KeysanityMode.SuperMetroid or KeysanityMode.Random; - public bool IsZeldaKeysanity => KeysanityMode is KeysanityMode.Both or KeysanityMode.Zelda; + public bool IsZeldaKeysanity => KeysanityMode is KeysanityMode.Both or KeysanityMode.Zelda or KeysanityMode.Random; public bool AlwaysHidden => false; public bool RandomizeGoalCounts diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/Location.cs b/src/TrackerCouncil.Smz3.Data/WorldData/Location.cs index b713c355..5849b976 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/Location.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/Location.cs @@ -413,7 +413,7 @@ public bool CanFill(Item item, Progression items) if (World.Config.MultiWorld && item.Type == ItemType.ProgressiveShield && item.World != World) fillable = false; // If the user wants the GT key in GT for the guessing game while playing Keysanity - else if (item is { Type: ItemType.BigKeyGT, World.Config.ZeldaKeysanity: true, World.Config.GameModeOptions.PlaceGTBigKeyInGT: true }) + else if (item is { Type: ItemType.BigKeyGT, World.Config.ZeldaKeysanity: true, World.Config.GameModeOptions.KeysanityGanonsTowerBigKeyLocation: KeysanityGanonsTowerBigKeyLocation.GanonsTower }) fillable = fillable && Region is GanonsTower; Item = oldItem; diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/SuperMetroid/Crateria/WestCrateria.cs b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/SuperMetroid/Crateria/WestCrateria.cs index 1475202f..750a27a9 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/SuperMetroid/Crateria/WestCrateria.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/SuperMetroid/Crateria/WestCrateria.cs @@ -129,7 +129,7 @@ public void UpdateMotherBrainAccessibility(Progression progression, bool hasAltG else if (World.LegacyWorld == null) { var canAccessStatueRoom = Terminator.Locations.First().IsAvailable(progression) && - (!World.Config.MetroidKeysanity || World.Config.GameModeOptions.SkipTourianBossDoor || + (!World.Config.MetroidKeysanity || World.Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Open || progression.CardCrateriaBoss); var rewardCount = World.Config.GameModeOptions.ShuffleMetroidBossTokens @@ -147,7 +147,7 @@ public void UpdateMotherBrainAccessibility(Progression progression, bool hasAltG else { var canAccessStatueRoom = World.LegacyWorld.IsLocationAccessible((int)LocationId.CrateriaTerminator, progression.LegacyProgression) && - (!World.Config.MetroidKeysanity || World.Config.GameModeOptions.SkipTourianBossDoor || + (!World.Config.MetroidKeysanity || World.Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Open || progression.CardCrateriaBoss); var canEnterTourian = World.Rewards.Count(x => x.MarkedReward?.IsInCategory(RewardCategory.Metroid) == true && x.HasReceivedReward) >= tourianBossRequirement; diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs index eff047e9..49875c0b 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/Regions/Zelda/DarkWorld/DarkWorldNorthEast.cs @@ -90,7 +90,7 @@ public void UpdateGanonAccessibility(Progression progression, Progression assume World.GanonsTower.Locations.All(x => x.IsAvailable(assumedKeyProgression)) && World.GanonsTower.CanBeatBoss(assumedKeyProgression, true)); - var isPyramidOpen = World.Config.GameModeOptions.OpenPyramid || canClimbGt; + var isPyramidOpen = World.Config.GameModeOptions.PyramidHole == PyramidHole.Open || canClimbGt; var hasMetGoal = hasAltGameMode ? isAltGameModeComplete : World.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla @@ -114,7 +114,7 @@ public void UpdateGanonAccessibility(Progression progression, Progression assume World.LegacyWorld.IsLocationAccessible((int)x.Id, assumedKeyProgression.LegacyProgression))); - var isPyramidOpen = World.Config.GameModeOptions.OpenPyramid || canClimbGt; + var isPyramidOpen = World.Config.GameModeOptions.PyramidHole == PyramidHole.Open || canClimbGt; var hasMetGoal = World.Config.GameModeOptions.SelectedGameModeType == GameModeType.Vanilla ? numAcquiredCrystals >= ganonCrystalRequirement : progression is { AllCrystals: true, AllPendants: true, MetroidBossCount: >= 4, Agahnim: true }; diff --git a/src/TrackerCouncil.Smz3.Data/WorldData/World.cs b/src/TrackerCouncil.Smz3.Data/WorldData/World.cs index 0bf173cb..46f083c6 100644 --- a/src/TrackerCouncil.Smz3.Data/WorldData/World.cs +++ b/src/TrackerCouncil.Smz3.Data/WorldData/World.cs @@ -273,6 +273,26 @@ public Boss GetBossOfType(BossType type) public void Setup(Random rnd) { + if (Config.GameModeOptions.KeysanityMode == KeysanityMode.Random) + { + Config.GameModeOptions.KeysanityMode = rnd.NextExcludingLast(); + } + + if (Config.GameModeOptions.KeysanityGanonsTowerBigKeyLocation == KeysanityGanonsTowerBigKeyLocation.Random) + { + Config.GameModeOptions.KeysanityGanonsTowerBigKeyLocation = rnd.NextExcludingLast(); + } + + if (Config.GameModeOptions.PyramidHole == PyramidHole.Random) + { + Config.GameModeOptions.PyramidHole = rnd.NextExcludingLast(); + } + + if (Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Random) + { + Config.GameModeOptions.TourianBossDoor = rnd.NextExcludingLast(); + } + SetMedallions(rnd); SetRewards(rnd); SetBottles(rnd); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs index 9dfff858..37b3fbb6 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/GoalsPatch.cs @@ -17,7 +17,7 @@ public override IEnumerable GetChanges(GetPatchesRequest data) } // Open pyramid - if (data.Config.GameModeOptions.OpenPyramid) + if (data.Config.GameModeOptions.PyramidHole == PyramidHole.Open) { yield return new GeneratedPatch(0x40008B, [0x01]); } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/MetroidKeysanityPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/MetroidKeysanityPatch.cs index c148c553..50ff1a69 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/MetroidKeysanityPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/MetroidKeysanityPatch.cs @@ -75,7 +75,7 @@ public override IEnumerable GetChanges(GetPatchesRequest data) foreach (var door in s_doorList) { - if (door[0] == 0x99BD && data.World.Config.GameModeOptions.SkipTourianBossDoor) + if (door[0] == 0x99BD && data.World.Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Open) { continue; } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs index 532e2a48..fba6946b 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/FileData/Patches/ZeldaTextsPatch.cs @@ -120,8 +120,15 @@ private IEnumerable GetFullTextPatchList(GetPatchesRequest data) GameLines.TriforceRoom, _plandoText.TriforceRoom, true); var gtCrystalCount = data.World.Config.GameModeOptions.GanonsTowerCrystalCount; - SetText(StringTable.GanonsTowerGoalSign, - GameLines.GanonsTowerGoalSign, _plandoText.GanonsTowerGoalSign, false, gtCrystalCount == 1 ? "1 crystal" : $"{gtCrystalCount} crystals"); + var gtCrystalCountText = gtCrystalCount == 1 ? "1 crystal" : $"{gtCrystalCount} crystals"; + var gtKeyLocation = data.Worlds.SelectMany(world => world.Locations) + .FirstOrDefault(l => l.ItemIs(ItemType.BigKeyGT, data.World)); + var gtKeyLocationPlayer = data.Config.MultiWorld && gtKeyLocation?.World != data.World + ? gtKeyLocation?.World.Player ?? "another player" + : "You"; + + SetText(StringTable.GanonsTowerGoalSign, GameLines.GanonsTowerGoalSign, _plandoText.GanonsTowerGoalSign, false, + gtCrystalCountText, gtKeyLocationPlayer, gtKeyLocation?.Region.Area ?? "unknown location"); SetText(StringTable.GanonGoalSign, null, _plandoText.GanonGoalSign ?? gameModeText?.GanonGoalSign); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs index 302afb3d..cf146b35 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/GameModes/AllDungeonsGameMode.cs @@ -62,14 +62,14 @@ public override bool IsComplete(World world) return progression is { AllPendants: true, AllCrystals: true } && progression.MetroidBossCount >= 4 && world.AllBosses.First(x => x.Type == BossType.Agahnim).Defeated && - (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.OpenPyramid); + (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.PyramidHole == PyramidHole.Open); } public override void UpdateWorld(World world, Random rng, GameModeOptions gameModeOptions) { gameModeOptions.GanonCrystalCount = 7; gameModeOptions.TourianBossCount = 4; - gameModeOptions.OpenPyramid = false; + gameModeOptions.PyramidHole = PyramidHole.Closed; } public override void UpdateInitialTrackerState(GameModeOptions gameModeOptions, TrackerState trackerState, @@ -108,7 +108,7 @@ public override List GetGoalUiDetails(World world, Progression pr dungeons++; } - if (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.OpenPyramid) + if (_tracker.AutoTracker?.OpenPyramid == true || world.AllBosses.First(x => x.Type == BossType.Ganon).Defeated || world.Config.GameModeOptions.PyramidHole == PyramidHole.Open) { dungeons++; } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs index cb473c19..857a848f 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/GameHintService.cs @@ -439,14 +439,14 @@ private LocationUsefulness VerifyWorld(Playthrough.Sphere sphere, World world, L } // Make sure the player can access tourian - if (world.Config is { MetroidKeysanity: true, GameModeOptions.SkipTourianBossDoor: false } && + if (world.Config is { MetroidKeysanity: true, GameModeOptions.TourianBossDoor: TourianBossDoor.Closed } && !progression.Contains(ItemType.CardCrateriaBoss)) { return LocationUsefulness.Mandatory; } // Make sure the player can access Ganon - if (!world.Config.GameModeOptions.OpenPyramid && !worldLocations.ContainsKey(LocationId.GanonsTowerMoldormChest)) + if (world.Config.GameModeOptions.PyramidHole == PyramidHole.Closed && !worldLocations.ContainsKey(LocationId.GanonsTowerMoldormChest)) { return LocationUsefulness.Mandatory; } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs index 9b05a337..c27f7fc3 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomGenerationService.cs @@ -57,8 +57,8 @@ public SeedData GeneratePlandoSeed(RandomizerOptions options, PlandoConfig pland config.GameModeOptions.LiftOffOnGoalCompletion = plandoConfig.LiftOffOnGoalCompletion; config.GameModeOptions.KeysanityMode = plandoConfig.KeysanityMode; - config.GameModeOptions.SkipTourianBossDoor = plandoConfig.SkipTourianBossDoor; - config.GameModeOptions.OpenPyramid = plandoConfig.OpenPyramid; + config.GameModeOptions.TourianBossDoor = plandoConfig.SkipTourianBossDoor ? TourianBossDoor.Open : TourianBossDoor.Closed; + config.GameModeOptions.PyramidHole = plandoConfig.OpenPyramid ? PyramidHole.Open : PyramidHole.Closed; config.LogicConfig = plandoConfig.Logic.Clone(); return plandomizer.GenerateSeed(config, CancellationToken.None); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs index 3917d362..c9f63a14 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/RomTextService.cs @@ -207,12 +207,12 @@ private string GetSpoilerLog(RandomizerOptions options, SeedData seed, bool conf if (world.Config.MetroidKeysanity) { - keysanityOptions += $", SkipTourianBossDoor = {gameModeOptions.SkipTourianBossDoor}"; + keysanityOptions += $", TourianBossDoor = {gameModeOptions.TourianBossDoor}"; } if (world.Config.ZeldaKeysanity) { - keysanityOptions += $", PlaceGTBigKeyInGT = {gameModeOptions.PlaceGTBigKeyInGT}"; + keysanityOptions += $", KeysanityGanonsTowerBigKeyLocation = {gameModeOptions.KeysanityGanonsTowerBigKeyLocation}"; } } else @@ -221,7 +221,7 @@ private string GetSpoilerLog(RandomizerOptions options, SeedData seed, bool conf } log.AppendLine( - $"Additional Settings: GanonsTowerCrystalCount = {gameModeOptions.GanonsTowerCrystalCount}, OpenPyramid = {gameModeOptions.OpenPyramid}, ShuffleMetroidBossTokens = {gameModeOptions.ShuffleMetroidBossTokens}, {keysanityOptions}"); + $"Additional Settings: GanonsTowerCrystalCount = {gameModeOptions.GanonsTowerCrystalCount}, PyramidHole = {gameModeOptions.PyramidHole}, ShuffleMetroidBossTokens = {gameModeOptions.ShuffleMetroidBossTokens}, {keysanityOptions}"); log.AppendLine(); } diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs index 5ed6a681..69e2c756 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/Smz3RomParser.cs @@ -140,7 +140,7 @@ public ParsedRomDetails ParseRomFile(string filePath) GanonsTowerCrystalCount = gtCrystalCount, GanonCrystalCount = ganonCrystalCount, TourianBossCount = tourianBossCount, - OpenPyarmid = openPyramid, + OpenPyramid = openPyramid, SkipTourianBossDoor = skippedTourianBossDoor, RomGenerator = romGenerator.Value, Players = players, @@ -174,8 +174,8 @@ public SeedData GenerateSeedData(RandomizerOptions options, ParsedRomDetails par config.GameModeOptions.GanonsTowerCrystalCount = parsedRomDetails.GanonsTowerCrystalCount; config.GameModeOptions.GanonCrystalCount = parsedRomDetails.GanonCrystalCount; config.GameModeOptions.TourianBossCount = parsedRomDetails.TourianBossCount; - config.GameModeOptions.OpenPyramid = parsedRomDetails.OpenPyarmid; - config.GameModeOptions.SkipTourianBossDoor = parsedRomDetails.SkipTourianBossDoor; + config.GameModeOptions.PyramidHole = parsedRomDetails.OpenPyramid ? PyramidHole.Open : PyramidHole.Closed; + config.GameModeOptions.TourianBossDoor = parsedRomDetails.SkipTourianBossDoor ? TourianBossDoor.Open : TourianBossDoor.Closed; config.GameModeOptions.SelectedGameModeType = parsedRomDetails.GameModeType; config.GameModeOptions.ShuffleMetroidBossTokens = true; config.LocationItems.Clear(); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/StandardFiller.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/StandardFiller.cs index 342aa6d5..e1dc772b 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Generation/StandardFiller.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Generation/StandardFiller.cs @@ -87,7 +87,7 @@ public void Fill(List worlds, Config config, CancellationToken cancellati AssumedFill(dungeon, progression.Concat(world.ItemPools.MetroidKeysanityItems).Concat(assumedInventory).Concat(preferenceItems).ToList(), worldLocations, [world], cancellationToken); } - else if (worldConfig.GameModeOptions.PlaceGTBigKeyInGT) + else if (worldConfig.GameModeOptions.KeysanityGanonsTowerBigKeyLocation == KeysanityGanonsTowerBigKeyLocation.GanonsTower) { _logger.LogDebug("Placing GT Big Key in GT"); var worldLocations = world.Locations.Empty().Shuffle(Random); diff --git a/src/TrackerCouncil.Smz3.SeedGenerator/Infrastructure/PlaythroughService.cs b/src/TrackerCouncil.Smz3.SeedGenerator/Infrastructure/PlaythroughService.cs index ed010a77..6fcbcc6b 100644 --- a/src/TrackerCouncil.Smz3.SeedGenerator/Infrastructure/PlaythroughService.cs +++ b/src/TrackerCouncil.Smz3.SeedGenerator/Infrastructure/PlaythroughService.cs @@ -172,13 +172,13 @@ public Playthrough Generate(IReadOnlyCollection worlds, Config config) foreach (var world in worlds) { - if (world.Config is { MetroidKeysanity: true, GameModeOptions.SkipTourianBossDoor: false } && !items.Any(x => x.Type == ItemType.CardCrateriaBoss && x.World == world)) + if (world.Config is { MetroidKeysanity: true, GameModeOptions.TourianBossDoor: TourianBossDoor.Closed } && !items.Any(x => x.Type == ItemType.CardCrateriaBoss && x.World == world)) { anyUnbeatableWorld = true; break; } - var crystalCountRequired = world.Config.GameModeOptions.OpenPyramid + var crystalCountRequired = world.Config.GameModeOptions.PyramidHole == PyramidHole.Open ? world.Config.GameModeOptions.GanonCrystalCount : Math.Max(world.Config.GameModeOptions.GanonCrystalCount, world.Config.GameModeOptions.GanonsTowerCrystalCount); diff --git a/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityGanonsTowerBigKeyLocation.cs b/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityGanonsTowerBigKeyLocation.cs new file mode 100644 index 00000000..016ee009 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityGanonsTowerBigKeyLocation.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace TrackerCouncil.Smz3.Shared.Enums; + +[DefaultValue(Anywhere)] +[TypeConverter(typeof(EnumDescriptionTypeConverter))] +public enum KeysanityGanonsTowerBigKeyLocation +{ + [Description("Randomly place anywhere")] + Anywhere, + + [Description("Place in Ganon's Tower")] + GanonsTower, + + [Description("50% Chance in Ganon's Tower")] + Random +} diff --git a/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityMode.cs b/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityMode.cs index 9094ebfb..9111d3ad 100644 --- a/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityMode.cs +++ b/src/TrackerCouncil.Smz3.Shared/Enums/KeysanityMode.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using TrackerCouncil.Smz3.Shared; namespace TrackerCouncil.Smz3.Shared.Enums; @@ -18,4 +17,7 @@ public enum KeysanityMode [Description("Metroid Only")] SuperMetroid, + + [Description("Random")] + Random, } diff --git a/src/TrackerCouncil.Smz3.Shared/Enums/PyramidHole.cs b/src/TrackerCouncil.Smz3.Shared/Enums/PyramidHole.cs new file mode 100644 index 00000000..01801a98 --- /dev/null +++ b/src/TrackerCouncil.Smz3.Shared/Enums/PyramidHole.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace TrackerCouncil.Smz3.Shared.Enums; + +[DefaultValue(Closed)] +[TypeConverter(typeof(EnumDescriptionTypeConverter))] +public enum PyramidHole +{ + [Description("Opens after Ganon's Tower is completed")] + Closed, + + [Description("Open by default")] + Open, + + [Description("Random")] + Random +} diff --git a/src/TrackerCouncil.Smz3.Shared/Enums/TourianBossDoor.cs b/src/TrackerCouncil.Smz3.Shared/Enums/TourianBossDoor.cs new file mode 100644 index 00000000..21acb39d --- /dev/null +++ b/src/TrackerCouncil.Smz3.Shared/Enums/TourianBossDoor.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace TrackerCouncil.Smz3.Shared.Enums; + +[DefaultValue(Closed)] +[TypeConverter(typeof(EnumDescriptionTypeConverter))] +public enum TourianBossDoor +{ + [Description("Require Crateria boss keycard")] + Closed, + + [Description("Open by default")] + Open, + + [Description("Random")] + Random +} diff --git a/src/TrackerCouncil.Smz3.Shared/RandomExtensions.cs b/src/TrackerCouncil.Smz3.Shared/RandomExtensions.cs index ef7c766b..6afd3bfa 100644 --- a/src/TrackerCouncil.Smz3.Shared/RandomExtensions.cs +++ b/src/TrackerCouncil.Smz3.Shared/RandomExtensions.cs @@ -4,18 +4,43 @@ namespace TrackerCouncil.Smz3.Shared; public static class RandomExtensions { - /// - /// .NET Random has an issue where the first few numbers can be subject to patterns - /// This function will skip over the first 10 results to help ensure randomness - /// /// - /// - public static Random Sanitize(this Random rnd) + extension(Random rnd) { - for (var i = 0; i < 10; i++) + /// + /// .NET Random has an issue where the first few numbers can be subject to patterns + /// This function will skip over the first 10 results to help ensure randomness + /// + /// + public Random Sanitize() { - rnd.Next(); + for (var i = 0; i < 10; i++) + { + rnd.Next(); + } + return rnd; + } + + /// + /// Returns a random value from the provided enum + /// + /// Enum type to return a random value from + /// A random value from the provided enum + public T Next() where T : struct, Enum + { + var values = Enum.GetValues(); + return values[rnd.Next(values.Length)]; + } + + /// + /// Returns a random value from the provided enum excluding the last record in the enumd + /// + /// Enum type to return a random value from + /// A random value from the provided enum + public T NextExcludingLast() where T : struct, Enum + { + var values = Enum.GetValues(); + return values.Length <= 1 ? values[0] : values[rnd.Next(values.Length - 1)]; } - return rnd; } } diff --git a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTracker.cs b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTracker.cs index 9e37bee6..8921dd78 100644 --- a/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTracker.cs +++ b/src/TrackerCouncil.Smz3.Tracking/AutoTracking/AutoTracker.cs @@ -211,7 +211,7 @@ public override void UpdateValidState(bool hasValidState) public override void IncrementGTItems(Location location) { - if (_foundGTKey || _config.Config is { ZeldaKeysanity: true, GameModeOptions.PlaceGTBigKeyInGT: false }) return; + if (_foundGTKey || _config.Config is { ZeldaKeysanity: true, GameModeOptions.KeysanityGanonsTowerBigKeyLocation: KeysanityGanonsTowerBigKeyLocation.Anywhere }) return; var chatIntegrationModule = _trackerModuleFactory.GetModule(); _numGTItems++; diff --git a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerItemService.cs b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerItemService.cs index cfc308ff..8dceb4be 100644 --- a/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerItemService.cs +++ b/src/TrackerCouncil.Smz3.Tracking/TrackingServices/TrackerItemService.cs @@ -42,7 +42,7 @@ public bool TrackItem(Item item, string? trackedAs = null, float? confidence = n var itemName = item.Name; var originalTrackingState = item.TrackingState; - var isGTPreBigKey = (!World.Config.ZeldaKeysanity || World.Config.GameModeOptions.PlaceGTBigKeyInGT) + var isGTPreBigKey = (!World.Config.ZeldaKeysanity || World.Config.GameModeOptions.KeysanityGanonsTowerBigKeyLocation == KeysanityGanonsTowerBigKeyLocation.GanonsTower) && autoTracked && location?.Region.GetType() == typeof(GanonsTower) && !playerProgressionService.GetProgression(false).BigKeyGT; diff --git a/src/TrackerCouncil.Smz3.UI/Services/TrackerMapWindowService.cs b/src/TrackerCouncil.Smz3.UI/Services/TrackerMapWindowService.cs index a0207830..5fe5c150 100644 --- a/src/TrackerCouncil.Smz3.UI/Services/TrackerMapWindowService.cs +++ b/src/TrackerCouncil.Smz3.UI/Services/TrackerMapWindowService.cs @@ -289,7 +289,7 @@ private List GetDoorLocationModels(TrackerMapRegion foreach (var door in doors) { - if (door.SkippableOnFastTourian && worldRegion.Config.GameModeOptions.SkipTourianBossDoor) + if (door.SkippableOnFastTourian && worldRegion.Config.GameModeOptions.TourianBossDoor == TourianBossDoor.Open) { continue; } diff --git a/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs b/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs index f572db04..76941b2b 100644 --- a/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs +++ b/tests/TrackerCouncil.Smz3.Tests/Text/GameHintTests.cs @@ -100,18 +100,18 @@ void VanillaGoalZeldaHints() // If we set the ganon crystal requirement to 0, it should show up as useless world.Config.GameModeOptions.GanonCrystalCount = 0; - world.Config.GameModeOptions.OpenPyramid = true; + world.Config.GameModeOptions.PyramidHole = PyramidHole.Open; Assert.Equal(LocationUsefulness.Useless, hintService.GetUsefulness(locations, [world], null)); // But if the player has to climb GT to access ganon, it should be mandatory world.Config.GameModeOptions.GanonCrystalCount = 0; - world.Config.GameModeOptions.OpenPyramid = false; + world.Config.GameModeOptions.PyramidHole = PyramidHole.Closed; Assert.Equal(LocationUsefulness.Mandatory, hintService.GetUsefulness(locations, [world], null)); // Unless GT also requires 0 crystals world.Config.GameModeOptions.GanonCrystalCount = 0; world.Config.GameModeOptions.GanonsTowerCrystalCount = 0; - world.Config.GameModeOptions.OpenPyramid = false; + world.Config.GameModeOptions.PyramidHole = PyramidHole.Closed; Assert.Equal(LocationUsefulness.Useless, hintService.GetUsefulness(locations, [world], null)); } @@ -137,7 +137,7 @@ void VanillaGoalSwordAndSilversHints() var world = GetVanillaWorld(); world.Config.GameModeOptions.GanonCrystalCount = 0; world.Config.GameModeOptions.GanonsTowerCrystalCount = 0; - world.Config.GameModeOptions.OpenPyramid = true; + world.Config.GameModeOptions.PyramidHole = PyramidHole.Open; var hintService = GetGameHintService(); // Move the bow and boots out of eastern palace for testing @@ -217,7 +217,7 @@ void KeysanityHints() Assert.Equal(LocationUsefulness.Key, hintService.GetUsefulness([crateriaBossKeycard], [world], null)); // But if tourian boss door is disabled, then it should be marked as useless - world.Config.GameModeOptions.SkipTourianBossDoor = true; + world.Config.GameModeOptions.TourianBossDoor = TourianBossDoor.Open; Assert.Equal(LocationUsefulness.Useless, hintService.GetUsefulness([crateriaBossKeycard], [world], null)); // Make sure the lower norfair boss keycard location shows up as a key initially