diff --git a/.github/workflows/build-debug.yaml b/.github/workflows/build-debug.yaml
index b4ded7d4e..35eb3f934 100644
--- a/.github/workflows/build-debug.yaml
+++ b/.github/workflows/build-debug.yaml
@@ -5,7 +5,9 @@ on:
branches-ignore:
- main
- testing
- - '5.1'
+ - extras
+ - sgl
+ - '5.2'
pull_request:
env:
diff --git a/.github/workflows/build-latest.yaml b/.github/workflows/build-latest.yaml
index 06f3f7c8d..54c37e8ae 100644
--- a/.github/workflows/build-latest.yaml
+++ b/.github/workflows/build-latest.yaml
@@ -5,7 +5,9 @@ on:
branches:
- main
- testing
- - '5.1'
+ - extras
+ - sgl
+ - '5.2'
env:
# I'm not a fan of the telemetry as-is, but this also suppresses some lines in the build log.
diff --git a/CoreSourceGenerator/CoreSourceGenerator.csproj b/CoreSourceGenerator/CoreSourceGenerator.csproj
index bed7384f5..b20c92c0b 100644
--- a/CoreSourceGenerator/CoreSourceGenerator.csproj
+++ b/CoreSourceGenerator/CoreSourceGenerator.csproj
@@ -11,10 +11,7 @@
Debug;Release;Unsafe Debug
AnyCPU
-
- true
- true
-
+
true
false
diff --git a/CoreSourceGenerator/FlagsSerializeGenerator.cs b/CoreSourceGenerator/FlagsSerializeGenerator.cs
index 55882e30d..683f04bca 100644
--- a/CoreSourceGenerator/FlagsSerializeGenerator.cs
+++ b/CoreSourceGenerator/FlagsSerializeGenerator.cs
@@ -58,7 +58,7 @@ private static bool IsPartialClass(SyntaxNode node)
.Select(f =>
{
// Look up the source code for the declaration to find if it has a default value
- var equalsSyntax = f.DeclaringSyntaxReferences[0].GetSyntax() switch
+ var equalsSyntax = f.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch
{
PropertyDeclarationSyntax property => property.Initializer,
VariableDeclaratorSyntax variable => variable.Initializer,
@@ -81,18 +81,32 @@ private static bool IsPartialClass(SyntaxNode node)
var serializeFields = classSymbol.GetMembers()
.OfType()
.Where(f => !HasIgnoreInFlagsAttribute(f))
- .Select(f => new SerializedFieldInfo()
+ .Select(f =>
{
- FieldName = f.Name,
- FieldType = f.Type.ToDisplayString(),
- IsDifficultyOnly = HasDifficultyOnlyAttribute(f),
- IsConditionallyIncluded = HasConditionallyIncludedInFlagsAttribute(f),
- DefaultValue = GetDefaultValue(f),
- IsEnum = f.Type.TypeKind == TypeKind.Enum,
- EnumSymbol = f.Type.TypeKind == TypeKind.Enum ? f.Type as INamedTypeSymbol : null,
- Minimum = GetCustomMinimum(f),
- Maximum = GetCustomMaximum(f),
- CustomSerializerName = GetCustomFlagSerializer(f),
+ var dictionaryInterface = f.Type.AllInterfaces
+ .FirstOrDefault(i => i.OriginalDefinition.ToDisplayString().StartsWith("System.Collections.Generic.IDictionary"));
+ var innerType = dictionaryInterface != null ? dictionaryInterface.TypeArguments[0] : f.Type;
+
+ var equalsSyntax = f.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch
+ {
+ PropertyDeclarationSyntax property => property.Initializer,
+ VariableDeclaratorSyntax variable => variable.Initializer,
+ _ => null
+ };
+
+ return new SerializedFieldInfo()
+ {
+ FieldName = f.Name,
+ FieldType = f.Type.ToDisplayString(),
+ IsDifficultyOnly = HasDifficultyOnlyAttribute(f),
+ IsConditionallyIncluded = HasConditionallyIncludedInFlagsAttribute(f),
+ DefaultValue = equalsSyntax?.Value.ToString(),
+ IsEnum = f.Type.TypeKind == TypeKind.Enum,
+ EnumSymbol = f.Type.TypeKind == TypeKind.Enum ? f.Type as INamedTypeSymbol : null,
+ Minimum = GetCustomMinimum(f),
+ Maximum = GetCustomMaximum(f),
+ CustomSerializerName = GetCustomFlagSerializer(f),
+ };
}).ToList();
classInfo.SerializedFields.AddRange(serializeFields);
@@ -140,14 +154,6 @@ private static bool HasDifficultyOnlyAttribute(IFieldSymbol field)
.Any(attr => attr.AttributeClass?.Name.StartsWith("DifficultyOnly") ?? false);
}
- private static string GetDefaultValue(IFieldSymbol f)
- {
- var defaultAttr = f.GetAttributes()
- .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "System.ComponentModel.DefaultValueAttribute");
- var arg = defaultAttr?.ConstructorArguments[0];
- return arg != null ? FormatArgument(arg.Value) : "default";
- }
-
private static bool HasIgnoreInFlagsAttribute(IFieldSymbol field)
{
return field.GetAttributes()
@@ -317,7 +323,10 @@ private static void GenerateReactiveProperty(StringBuilder sb, ReactiveFieldInfo
sb.AppendLine($"{indent} {{");
sb.AppendLine($"{indent} {field.FieldName} = value;");
sb.AppendLine($"{indent} OnPropertyChanged(nameof({field.PropertyName}));");
- sb.AppendLine($"{indent} OnPropertyChanged(\"Flags\");");
+ sb.AppendLine($"{indent} if (!_inDeserializeFlags)");
+ sb.AppendLine($"{indent} {{");
+ sb.AppendLine($"{indent} OnPropertyChanged(\"Flags\");");
+ sb.AppendLine($"{indent} }}");
sb.AppendLine($"{indent} }}");
sb.AppendLine($"{indent} }}");
sb.AppendLine($"{indent} }}{defaultValue}");
diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj
index 3bbe79176..a63b7f6d5 100644
--- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj
+++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj
@@ -22,8 +22,14 @@
false
false
+ true
+ true
+
+
+
+
true
diff --git a/CrossPlatformUI/Assets/palace-blockers.png b/CrossPlatformUI/Assets/palace-blockers.png
deleted file mode 100644
index 7493ce1b0..000000000
Binary files a/CrossPlatformUI/Assets/palace-blockers.png and /dev/null differ
diff --git a/CrossPlatformUI/CrossPlatformUI.csproj b/CrossPlatformUI/CrossPlatformUI.csproj
index 36406d1c1..fa91327a7 100644
--- a/CrossPlatformUI/CrossPlatformUI.csproj
+++ b/CrossPlatformUI/CrossPlatformUI.csproj
@@ -13,11 +13,6 @@
true
-
-
- $(NoWarn);IL2026
-
-
true
false
@@ -35,6 +30,7 @@
+
@@ -46,6 +42,11 @@
+
+
+
+
+
diff --git a/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx
index 1a2dbc393..0d6dbec3b 100644
--- a/CrossPlatformUI/Lang/Resources.resx
+++ b/CrossPlatformUI/Lang/Resources.resx
@@ -231,9 +231,20 @@ group is the same as the number in that group that steal EXP in vanilla.
If selected, the amount of EXP each stealer steals is between 50% and 150% of the vanilla
amount.
-
-Randomly reassigns which enemies are immune to stabs. The number of enemies that are
-immune in each group is the same as the number in that group that are immune in vanilla.
+
+Vanilla:
+Uses the game's original sword immunities. (Tektites and Zoras)
+
+Shuffle:
+Randomly reassigns which enemies are immune to sword attacks and require the Fire spell
+to damage. On average, the same number of enemies are immune as in the vanilla game.
+
+Shuffle/None if Fire unusable:
+Uses Shuffle if the Fire spell is usable (not replaced by Dash nor linked with Fairy).
+Otherwise, removes all sword immunities.
+
+None:
+Removes all sword immunities.
Randomizes the color palettes of enemies and NPCs. Health bar color is also randomized.
@@ -692,9 +703,9 @@ linear.
make room.
Random Item Rooms Per Palace:
+ • Each palace gets 0–3 item rooms (varies by palace length/style).
• Clouds in the entrance screen and Ironknuckle statues by the entrance elevator
indicate how many items there are in the palace.
- • Each palace gets 0–3 item rooms (varies by palace length/style).
If selected, the normal zelda 2 rooms will be available for palace generation. Note that
@@ -766,8 +777,10 @@ considered duplicates of each other. Has no effect on vanilla palaces. Does not
prevent duplicates for Random Walk generated palaces.
-Allows rooms that require an item or spell to traverse to appear in any palace. Otherwise,
-palaces can only contain blocks according to the following table.
+Allows rooms that cannot be passed without a specific item or spell to appear in any palace.
+Otherwise, the allowed requirements for rooms in palaces are based on the items and spells
+needed to pass through every room in that palace in vanilla (from either direction).
+See the Wiki for the full table.
Removes dead-end rooms from the room pool. This helps newer players who are not yet
diff --git a/CrossPlatformUI/Presets/BeginnerPreset.cs b/CrossPlatformUI/Presets/BeginnerPreset.cs
index 644b69e3d..c077f3ebe 100644
--- a/CrossPlatformUI/Presets/BeginnerPreset.cs
+++ b/CrossPlatformUI/Presets/BeginnerPreset.cs
@@ -31,8 +31,8 @@ public static class BeginnerPreset
EastBiome = Biome.VANILLALIKE,
DmBiome = Biome.VANILLALIKE,
MazeBiome = Biome.VANILLALIKE,
- WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST,
- EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST,
+ WestClimate = ClimateEnum.VANILLA_WEIGHTED,
+ EastClimate = ClimateEnum.VANILLA_WEIGHTED,
//Palaces
NormalPalaceStyle = PalaceStyle.VANILLA_WEIGHTED,
diff --git a/CrossPlatformUI/Presets/FullShufflePreset.cs b/CrossPlatformUI/Presets/FullShufflePreset.cs
index 51cc1e5be..4e7e47541 100644
--- a/CrossPlatformUI/Presets/FullShufflePreset.cs
+++ b/CrossPlatformUI/Presets/FullShufflePreset.cs
@@ -29,8 +29,8 @@ public static class FullShufflePreset
EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE,
DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE,
MazeBiome = Biome.VANILLALIKE,
- WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST,
- EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST,
+ WestClimate = ClimateEnum.VANILLA_WEIGHTED,
+ EastClimate = ClimateEnum.VANILLA_WEIGHTED,
ContinentConnectionType = ContinentConnectionType.ANYTHING_GOES,
//Palaces
@@ -77,7 +77,7 @@ public static class FullShufflePreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/MaxRando2025Preset.cs b/CrossPlatformUI/Presets/MaxRando2025Preset.cs
index 0eb0834dc..9668fee8e 100644
--- a/CrossPlatformUI/Presets/MaxRando2025Preset.cs
+++ b/CrossPlatformUI/Presets/MaxRando2025Preset.cs
@@ -79,7 +79,7 @@ public static class MaxRando2025Preset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/MaxRandoPreset.cs b/CrossPlatformUI/Presets/MaxRandoPreset.cs
index c7898e406..1296f963b 100644
--- a/CrossPlatformUI/Presets/MaxRandoPreset.cs
+++ b/CrossPlatformUI/Presets/MaxRandoPreset.cs
@@ -90,7 +90,7 @@ public static class MaxRandoPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.WIDE,
//Items
diff --git a/CrossPlatformUI/Presets/NormalPreset.cs b/CrossPlatformUI/Presets/NormalPreset.cs
index d2c6b421d..5f723b002 100644
--- a/CrossPlatformUI/Presets/NormalPreset.cs
+++ b/CrossPlatformUI/Presets/NormalPreset.cs
@@ -27,8 +27,8 @@ public static class NormalPreset
EastBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE,
MazeBiome = Biome.VANILLALIKE,
DmBiome = Biome.RANDOM_NO_VANILLA_OR_SHUFFLE,
- WestClimate = ClimateEnum.VANILLA_WEIGHTED_WEST,
- EastClimate = ClimateEnum.VANILLA_WEIGHTED_EAST,
+ WestClimate = ClimateEnum.VANILLA_WEIGHTED,
+ EastClimate = ClimateEnum.VANILLA_WEIGHTED,
ContinentConnectionType = ContinentConnectionType.TRANSPORTATION_SHUFFLE,
//Palaces
@@ -70,7 +70,7 @@ public static class NormalPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/RandomPercentPreset.cs b/CrossPlatformUI/Presets/RandomPercentPreset.cs
index 8825f8310..e1389ac38 100644
--- a/CrossPlatformUI/Presets/RandomPercentPreset.cs
+++ b/CrossPlatformUI/Presets/RandomPercentPreset.cs
@@ -88,7 +88,7 @@ public static class RandomPercentPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/StandardPreset.cs b/CrossPlatformUI/Presets/StandardPreset.cs
index 10de722b9..d3b6e1b38 100644
--- a/CrossPlatformUI/Presets/StandardPreset.cs
+++ b/CrossPlatformUI/Presets/StandardPreset.cs
@@ -74,7 +74,7 @@ public static class StandardPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/StandardSwissPreset.cs b/CrossPlatformUI/Presets/StandardSwissPreset.cs
index bd611d5eb..e008f96a0 100644
--- a/CrossPlatformUI/Presets/StandardSwissPreset.cs
+++ b/CrossPlatformUI/Presets/StandardSwissPreset.cs
@@ -73,7 +73,7 @@ public static class StandardSwissPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs b/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs
index d2e536bd3..fa975a522 100644
--- a/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs
+++ b/CrossPlatformUI/Presets/UpstartsTournamentPreset.cs
@@ -65,7 +65,7 @@ public static class UpstartsTournamentPreset
ShuffleBossHP = EnemyLifeOption.MEDIUM,
ShuffleXPStealers = true,
ShuffleXPStolenAmount = true,
- ShuffleSwordImmunity = true,
+ SwordImmunityOption = SwordImmunityOption.SHUFFLE,
EnemyXPDrops = XPEffectiveness.RANDOM,
//Items
diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs
index 8cf82394a..e33ddadf5 100644
--- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs
+++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs
@@ -31,25 +31,8 @@ public class RandomizerViewModel : ReactiveValidationObject, IRoutableViewModel,
private static bool IsFlagStringValid(string flags) => FlagPasteParser.IsValidFlagString(flags);
- private string flagInput = "";
-
[JsonIgnore]
- public string FlagInput
- {
- get => flagInput;
- set
- {
- var trimmedValue = value?.Trim() ?? "";
- var (extractedFlags, extractedSeed) = FlagPasteParser.Parse(trimmedValue);
-
- if (Main is not null && !string.IsNullOrEmpty(extractedSeed))
- {
- Main.Config.Seed = extractedSeed;
- }
-
- this.RaiseAndSetIfChanged(ref flagInput, extractedFlags ?? trimmedValue);
- }
- }
+ public string FlagInput { get; set { field = value.Trim(); this.RaisePropertyChanged(); } } = "";
[JsonIgnore]
public string Seed
@@ -95,6 +78,7 @@ public RandomizerViewModel(MainViewModel main)
HostScreen = Main;
BiomesViewModel = new(Main);
PalacesViewModel = new(Main);
+ SpellsViewModel = new(Main);
ItemsViewModel = new(Main);
HintsViewModel = new(Main, this);
CustomizeViewModel = new(Main);
@@ -226,16 +210,28 @@ private void OnActivate(CompositeDisposable disposables)
// flag updates from RandomizerConfiguration always overwrites our flag input
Main.FlagsObservable
- .Do(x => FlagsValidSubject.OnNext(true))
- .Subscribe(flags => FlagInput = flags)
+ .Subscribe(flags =>
+ {
+ FlagsValidSubject.OnNext(true);
+ FlagInput = flags;
+ })
.DisposeWith(disposables);
- this.WhenAnyValue(x => x.FlagInput)
- .WithLatestFrom(Main.FlagsObservable,
- (Input, Current) => (Input, Current, IsValid: Input == Current || IsFlagStringValid(Input)))
- .Do(x => FlagsValidSubject.OnNext(x.IsValid))
- .Where(x => x.IsValid && x.Input != x.Current)
- .Subscribe(x => Main.Config.DeserializeFlags(x.Input))
+ this.WhenAnyValue(viewModel => viewModel.FlagInput)
+ .WithLatestFrom(Main.FlagsObservable, (Input, Current) => (Input, Current))
+ .Subscribe(tuple =>
+ {
+ var isNew = tuple.Input != tuple.Current;
+ if (isNew)
+ {
+ bool isValid = IsFlagStringValid(tuple.Input);
+ FlagsValidSubject.OnNext(isValid);
+ if (isValid)
+ {
+ Main.Config.DeserializeFlags(tuple.Input);
+ }
+ }
+ })
.DisposeWith(disposables);
Main.Config.PropertyChanged += (sender, args) =>
@@ -248,7 +244,7 @@ private void OnActivate(CompositeDisposable disposables)
}
};
- this.ValidationRule(x => x.FlagInput, FlagsValidSubject, "Invalid Flags");
+ this.ValidationRule(viewModel => viewModel.FlagInput, FlagsValidSubject, "Invalid Flags");
AddValidationRules();
}
@@ -330,6 +326,7 @@ private void AddValidationRules()
[JsonIgnore]
public BiomesViewModel BiomesViewModel { get; }
public PalacesViewModel PalacesViewModel { get; }
+ public SpellsViewModel SpellsViewModel { get; }
public ItemsViewModel ItemsViewModel { get; }
public HintsViewModel HintsViewModel { get; }
public CustomizeViewModel CustomizeViewModel { get; }
diff --git a/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs b/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs
index 37748c7da..6892599b2 100644
--- a/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs
+++ b/CrossPlatformUI/ViewModels/Tabs/PalacesViewModel.cs
@@ -12,15 +12,36 @@ public class PalacesViewModel : ReactiveObject, IActivatableViewModel
public ViewModelActivator Activator { get; }
public MainViewModel Main { get; }
+ public IObservable BossRoomsExitTypeIncludedObservable { get; }
+ public IObservable NoDuplicateRoomsByLayoutIncludedObservable { get; }
+ public IObservable NoDuplicateRoomsByEnemiesIncludedObservable { get; }
public IObservable RandomStylesAllowVanillaIncludedObservable { get; }
public IObservable RemoveLongDeadEndsIncludedObservable { get; }
+ public IObservable IncludeVanillaRoomsIncludedObservable { get; }
+ public IObservable Includev4_0RoomsIncludedObservable { get; }
+ public IObservable Includev5_0RoomsIncludedObservable { get; }
public IObservable IncludeExpertRoomsIncludedObservable { get; }
+ public IObservable BlockingRoomsInAnyPalaceIncludedObservable { get; }
+ public IObservable RemoveTBirdIncludedObservable { get; }
+ public IObservable TBirdRequiredIncludedObservable { get; }
public PalacesViewModel(MainViewModel main)
{
Main = main;
Activator = new();
+ BossRoomsExitTypeIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.bossRoomsExitTypeIncluded())
+ .DistinctUntilChanged();
+
+ NoDuplicateRoomsByLayoutIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.noDuplicateRoomsByLayoutIncluded())
+ .DistinctUntilChanged();
+
+ NoDuplicateRoomsByEnemiesIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.noDuplicateRoomsByEnemiesIncluded())
+ .DistinctUntilChanged();
+
RandomStylesAllowVanillaIncludedObservable = Main.FlagsChanged
.Select(_ => Main.Config.randomStylesAllowVanillaIncluded())
.DistinctUntilChanged();
@@ -29,10 +50,34 @@ public PalacesViewModel(MainViewModel main)
.Select(_ => Main.Config.removeLongDeadEndsIncluded())
.DistinctUntilChanged();
+ IncludeVanillaRoomsIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.includeVanillaRoomsIncluded())
+ .DistinctUntilChanged();
+
+ Includev4_0RoomsIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.includev4_0RoomsIncluded())
+ .DistinctUntilChanged();
+
+ Includev5_0RoomsIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.includev5_0RoomsIncluded())
+ .DistinctUntilChanged();
+
IncludeExpertRoomsIncludedObservable = Main.FlagsChanged
.Select(_ => Main.Config.includeExpertRoomsIncluded())
.DistinctUntilChanged();
+ BlockingRoomsInAnyPalaceIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.blockingRoomsInAnyPalaceIncluded())
+ .DistinctUntilChanged();
+
+ RemoveTBirdIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.removeTBirdIncluded())
+ .DistinctUntilChanged();
+
+ TBirdRequiredIncludedObservable = Main.FlagsChanged
+ .Select(_ => Main.Config.tBirdRequiredIncluded())
+ .DistinctUntilChanged();
+
this.WhenActivated(OnActivate);
}
diff --git a/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs b/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs
new file mode 100644
index 000000000..4993e8c74
--- /dev/null
+++ b/CrossPlatformUI/ViewModels/Tabs/SpellsViewModel.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using ReactiveUI;
+
+namespace CrossPlatformUI.ViewModels.Tabs;
+
+[RequiresUnreferencedCode("ReactiveUI uses reflection")]
+public class SpellsViewModel : ReactiveObject, IActivatableViewModel
+{
+ public ViewModelActivator Activator { get; }
+ public MainViewModel Main { get; }
+
+ public SpellsViewModel(MainViewModel main)
+ {
+ Main = main;
+ Activator = new();
+
+ this.WhenActivated(OnActivate);
+ }
+
+ internal void OnActivate(CompositeDisposable disposables)
+ {
+ }
+}
diff --git a/CrossPlatformUI/Views/NesColorDropdown.axaml b/CrossPlatformUI/Views/NesColorDropdown.axaml
index ac0761779..de79d87a1 100644
--- a/CrossPlatformUI/Views/NesColorDropdown.axaml
+++ b/CrossPlatformUI/Views/NesColorDropdown.axaml
@@ -3,10 +3,10 @@
xmlns:assists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
x:Class="CrossPlatformUI.Views.NesColorDropdown"
Width="340" Height="40">
-
-
+
+
-
diff --git a/CrossPlatformUI/Views/Tabs/EnemiesView.axaml b/CrossPlatformUI/Views/Tabs/EnemiesView.axaml
index dccf7088d..8b4f92234 100644
--- a/CrossPlatformUI/Views/Tabs/EnemiesView.axaml
+++ b/CrossPlatformUI/Views/Tabs/EnemiesView.axaml
@@ -86,20 +86,20 @@
-
+
-
-
-
+
+
+
diff --git a/CrossPlatformUI/Views/Tabs/PalacesView.axaml b/CrossPlatformUI/Views/Tabs/PalacesView.axaml
index 9f0153f5b..c693715b5 100644
--- a/CrossPlatformUI/Views/Tabs/PalacesView.axaml
+++ b/CrossPlatformUI/Views/Tabs/PalacesView.axaml
@@ -23,7 +23,7 @@
Foreground="#4557c4"
Cursor="Hand"
/>
-
+
+
+
+
+
+
+
+
+
Number of Palaces to Complete
@@ -123,20 +132,24 @@
-
-
+
+
No Duplicate Rooms(By Layout)
-
+
+
No Duplicate Rooms(By Layout and Enemy Set)
@@ -147,11 +160,13 @@
Content="Restart at Palaces on Game Over">
+
50/50 Palace Statue CyclesBetween Jar and Iron Knuckle
+
@@ -160,22 +175,27 @@
-
-
-
@@ -191,21 +211,17 @@
-
-
-
-
-
-
-
-
+
+
+
@@ -223,31 +239,28 @@
Content="Harder Carock">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs b/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs
index 9ec5991e2..fb792b4a4 100644
--- a/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs
+++ b/CrossPlatformUI/Views/Tabs/PalacesView.axaml.cs
@@ -22,12 +22,11 @@ public PalacesView()
this.WhenActivated(disposables =>
{
+ // at most one of these two checkboxes must be checked
CheckBox noDuplicateRoomsByLayoutCheckbox = this.FindControl("NoDuplicateRoomsByEnemiesCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
CheckBox noDuplicateRoomsByEnemiesCheckbox = this.FindControl("NoDuplicateRoomsByLayoutCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
-
IObservable byLayoutObservable = noDuplicateRoomsByLayoutCheckbox.GetObservable(CheckBox.IsCheckedProperty);
IObservable byEnemiesObservable = noDuplicateRoomsByEnemiesCheckbox.GetObservable(CheckBox.IsCheckedProperty);
-
byLayoutObservable.Subscribe(byLayoutValue =>
{
if (byLayoutValue ?? false)
@@ -36,7 +35,6 @@ public PalacesView()
}
})
.DisposeWith(disposables);
-
byEnemiesObservable.Subscribe(byEnemiesValue =>
{
if (byEnemiesValue ?? false)
@@ -46,70 +44,7 @@ public PalacesView()
})
.DisposeWith(disposables);
- ComboBox normalPalaceStyleSelector = this.FindControl("NormalPalaceStyleSelector") ?? throw new System.Exception("Missing Required Validation Element");
- ComboBox gpStyleSelector = this.FindControl("GPPalaceStyleSelector") ?? throw new System.Exception("Missing Required Validation Element");
- CheckBox thunderbirdRequiredCheckbox = this.FindControl("TbirdRequiredCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
- CheckBox includeVanillaCheckbox = this.FindControl("IncludeVanillaRoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
- CheckBox include4_0Checkbox = this.FindControl("Include4_0RoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
- CheckBox include5_0Checkbox = this.FindControl("Include5_0RoomsCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
-
- CheckBox blockingRoomsAnywhereCheckbox = this.FindControl("BlockingRoomsInAnyPalaceCheckbox") ?? throw new System.Exception("Missing Required Validation Element");
- ComboBox bossRoomsExitTypeSelector = this.FindControl("BossRoomsExitTypeSelector") ?? throw new System.Exception("Missing Required Validation Element");
-
-
- var normalStyleObservable = normalPalaceStyleSelector.GetObservable(ComboBox.SelectedItemProperty);
- var gpStyleObservable = gpStyleSelector.GetObservable(ComboBox.SelectedItemProperty);
-
- gpStyleObservable.Subscribe(selectedItem =>
- {
- EnumDescription? selectedDescription = selectedItem as EnumDescription;
- if(selectedDescription != null)
- {
- PalaceStyle palaceStyle = (PalaceStyle)(selectedDescription.Value ?? PalaceStyle.RECONSTRUCTED);
- if (palaceStyle == PalaceStyle.VANILLA)
- {
- thunderbirdRequiredCheckbox.IsChecked = true;
- thunderbirdRequiredCheckbox.IsEnabled = false;
- }
- else
- {
- thunderbirdRequiredCheckbox.IsEnabled = true;
- }
- }
- })
- .DisposeWith(disposables);
-
- normalStyleObservable.CombineLatest(gpStyleObservable, (normal, gp) =>
- {
- EnumDescription? normalStyleDescription = normal as EnumDescription;
- PalaceStyle normalPalaceStyle = (PalaceStyle)(normalStyleDescription?.Value ?? PalaceStyle.RECONSTRUCTED);
- EnumDescription? gpPalaceStyleDescription = gp as EnumDescription;
- PalaceStyle gpPalaceStyle = (PalaceStyle)(gpPalaceStyleDescription?.Value ?? PalaceStyle.RECONSTRUCTED);
- return !((normalPalaceStyle == PalaceStyle.VANILLA || normalPalaceStyle == PalaceStyle.SHUFFLED)
- && (gpPalaceStyle == PalaceStyle.VANILLA || gpPalaceStyle == PalaceStyle.SHUFFLED));
- })
- .Subscribe(enableRoomSelection =>
- {
- includeVanillaCheckbox.IsEnabled = enableRoomSelection;
- include4_0Checkbox.IsEnabled = enableRoomSelection;
- include5_0Checkbox.IsEnabled = enableRoomSelection;
- noDuplicateRoomsByLayoutCheckbox.IsEnabled = enableRoomSelection;
- noDuplicateRoomsByEnemiesCheckbox.IsEnabled = enableRoomSelection;
- blockingRoomsAnywhereCheckbox.IsEnabled = enableRoomSelection;
- bossRoomsExitTypeSelector.IsEnabled = enableRoomSelection;
- if (!enableRoomSelection)
- {
- includeVanillaCheckbox.IsChecked = true;
- include4_0Checkbox.IsChecked = false;
- include5_0Checkbox.IsChecked = false;
- noDuplicateRoomsByLayoutCheckbox.IsChecked = enableRoomSelection;
- noDuplicateRoomsByEnemiesCheckbox.IsChecked = enableRoomSelection;
- blockingRoomsAnywhereCheckbox.IsChecked = enableRoomSelection;
- bossRoomsExitTypeSelector.SelectedIndex = 0;
- }
- })
- .DisposeWith(disposables);
});
}
}
diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml
index 2abd5d472..d7905fc41 100644
--- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml
+++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml
@@ -6,42 +6,43 @@
xmlns:rc="clr-namespace:Z2Randomizer.RandomizerCore;assembly=RandomizerCore"
xmlns:ui="clr-namespace:CrossPlatformUI"
xmlns:vm="clr-namespace:CrossPlatformUI.ViewModels"
+ xmlns:vmt="clr-namespace:CrossPlatformUI.ViewModels.Tabs"
xmlns:lang="clr-namespace:Z2Randomizer.CrossPlatformUI.Lang"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
- x:DataType="vm:MainViewModel"
- x:Class="CrossPlatformUI.Views.Tabs.SpellsView">
+ x:Class="CrossPlatformUI.Views.Tabs.SpellsView"
+ x:DataType="vmt:SpellsViewModel">
@@ -50,26 +51,26 @@
Theme="{StaticResource MaterialOutlineComboBox}"
assists:ComboBoxAssist.Label="Fire Spell"
ItemsSource="{Binding Source={x:Static rc:Enums.FireOptionList}}"
- SelectedItem="{Binding Config.FireOption, Converter={x:Static ui:Util.EnumConvert}}"
+ SelectedItem="{Binding Main.Config.FireOption, Converter={x:Static ui:Util.EnumConvert}}"
>
diff --git a/Directory.Build.props b/Directory.Build.props
index 576f389f9..0b154ef44 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,18 +3,12 @@
net10.0
enable
true
- false
- true
- true
+ true
+ false
false
-
partial
- true
+ true
-
-
-
-
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 46473f16b..e1bae488a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,15 +2,15 @@
true
true
- 5.3.0
+ 5.6.0
7.5.1
- 1.0.7
- 12.0.4
+ 1.0.9
+ 12.1.0
3.119.4
-
+
@@ -30,34 +30,38 @@
-
+
+
-
+
-
-
+
+
-
+
+
+
+
-
-
+
+
-
-
-
+
+
+
diff --git a/FtRandoLib b/FtRandoLib
index 068c5f14b..fca211534 160000
--- a/FtRandoLib
+++ b/FtRandoLib
@@ -1 +1 @@
-Subproject commit 068c5f14bfdbd8768ba09961e8266d3d62be3ff4
+Subproject commit fca2115348e63b939118e154f80e93d6410be3c6
diff --git a/RandomizerCore/App.config b/RandomizerCore/App.config
deleted file mode 100644
index e0f14fb3a..000000000
--- a/RandomizerCore/App.config
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- False
-
-
- 0
-
-
- False
-
-
- False
-
-
- False
-
-
- 0
-
-
- 7
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
-
-
- 0
-
-
- False
-
-
-
-
-
- True
-
-
- False
-
-
-
-
diff --git a/RandomizerCore/Asm/BuffCarock.s b/RandomizerCore/Asm/BuffCarock.s
index 94b765269..be7d828f2 100644
--- a/RandomizerCore/Asm/BuffCarock.s
+++ b/RandomizerCore/Asm/BuffCarock.s
@@ -2,7 +2,6 @@
.segment "PRG4"
-ProjectileEnemyData = $07c0
; SinWaveVelocityIncrement = $ba1c
; Hook into carrock's update code to add the new position calculation
diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s
index 88747cacb..3484426c1 100644
--- a/RandomizerCore/Asm/Init.s
+++ b/RandomizerCore/Asm/Init.s
@@ -31,6 +31,7 @@
; Mark unused areas in the ROM so the linker can place stuff here
+FREE "PRG0" [$a89e, $a980)
; FREE "PRG0" [$AA40, $c000)
FREE "PRG0" [$AB00, $c000) ; give room for z2edit to patch $aa40
@@ -44,9 +45,9 @@ FREE "PRG2" [$93c9, $9400)
FREE "PRG2" [$9f85, $a000)
FREE "PRG2" [$a933, $b480)
-;FREE "PRG3" [$8bb1, $90f0]
-;FREE "PRG3" [$9134, $9400]
-;FREE "PRG3" [$9d0a, $a000]
+FREE "PRG3" [$8cc0, $90f0)
+FREE "PRG3" [$9135, $9400)
+FREE "PRG3" [$9d0b, $a000)
FREE "PRG3" [$B803, $c000)
FREE "PRG4" [$83DC, $8470)
@@ -76,6 +77,7 @@ FREE "PRG5" [$bda1, $c000)
;FREE "PRG6" [$ac09, $c000) TEMP
FREE "PRG6" [$ac21, $c000)
+FREE "PRG7" [$d39a, $d3ca)
; DPCM data, will affect dpcm sfx but not gameplay so its fine to use this as a last ditch
; free space for patches. Keep it disabled as much as possible
; FREE "PRG7" [$f369, $fcfb)
diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc
index 0b0d108c0..6da45c43d 100644
--- a/RandomizerCore/Asm/ram.inc
+++ b/RandomizerCore/Asm/ram.inc
@@ -1,7 +1,7 @@
; Define low zero page addresses even if they're not well-defined
; so they will have debug info precedence over other constants
-zp_00 = $00
-zp_01 = $01
+zp_00 = $00 ; LSB of general-purpose pointer
+zp_01 = $01 ; MSB of general-purpose pointer
zp_02 = $02
zp_03 = $03
zp_04 = $04
@@ -16,13 +16,13 @@ zp_0c = $0c
zp_0d = $0d
zp_0e = $0e
zp_0f = $0f
-zp_10 = $10
-zp_11 = $11
+EnemyIndex = $10 ; 0-5=Enemy index being processed
+EntityIndex = $11 ; 0=Link, 1-6 - Enemy index(-1) being processed
FrameCounter = $12
FairyState = $13 ; 0 = Not in Fairy state
-zp_14 = $14
-zp_15 = $15
-zp_16 = $16
+LinkXFrameDiff = $14
+LinkScreenX = $15 ; Link's X position on the screen (not in the map)
+LinksShadowScreenX = $16
LinkStanding = $17 ; 0 = Link in ducked shield position, 1 = Link standing
DarkLinkStanding = $18 ; 0 = Dark Link ducking, 1 = Dark Link standing
LinkYPositionHi = $19 ; [SS] 0 = Invisible, 1 = Normal, 2-FF = Fall in hole - [FILE] Cursor position
@@ -39,8 +39,8 @@ Enemy3Condition = $23
Enemy4Condition = $24
Enemy5Condition = $25
OverworldStepCounter = $26
-zp_27 = $27
-zp_28 = $28
+VramAddrHi = $27
+VramAddrLo = $28
LinkYPos = $29
Enemy0YPositionLo = $2a
Enemy1YPositionLo = $2b
@@ -56,12 +56,12 @@ Projectile4YPosition = $34
Projectile5YPosition = $35
LinkProjectile0YPosition = $36
LinkProjectile1YPosition = $37
-zp_38 = $38
-LinkProjectileYPositionRelated = $39
-zp_3a = $3a
-LinkXPositionHi = $3b
-Enemy0XPositionHi = $3c
-Enemy1XPositionHi = $3d
+EndingDisplayTileY0 = $38
+LinkSwordYBreakableBlockAdjusted = $39 ; [SS] Link's sword hitbox Y as used for breakable blocks - [Ending] Y of tile 1 in ending sequence
+EndingDisplayTileY2 = $3a
+LinkXPositionHi = $3b ; [SS] Which page of the room Link is in - [Ending] X of tile 0 in ending sequence
+Enemy0XPositionHi = $3c ; [SS] Which page of the room Enemy0 is in - [Ending] X of tile 1 in ending sequence
+Enemy1XPositionHi = $3d ; [SS] Which page of the room Enemy1 is in - [Ending] X of tile 2 in ending sequence
Enemy2XPositionHi = $3e
Enemy3XPositionHi = $3f
Enemy4XPositionHi = $40
@@ -74,9 +74,9 @@ Projectile4XPositionHi = $46
Projectile5XPositionHi = $47
LinkProjectile0XPositionHi = $48
LinkProjectile1XPositionHi = $49
-zp_4a = $4a
-LinkProjectileXPositionHiRelated = $4b
-zp_4c = $4c
+zp_4a = $4a ; Unused? (Likely leftover of Link having 3 projectiles)
+LinkSwordBreakableBlockAdjusted = $4b
+zp_4c = $4c ; Unused?
LinkXPositionLo = $4d
Enemy0XPositionLo = $4e
Enemy1XPositionLo = $4f
@@ -92,9 +92,9 @@ Projectile4XPositionLo = $58
Projectile5XPositionLo = $59
LinkProjectile0XPositionLo = $5a
LinkProjectile1XPositionLo = $5b
-zp_5c = $5c
-LinkProjectileXPositionLoRelated = $5d
-zp_5e = $5e
+zp_5c = $5c ; Unused?
+LinkSwordXBreakableBlockAdjusted = $5d
+zp_5e = $5e ; Unused?
LinkFacingDirection = $5f
Enemy0XFacing = $60
Enemy1XFacing = $61
@@ -108,10 +108,10 @@ Projectile2XFacing = $68
Projectile3XFacing = $69
Projectile4XFacing = $6a
Projectile5XFacing = $6b
-zp_6c = $6c
-zp_6d = $6d
-zp_6e = $6e
-zp_6f = $6f
+zp_6c = $6c ; Unused?
+LinkProjectile0Facing = $6d
+LinkProjectile1Facing = $6e
+zp_6f = $6f ; Unused?
LinkXVelocity = $70
Enemy0XVelocity = $71
Enemy1XVelocity = $72
@@ -127,7 +127,7 @@ Projectile4XVelocity = $7b
Projectile5XVelocity = $7c
LinkProjectile0XVelocity = $7d
LinkProjectile1XVelocity = $7e
-zp_7f = $7f
+OverworldYScroll = $7f
LinkAnimationFrame = $80
Enemy0AnimationFrame = $81
OverworldEnemy0Type = $82 ; [SS] Enemy1AnimationFrame - [OW] OverworldEnemy0Type, 1 = Weak, 2 = Strong, 3 = Fairy
@@ -143,7 +143,7 @@ Projectile4Type = $8b
Projectile5Type = $8c
LinkProjectile0State = $8d
LinkProjectile1State = $8e
-zp_8f = $8f
+zp_8f = $8f ; Unused?
SpriteShuffleOffsetLink = $90
SpriteShuffleOffsetEnemy0 = $91
SpriteShuffleOffsetEnemy1 = $92
@@ -160,7 +160,7 @@ SpriteShuffleOffsetProjectile5 = $9c
SpriteShuffleOffsetLinkProjectile0 = $9d
SpriteShuffleOffsetLinkProjectile1 = $9e
DpadHorizontalState = $9f ; 1 = Right, 2 = Left
-zp_a0 = $a0
+LinksShadowFacingDirection = $a0 ; Link's Shadow's facing direction when his shadow is displayed during a boss explosion and during the final battle
Enemy0Type = $a1 ; Enemy (Elevator) slot
Enemy1Type = $a2 ; Enemy (Locked door) slot
Enemy2Type = $a3
@@ -174,7 +174,7 @@ Enemy2State = $aa
Enemy3State = $ab
Enemy4State = $ac
Enemy5State = $ad
-zp_ae = $ae
+LinkWalkingAnimationFrame = $ae ; 0-2 cycling
Enemy0State2 = $af
Enemy1State2 = $b0
Enemy2State2 = $b1
@@ -201,7 +201,7 @@ Enemy3HP = $c5
Enemy4HP = $c6
Enemy5HP = $c7
SideviewExitScreen = $c8 ; 3 = Exit screen
-zp_c9 = $c9
+EnemyBitFlags = $c9
zp_ca = $ca
zp_cb = $cb
LinkXScreenPosition = $cc
@@ -226,16 +226,40 @@ zp_de = $de
zp_df = $df ; Enemy type temporary storage
; $e0-$ef are music and sound variables
+Z2NoiseSoundQueue = $ed
+Z2Square2SoundQueue = $ee
+Z2Square1SoundQueue = $ef
+
+NoiseSoundQueue = $f0
+Square2SoundQueue = $f1
+Square1SoundQueue = $f2
; Potentially free zero page memory bytes at $f1-$f4 and since they are used by FDS BIOS but not the NES
Controller1ButtonsPressed = $f5
Controller2ButtonsPressed = $f6
Controller1ButtonsHeld = $f7
Controller2ButtonsHeld = $f8
-; Potentially free zero page memory bytes at $f9-$fc
+; Free zero page memory bytes at $f9-$fb (would have been reserved for Famicom Disk System BIOS)
+OverworldXScroll = $fc
ScrollPosShadow = $fd
PpuCtrlShadow = $ff
+; $100-$1ff stack memory
+
+; $200-$2ff OAM buffer
+SpriteYPosition = $200
+SpriteTilenumber = $201
+SpriteAttributes = $202
+SpriteXPosition = $203
+SpriteData = SpriteYPosition
+
+PpuBufferLength = $0301
+PpuAddrHi = $0302
+PpuAddrLo = $0303
+PpuTextLength = $0304
+PpuTextData = $0305
+PpuBuffer2Length = $0362
+
LinkXSubpixel = $03d6
Enemy0XSubpixel = $03d7
Enemy1XSubpixel = $03d8
@@ -247,15 +271,22 @@ LinkYVelocityLo = $03e6
LinkSwordAttackFrame = $0400
Enemy0HitState = $040e
+BackgroundColumnUpdateArray = $0464 ; Palette & Tile index for each y position 0 to 12
LinkInAir = $0479 ; 0 = On ground, 1 = Falling from walking off platform, 2 = Jumping
+LinkSwordXHitbox = $047e ; Link's sword's X position in screen space
+LinksShadowSwordXHitbox = $047f
+LinkSwordYHitbox = $0480 ; 0xf8 when not attacking
+LinksShadowSwordYHitbox = $0481
LandingAnimationTimer = $0497
HitboxXCoord = $047e
HitboxYCoord = $0480
+SpriteSlotShuffleCounter = $0485 ; 0-5
LinkIFrameTimer = $500
timer_501 = $0501 ; $501 through $50d are timers that are decreased each frame until 0
LinkEntrySlideTime = $0503
SwordAttackTimer = $050a
+InjuryTimer = $050c
EncounterSpawnTimer = $0516
RNG = $051b
MapNumber = $0561
@@ -263,6 +294,12 @@ TownNumber = $056b
PalaceRegionIndex = $056c ; Palace index in continent (0-2)
LinkYVelocityHi = $057d
Enemy0YVelocity = $057e
+TownfolkTimer0 = $05bd
+TownfolkTimer1 = $05be
+TownfolkTimer2 = $05bf
+TownfolkTimer3 = $05c0
+TownfolkTimer4 = $05c1
+TownfolkTimer5 = $05c2
LinkProjectile0Timer = $05ca
LinkProjectile1Timer = $05cb ; Always 0 since Fire projectiles don't expire
SavedDoorExit1Page = $05d1 ; accessed as $5cc,X with X = 5
@@ -272,25 +309,30 @@ SavedDoorExit2X = $05d9
SmallDropCount = $05df
LargeDropCount = $05e0
+OverworldStepCounterHi = $06e0 ; [Z2R] Used for halved encounter rate to only trigger encounters every other time the steps are reached
+
Lives = $0700
EntryFacing = $0701 ; Sideview entry facing direction. 0=Facing right (enter from the left), 1=Facing left (enter from the right)
EnterFromElevatorAbove = $0704 ; >0 Link enters the room from the top
EnterCentered = $0705 ; >0 Link enters centered; like at North Palace, taking an elevator exit or entering a random encounter
RegionNumber = $0706 ; 0=West, 1=Death Mountain, 2=East, 3=Maze Island
WorldNumber = $0707 ; 0=Caves & enemy encounters, 1=West towns, 2=East towns, 3=Palace 1/2/5, 4=Palace 3/4/6, 5=Great Palace
+SideviewObjectPage = $0717 ; 0-3 page of current sideview object being processed
ScrollDirection = $071f ; Current scroll direction. 1=Scrolling right, 2=Scrolling left
ColumnScrollDirection = $0720 ; Scroll direction that is updated only when scrolling crosses a column boundary
ColumnScrollX = $0721 ; Scroll position within the current tile column ($00-$1f)
DialogBoxDrawing = $0726 ; >0 when drawing dialog and during black screen transitions
ScrollFrozen = $0728 ; 1=Screen scrolling frozen (for boss fights)
-ScrollPage = $072a ; Which page the left-most pixel of the screen is on (0-3)
-ScrollX = $072c
+ScrollLeftPage = $072a ; Which page the left-most pixel of the screen is on (0-3)
+ScrollRightPage = $72b
+ScrollLeftX = $072c
+ScrollRightX = $072d
SideviewCommandY = $0730
SideviewCommandOp = $0731
-ScrollLeftPage = $0732
-ScrollRightPage = $0733
-ScrollLeftColumn = $0734 ; Which left background column to update during scrolling
-ScrollRightColumn = $0735 ; Which right background column to update during scrolling
+UpdateScrollColumnLeftPage = $0732
+UpdateScrollColumnRightPage = $0733
+UpdateScrollColumnLeftTile = $0734 ; Which left background column to update during scrolling
+UpdateScrollColumnRightTile = $0735 ; Which right background column to update during scrolling
GameMode = $0736
GameModeCurrent = $0737 ; GameMode is compared to this and only triggered if it has changed
@@ -302,6 +344,8 @@ LocationNumber = $0748 ; Location index sideview exits to or $
MagicSelectorPosition = $0749
LinkFlashingTimer = $074b ; Link changes palette and this value is counted down until it's 0. 0b10000000 bit determines if the background changes color.
CurrentDialogType = $074c ; 0=None, 1 = Level Up, 2 = Talking
+HudSectionFlag = $074f ; 0x80=Draw Magic & Attack, 0x40=Draw Life & XP
+HudDrawState = $0750
ExpToAddHi = $0755
ExpToAddLo = $0756
DoorDepth = $075b ; Used as an index for saving/loading your x position when you enter/leave (nested) doors
@@ -340,6 +384,12 @@ HaveFlute = $0789
HaveCross = $078a
HaveHammer = $078b
HaveMagicKey = $078c
+Crystal1Placed = $078d
+Crystal2Placed = $078e
+Crystal3Placed = $078f
+Crystal4Placed = $0790
+Crystal5Placed = $0791
+Crystal6Placed = $0792
Keys = $0793 ; Current number of Keys
Crystals = $0794 ; Number of Crystals left
HaveStabs = $0796 ; 0x04 = Have Downstab | 0x10 = Have Upstab
@@ -351,6 +401,13 @@ HaveChild = $079c ; 0x20 = Have Child
HaveBasementAccess = $079d ; 0x08 = Have New Kasuto Basement access
ContinuesUsed = $079f
+; New variables that use free memory
+ProjectileEnemyData = $07c0 ; [Z2R] 6 bytes for Hard Carock projectile type
+
+BackgroundTilesPage1 = $6000
+BackgroundTilesPage2 = $60d0
+BackgroundTilesPage3 = $61a0
+BackgroundTilesPage4 = $6270
SavedDoorExit1ScrollPage = $69aa ; accessed as $69a5,X with X = 5
SavedDoorExit1ScrollX = $69b1 ; accessed as $69ac,X with X = 5
diff --git a/RandomizerCore/CHR.cs b/RandomizerCore/CHR.cs
new file mode 100644
index 000000000..1c83f6ef7
--- /dev/null
+++ b/RandomizerCore/CHR.cs
@@ -0,0 +1,263 @@
+using System.Collections.Generic;
+
+namespace Z2Randomizer.RandomizerCore;
+
+
+public static class CHR
+{
+ public static readonly Dictionary TERRAIN_TILE_ADDRS = new()
+ {
+ { Terrain.TOWN, 0x115c0 },
+ { Terrain.CAVE, 0x11f40 },
+ { Terrain.PALACE, 0x11600 },
+ { Terrain.BRIDGE, 0x115a0 },
+ { Terrain.DESERT, 0x116c0 },
+ { Terrain.GRASS, 0x116d0 },
+ { Terrain.FOREST, 0x11680 },
+ { Terrain.SWAMP, 0x116f0 },
+ { Terrain.GRAVE, 0x11700 },
+ { Terrain.ROAD, 0x11fe0},
+ { Terrain.LAVA, 0x116e0 },
+ { Terrain.MOUNTAIN, 0x11640 },
+ { Terrain.WATER, 0x116e0 },
+ { Terrain.PREPLACED_WATER, 0x116e0 },
+ { Terrain.WALKABLEWATER, 0x116e0 },
+ { Terrain.PREPLACED_WATER_WALKABLE, 0x116e0 },
+ { Terrain.ROCK, 0x11560 },
+ { Terrain.RIVER_DEVIL, 0x11400 },
+ };
+
+ // Palace objects
+ public static int BRICKS_P2 = 0x9640;
+ public static int BRICKS_GP = 0xd640;
+ public static int ELEVATOR = 0x8ac0;
+ public static int LOCKED_DOOR = 0x8740;
+ public static int STEEL_BRICK = 0x95c0;
+ public static int BRIDGE = 0x97c0;
+ public static int CURTAIN = 0x9840;
+ public static int PILLAR_TOP = 0x98e0;
+ public static int PILLAR_FILL = 0x9a50;
+ public static int CRUMBLE_BRIDGE = 0x9920;
+ public static int LAVA_TOP = 0x9980;
+ public static int LAVA_FILL = 0x9fe0;
+ public static int BREAKABLE_BLOCK = 0x9ba0;
+ public static int FINAL_BOSS_CANOPY = 0xd780;
+ public static int NORTH_CASTLE_BRICK = 0xd9a0;
+
+ public static readonly SpriteTile[] LockedDoorTiles = [
+ new(LOCKED_DOOR, 1, 2, [(0, 0), (0, 2)], false),
+ new(LOCKED_DOOR + 0x20, 1, 2, [(0, 1)]),
+ ];
+
+ public static readonly SpriteTile[] WindowTiles = [
+ new(0x96a0, 2, 1, [(0, 0)], false),
+ new(0x96c0, 2, 1, [(0, 1), (0, 2)], false),
+ new(0x96e0, 2, 1, [(0, 3)], false),
+ ];
+
+ public static readonly SpriteTile[] CrystalStatueTiles = [
+ new(0x9700, 2, 1, [(3, 1)]),
+ new(0x9720, 4, 1, [(2, 2)]),
+ new(0x9760, 1, 1, [(2, 5)]),
+ new(0x9770, 1, 1, [(3, 3), (3, 4), (3, 5), (3, 6), (3, 8), (3, 9), (4, 8), (4, 9)]),
+ new(0x9780, 1, 1, [(4, 3)]),
+ new(0x9790, 1, 1, [(4, 4), (4, 5), (4, 6)]),
+ new(0x97a0, 2, 1, [(3, 7)]),
+ new(0x9880, 1, 1, [(2, 0), (0, 6), (6, 6)]),
+ new(0x98a0, 1, 1, [(3, 0), (4, 0)]),
+ new(0x98c0, 1, 1, [(5, 0), (1, 6), (7, 6)]),
+ new(0x9a80, 1, 1, [(0, 7), (0, 8), (0, 9), (2, 1), (2, 3), (2, 4), (2, 6), (2, 7), (2, 8), (2, 9), (6, 7), (6, 8), (6, 9)]),
+ new(0x9aa0, 1, 1, [(1, 7), (1, 8), (1, 9), (5, 1), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (7, 7), (7, 8), (7, 9)]),
+ ];
+
+ public static readonly SpriteTile[] LargeCloudTiles = [
+ new(0x9b30, 1, 2, [(0, 0)]),
+ new(0x9b50, 1, 2, [(1, 0), (2, 0)]),
+ new(0x9b70, 1, 2, [(3, 0)]),
+ ];
+
+ public static readonly SpriteTile[] SmallCloudTiles = [
+ new(0x9b30, 1, 2, [(0, 0)]),
+ new(0x9b50, 1, 2, [(1, 0)]),
+ new(0x9b70, 1, 2, [(2, 0)]),
+ ];
+
+ // Enemies
+ public static int FLAME = 0x8520;
+ public static int RA_HEAD = 0x9340;
+ public static int MAU_HEAD = 0x9380;
+ public static int IRON_KNUCKLE = 0x9400;
+ public static int DRIPPER = 0xb8e0; // P2 pillar top
+ public static int FOKKERU = 0xce00;
+ public static int FOKKA = 0xd400;
+ public static int DOOMKNOCKER = 0x12e00;
+
+ public static readonly SpriteTile[] BotTiles = [new(0x8b40, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+
+ public static readonly SpriteTile[] MoaTiles = [new(0x8b60, 2, 2, [(0, 0)])];
+
+ public static readonly SpriteTile[] BagoBagoTiles = [new(0x8c00, 2, 2, [(0, 0)])];
+
+ public static readonly SpriteTile[] IronKnuckleTiles = [
+ new(IRON_KNUCKLE, 2, 2, [(0, 0)]),
+ new(IRON_KNUCKLE + 0x80, 2, 2, [(0, 2)]),
+ ];
+
+ public static readonly SpriteTile[] BubbleTiles = [new(0x9660, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+
+ public static readonly SpriteTile[] KingBotTiles = [new(0xcf00, 3, 4, [(0, 0), (3, 0, flipH: true)])];
+
+ public static readonly SpriteTile[] FokkaTiles = [
+ new(FOKKA, 2, 2, [(0, 0)]),
+ new(FOKKA + 0x80, 2, 2, [(0, 2)]),
+ ];
+
+ // Items
+ public static int HEART_CONTAINER = 0x1800;
+ public static int MAGIC_CONTAINER = 0x1820;
+ public static int TROPHY = 0x32e0;
+ public static int MIRROR = 0x1a260;
+ public static int BAGUS_NOTE = 0x1a280;
+ public static int MEDICINE = 0x3300;
+ public static int WATER = 0x1a2c0;
+ public static int CHILD = 0x5300;
+ public static int KEY = 0x8660;
+ public static int FAIRY = 0x86a0;
+ public static int PBAG = 0x8720;
+ public static int JAR = 0x88a0;
+ public static int CANDLE = 0x88c0;
+ public static int GLOVE = 0x88e0;
+ public static int RAFT = 0x8900;
+ public static int BOOTS = 0x8920;
+ public static int FLUTE = 0x8940;
+ public static int CROSS = 0x8960;
+ public static int HAMMER = 0x8980;
+ public static int MAGIC_KEY = 0x89a0;
+ public static int ONEUP = 0x8a80;
+
+ public static readonly SpriteTile[] HeartContainerTiles = [new(HEART_CONTAINER, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] MagicContainerTiles = [new(MAGIC_CONTAINER, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] TrophyTiles = [new(TROPHY, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] MirrorTiles = [new(MIRROR, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] BagusNoteTiles = [new(BAGUS_NOTE, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] MedicineTiles = [new(MEDICINE, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] WaterTiles = [new(WATER, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] ChildTiles = [new(CHILD, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] KeyTiles = [new(KEY, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] FairyTiles = [new(FAIRY, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] PBagTiles = [new(PBAG, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] JarTiles = [new(JAR, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] ShieldTiles = [new(0x1a3a0, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] JumpTiles = [new(0x1a140, 1, 2, [(0, 0)]), new(0x1a120, 1, 2, [(1, 0)])];
+ public static readonly SpriteTile[] LifeTiles = [new(0x1a160, 1, 2, [(0, 0)]), new(0x1a180, 1, 2, [(1, 0, flipH: true)])];
+ public static readonly SpriteTile[] FireTiles = [new(0x520, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] ReflectTiles = [new(0x1a1a0, 1, 2, [(0, 0)]), new(0x1a1c0, 1, 2, [(1, 0, flipH: true)])];
+ public static readonly SpriteTile[] SpellTiles = [new(0x1a300, 1, 2, [(0, 0), (1, 0, flipH: true)])];
+ public static readonly SpriteTile[] ThunderTiles = [new(0x1a360, 1, 2, [(0, 0)]), new(0x1a380, 1, 2, [(1, 0, flipH: true)])];
+ public static readonly SpriteTile[] DashTiles = [new(0x1a0e0, 1, 2, [(0, 0)]), new(0x1a140, 1, 2, [(1, 0)])];
+ public static readonly SpriteTile[] UpstabTiles = [new(0x1a200, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] DownstabTiles = [new(0x1a220, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] CandleTiles = [new(CANDLE, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] GloveTiles = [new(GLOVE, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] RaftTiles = [new(RAFT, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] BootsTiles = [new(BOOTS, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] FluteTiles = [new(FLUTE, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] CrossTiles = [new(CROSS, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] HammerTiles = [new(HAMMER, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] MagicKeyTiles = [new(MAGIC_KEY, 1, 2, [(0, 0)])];
+ public static readonly SpriteTile[] OneUpTiles = [new(ONEUP, 1, 2, [(0, 0)])];
+
+ public static readonly Dictionary COLLECTABLE_TILES = new()
+ {
+ { Collectable.CANDLE, CandleTiles },
+ { Collectable.GLOVE, GloveTiles },
+ { Collectable.RAFT, RaftTiles },
+ { Collectable.BOOTS, BootsTiles },
+ { Collectable.FLUTE, FluteTiles },
+ { Collectable.CROSS, CrossTiles },
+ { Collectable.HAMMER, HammerTiles },
+ { Collectable.MAGIC_KEY, MagicKeyTiles },
+ { Collectable.KEY, KeyTiles },
+ { Collectable.SMALL_BAG, PBagTiles },
+ { Collectable.MEDIUM_BAG, PBagTiles },
+ { Collectable.LARGE_BAG, PBagTiles },
+ { Collectable.XL_BAG, PBagTiles },
+ { Collectable.MAGIC_CONTAINER, MagicContainerTiles },
+ { Collectable.HEART_CONTAINER, HeartContainerTiles },
+ { Collectable.BLUE_JAR, JarTiles },
+ { Collectable.RED_JAR, JarTiles },
+ { Collectable.ONEUP, OneUpTiles },
+ { Collectable.CHILD, ChildTiles },
+ { Collectable.TROPHY, TrophyTiles },
+ { Collectable.MEDICINE, MedicineTiles },
+ { Collectable.UPSTAB, UpstabTiles },
+ { Collectable.DOWNSTAB, DownstabTiles },
+ { Collectable.BAGUS_NOTE, BagusNoteTiles },
+ { Collectable.MIRROR, MirrorTiles },
+ { Collectable.WATER, WaterTiles },
+ { Collectable.SHIELD_SPELL, ShieldTiles },
+ { Collectable.JUMP_SPELL, JumpTiles },
+ { Collectable.LIFE_SPELL, LifeTiles },
+ { Collectable.FAIRY_SPELL, FairyTiles },
+ { Collectable.FIRE_SPELL, FireTiles },
+ { Collectable.REFLECT_SPELL, ReflectTiles },
+ { Collectable.SPELL_SPELL, SpellTiles },
+ { Collectable.THUNDER_SPELL, ThunderTiles },
+ { Collectable.DASH_SPELL, DashTiles },
+ };
+}
+
+public static class Palettes
+{
+ public static int ORANGE = ROM.RomHdrSize + 0x100a2;
+
+ public static readonly Dictionary TERRAIN_ADDRS = new()
+ {
+ { Terrain.TOWN, 0x1c463 },
+ { Terrain.CAVE, 0x1c45f },
+ { Terrain.PALACE, 0x1c463 },
+ { Terrain.BRIDGE, 0x1c45f },
+ { Terrain.DESERT, 0x1c467 },
+ { Terrain.GRASS, 0x1c45b },
+ { Terrain.FOREST, 0x1c45b },
+ { Terrain.SWAMP, 0x1c45b },
+ { Terrain.GRAVE, 0x1c45f },
+ { Terrain.ROAD, 0x1c45f },
+ { Terrain.LAVA, 0x1c45f },
+ { Terrain.MOUNTAIN, 0x1c45f },
+ { Terrain.WATER, 0x100aa },
+ { Terrain.PREPLACED_WATER, 0x100aa },
+ { Terrain.WALKABLEWATER, 0x1c467 },
+ { Terrain.PREPLACED_WATER_WALKABLE, 0x1c467 },
+ { Terrain.ROCK, 0x1c45f },
+ { Terrain.RIVER_DEVIL, 0x1c45f },
+ };
+}
+
+public readonly record struct SpriteTilePlacement(
+ /// X offset in 8-pixel units from sprite origin
+ int X,
+ /// Y offset in 8-pixel units from sprite origin
+ int Y,
+ /// Whether to mirror the tile horizontally
+ bool FlipH)
+{
+ public static implicit operator SpriteTilePlacement((int x, int y) t)
+ => new(t.x, t.y, false);
+
+ public static implicit operator SpriteTilePlacement((int x, int y, bool flipH) t)
+ => new(t.x, t.y, t.flipH);
+}
+
+public readonly record struct SpriteTile(
+ /// CHR ROM address of the tile data
+ int Addr,
+ /// Number of tiles to read from address horizontally
+ int W,
+ /// Number of tiles to read from address vertically
+ int H,
+ /// Positions where this tile is drawn
+ SpriteTilePlacement[] Placement,
+ /// Whether to keep the alpha channel (false = opaque)
+ bool Alpha = true
+);
diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs
index 0e09c2b12..2635a9ff5 100644
--- a/RandomizerCore/CustomTexts.cs
+++ b/RandomizerCore/CustomTexts.cs
@@ -619,7 +619,7 @@ public static List GenerateTexts(
if (props.TownNameHints)
{
- GenerateTownNameHints(texts, locations, props.CombineFire);
+ GenerateTownNameHints(texts, locations, props.LinkedFireSpell != null);
}
} while (TextLength(texts) > MAX_TEXT_LENGTH);
diff --git a/RandomizerCore/Direction.cs b/RandomizerCore/Direction.cs
index 623666310..cefd5a202 100644
--- a/RandomizerCore/Direction.cs
+++ b/RandomizerCore/Direction.cs
@@ -20,6 +20,18 @@ public static Direction Reverse(this Direction direction)
};
}
+ public static IntVector2 ToIntVector2(this Direction direction)
+ {
+ return direction switch
+ {
+ Direction.NORTH => IntVector2.NORTH,
+ Direction.SOUTH => IntVector2.SOUTH,
+ Direction.EAST => IntVector2.EAST,
+ Direction.WEST => IntVector2.WEST,
+ _ => throw new ArgumentException("Invalid direction: " + direction)
+ };
+ }
+
public static int DeltaX(this Direction direction)
{
return direction switch
diff --git a/RandomizerCore/DropRandomizer.cs b/RandomizerCore/DropRandomizer.cs
new file mode 100644
index 000000000..1789876f5
--- /dev/null
+++ b/RandomizerCore/DropRandomizer.cs
@@ -0,0 +1,272 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+
+namespace Z2Randomizer.RandomizerCore;
+
+///
+/// Responsible for randomizing enemy drops, pbag amounts, and boss drops.
+/// Each table should be randomized at most once. If you have to re-roll for some reason,
+/// a new DropRandomizer object should be created.
+///
+public class DropRandomizer
+{
+ public const int DROP_TABLE_SIZE = 8;
+ public const int PBAG_NUM_TYPES = 4;
+
+ /* 0 - 0
+ * 1 - 2
+ * 2 - 3
+ * 3 - 5
+ * 4 - 10
+ * 5 - 20
+ * 6 - 30
+ * 7 - 50
+ * 8 - 70
+ * 9 - 100
+ * 10 - 150
+ * 11 - 200
+ * 12 - 300
+ * 13 - 500
+ * 14 - 700
+ * 15 - 1000 */
+ public static ReadOnlyCollection XP_VALUES = [0, 2, 3, 5, 10, 20, 30, 50, 70, 100, 150, 200, 300, 500, 700, 1000];
+
+ public byte[] SmallDropTable { get; private set; } = null!;
+ public byte[] LargeDropTable { get; private set; } = null!;
+ public byte[] PbagAmountTable { get; private set; } = null!;
+ public SmallItem BossDrop { get; private set; }
+ public byte DropFrequency { get; private set; }
+
+ protected RandomizerProperties props { get; }
+
+#if DEBUG
+ private bool hasRandomized = false;
+ private bool hasWritten = false;
+#endif
+
+ public DropRandomizer(ROM rom, RandomizerProperties props)
+ {
+ this.props = props;
+ ReadDrops(rom);
+ ReadPbagAmounts(rom);
+ ReadBossDrop(rom);
+ ReadDropFrequency(rom);
+ }
+
+ public void Randomize(Random r)
+ {
+#if DEBUG
+ Debug.Assert(!hasRandomized);
+ hasRandomized = true;
+#endif
+ RandomizeDrops(r);
+ RandomizePbagAmounts(r);
+ RandomizeBossDrop(r);
+ RandomizeDropFrequency(r);
+ }
+
+ public void Write(ROM rom)
+ {
+#if DEBUG
+ Debug.Assert(!hasWritten);
+ hasWritten = true;
+#endif
+ WriteDrops(rom);
+ WritePbagAmounts(rom);
+ WriteBossDrop(rom);
+ WriteDropFrequency(rom);
+ }
+
+ [Conditional("DEBUG")]
+ public void AssertHasRandomized(bool value = true)
+ {
+#if DEBUG
+ Debug.Assert(hasRandomized == value);
+#endif
+ }
+
+ [Conditional("DEBUG")]
+ public void AssertHasWritten(bool value = true)
+ {
+#if DEBUG
+ Debug.Assert(hasWritten == value);
+#endif
+ }
+
+ protected void ReadDrops(ROM rom)
+ {
+ SmallDropTable = rom.GetBytes(RomMap.SMALL_DROP_TABLE, DROP_TABLE_SIZE);
+ LargeDropTable = rom.GetBytes(RomMap.LARGE_DROP_TABLE, DROP_TABLE_SIZE);
+ }
+
+ protected void WriteDrops(ROM rom)
+ {
+ rom.Put(RomMap.SMALL_DROP_TABLE, SmallDropTable);
+ rom.Put(RomMap.LARGE_DROP_TABLE, LargeDropTable);
+ }
+
+ protected void ReadPbagAmounts(ROM rom)
+ {
+ PbagAmountTable = rom.GetBytes(RomMap.PBAG_XP_TABLE, PBAG_NUM_TYPES);
+ }
+
+ protected void WritePbagAmounts(ROM rom)
+ {
+ rom.Put(RomMap.PBAG_XP_TABLE, PbagAmountTable);
+ }
+
+ protected void ReadBossDrop(ROM rom)
+ {
+ byte rawValue = rom.GetByte(RomMap.BOSS_DROP_COLLECTABLE);
+ BossDrop = (SmallItem)(rawValue + 0x80);
+ }
+
+ protected void WriteBossDrop(ROM rom)
+ {
+ rom.Put(RomMap.BOSS_DROP_COLLECTABLE, (byte)(BossDrop - 0x80));
+ }
+
+ protected void ReadDropFrequency(ROM rom)
+ {
+ DropFrequency = rom.GetByte(RomMap.ENEMY_DROP_FREQUENCY);
+ }
+
+ protected void WriteDropFrequency(ROM rom)
+ {
+ rom.Put(RomMap.ENEMY_DROP_FREQUENCY, DropFrequency);
+ }
+
+ protected void RandomizeDropFrequency(Random r)
+ {
+ if (!props.ShuffleItemDropFrequency) { return; }
+
+ DropFrequency = (byte)(r.Next(5) + 4);
+ }
+
+ protected void RandomizeDrops(Random r)
+ {
+ List small = BuildDropList(props.Smallbluejar, props.Smallredjar,
+ props.Small50, props.Small100, props.Small200, props.Small500,
+ props.Small1up, props.Smallkey);
+
+ List large = BuildDropList(props.Largebluejar, props.Largeredjar,
+ props.Large50, props.Large100, props.Large200, props.Large500,
+ props.Large1up, props.Largekey);
+
+ // drops are kept vanilla if nothing is selected & RandomizeDrops is off
+ if (small.Count > 0)
+ {
+ SmallDropTable = ShuffleDropTable(r, small);
+ }
+ if (large.Count > 0)
+ {
+ LargeDropTable = ShuffleDropTable(r, large);
+ }
+ }
+
+ protected static List BuildDropList(bool blueJar, bool redJar, bool bag50, bool bag100, bool bag200, bool bag500, bool oneUp, bool key)
+ {
+ List list = [];
+ if (blueJar) { list.Add(SmallItem.BLUE_JAR); }
+ if (redJar) { list.Add(SmallItem.RED_JAR); }
+ if (bag50) { list.Add(SmallItem.SMALL_BAG); }
+ if (bag100) { list.Add(SmallItem.MEDIUM_BAG); }
+ if (bag200) { list.Add(SmallItem.LARGE_BAG); }
+ if (bag500) { list.Add(SmallItem.XL_BAG); }
+ if (oneUp) { list.Add(SmallItem.ONEUP); }
+ if (key) { list.Add(SmallItem.KEY); }
+ return list;
+ }
+
+ protected byte[] ShuffleDropTable(Random r, List drops)
+ {
+ for (int i = 0; i < drops.Count; i++)
+ {
+ int swap = r.Next(drops.Count);
+ (drops[i], drops[swap]) = (drops[swap], drops[i]);
+ }
+
+ byte[] table = new byte[DROP_TABLE_SIZE];
+ for (int i = 0; i < DROP_TABLE_SIZE; i++)
+ {
+ if (i < drops.Count)
+ {
+ table[i] = (byte)drops[i];
+ }
+ else
+ {
+ table[i] = (byte)drops[r.Next(drops.Count)];
+ }
+ }
+ return table;
+ }
+
+ protected void RandomizePbagAmounts(Random r)
+ {
+ if (!props.ShufflePbagXp) { return; }
+
+ PbagAmountTable = [
+ (byte)r.Next(5, 10),
+ (byte)r.Next(7, 12),
+ (byte)r.Next(9, 14),
+ (byte)r.Next(11, 16),
+ ];
+ }
+
+ protected void RandomizeBossDrop(Random r)
+ {
+ if (!props.BossItem) { return; }
+
+ var options = Enum.GetValues();
+ BossDrop = options.Sample(r);
+ }
+
+ public string GenerateSpoiler()
+ {
+ StringBuilder sb = new();
+
+ sb.AppendLine("P-BAG AMOUNTS");
+ sb.AppendLine("================");
+ for (int i = 0; i < PBAG_NUM_TYPES; i++)
+ {
+ Collectable pbagCollectable = Collectable.SMALL_BAG + i;
+ var xpIndex = PbagAmountTable[i];
+ sb.AppendLine($"{pbagCollectable.ToString() + ":",-11} {XP_VALUES[xpIndex]}");
+ }
+ sb.AppendLine("----------------");
+ sb.AppendLine();
+
+ sb.AppendLine("ENEMY DROPS");
+ var smallDropString = "SMALL: " + string.Join(" ", SmallDropTable.Select(b => ToShortString((SmallItem)b)));
+ var largeDropString = "LARGE: " + string.Join(" ", LargeDropTable.Select(b => ToShortString((SmallItem)b)));
+ var width = Math.Max(smallDropString.Length, largeDropString.Length);
+ sb.AppendLine(new string('=', width));
+ sb.AppendLine(smallDropString);
+ sb.AppendLine(largeDropString);
+ sb.AppendLine("BOSS: " + ToShortString(BossDrop));
+ sb.AppendLine(new string('-', width));
+ sb.AppendLine($"Drops every {DropFrequency} enemies");
+
+ return sb.ToString();
+ }
+
+ public string ToShortString(SmallItem b)
+ {
+ return (b switch
+ {
+ SmallItem.KEY => "Key",
+ SmallItem.SMALL_BAG => XP_VALUES[PbagAmountTable[0]].ToString(),
+ SmallItem.MEDIUM_BAG => XP_VALUES[PbagAmountTable[1]].ToString(),
+ SmallItem.LARGE_BAG => XP_VALUES[PbagAmountTable[2]].ToString(),
+ SmallItem.XL_BAG => XP_VALUES[PbagAmountTable[3]].ToString(),
+ SmallItem.BLUE_JAR => "Blue",
+ SmallItem.RED_JAR => "Red",
+ SmallItem.ONEUP => "1-Up",
+ _ => "?",
+ }).PadRight(4);
+ }
+}
diff --git a/RandomizerCore/EnumTypes.cs b/RandomizerCore/EnumTypes.cs
index 3365a9e56..1e079e5c8 100644
--- a/RandomizerCore/EnumTypes.cs
+++ b/RandomizerCore/EnumTypes.cs
@@ -78,17 +78,17 @@ public static bool StartWithUpstab(this StartingTechs techs)
[DefaultValue(VANILLA)]
public enum AttackEffectiveness
{
- [Description("Vanilla")]
+ [Description("Vanilla"), FixedBytes(2, 3, 4, 6, 9, 12, 18, 24)]
VANILLA,
- [Description("Low Attack")]
+ [Description("Low Attack"), FixedBytes(1, 2, 3, 4, 5, 6, 9, 12)]
LOW,
- [Description("Randomize (Low)")]
+ [Description("Randomize (Low)"), RandomRangeDouble(Low = .5, High = 1.0)]
AVERAGE_LOW,
- [Description("Randomize")]
+ [Description("Randomize"), RandomRangeDouble(Low = .667, High = 1.5)]
AVERAGE,
- [Description("Randomize (High)")]
+ [Description("Randomize (High)"), RandomRangeDouble(Low = 1.0, High = 1.5)]
AVERAGE_HIGH,
- [Description("High Attack")]
+ [Description("High Attack"), FixedBytes(3, 4, 6, 9, 13, 18, 27, 36)]
HIGH,
[Description("Instant Kill")]
OHKO
@@ -99,15 +99,15 @@ public enum MagicEffectiveness
{
[Description("Vanilla")]
VANILLA,
- [Description("High Spell Cost")]
+ [Description("High Spell Cost"), RandomRangeDouble(Low = 1.5, High = 1.5)]
HIGH_COST,
- [Description("Randomize (High Cost)")]
+ [Description("Randomize (High Cost)"), RandomRangeDouble(Low = 1.0, High = 1.5)]
AVERAGE_HIGH_COST,
- [Description("Randomize")]
+ [Description("Randomize"), RandomRangeDouble(Low = .5, High = 1.5)]
AVERAGE,
- [Description("Randomize (Low Cost)")]
+ [Description("Randomize (Low Cost)"), RandomRangeDouble(Low = .5, High = 1.0)]
AVERAGE_LOW_COST,
- [Description("Low Spell Cost")]
+ [Description("Low Spell Cost"), RandomRangeDouble(Low = .5, High = .5)]
LOW_COST,
[Description("Free Spells")]
FREE
@@ -120,13 +120,13 @@ public enum LifeEffectiveness
VANILLA,
[Description("OHKO Link")]
OHKO,
- [Description("Randomize (Low)")]
+ [Description("Randomize (Low)"), RandomRangeDouble(Low = 1.0, High = 1.5)]
AVERAGE_LOW,
- [Description("Randomize")]
+ [Description("Randomize"), RandomRangeDouble(Low = .75, High = 1.5)]
AVERAGE,
- [Description("Randomize (High)")]
+ [Description("Randomize (High)"), RandomRangeDouble(Low = .5, High = 1.0)]
AVERAGE_HIGH,
- [Description("High Defense")]
+ [Description("High Defense"), RandomRangeDouble(Low = .5, High = .5)]
HIGH,
[Description("Invincible")]
INVINCIBLE
@@ -157,14 +157,16 @@ public enum EnemyLifeOption
{
[Description("Vanilla; (No Randomization)")]
VANILLA,
+ [Description("Narrow; [-25% to +25%]"), RandomRangeDouble(Low = 0.75, High = 1.25)]
+ NARROW,
[Description("Medium; [-50% to +50%]"), RandomRangeDouble(Low = 0.5, High = 1.5)]
MEDIUM,
- [Description("High; [-0% to +100%]"), RandomRangeDouble(Low = 1.0, High = 2.0)]
- HIGH,
- [Description("Medium High; [-50% to +100%]"), RandomRangeDouble(Low = 0.5, High = 2.0)]
- MEDIUM_HIGH,
[Description("Wide; [-75% to +200%]"), RandomRangeDouble(Low = 0.25, High = 3.0)]
WIDE,
+ [Description("Medium High; [-50% to +100%]"), RandomRangeDouble(Low = 0.5, High = 2.0)]
+ MEDIUM_HIGH,
+ [Description("High; [-0% to +100%]"), RandomRangeDouble(Low = 1.0, High = 2.0)]
+ HIGH,
}
[DefaultValue(ONLY_BOTS)]
@@ -193,6 +195,33 @@ public enum FireOption
RANDOM
}
+public static class FireOptionExtensions
+{
+ public static bool CanBeDash(this FireOption fireOption)
+ {
+ return fireOption switch
+ {
+ FireOption.NORMAL => false,
+ FireOption.PAIR_WITH_RANDOM => false,
+ FireOption.REPLACE_WITH_DASH => true,
+ FireOption.RANDOM => true,
+ _ => throw new NotImplementedException(),
+ };
+ }
+}
+
+[DefaultValue(VANILLA)]
+public enum SwordImmunityOption
+{
+ [Description("Vanilla")]
+ VANILLA,
+ [Description("Shuffle")]
+ SHUFFLE,
+ [Description("Shuffle/None if Fire unusable")]
+ SHUFFLE_CONDITIONAL,
+ [Description("None")]
+ NONE,
+}
[DefaultValue(VANILLA)]
public enum PalaceStyle
@@ -403,9 +432,7 @@ public enum ClimateEnum
[Description("Classic"), DefaultWeight(1)]
CLASSIC,
[Description("Vanilla-Weighted"), DefaultWeight(1)]
- VANILLA_WEIGHTED_WEST,
- [Description("Vanilla-Weighted"), DefaultWeight(1)]
- VANILLA_WEIGHTED_EAST,
+ VANILLA_WEIGHTED,
[Description("Chaos"), DefaultWeight(1)]
CHAOS,
[Description("Wetlands"), DefaultWeight(0)]
@@ -414,8 +441,6 @@ public enum ClimateEnum
GREAT_LAKES,
[Description("Scrubland"), DefaultWeight(1)]
SCRUBLAND,
- [Description("Scrubland"), DefaultWeight(1)]
- DM_SCRUBLAND,
[Description("Random"), Metastyle]
RANDOM
}
@@ -426,9 +451,7 @@ public static bool IsWestClimate(this ClimateEnum climate)
{
return climate switch
{
- ClimateEnum.VANILLA_WEIGHTED_WEST => true,
- ClimateEnum.VANILLA_WEIGHTED_EAST => false,
- ClimateEnum.DM_SCRUBLAND => false,
+ ClimateEnum.VANILLA_WEIGHTED => true,
_ => true,
};
}
@@ -437,9 +460,7 @@ public static bool IsEastClimate(this ClimateEnum climate)
{
return climate switch
{
- ClimateEnum.VANILLA_WEIGHTED_WEST => false,
- ClimateEnum.VANILLA_WEIGHTED_EAST => true,
- ClimateEnum.DM_SCRUBLAND => false,
+ ClimateEnum.VANILLA_WEIGHTED => true,
_ => true,
};
}
@@ -448,9 +469,7 @@ public static bool IsDmClimate(this ClimateEnum climate)
{
return climate switch
{
- ClimateEnum.VANILLA_WEIGHTED_WEST => false,
- ClimateEnum.VANILLA_WEIGHTED_EAST => false,
- ClimateEnum.SCRUBLAND => false,
+ ClimateEnum.VANILLA_WEIGHTED => false,
_ => true,
};
}
@@ -563,10 +582,24 @@ public enum EncounterRate
HALF,
[Description("Normal")]
NORMAL,
- [Description("Random")]
+ [Description("Random"), Metastyle]
RANDOM
}
+public static class EncounterRateExtensions
+{
+ public static byte GetAsmByte(this EncounterRate encounterRate)
+ {
+ return encounterRate switch
+ {
+ EncounterRate.NORMAL => 0x0, // 0 so we can check zero flag when it's loaded from memory
+ EncounterRate.NONE => 0x1,
+ EncounterRate.HALF => 0x2,
+ _ => throw new NotImplementedException(),
+ };
+ }
+}
+
[DefaultValue(Lives3)]
public enum StartingLives
{
@@ -875,6 +908,17 @@ public class DefaultWeightAttribute(int weight) : Attribute
public int Weight { get; init; } = weight;
}
+[AttributeUsage(AttributeTargets.Field)]
+public class FixedBytesAttribute : Attribute
+{
+ public byte[] Values { get; }
+
+ public FixedBytesAttribute(params byte[] values)
+ {
+ Values = values;
+ }
+}
+
[AttributeUsage(AttributeTargets.Field)]
public class RandomRangeDoubleAttribute : Attribute
{
@@ -927,6 +971,7 @@ public static class Enums
public static IEnumerable EnemyLifeOptionList { get; } = ToDescriptions();
public static IEnumerable BossLifeOptionList { get; } = ToDescriptions(i => i != EnemyLifeOption.WIDE);
public static IEnumerable FireOptionList { get; } = ToDescriptions();
+ public static IEnumerable SwordImmunityOptionList { get; } = ToDescriptions();
public static IEnumerable BossRoomMinDistanceOptions { get; } = ToDescriptions();
public static IEnumerable PalaceLengthOptionList { get; } = ToDescriptions();
public static IEnumerable PalaceItemRoomCountOptions { get; } = ToDescriptions();
@@ -1045,6 +1090,16 @@ public static List GetShufflableList() where T : struct, Enum
.ToList();
}
+ public static FixedBytesAttribute? GetFixedBytes(this Enum self)
+ {
+ Type type = self.GetType();
+ string? name = Enum.GetName(type, self);
+ if (name == null) { return null; }
+ FieldInfo? fieldInfo = type.GetField(name);
+ if (fieldInfo == null) { return null; }
+ return fieldInfo.GetCustomAttribute(inherit: false);
+ }
+
public static RandomRangeDoubleAttribute? GetRandomRangeDouble(this Enum self)
{
Type type = self.GetType();
diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs
index 8f01c64f7..2ed56125f 100644
--- a/RandomizerCore/Hyrule.cs
+++ b/RandomizerCore/Hyrule.cs
@@ -23,6 +23,23 @@ public readonly record struct RandomizerResult(
string? debuginfo = null,
string? messages = null);
+public enum ProgressEnum
+{
+ GENERATING_PALACES = 1,
+ PROCESSING_OVERWORLD,
+ GENERATING_WEST_HYRULE,
+ GENERATING_DEATH_MOUNTAIN,
+ GENERATING_EAST_HYRULE,
+ GENERATING_MAZE_ISLAND,
+ SHUFFLING_ITEMS_AND_SPELLS,
+ RUNNING_COMPLETABILITY_CHECKS,
+ SHUFFLING_ENEMIES,
+ GENERATING_HINTS,
+ APPLYING_PATCHES,
+ LINKING_ASSEMBLY,
+ FINISHING_UP,
+}
+
public class Hyrule
{
public delegate Assembler NewAssemblerFn(Js65Options? options = null, bool debugJavaScript = false);
@@ -109,11 +126,12 @@ public class Hyrule
private MazeIsland mazeIsland;
private DeathMountain deathMountain;
- private Shuffler shuffler;
private RandomizerProperties props;
public List worlds;
public List palaces;
public List rooms;
+ public StatRandomizer randomizedStats;
+ public DropRandomizer randomizedDrops;
//DEBUG/STATS
#pragma warning disable CS0414 // Field is assigned but its value is never used
@@ -249,7 +267,6 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
using Assembler assembler = CreateAssemblyEngine();
logger.Info($"Started generation for flags: {Flags} sharedseedflags: {sharedSeedFlags} seed: {config.Seed} seedhash: {SeedHash}");
//character = new Character(props);
- shuffler = new Shuffler(props);
palaces = [];
ItemGet = [];
@@ -273,7 +290,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
bool passedValidation = false;
HashSet freeBanks = [];
if (ct.IsCancellationRequested) { return new RandomizerResult(false); }
- UpdateProgress(progress, 1);
+ UpdateProgress(progress, ProgressEnum.GENERATING_PALACES);
while (palaces.Count != 7 || passedValidation == false)
{
@@ -325,8 +342,9 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
ROMData.DoHackyFixes();
ROMData.AdjustGpProjectileDamage();
- shuffler.ShuffleDrops(ROMData, r);
- shuffler.ShufflePbagAmounts(ROMData, r);
+ randomizedDrops = new(ROMData, props);
+ randomizedDrops.Randomize(r);
+ randomizedDrops.Write(ROMData);
ROMData.DisableTurningPalacesToStone();
ROMData.UpdateMapPointers();
@@ -347,7 +365,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
firstProcessOverworldTimestamp = DateTime.Now;
await ProcessOverworld(progress, ct);
if (ct.IsCancellationRequested) { return new RandomizerResult(false); }
- UpdateProgress(progress, 8);
+ UpdateProgress(progress, ProgressEnum.SHUFFLING_ENEMIES);
if (props.ShuffleOverworldEnemies)
{
@@ -356,7 +374,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
OverworldEnemyShuffler.Shuffle(worlds, assembler, ROMData, props.MixLargeAndSmallEnemies, props.GeneratorsAlwaysMatch, r);
}
- if (props.CombineFire)
+ if (props.LinkedFireSpell != null)
{
List? customSpellOrder = props.IncludeSpellsInShuffle
? null
@@ -365,7 +383,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
//This makes the assumption that is currently true that each "town" has exactly one item.
//If we later restructure towns to be omni-towns to get rid of fake towns, this will be untrue
.Select(l => l.Collectables[0]).ToList();
- ROMData.CombineFireSpell(assembler, customSpellOrder, r);
+ ROMData.CombineFireSpell(assembler, props.LinkedFireSpell.Value, customSpellOrder);
}
Dictionary spellMap = new()
@@ -406,10 +424,10 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
}
if (ct.IsCancellationRequested) { return new RandomizerResult(false); }
- UpdateProgress(progress, 9);
+ UpdateProgress(progress, ProgressEnum.GENERATING_HINTS);
List texts = CustomTexts.GenerateTexts(AllLocationsForReal(), itemLocs, ROMData.GetGameText(), props, r);
- StatRandomizer randomizedStats = new(ROMData, props);
+ randomizedStats = new(ROMData, props);
randomizedStats.Randomize(r, skipDifficultyOnly: shareSeedAcrossDifficulty);
// Apply difficulty after shared randomization, then let ApplyAsmPatches see full
@@ -448,13 +466,16 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC
randomizeMusic = props.RandomizeMusic;
}
+ UpdateProgress(progress, ProgressEnum.APPLYING_PATCHES);
ApplyAsmPatches(props, assembler, r, texts, ROMData, randomizedStats);
+ UpdateProgress(progress, ProgressEnum.LINKING_ASSEMBLY);
var rom = await ROMData.ApplyAsm(assembler);
if (!rom.success)
{
return new RandomizerResult(false, null, null, string.Join(Environment.NewLine, rom.messages));
}
+ UpdateProgress(progress, ProgressEnum.FINISHING_UP);
ROMData = new ROM(rom.romdata);
if (randomizeMusic)
@@ -718,7 +739,7 @@ private void ShuffleItems()
else
{
westHyrule.pbagCave.Collectables = [Collectable.LARGE_BAG];
- eastHyrule.pbagCave1.Collectables = [Collectable.LARGE_BAG];
+ eastHyrule.pbagCave1.Collectables = [Collectable.XL_BAG];
eastHyrule.pbagCave2.Collectables = [Collectable.XL_BAG];
}
@@ -1408,6 +1429,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule)
sideviewModule.Byt(itemBits);
}
+ /* this shouldn't be needed anymore
try
{
ROM testRom = new(ROMData);
@@ -1430,6 +1452,7 @@ private async Task FillPalaceRooms(AsmModule sideviewModule)
logger.Error(e, "Failed to build assembly patches");
throw;
}
+ */
return true;
}
@@ -1455,7 +1478,7 @@ private bool IsEverythingReachable(Dictionary itemGet)
previousReachableLocationsCount = reachableLocationsCount;
previousGettableItemsCount = gettableItemsCount;
gettableItemsCount = UpdateItemGets(props);
- List requireables = GetRequireables(props);
+ IReadOnlySet requireables = GetRequireables(props);
westHyrule.UpdateVisit(requireables);
deathMountain.UpdateVisit(requireables);
eastHyrule.UpdateVisit(requireables);
@@ -1640,7 +1663,7 @@ private void ShortenWizards()
/// Whether any items were marked accessable
private int UpdateItemGets(RandomizerProperties props)
{
- List requireables;
+ IReadOnlySet requireables;
accessibleMagicContainers = props.StartMagicContainers;
accessibleHeartContainers = props.StartHearts;
Location newKasuto = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO);
@@ -1705,9 +1728,9 @@ private int UpdateItemGets(RandomizerProperties props)
return gottenItems.Count;
}
- public List GetRequireables(RandomizerProperties props)
+ public IReadOnlySet GetRequireables(RandomizerProperties props)
{
- List requireables = [];
+ HashSet requireables = [];
foreach(Collectable item in ItemGet.Keys)
{
@@ -1758,6 +1781,7 @@ public List GetRequireables(RandomizerProperties props)
private async Task ProcessOverworld(Func progress, CancellationToken ct)
{
+ UpdateProgress(progress, ProgressEnum.PROCESSING_OVERWORLD);
if (props.RandomizeSmallItems)
{
RandomizeSmallItems(1, true);
@@ -2003,7 +2027,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok
{
//GENERATE WEST
if (ct.IsCancellationRequested) { return; }
- UpdateProgress(progress, 2);
+ UpdateProgress(progress, ProgressEnum.GENERATING_WEST_HYRULE);
nonContinentGenerationAttempts++;
timestamp = DateTime.Now;
if (!westHyrule.AllReached)
@@ -2022,7 +2046,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok
//GENERATE DM
if (ct.IsCancellationRequested) { return; }
- UpdateProgress(progress, 3);
+ UpdateProgress(progress, ProgressEnum.GENERATING_DEATH_MOUNTAIN);
timestamp = DateTime.Now;
if (!deathMountain.AllReached)
{
@@ -2040,7 +2064,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok
//GENERATE EAST
if (ct.IsCancellationRequested) { return; }
- UpdateProgress(progress, 4);
+ UpdateProgress(progress, ProgressEnum.GENERATING_EAST_HYRULE);
timestamp = DateTime.Now;
if (!eastHyrule.AllReached)
{
@@ -2058,7 +2082,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok
//GENERATE MAZE ISLAND
if (ct.IsCancellationRequested) { return; }
- UpdateProgress(progress, 5);
+ UpdateProgress(progress, ProgressEnum.GENERATING_MAZE_ISLAND);
timestamp = DateTime.Now;
if (!mazeIsland.AllReached)
{
@@ -2078,7 +2102,7 @@ private async Task ProcessOverworld(Func progress, CancellationTok
worlds.ForEach(i => i.SynchronizeLinkedLocations());
if (ct.IsCancellationRequested) { return; }
- UpdateProgress(progress, 6);
+ UpdateProgress(progress, ProgressEnum.SHUFFLING_ITEMS_AND_SPELLS);
//Then perform non-terrain shuffles looking for one that works.
nonTerrainShuffleAttempt = 0;
@@ -2133,35 +2157,47 @@ private async Task ProcessOverworld(Func progress, CancellationTok
} while (!IsEverythingReachable(ItemGet));
}
- private async void UpdateProgress(Func progress, int v)
+ private async void UpdateProgress(Func progress, ProgressEnum v)
{
switch (v)
{
- case 1:
+ case ProgressEnum.GENERATING_PALACES:
await progress.Invoke("Generating Palaces");
break;
- case 2:
+ case ProgressEnum.PROCESSING_OVERWORLD:
+ await progress.Invoke("Processing Overworld");
+ break;
+ case ProgressEnum.GENERATING_WEST_HYRULE:
await progress.Invoke("Generating Western Hyrule");
break;
- case 3:
+ case ProgressEnum.GENERATING_DEATH_MOUNTAIN:
await progress.Invoke("Generating Death Mountain");
break;
- case 4:
+ case ProgressEnum.GENERATING_EAST_HYRULE:
await progress.Invoke("Generating East Hyrule");
break;
- case 5:
+ case ProgressEnum.GENERATING_MAZE_ISLAND:
await progress.Invoke("Generating Maze Island");
break;
- case 6:
+ case ProgressEnum.SHUFFLING_ITEMS_AND_SPELLS:
await progress.Invoke("Shuffling Items and Spells");
break;
- case 7:
+ case ProgressEnum.RUNNING_COMPLETABILITY_CHECKS:
await progress.Invoke("Running Seed Completability Checks");
break;
- case 8:
+ case ProgressEnum.SHUFFLING_ENEMIES:
+ await progress.Invoke("Shuffling Enemies");
+ break;
+ case ProgressEnum.GENERATING_HINTS:
await progress.Invoke("Generating Hints");
break;
- case 9:
+ case ProgressEnum.APPLYING_PATCHES:
+ await progress.Invoke("Applying Patches");
+ break;
+ case ProgressEnum.LINKING_ASSEMBLY:
+ await progress.Invoke("Linking Assembly");
+ break;
+ case ProgressEnum.FINISHING_UP:
await progress.Invoke("Finishing up");
break;
}
@@ -2429,7 +2465,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra
}
if (props.BossItem)
{
- shuffler.ShuffleBossDrop(rom, r, a);
+ rom.HandleRandomBossDrop(a);
}
if (props.StartWithSpellItems)
@@ -2447,7 +2483,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra
rom.Put(ROM.ChrRomOffset + 0x1a000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.item_sprites.chr"));
//Linked fire/dash custom sprites replace fire's sprite. This lets us free up c7 for future use.
- if(props.CombineFire)
+ if(props.LinkedFireSpell != null)
{
rom.Put(ROM.ChrRomOffset + 0x1a0E0, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.linkedFire.chr"));
}
@@ -2459,43 +2495,6 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra
rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite);
rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr"));
- if (props.EncounterRates == EncounterRate.NONE)
- {
- rom.Put(0x294, 0x60); //skips the whole routine
- }
-
- if (props.EncounterRates == EncounterRate.HALF)
- {
- //terrain timers
- rom.Put(0x250, 0x40); // grass
- rom.Put(0x251, 0x30); // desert
- rom.Put(0x252, 0x30); // forest
- rom.Put(0x253, 0x40);
- rom.Put(0x254, 0x12);
- rom.Put(0x255, 0x06);
-
- //initial overworld timer
- rom.Put(0x88A, 0x10);
-
- /*
- * insert jump to a8aa at 2a3 (4c AA A8)
- *
- * At a8aa
- * Load $26 (A5 26)
- * bne to end (2 bytes) (D0 0D)
- * inc new step counter (where?) EE E0 06
- * Load 1 to accumulator (A9 01)
- * xor new step counter with 1 (2D E0 06)
- * bne to end (D0 03)
- * jump to encounter spawn 8298 (4C 98 82)
- * jump to rts 829f (4C 93 82)
- */
- rom.Put(0x29f, new byte[] { 0x4C, 0xAA, 0xA8 });
-
- rom.Put(0x28ba, new byte[] { 0xA5, 0x26, 0xD0, 0x0D, 0xEE, 0xE0, 0x06, 0xA9, 0x01, 0x2D, 0xE0, 0x06, 0xD0, 0x03, 0x4C, 0x98, 0x82, 0x4C, 0x93, 0x82 });
- }
-
-
if (props.ShuffleLifeRefill)
{
int lifeRefill = r.Next(1, 6);
@@ -2590,15 +2589,11 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra
if (props.ShufflePalacePalettes)
{
- shuffler.ShufflePalacePalettes(rom, r);
+ Shuffler.ShufflePalacePalettes(rom, r);
}
- if (props.ShuffleItemDropFrequency)
- {
- int drop = r.Next(5) + 4;
- rom.Put(0x1E8B0, (byte)drop);
+
}
- }
private static void ApplyBeepSettings(RandomizerProperties props, ROM rom)
{
@@ -2750,8 +2745,6 @@ what is visible in the dark and also doesn't add enough to be worth it imo.
ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.Collectables[0]);
ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.Collectables[0]);
- ROMData.ElevatorBossFix(props.BossItem);
-
// Update item rooms and entrances for all palaces
Location[] locations = [
westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3,
@@ -2864,7 +2857,7 @@ what is visible in the dark and also doesn't add enough to be worth it imo.
//ROMData.UpdateWizardText(WizardCollectables);
// Add marker for linked fire. Do this before old spell name shuffle so this gets shuffled in there too
- if (props.CombineFire)
+ if (props.LinkedFireSpell != null)
{
ROMData.Put(0x1c72, [..ROM.StringToZ2Bytes("FIRE"), 0xFC]);
}
@@ -3048,45 +3041,49 @@ private void RerollPaletteTable(int paletteTableAddr, Random r)
}
}
- public void RandomizeSmallItems(int world, bool first)
+ public void RandomizeSmallItems(int bank, bool firstTable)
{
- logger.Debug("World: " + world);
- List addresses = new List();
- List items = new List();
+ logger.Debug("Bank: " + bank);
+ List visited = [];
+ List items = [];
int startAddr;
- if (first)
+ if (firstTable) // West/East
{
- startAddr = 0x8523 - 0x8000 + (world * 0x4000) + 0x10;
+ startAddr = 0x8523 - 0x8000 + (bank * 0x4000) + 0x10;
}
- else
+ else // DM/Maze
{
- startAddr = 0xA000 - 0x8000 + (world * 0x4000) + 0x10;
+ startAddr = 0xa000 - 0x8000 + (bank * 0x4000) + 0x10;
}
int map = 0;
for (int i = startAddr; i < startAddr + 126; i = i + 2)
{
map++;
- int low = ROMData.GetByte(i);
- int hi = ROMData.GetByte(i + 1) * 256;
- int numBytes = ROMData.GetByte(hi + low + 16 - 0x8000 + (world * 0x4000));
+ int ptr = ROMData.GetShort(i + 1, i);
+ int offset = 16 - 0x8000 + (bank * 0x4000);
+ int numBytes = ROMData.GetByte(ptr + offset);
for (int j = 4; j < numBytes; j = j + 2)
{
- int yPos = ROMData.GetByte(hi + low + j + 16 - 0x8000 + (world * 0x4000)) & 0xF0;
+ int yPos = ROMData.GetByte(ptr + j + offset) & 0xF0;
yPos = yPos >> 4;
- if (ROMData.GetByte(hi + low + j + 1 + 16 - 0x8000 + (world * 0x4000)) == 0x0F && yPos < 13)
+ if (ROMData.GetByte(ptr + j + 1 + offset) == 0x0F && yPos < 13)
{
- int addr = hi + low + j + 2 + 16 - 0x8000 + (world * 0x4000);
- int item = ROMData.GetByte(addr);
- if (item == 8 || (item > 9 && item < 14) || (item > 15 && item < 19) && !addresses.Contains(addr))
+ int addr = ptr + j + 2 + offset;
+ if (!visited.Contains(addr))
{
-#if UNSAFE_DEBUG
- logger.Debug("Map: " + map);
- logger.Debug("Item: " + item);
- logger.Debug($"Address: {addr:X}");
-#endif
- addresses.Add(addr);
- items.Add(item);
+ int item = ROMData.GetByte(addr);
+ Collectable collectable = (Collectable)item;
+ if (collectable.IsMinorItem())
+ {
+ #if UNSAFE_DEBUG
+ logger.Debug("Map: " + map);
+ logger.Debug("Item: " + item);
+ logger.Debug($"Address: {addr:X}");
+ #endif
+ visited.Add(addr);
+ items.Add(item);
+ }
}
j++;
}
@@ -3098,9 +3095,9 @@ public void RandomizeSmallItems(int world, bool first)
int swap = r.Next(i, items.Count);
(items[swap], items[i]) = (items[i], items[swap]);
}
- for (int i = 0; i < addresses.Count; i++)
+ for (int i = 0; i < visited.Count; i++)
{
- ROMData.Put(addresses[i], (byte)items[i]);
+ ROMData.Put(visited[i], (byte)items[i]);
}
}
@@ -3196,7 +3193,13 @@ public string GenerateSpoiler()
sb.AppendLine("\nGP:\n");
sb.AppendLine(palaces[6].GetLayoutDebug(props.PalaceStyles[6], false));
- sb.AppendLine("DETAILS: ");
+ sb.AppendLine("\nDROPS:\n");
+ sb.AppendLine(randomizedDrops.GenerateSpoiler());
+
+ sb.AppendLine("\nSTATS:\n");
+ sb.AppendLine(randomizedStats.GenerateSpoiler());
+
+ sb.AppendLine("\nDETAILS: ");
sb.Append(JsonSerializer.Serialize(props, SourceGenerationContext.Default.RandomizerProperties));
return sb.ToString().Replace('$', ' ');
@@ -3830,9 +3833,11 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando
ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic); // will make output vary with customize tab options
rom.AddRandomizerToTitle(engine);
AddCropGuideBoxesToFileSelect(engine);
+ rom.SetEncounterRate(engine, props, r);
FixHelmetheadBossRoom(engine);
FullItemShuffle(engine, GetNonSideviewItemLocations());
rom.DontCountExpDuringTalking(engine);
+ rom.ElevatorBossFix(engine, props.BossItem);
rom.FixElevatorPositionInFallRooms(engine);
rom.AllowForChangingDoorYPosition(engine);
rom.AllowForChangingElevatorYPosition(engine);
diff --git a/RandomizerCore/IntVector2.cs b/RandomizerCore/IntVector2.cs
index 06a704477..701718899 100644
--- a/RandomizerCore/IntVector2.cs
+++ b/RandomizerCore/IntVector2.cs
@@ -1,9 +1,25 @@
using System;
+using System.Diagnostics;
namespace Z2Randomizer.RandomizerCore;
public readonly struct IntVector2 : IEquatable
{
+ public static readonly IntVector2 ZERO = new(0, 0);
+ public static readonly IntVector2 ORIGIN = new(0, 0);
+ public static readonly IntVector2 NORTH = new(0, -1);
+ public static readonly IntVector2 EAST = new(1, 0);
+ public static readonly IntVector2 SOUTH = new(0, 1);
+ public static readonly IntVector2 WEST = new(-1, 0);
+ public static readonly IntVector2 NORTH_EAST = new(1, -1);
+ public static readonly IntVector2 SOUTH_EAST = new(1, 1);
+ public static readonly IntVector2 SOUTH_WEST = new(-1, 1);
+ public static readonly IntVector2 NORTH_WEST = new(-1, -1);
+
+ public static readonly IntVector2[] CARDINALS = [NORTH, EAST, SOUTH, WEST];
+ public static readonly IntVector2[] ORDINALS = [NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST];
+ public static readonly IntVector2[] DIRECTIONS = [NORTH, NORTH_EAST, EAST, SOUTH_EAST, SOUTH, SOUTH_WEST, WEST, NORTH_WEST];
+
public readonly int X;
public readonly int Y;
@@ -31,12 +47,70 @@ public IntVector2(int x, int y)
public static IntVector2 operator *(int k, IntVector2 v)
=> new(k * v.X, k * v.Y);
- public static readonly IntVector2 ZERO = new(0, 0);
- public static readonly IntVector2 WEST = new(-1, 0);
- public static readonly IntVector2 EAST = new(1, 0);
- public static readonly IntVector2 NORTH = new(0, -1);
- public static readonly IntVector2 SOUTH = new(0, 1);
- public static readonly IntVector2[] CARDINALS = [NORTH, SOUTH, EAST, WEST];
+ public static IntVector2 operator /(IntVector2 v, int d)
+ => new(v.X / d, v.Y / d);
+
+ public static int Dot(IntVector2 a, IntVector2 b)
+ => a.X * b.X + a.Y * b.Y;
+
+ public static int Cross(IntVector2 a, IntVector2 b)
+ => a.X * b.Y - a.Y * b.X;
+
+ public IntVector2 ComponentMultiply(IntVector2 other)
+ => new(X * other.X, Y * other.Y);
+
+ public IntVector2 Normalize()
+ {
+ Debug.Assert(X == 0 || Y == 0, "Normalized IntVector2 can't be diagonal");
+ if (X != 0)
+ {
+ return X > 0 ? EAST : WEST;
+ }
+ else if (Y != 0)
+ {
+ return Y > 0 ? SOUTH : NORTH;
+ }
+ else
+ {
+ return ZERO;
+ }
+ }
+
+ public bool IsZero
+ => X == 0 && Y == 0;
+
+ public IntVector2 Abs()
+ => new(Math.Abs(X), Math.Abs(Y));
+
+ public int MinComponent()
+ => Math.Min(X, Y);
+
+ public int MaxComponent()
+ => Math.Max(X, Y);
+
+ public static IntVector2 Min(IntVector2 a, IntVector2 b)
+ => new(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y));
+
+ public static IntVector2 Max(IntVector2 a, IntVector2 b)
+ => new(Math.Max(a.X, b.X), Math.Max(a.Y, b.Y));
+
+ public int LengthSquared
+ => X * X + Y * Y;
+
+ public double Length
+ => Math.Sqrt(LengthSquared);
+
+ public int ManhattanLength
+ => Math.Abs(X) + Math.Abs(Y);
+
+ public int DistanceSquared(IntVector2 other)
+ => (this - other).LengthSquared;
+
+ public double Distance(IntVector2 other)
+ => Math.Sqrt(DistanceSquared(other));
+
+ public int ManhattanDistance(IntVector2 other)
+ => Math.Abs(X - other.X) + Math.Abs(Y - other.Y);
public static IntVector2 Random(Random r, int maxX, int maxY)
{
@@ -66,17 +140,36 @@ public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
+
+ public override string ToString()
+ {
+ return $"{X},{Y}";
+ }
}
public static class IntVector2Ext
{
public static IntVector2 Perpendicular(this IntVector2 v)
{
- return new(v.Y, -v.X);
+ return new(-v.Y, v.X);
}
public static IntVector2 PerpendicularCounterClockwise(this IntVector2 v)
{
- return new(-v.Y, v.X);
+ return new(v.Y, -v.X);
+ }
+
+ ///
+ /// Determines whether lies behind
+ /// along the specified direction.
+ ///
+ /// The point being tested.
+ /// The reference point to compare against.
+ /// The forward direction from the reference point.
+ /// true if is behind ; otherwise, false.
+ public static bool IsBehind(this IntVector2 self, IntVector2 other, IntVector2 direction)
+ {
+ // A negative dot product means the direction from 'other' to 'self' faces away from the 'direction' vector.
+ return IntVector2.Dot(self - other, direction) < 0;
}
}
diff --git a/RandomizerCore/Overworld/Climates.cs b/RandomizerCore/Overworld/Climates.cs
index 0292f11e3..1e8a280de 100644
--- a/RandomizerCore/Overworld/Climates.cs
+++ b/RandomizerCore/Overworld/Climates.cs
@@ -67,7 +67,7 @@ public static class Climates
( Terrain.ROAD, 328 ),
]),
30,
- ClimateEnum.VANILLA_WEIGHTED_WEST
+ ClimateEnum.VANILLA_WEIGHTED
);
private static readonly Climate VANILLA_WEIGHTED_EAST = new(
@@ -100,7 +100,7 @@ public static class Climates
( Terrain.ROAD, 128 ),
]),
30,
- ClimateEnum.VANILLA_WEIGHTED_EAST
+ ClimateEnum.VANILLA_WEIGHTED
);
private static readonly Climate CHAOS = new(
@@ -243,7 +243,7 @@ public static class Climates
( Terrain.ROAD, 2 ),
]),
30,
- ClimateEnum.DM_SCRUBLAND
+ ClimateEnum.SCRUBLAND
);
private static readonly Climate GREAT_LAKES = new(
@@ -281,18 +281,20 @@ public static class Climates
ClimateEnum.GREAT_LAKES
);
- public static Climate Create(ClimateEnum climate)
+ public static Climate Create(Continent continentId, ClimateEnum climate)
{
return climate switch
{
ClimateEnum.CLASSIC => CLASSIC.CloneWithInvertedDistances(),
- ClimateEnum.VANILLA_WEIGHTED_WEST => VANILLA_WEIGHTED_WEST.Clone(),
- ClimateEnum.VANILLA_WEIGHTED_EAST => VANILLA_WEIGHTED_EAST.Clone(),
+ ClimateEnum.VANILLA_WEIGHTED => continentId is Continent.WEST ?
+ VANILLA_WEIGHTED_WEST.Clone() :
+ VANILLA_WEIGHTED_EAST.Clone(),
ClimateEnum.CHAOS => CHAOS.CloneWithInvertedDistances(),
ClimateEnum.WETLANDS => WETLANDS.CloneWithInvertedDistances(),
ClimateEnum.GREAT_LAKES => GREAT_LAKES.CloneWithInvertedDistances(),
- ClimateEnum.SCRUBLAND => SCRUBLAND.CloneWithInvertedDistances(),
- ClimateEnum.DM_SCRUBLAND => DM_SCRUBLAND.CloneWithInvertedDistances(),
+ ClimateEnum.SCRUBLAND => continentId is Continent.DM ?
+ DM_SCRUBLAND.CloneWithInvertedDistances() :
+ SCRUBLAND.CloneWithInvertedDistances(),
_ => throw new NotImplementedException()
};
}
diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs
index c2bcbdeb2..a667d6b90 100644
--- a/RandomizerCore/Overworld/DeathMountain.cs
+++ b/RandomizerCore/Overworld/DeathMountain.cs
@@ -191,7 +191,7 @@ .. rom.LoadLocations(LocationID.DM_SPEC_ROCK, 1, terrains),
walkableTerrains = new List() { Terrain.DESERT, Terrain.FOREST, Terrain.GRAVE };
randomTerrainFilter = new List() { Terrain.DESERT, Terrain.FOREST, Terrain.GRAVE, Terrain.MOUNTAIN, Terrain.WALKABLEWATER, Terrain.WATER };
- climate = Climates.Create(props.DmClimate);
+ climate = Climates.Create(continentId, props.DmClimate);
climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit());
SetVanillaCollectables(props.ReplaceFireWithDash);
}
@@ -226,7 +226,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
{
Debug.Assert(MapRows == 75);
Debug.Assert(MapColumns == 64);
- map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns);
+ map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns));
if (biome == Biome.VANILLA_SHUFFLE)
{
ShuffleLocations(AllLocations);
@@ -245,7 +245,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
int bytesWritten = 2000;
while (bytesWritten > MAP_SIZE_BYTES)
{
- map = new Terrain[MapRows, MapColumns];
+ map = new OverworldMap(MapRows, MapColumns);
Terrain riverT = Terrain.MOUNTAIN;
if (biome != Biome.CANYON && biome != Biome.DRY_CANYON && biome != Biome.CALDERA && biome != Biome.ISLANDS)
{
@@ -470,7 +470,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
location.Y = y;
if (location.TerrainType == Terrain.CAVE)
{
- var f = TerraformCaveExpansion(props, ref x, ref y, location);
+ var f = TerraformCaveExpansion(props, location);
if (!f)
{
logger.LogDebug($"TerraformCaveExpansion failed for {location.Name}");
@@ -592,245 +592,84 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
return true;
}
- private bool TerraformCaveExpansion(RandomizerProperties props, ref int x, ref int y, Location location)
+ private bool TerraformCaveExpansion(RandomizerProperties props, Location location)
{
- Direction direction = (Direction)RNG.Next(4);
-
- Terrain s = biome == Biome.VANILLALIKE ? Terrain.ROAD : climate.GetRandomTerrain(RNG, walkableTerrains);
- int tries;
+ // Entrance terrain will be the same for all connecting caves (!)
+ Terrain entranceTerrain = biome == Biome.VANILLALIKE ? Terrain.ROAD : climate.GetRandomTerrain(RNG, walkableTerrains);
if (props.SaneCaves && connectionsDM.ContainsKey(location))
{
- if ((location.MapPage == 0 || location.IsFallInHole) && !location.ForceEnterRight)
- {
- if (direction == Direction.NORTH)
- {
- direction = Direction.SOUTH;
- }
+ map[location.Pos] = Terrain.NONE;
- if (direction == Direction.WEST)
- {
- direction = Direction.EAST;
- }
- }
- else
+ if (!(PickCavePositionAndDirection() is var (cave1pos, cave1dir)))
{
- if (direction == Direction.SOUTH)
- {
- direction = Direction.NORTH;
- }
-
- if (direction == Direction.EAST)
- {
- direction = Direction.WEST;
- }
+ return false;
}
- map[y, x] = Terrain.NONE;
- tries = 0;
- do
+ Func rollSpacing = biome switch
{
- x = RNG.Next(MapColumns - 2) + 1;
- y = RNG.Next(MapRows - 2) + 1;
- if (++tries >= 100)
- {
- return false;
- }
- } while (x < 5 || x > MapColumns - 5
- || y < 5 || y > MapRows - 5
- || !AllTerrainIn3x3Equals(x, y, Terrain.NONE));
-
- int minDistX = Math.Min(MapColumns / 2 - 1, 15);
- int minDistY = Math.Min(MapRows / 2 - 1, 15);
-
- while ((direction == Direction.NORTH && y < minDistY)
- || (direction == Direction.EAST && x > MapColumns - minDistX)
- || (direction == Direction.SOUTH && y > MapRows - minDistY)
- || (direction == Direction.WEST && x < minDistX))
+ Biome.ISLANDS => () => RNG.Next(5, 12),
+ _ => () => RNG.Next(3, 10),
+ };
+ if (!(PickMatchingSaneCavePosition(cave1pos, cave1dir, rollSpacing, (_) => true) is IntVector2 cave2pos))
{
- direction = (Direction)RNG.Next(4);
+ return false;
}
+
if (connectionsDM[location].Count == 1)
{
- int otherx = 0;
- int othery = 0;
- tries = 0;
- do
- {
- int range = 7;
- int offset = 3;
- if (biome == Biome.ISLANDS)
- {
- range = 7;
- offset = 5;
- }
- if (direction == Direction.NORTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y - (RNG.Next(range) + offset);
- }
- else if (direction == Direction.EAST)
- {
- otherx = x + (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- else if (direction == Direction.SOUTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y + (RNG.Next(range) + offset);
- }
- else //west
- {
- otherx = x - (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- if (++tries >= 100)
- {
- return false;
- }
- } while (otherx <= 1 || otherx >= MapColumns - 1
- || othery <= 1 || othery >= MapRows - 1
- || !AllTerrainIn3x3Equals(otherx, othery, Terrain.NONE));
-
- List l2 = connectionsDM[location];
- var location2 = l2[0];
+ var location2 = connectionsDM[location][0];
location.CanShuffle = false;
- location.Xpos = x;
- location.Y = y;
+ location.Pos = cave1pos;
location2.CanShuffle = false;
- location2.Xpos = otherx;
- location2.Y = othery;
- PlaceCave(x, y, direction, s);
- PlaceCave(otherx, othery, direction.Reverse(), s);
- AlignCavePositionsLeftToRight(direction, location, location2);
+ location2.Pos = cave2pos;
+ PlaceCave(cave1pos, cave1dir, entranceTerrain);
+ PlaceCave(cave2pos, -cave1dir, entranceTerrain);
+ AlignCavePositionsLeftToRight(cave1dir, location, location2);
}
- else //4-way caves
+ else // 4-way cave
{
- int otherx = 0;
- int othery = 0;
- tries = 0;
- do
- {
- int range = 7;
- int offset = 3;
- if (biome == Biome.ISLANDS)
- {
- range = 7;
- offset = 5;
- }
- if (direction == Direction.NORTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y - (RNG.Next(range) + offset);
- }
- else if (direction == Direction.EAST)
- {
- otherx = x + (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- else if (direction == Direction.SOUTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y + (RNG.Next(range) + offset);
- }
- else //west
- {
- otherx = x - (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- if (++tries >= 100)
- {
- return false;
- }
- } while (otherx <= 1 || otherx >= MapColumns - 1
- || othery <= 1 || othery >= MapRows - 1
- || !AllTerrainIn3x3Equals(otherx, othery, Terrain.NONE));
-
- List caveExits = connectionsDM[location];
+ var caveExits = connectionsDM[location];
var location2 = caveExits[0];
var location3 = caveExits[1];
var location4 = caveExits[2];
location.CanShuffle = false;
- location.Xpos = x;
- location.Y = y;
+ location.Pos = cave1pos;
location2.CanShuffle = false;
- location2.Xpos = otherx;
- location2.Y = othery;
- PlaceCave(x, y, direction, s);
- PlaceCave(otherx, othery, direction.Reverse(), s);
- AlignCavePositionsLeftToRight(direction, location, location2);
-
- int newx = 0;
- int newy = 0;
- tries = 0;
- do
- {
- newx = x + RNG.Next(7) - 3;
- newy = y + RNG.Next(7) - 3;
- if (++tries >= 100)
- {
- return false;
- }
- } while (newx > 2 && newx < MapColumns - 2
- && newy > 2 && newy < MapRows - 2
- && !AllTerrainIn3x3Equals(newx, newy, Terrain.NONE));
+ location2.Pos = cave2pos;
+ PlaceCave(cave1pos, cave1dir, entranceTerrain);
+ PlaceCave(cave2pos, -cave1dir, entranceTerrain);
+ AlignCavePositionsLeftToRight(cave1dir, location, location2);
- location3.CanShuffle = false;
- location3.Xpos = newx;
- location3.Y = newy;
- PlaceCave(newx, newy, direction, s);
-
- y = newy;
- x = newx;
- tries = 0;
- do
+ IntVector2 cave3pos;
+ for (int tries = 0; ; tries++)
{
- int range = 7;
- int offset = 3;
- if (biome == Biome.ISLANDS)
+ if (tries == 100) { return false; }
+ cave3pos = cave1pos + new IntVector2(RNG.Next(-3, 4), RNG.Next(-3, 4));
+ if (WithinMapBounds(cave3pos, 1) && AllTerrainIn3x3Equals(cave3pos, Terrain.NONE))
{
- range = 7;
- offset = 5;
+ break;
}
+ }
- if (direction == Direction.NORTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y - (RNG.Next(range) + offset);
- }
- else if (direction == Direction.EAST)
- {
- otherx = x + (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- else if (direction == Direction.SOUTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y + (RNG.Next(range) + offset);
- }
- else //west
- {
- otherx = x - (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- if (++tries >= 100)
- {
- return false;
- }
- } while (otherx <= 1 || otherx >= MapColumns - 1
- || othery <= 1 || othery >= MapRows - 1
- || !AllTerrainIn3x3Equals(otherx, othery, Terrain.NONE));
+ if (!(PickMatchingSaneCavePosition(cave3pos, cave1dir, rollSpacing, (_) => true) is IntVector2 cave4pos))
+ {
+ return false;
+ }
+ location3.CanShuffle = false;
+ location3.Pos = cave3pos;
location4.CanShuffle = false;
- location4.Xpos = otherx;
- location4.Y = othery;
- PlaceCave(otherx, othery, direction.Reverse(), s);
- AlignCavePositionsLeftToRight(direction, location3, location4);
+ location4.Pos = cave4pos;
+ PlaceCave(cave3pos, cave1dir, entranceTerrain);
+ PlaceCave(cave4pos, -cave1dir, entranceTerrain);
+ AlignCavePositionsLeftToRight(cave1dir, location3, location4);
}
}
- else
+ else // non-sane caves
{
- PlaceCave(x, y, direction, s);
+ IntVector2 dir = IntVector2.CARDINALS.Sample(RNG);
+ PlaceCave(location.Pos, dir, entranceTerrain);
}
return true;
}
@@ -1103,7 +942,7 @@ private bool MakeCaldera(bool canWalkOnWaterWithBoots)
///
/// Updates the visitation matrix and location reachability
///
- public override void UpdateVisit(List requireables)
+ public override void UpdateVisit(IReadOnlySet requireables)
{
UpdateReachable(requireables);
diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs
index 5a8b46a66..deba37f19 100644
--- a/RandomizerCore/Overworld/EastHyrule.cs
+++ b/RandomizerCore/Overworld/EastHyrule.cs
@@ -363,7 +363,7 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains),
hiddenKasutoLocation = townAtNewKasuto;
//Climate filtering
- climate = Climates.Create(props.EastClimate);
+ climate = Climates.Create(continentId, props.EastClimate);
climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit());
climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER);
//climate.DisallowTerrain(Terrain.LAVA);
@@ -406,7 +406,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
{
Debug.Assert(MapRows == 75);
Debug.Assert(MapColumns == 64);
- map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns);
+ map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns));
if (biome == Biome.VANILLA_SHUFFLE)
{
@@ -426,7 +426,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
foreach (Location location in AllLocations)
{
// section uses tuples with the Y+30 offset
- areasByLocation[section[location.CoordsY30Offset]].Add(GetLocationByPos(location.Pos)!);
+ areasByLocation[section[location.CoordsY30Offset]].Add(GetLocationAt(location.Pos)!);
}
ChooseConn("kasuto", connections, true);
@@ -511,7 +511,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
};
}
- map = new Terrain[MapRows, MapColumns];
+ map = new OverworldMap(MapRows, MapColumns);
for (int i = 0; i < MapRows; i++)
{
@@ -894,7 +894,11 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
public bool MakeValleyOfDeath()
{
- //DM passthrough locations
+ bool isCanyon = biome is Biome.CANYON or Biome.DRY_CANYON;
+ bool isCalderaLike = biome is Biome.CANYON or Biome.DRY_CANYON or Biome.VOLCANO;
+ bool horizontalPath = isHorizontal ^ isCanyon;
+
+ // VoD passthrough locations
List passthroughLocations = [
GetLocation(LocationID.EAST_TRAP_LAVA1),
GetLocation(LocationID.EAST_TRAP_LAVA2),
@@ -903,7 +907,7 @@ public bool MakeValleyOfDeath()
//Clean them up in case of data leakage (this is not hypothetical)
passthroughLocations.ForEach(i => i.YRaw = 0);
- //Pick a spot for the center of the GP hole
+ // Pick a spot for the center of the GP hole
int xmin, xmax, ymin, ymax;
if (biome == Biome.VOLCANO)
{
@@ -921,136 +925,61 @@ public bool MakeValleyOfDeath()
xmax = MapColumns - 6;
ymax = MapColumns - 6;
}
- int palacex = RNG.Next(xmin, xmax);
- int palacey = RNG.Next(ymin, ymax);
+ IntVector2 palacePos = new(RNG.Next(xmin, xmax), RNG.Next(ymin, ymax));
- //Ensure there is enough unallocated space to draw the whole opening
- //Why does this happen only for caldera-shaped biomes?
- if (biome == Biome.VOLCANO || biome == Biome.CANYON || biome == Biome.DRY_CANYON)
+ // Ensure there is enough unallocated space to draw the whole opening
+ if (isCalderaLike)
{
int tries = 0;
- bool placeable;
+ var offsets =
+ Enumerable.Range(-4, 9)
+ .SelectMany(dy => Enumerable.Range(-4, 9)
+ .Select(dx => new IntVector2(dx, dy)));
+
+ bool allMountains;
do
{
- palacex = RNG.Next(xmin, xmax);
- palacey = RNG.Next(ymin, ymax);
- placeable = true;
- for (int i = palacey - 4; i < palacey + 5; i++)
- {
- for (int j = palacex - 4; j < palacex + 5; j++)
- {
- if (map[i, j] != Terrain.MOUNTAIN)
- {
- placeable = false;
- break; // end inner for
- }
- if (!placeable) {
- break; // end outer for
- }
- }
- }
- if (++tries == 1000)
- {
- return false;
- }
- } while (!placeable);
+ if (tries++ == 1000) { return false; }
+ palacePos = IntVector2.Random(RNG, xmin, xmax, ymin, ymax);
+ allMountains = offsets.All(offset => map[palacePos + offset] == Terrain.MOUNTAIN);
+ } while (!allMountains);
}
- //Actually draw the center of the GP pocket
- for (int i = 0; i < 7; i++)
+ // VoD center
+ for (int y = -3; y <= 3; y++)
{
- for (int j = 0; j < 7; j++)
+ for (int x = -3; x <= 3; x++)
{
- if (!((i == 0 && j == 0) || (i == 0 && j == 6) || (i == 6 && j == 0) || (i == 6 && j == 6) || (i == 3 && j == 3)))
- {
- map[palacey - 3 + i, palacex - 3 + j] = Terrain.LAVA;
- }
- else
- {
- map[palacey - 3 + i, palacex - 3 + j] = Terrain.MOUNTAIN;
- }
- if (i == 0)
- {
- map[palacey - 4, palacex - 3 + j] = Terrain.MOUNTAIN;
- }
- if (i == 6)
- {
- map[palacey + 4, palacex - 3 + j] = Terrain.MOUNTAIN;
- }
- if (j == 0)
- {
- map[palacey - 3 + i, palacex - 4] = Terrain.MOUNTAIN;
- }
- if (j == 6)
- {
- map[palacey - 3 + i, palacex + 4] = Terrain.MOUNTAIN;
- }
+ map[palacePos + new IntVector2(x, y)] = Terrain.LAVA;
}
}
- map[palacey, palacex] = Terrain.PALACE;
- locationAtGP.Xpos = palacex;
- locationAtGP.Y = palacey;
- locationAtGP.CanShuffle = false;
-
- int length = 20;
- if (biome != Biome.CANYON && biome != Biome.DRY_CANYON && biome != Biome.VOLCANO)
+ List vodBorderPositions = [
+ .. Enumerable.Range(0, 9).Select(i => palacePos + new IntVector2(-4, -4) + i * IntVector2.EAST),
+ .. Enumerable.Range(1, 7).Select(i => palacePos + new IntVector2(4, -4) + i * IntVector2.SOUTH),
+ .. Enumerable.Range(0, 9).Select(i => palacePos + new IntVector2(-4, 4) + i * IntVector2.EAST),
+ .. Enumerable.Range(1, 7).Select(i => palacePos + new IntVector2(-4, -4) + i * IntVector2.SOUTH),
+ .. new IntVector2[] { new(-3, -3), new(3, -3), new(-3, 3), new(3, 3) }.Select(p => palacePos + p)
+ ];
+ foreach (var borderPos in vodBorderPositions)
{
- length = RNG.Next(5, 16);
+ map[borderPos] = Terrain.MOUNTAIN;
}
- int deltax = 1;
- int deltay = 0;
- int starty = palacey;
- int startx = palacex + 4;
+ map[palacePos] = Terrain.PALACE;
+ locationAtGP.Pos = palacePos;
+ locationAtGP.CanShuffle = false;
+ int length = isCalderaLike ? 20 : RNG.Next(5, 16);
- if (biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (palacex > MapColumns / 2)
- {
- deltax = -1;
- startx = palacex - 4;
- }
- if (!isHorizontal)
- {
- deltax = 0;
- deltay = 1;
- starty = palacey + 4;
- startx = palacex;
- if (palacey > MapRows / 2)
- {
- deltay = -1;
- starty = palacey - 4;
- }
- }
- }
- else
- {
- if (isHorizontal)
- {
- if (palacey < MapRows / 2)
- {
- deltay = 1;
- deltax = 0;
- starty = palacey + 4;
- startx = palacex;
- }
- else
- {
- deltay = -1;
- deltax = 0;
- starty = palacey - 4;
- startx = palacex;
- }
- }
- else
- {
- if (palacex > MapColumns / 2)
- {
- deltax = -1;
- startx = palacex - 4;
- }
- }
- }
+
+ // Initial delta direction.
+ // Non-canyon: horizontalPath uses X axis, !horizontalPath uses Y axis with >
+ // Canyon: horizontalPath uses X axis, !horizontalPath uses Y axis with >=
+ IntVector2 delta = horizontalPath
+ ? (palacePos.X > MapColumns / 2 ? IntVector2.WEST : IntVector2.EAST)
+ : ((isCanyon ? palacePos.Y >= MapRows / 2 : palacePos.Y > MapRows / 2) // TODO: verify that these can be the same
+ ? IntVector2.NORTH : IntVector2.SOUTH);
+
+ IntVector2 currentPos = palacePos + 4 * delta;
bool cavePlaced = false;
Location? vodcave1, vodcave2, vodcave3, vodcave4;
canyonShort = RNG.NextDouble() > .5;
@@ -1069,469 +998,297 @@ public bool MakeValleyOfDeath()
vodcave4 = GetLocation(LocationID.EAST_CAVE_VOD_PASSTHROUGH2_END);
}
- int forced = 0;
- int vodRoutes = RNG.Next(1, 3);
+ int traps = 0;
+ int vodRoutes = biome == Biome.VOLCANO ? RNG.Next(1, 3) : 1;
+ IntVector2 forwardDir = horizontalPath ? IntVector2.EAST : IntVector2.SOUTH;
+ IntVector2 sideDir = horizontalPath ? IntVector2.SOUTH : IntVector2.EAST;
- bool horizontalPath = isHorizontal ^ (biome == Biome.CANYON || biome == Biome.DRY_CANYON);
+ void TerraformVodCave(IntVector2 p, IntVector2 delta)
+ {
+ map[p] = Terrain.CAVE;
+ map[p + delta] = Terrain.MOUNTAIN;
+ map[p - sideDir] = Terrain.MOUNTAIN;
+ map[p + sideDir] = Terrain.MOUNTAIN;
+ }
- if (biome != Biome.VOLCANO)
+ void TerraformVodCavePair(IntVector2 first, IntVector2 second, IntVector2 delta)
{
- vodRoutes = 1;
+ if (WithinMapBounds(vodcave1.Pos))
+ {
+ map[vodcave1.Pos] = Terrain.MOUNTAIN;
+ }
+ if (WithinMapBounds(vodcave2.Pos))
+ {
+ map[vodcave2.Pos] = Terrain.MOUNTAIN;
+ }
+ TerraformVodCave(first, delta);
+ vodcave1.Pos = first;
+ vodcave1.CanShuffle = false;
+ TerraformVodCave(second, -delta);
+ vodcave2.Pos = second;
+ vodcave2.CanShuffle = false;
}
- for (int k = 0; k < vodRoutes; k++)
+
+ // Rolls a zig-zag offset that stays within map bounds (1-tile margin).
+ int RollAdjust(IntVector2 pos, IntVector2 d, int minA, int maxA)
+ {
+ int adj;
+ do
+ {
+ adj = RNG.Next(minA, maxA);
+ }
+ while (!WithinMapBounds(pos + adj * d, 1));
+ return adj;
+ }
+
+ // Draws a perpendicular lava segment (when zig-zagging), bordered by mountains.
+ // Returns false if a non-shuffleable location blocks the segment.
+ bool DrawPerpendicularLavaSegment(IntVector2 movement)
{
- int forcedPlaced = 3;
- if (vodRoutes == 2)
+ IntVector2 step = movement.Normalize();
+ int steps = movement.ManhattanLength;
+
+ if (!isCalderaLike)
{
- if (k == 0)
+ map[currentPos - step] = Terrain.MOUNTAIN;
+ }
+
+ for (int i = 0; i <= steps; i++)
+ {
+ IntVector2 pos = currentPos + i * step;
+ if (!WithinMapBounds(pos)) { break; }
+
+ map[pos] = Terrain.LAVA;
+
+ if (!isCalderaLike)
{
- forcedPlaced = 2;
+ foreach (IntVector2 wallPos in new[] { pos - forwardDir, pos + forwardDir })
+ {
+ var terrain = map[wallPos];
+ if (terrain != Terrain.LAVA && terrain != Terrain.CAVE)
+ {
+ map[wallPos] = Terrain.MOUNTAIN;
+ }
+ }
}
- else
+
+ if (GetLocationAt(pos) is Location loc && !loc.CanShuffle) { return false; }
+ }
+
+ if (!isCalderaLike)
+ {
+ var capPos = currentPos + (steps + 1) * step;
+ if (WithinMapBounds(capPos))
{
- forcedPlaced = 1;
+ map[capPos] = Terrain.MOUNTAIN;
}
}
- int minadjust = -1;
- int maxadjust = 2;
- int c = 0;
- while (startx > 1
- && startx < MapColumns - 1
- && starty > 1
- && starty < MapRows - 1
- && (((biome == Biome.VOLCANO || biome == Biome.CANYON || biome == Biome.DRY_CANYON) && map[starty, startx] == Terrain.MOUNTAIN) || (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON && c < length)))
+
+ return true;
+ }
+
+ // tries to place a enemy trap
+ bool TryPlaceTrap(IntVector2 trapPos, ref int trapsToPlaceRemaining, ref int trapIndex)
+ {
+ var trap = passthroughLocations[trapIndex];
+ if (ValidTrapTilePosition(trapPos) != null)
+ {
+ trap.Pos = trapPos;
+ trap.CanShuffle = false;
+ trapsToPlaceRemaining--;
+ trapIndex++;
+ return true;
+ }
+ return false;
+ }
+
+ // given a successful trap placement, narrows the zig-zag range to steer away.
+ // Returns null when adjust == 0 (no range change needed).
+ (int, int)? AdjustRangeAfterTrap(int adjust)
+ {
+ return adjust switch
+ {
+ > 0 => (0, 4),
+ < 0 => (-3, 1),
+ _ => null
+ };
+ }
+
+ for (int k = 0; k < vodRoutes; k++)
+ {
+ int trapsPlaced = vodRoutes == 2 ? (k == 0 ? 2 : 1) : 3;
+ int minAdjust = -1;
+ int maxAdjust = 2;
+ int tilesPlaced = 0;
+
+ while (WithinMapBounds(currentPos, 1))
{
- c++;
- map[starty, startx] = Terrain.LAVA;
- int adjust = RNG.Next(minadjust, maxadjust);
- while ((deltax != 0 && (starty + adjust < 1 || starty + adjust > MapRows - 2)) || (deltay != 0 && (startx + adjust < 1 || startx + adjust > MapColumns - 2)))
+ if (isCalderaLike)
{
- adjust = RNG.Next(minadjust, maxadjust);
+ if (map[currentPos] != Terrain.MOUNTAIN) { break; }
}
- if (adjust > 0)
+ else
{
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (deltax != 0)
- {
- map[starty - 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx - 1] = Terrain.MOUNTAIN;
- }
- }
- for (int i = 0; i <= adjust; i++)
- {
- if (horizontalPath)
- {
- map[starty + i, startx] = Terrain.LAVA;
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (map[starty + i, startx - 1] != Terrain.LAVA && map[starty + i, startx - 1] != Terrain.CAVE)
- {
- map[starty + i, startx - 1] = Terrain.MOUNTAIN;
- }
- if (map[starty + i, startx + 1] != Terrain.LAVA && map[starty + i, startx + 1] != Terrain.CAVE)
- {
- map[starty + i, startx + 1] = Terrain.MOUNTAIN;
- }
- }
- Location? location = GetLocationByCoordsNoOffset((starty + i, startx));
- if (location != null && !location.CanShuffle)
- {
- return false;
- }
- }
- else
- {
- map[starty, startx + i] = Terrain.LAVA;
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (map[starty - 1, startx + i] != Terrain.LAVA && map[starty - 1, startx + i] != Terrain.CAVE)
- {
- map[starty - 1, startx + i] = Terrain.MOUNTAIN;
- }
- if (map[starty + 1, startx + i] != Terrain.LAVA && map[starty + 1, startx + i] != Terrain.CAVE)
- {
- map[starty + 1, startx + i] = Terrain.MOUNTAIN;
- }
- }
- Location? location = GetLocationByCoordsNoOffset((starty, startx + i));
- if (location != null && !location.CanShuffle)
- {
- return false;
- }
- }
- }
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (deltax != 0)
- {
- map[starty + adjust + 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx + adjust + 1] = Terrain.MOUNTAIN;
- }
- }
+ if (tilesPlaced >= length) { break; }
}
- else if (adjust < 0)
- {
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (deltax != 0)
- {
- map[starty + 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx + 1] = Terrain.MOUNTAIN;
- }
- }
- if (horizontalPath)
- {
- for (int i = 0; i <= Math.Abs(adjust); i++)
- {
- map[starty - i, startx] = Terrain.LAVA;
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (map[starty - i, startx - 1] != Terrain.LAVA && map[starty - i, startx - 1] != Terrain.CAVE)
- {
- map[starty - i, startx - 1] = Terrain.MOUNTAIN;
- }
- if (map[starty - i, startx + 1] != Terrain.LAVA && map[starty - i, startx + 1] != Terrain.CAVE)
- {
- map[starty - i, startx + 1] = Terrain.MOUNTAIN;
- }
- }
- Location? l = GetLocationByCoordsNoOffset((starty - i, startx));
- if (l != null && !l.CanShuffle)
- {
- return false;
- }
- }
- }
- else
- {
- for (int i = 0; i <= Math.Abs(adjust); i++)
- {
- map[starty, startx - i] = Terrain.LAVA;
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- if (map[starty - 1, startx - i] != Terrain.LAVA && map[starty - 1, startx - i] != Terrain.CAVE)
- {
- map[starty - 1, startx - i] = Terrain.MOUNTAIN;
- }
- if (map[starty + 1, startx - i] != Terrain.LAVA && map[starty + 1, startx - i] != Terrain.CAVE)
- {
- map[starty + 1, startx - i] = Terrain.MOUNTAIN;
- }
- }
- Location? l = GetLocationByCoordsNoOffset((starty, startx - i));
- if (l != null && !l.CanShuffle)
- {
- return false;
- }
- }
- }
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
+ tilesPlaced++;
+ map[currentPos] = Terrain.LAVA;
+
+ // roll potential zig-zag offset
+ int adjust = RollAdjust(currentPos, delta, minAdjust, maxAdjust);
+
+ if (adjust != 0)
+ {
+ IntVector2 movement = adjust * sideDir;
+ if (!DrawPerpendicularLavaSegment(movement))
{
- if (deltax != 0)
- {
- map[starty + adjust - 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx + adjust - 1] = Terrain.MOUNTAIN;
- }
+ return false;
}
}
- else
+ else // moving straight forward
{
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
+ if (map[currentPos] != Terrain.CAVE)
{
- if (deltax != 0)
+ if (!isCalderaLike)
{
- map[starty - 1, startx] = Terrain.MOUNTAIN;
- map[starty + 1, startx] = Terrain.MOUNTAIN;
+ map[currentPos - sideDir] = Terrain.MOUNTAIN;
+ map[currentPos + sideDir] = Terrain.MOUNTAIN;
}
- else
- {
- map[starty, startx - 1] = Terrain.MOUNTAIN;
- map[starty, startx + 1] = Terrain.MOUNTAIN;
- }
- }
- if (map[starty, startx] != Terrain.CAVE)
- {
- map[starty, startx] = Terrain.LAVA;
- if (GetLocationByCoordsNoOffset((starty + deltay, startx + deltax)) != null)
+ map[currentPos] = Terrain.LAVA;
+ if (GetLocationAt(currentPos + delta) != null)
{
return false;
}
}
}
- if (horizontalPath)
- {
- starty += adjust;
- }
- else
- {
- startx += adjust;
- }
- if (((cavePlaced && adjust == 0) || adjust > 1 || adjust < -1) && forcedPlaced > 0)
+ currentPos += adjust * sideDir;
+
+ // try to place trap locations
+ bool shouldPlaceTrap = (cavePlaced && adjust == 0) || Math.Abs(adjust) > 1;
+ if (shouldPlaceTrap && trapsPlaced > 0)
{
- Location f = GetLocation(LocationID.EAST_TRAP_LAVA1)!;
- if (forced == 1)
+ var rotate = isCanyon ^ isHorizontal;
+ IntVector2 trapDir = rotate ? -sideDir : -forwardDir;
+ if (adjust < 0)
{
- f = GetLocation(LocationID.EAST_TRAP_LAVA2)!;
+ // not sure why negative adjusts would not rotate like positive adjustment,
+ // but it was the old behavior. (probably always fails placement?)
+ trapDir = sideDir;
}
- else if (forced == 2)
+ //new behavior: if multiple steps along the zig-zag path would work as trap tiles, pick one at random
+ var trapStep = adjust switch
{
- f = GetLocation(LocationID.EAST_TRAP_LAVA3)!;
- }
-
- if (adjust == 0)
- {
- if (ValidTrapTilePosition(new IntVector2(startx, starty)) != null)
- {
- f.Xpos = startx;
- f.Y = starty;
- f.CanShuffle = false;
- forcedPlaced--;
- forced++;
- }
- }
- else if (adjust > 0)
+ 0 => 0,
+ >3 => RNG.Next(adjust - 3) + 1,
+ _ => 1,
+ };
+ IntVector2 trapTilePos = currentPos + trapStep * trapDir;
+ if (TryPlaceTrap(trapTilePos, ref trapsPlaced, ref traps))
{
- bool isCanyon = biome == Biome.CANYON || biome == Biome.DRY_CANYON;
- if (isHorizontal != isCanyon) // exactly one of isHorizontal or isCanyon is true
+ if (AdjustRangeAfterTrap(adjust) is ValueTuple newRange)
{
- if (ValidTrapTilePosition(new IntVector2(startx, starty - 1)) != null)
- {
- f.Xpos = startx;
- f.Y = starty - 1;
- f.CanShuffle = false;
- forcedPlaced--;
- forced++;
- }
+ (minAdjust, maxAdjust) = newRange;
}
- else
- {
- if (ValidTrapTilePosition(new IntVector2(startx - 1, starty)) != null)
- {
- f.Xpos = startx - 1;
- f.Y = starty;
- f.CanShuffle = false;
- forcedPlaced--;
- forced++;
- }
- }
- minadjust = 0;
- maxadjust = 4;
}
- else if (adjust < 0)
- {
- if (horizontalPath)
- {
- if (ValidTrapTilePosition(new IntVector2(startx, starty + 1)) != null)
- {
- f.Xpos = startx;
- f.Y = starty + 1;
- f.CanShuffle = false;
- forcedPlaced--;
- forced++;
- }
- }
- else
- {
- if (ValidTrapTilePosition(new IntVector2(startx + 1, starty)) != null)
- {
- f.Xpos = startx + 1;
- f.Y = starty;
- f.CanShuffle = false;
- forcedPlaced--;
- forced++;
- }
- }
- minadjust = -3;
- maxadjust = 1;
- }
-
}
else if (adjust == 0 && !cavePlaced)
{
+ // place cave pair, then set pos to cave exit position
if (k != 0)
{
vodcave1 = vodcave3;
vodcave2 = vodcave4;
}
- if (vodcave1.Y < MapRows && vodcave1.Xpos < MapColumns)
- {
- map[vodcave1.Y, vodcave1.Xpos] = Terrain.MOUNTAIN;
- }
- map[starty, startx] = Terrain.CAVE;
- map[starty + deltay, startx + deltax] = Terrain.MOUNTAIN;
- if (deltax != 0)
- {
- map[starty + 1, startx] = Terrain.MOUNTAIN;
- map[starty - 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx + 1] = Terrain.MOUNTAIN;
- map[starty, startx - 1] = Terrain.MOUNTAIN;
- }
- vodcave1.Xpos = startx;
- vodcave1.Y = starty;
if (RNG.NextDouble() > .5 && vodRoutes != 2 && biome == Biome.VOLCANO)
{
- if (isHorizontal)
- {
- deltax = -deltax;
- }
- else
- {
- deltay = -deltay;
- }
+ delta = -delta;
}
+ int caveOffset;
if (horizontalPath)
{
- if (starty > MapRows / 2)
- {
- starty += RNG.Next(-9, -4);
- }
- else
- {
- starty += RNG.Next(5, 10);
- }
+ caveOffset = currentPos.Y > MapRows / 2 ? RNG.Next(-9, -4) : RNG.Next(5, 10);
}
else
{
- if (startx > MapColumns / 2)
- {
- startx += RNG.Next(-9, -4);
- }
- else
- {
- startx += RNG.Next(5, 10);
- }
+ caveOffset = currentPos.X > MapColumns / 2 ? RNG.Next(-9, -4) : RNG.Next(5, 10);
}
- if (map[starty, startx] != Terrain.MOUNTAIN && (biome == Biome.VOLCANO || biome == Biome.CANYON || biome == Biome.DRY_CANYON))
+ IntVector2 cave2Pos = currentPos + caveOffset * sideDir;
+
+ if (isCalderaLike && map[cave2Pos] != Terrain.MOUNTAIN)
{
return false;
}
- if (vodcave2.Y < MapRows && vodcave2.Xpos < MapColumns)
- {
- map[vodcave2.Y, vodcave2.Xpos] = Terrain.MOUNTAIN;
- }
- map[starty - deltay, startx - deltax] = Terrain.MOUNTAIN;
- map[starty, startx] = Terrain.CAVE;
- if (deltax != 0)
- {
- map[starty + 1, startx] = Terrain.MOUNTAIN;
- map[starty - 1, startx] = Terrain.MOUNTAIN;
- }
- else
- {
- map[starty, startx + 1] = Terrain.MOUNTAIN;
- map[starty, startx - 1] = Terrain.MOUNTAIN;
- }
- vodcave2.Xpos = startx;
- vodcave2.Y = starty;
+
+ TerraformVodCavePair(currentPos, cave2Pos, delta);
+ currentPos = cave2Pos;
cavePlaced = true;
- vodcave1.CanShuffle = false;
- vodcave2.CanShuffle = false;
- //startx += deltax;
}
else
{
- minadjust = -3;
- maxadjust = 4;
+ (minAdjust, maxAdjust) = (-3, 4);
}
- if (horizontalPath)
- {
- if (GetLocationByCoordsNoOffset((starty, startx + deltax)) != null)
- {
- map[starty, startx] = Terrain.MOUNTAIN;
- startx -= deltax;
- }
- else
- {
- startx += deltax;
- }
- }
- else
- {
- if (GetLocationByCoordsNoOffset((starty + deltay, startx)) != null)
- {
- map[starty, startx] = Terrain.MOUNTAIN;
- starty -= deltay;
- }
- else
- {
- starty += deltay;
- }
- }
-
- }
- if (biome != Biome.VOLCANO && biome != Biome.CANYON && biome != Biome.DRY_CANYON)
- {
- map[starty, startx] = Terrain.LAVA;
- if (deltax != 0)
+ // advance forward (or retreat if an existing location blocks the path)
+ if (GetLocationAt(currentPos + delta) != null)
{
- map[starty + 1, startx] = Terrain.LAVA;
- map[starty - 1, startx] = Terrain.LAVA;
- map[starty + 1, startx + deltax] = Terrain.LAVA;
- map[starty - 1, startx + deltax] = Terrain.LAVA;
- map[starty, startx + deltax] = Terrain.LAVA;
+ map[currentPos] = Terrain.MOUNTAIN;
+ currentPos -= delta;
}
else
{
- map[starty, startx + 1] = Terrain.LAVA;
- map[starty, startx - 1] = Terrain.LAVA;
- map[starty + deltay, startx + 1] = Terrain.LAVA;
- map[starty + deltay, startx - 1] = Terrain.LAVA;
- map[starty + deltay, startx] = Terrain.LAVA;
-
+ currentPos += delta;
}
}
- if (horizontalPath)
- {
- if (deltax < 0)
- {
- startx = palacex + 4;
- starty = palacey;
- }
- else
- {
- startx = palacex - 4;
- starty = palacey;
- }
- }
- else
+ // finally, open up VoD entrance
+ if (!isCalderaLike)
{
- if (deltay < 0)
- {
- startx = palacex;
- starty = palacey + 4;
- }
- else
- {
- startx = palacex;
- starty = palacey - 4;
+ // since this code is changing the terrain *after* the
+ // trap tiles have been validated, we must check again
+ // to avoid adding unwanted entrances to trap tiles
+ List newLavaTilePositions = [
+ currentPos,
+ currentPos + sideDir,
+ currentPos - sideDir,
+ currentPos + forwardDir,
+ currentPos + forwardDir + sideDir,
+ currentPos + forwardDir - sideDir
+ ];
+ foreach (var pos in newLavaTilePositions)
+ {
+ if (!WithinMapBounds(pos)) { continue; }
+ bool illegalTrapTileEntrance = false;
+ foreach (var loc in LocationsOrthogonalTo(pos))
+ {
+ var posBehind = pos + 2 * (loc.Pos - pos);
+ if (!WithinMapBounds(posBehind))
+ {
+ illegalTrapTileEntrance = true;
+ break;
+ }
+ var terrainBehind = map[posBehind];
+ if (!terrainBehind.IsWalkable())
+ {
+ illegalTrapTileEntrance = true;
+ break;
+ }
+ }
+ if (!illegalTrapTileEntrance)
+ {
+ map[pos] = Terrain.LAVA;
+ }
}
}
- deltax = -deltax;
- deltay = -deltay;
- minadjust = -1;
- maxadjust = 2;
+
+ // set to opposite side of palace for 2nd VoD entrance
+ currentPos = palacePos - 4 * delta;
+ delta = -delta;
+ minAdjust = -1;
+ maxAdjust = 2;
cavePlaced = false;
}
@@ -1669,28 +1426,6 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut
}
}
- public override void UpdateVisit(List requireables)
- {
- UpdateReachable(requireables);
-
- foreach (Location location in AllLocations)
- {
- if (location.Y > 0 && visitation[location.Y, location.Xpos])
- {
- if(location.AccessRequirements.AreSatisfiedBy(requireables))
- {
- location.Reachable = true;
- if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables))
- {
- Location connectedLocation = connections[location];
- connectedLocation.Reachable = true;
- visitation[connectedLocation.Y, connectedLocation.Xpos] = true;
- }
- }
- }
- }
- }
-
private double ComputeDistance(Location l, Location l2)
{
return Math.Sqrt(Math.Pow(l.Xpos - l2.Xpos, 2) + Math.Pow(l.Y - l2.Y, 2));
@@ -1926,6 +1661,15 @@ private void DrawMountains(bool useRiverDevil)
}
}
+ protected override bool IsReserved(IntVector2 pos)
+ {
+ if ((locationAtGP.Pos - pos).Abs().MinComponent() < 4)
+ {
+ return true;
+ }
+ return false;
+ }
+
protected override List GetPathingStarts()
{
/*return new List
diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs
index a55458a1d..8daa34803 100644
--- a/RandomizerCore/Overworld/MazeIsland.cs
+++ b/RandomizerCore/Overworld/MazeIsland.cs
@@ -120,7 +120,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
{
Debug.Assert(MapRows == 75);
Debug.Assert(MapColumns == 64);
- map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns);
+ map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns));
if (biome == Biome.VANILLA_SHUFFLE)
{
ShuffleLocations(AllLocations);
@@ -145,543 +145,240 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
}
while (bytesWritten > MAP_SIZE_BYTES)
{
-
- map = new Terrain[MapRows, 64];
+ map = new OverworldMap(MapRows, 64);
bool[,] visited = new bool[MapRows, MapColumns];
+ List placedLocations = new();
- for (int i = 0; i < MapColumns; i++)
+ // create walkable water border
+ for (int x = 0; x < MapColumns; x++)
{
- map[0, i] = Terrain.WALKABLEWATER;
- map[MapRows - 1, i] = Terrain.WALKABLEWATER;
- visited[MapRows - 1, i] = true;
-
- visited[0, i] = true;
-
+ IntVector2 top = new(x, 0);
+ map[top] = Terrain.WALKABLEWATER;
+ visited[top.Y, top.X] = true;
+ IntVector2 bottom = new(x, MapRows - 1);
+ map[bottom] = Terrain.WALKABLEWATER;
+ visited[bottom.Y, bottom.X] = true;
}
-
- for (int i = 0; i < MapRows; i++)
+ for (int y = 0; y < MapRows; y++)
{
- map[i, 0] = Terrain.WALKABLEWATER;
- visited[i, 0] = true;
- map[i, MapColumns - 1] = Terrain.WALKABLEWATER;
- visited[i, MapColumns - 1] = true;
+ IntVector2 left = new(0, y);
+ map[left] = Terrain.WALKABLEWATER;
+ visited[left.Y, left.X] = true;
+ IntVector2 right = new(MapColumns - 1, y);
+ map[right] = Terrain.WALKABLEWATER;
+ visited[right.Y, left.X] = true;
}
- for (int i = 1; i < MapRows - 1; i += 2)
- {
- for (int j = 1; j < MapColumns - 1; j++)
+ // fill non-walkable water to the right of the island water border
+ for (int y = 0; y < MapRows; y++)
+ {
+ for (int x = MapColumns; x < 64; x++)
{
- map[i, j] = Terrain.MOUNTAIN;
+ map[y, x] = Terrain.WATER;
}
}
- for (int i = 0; i < MapRows; i++)
+ // fill every other row with mountain
+ for (int y = 1; y < MapRows - 1; y += 2)
{
- for (int j = MapColumns; j < 64; j++)
+ for (int x = 1; x < MapColumns - 1; x++)
{
- map[i, j] = Terrain.WATER;
+ map[y, x] = Terrain.MOUNTAIN;
}
}
- for (int j = 1; j < MapColumns; j += 2)
+ // fill every other column with mountain
+ for (int x = 1; x < MapColumns; x += 2)
{
- for (int i = 1; i < MapRows - 1; i++)
+ for (int y = 1; y < MapRows - 1; y++)
{
- {
- map[i, j] = Terrain.MOUNTAIN;
+ map[y, x] = Terrain.MOUNTAIN;
}
}
- }
- for (int i = 1; i < MapRows; i++)
+ for (int y = 1; y < MapRows; y++)
{
- for (int j = 1; j < MapColumns; j++)
+ for (int x = 1; x < MapColumns; x++)
{
- if (map[i, j] != Terrain.MOUNTAIN && map[i, j] != Terrain.WATER && map[i, j] != Terrain.WALKABLEWATER)
+ if (map[y, x] != Terrain.MOUNTAIN && map[y, x] != Terrain.WATER && map[y, x] != Terrain.WALKABLEWATER)
{
- map[i, j] = Terrain.ROAD;
- visited[i, j] = false;
+ map[y, x] = Terrain.ROAD;
+ visited[y, x] = false;
}
else
{
- visited[i, j] = true;
+ visited[y, x] = true;
}
}
}
- //choose starting position
- int starty = RNG.Next(2, MapRows);
- if (starty == 0)
- {
- starty++;
- }
- else if (starty % 2 == 1)
+
+ //choose starting Y position
+ int starty = RNG.Next(2, MapRows - 1);
+ if (starty % 2 == 1)
{
starty--;
}
-
//generate maze
- int currx = 2;
- int curry = starty;
- Stack<(int, int)> stack = new();
+ IntVector2 current = new(2, starty);
+ Stack stack = new();
bool canPlaceCave = true;
while (MoreToVisit(visited))
{
- List<(int, int)> neighbors = GetListOfNeighbors(currx, curry, visited);
- if (neighbors.Count > 0)
+ var neighbors = GetPositionsTwoTilesAway(current, visited, RNG).ToArray();
+ RNG.Shuffle(neighbors);
+ if (neighbors.Length > 0)
{
canPlaceCave = true;
- (int, int)next = neighbors[RNG.Next(neighbors.Count)];
+ var next = neighbors[RNG.Next(neighbors.Length)];
stack.Push(next);
- if (next.Item1 > currx)
- {
- map[curry, currx + 1] = Terrain.ROAD;
- }
- else if (next.Item1 < currx)
- {
- map[curry, currx - 1] = Terrain.ROAD;
- }
- else if (next.Item2 > curry)
- {
- map[curry + 1, currx] = Terrain.ROAD;
+ var delta = (next - current) / 2; // neighbors are all 2 tiles away
+ map[current + delta] = Terrain.ROAD;
+ current = next;
+ visited[current.Y, current.X] = true;
}
- else
- {
- map[curry - 1, currx] = Terrain.ROAD;
- }
- currx = next.Item1;
- curry = next.Item2;
- visited[curry, currx] = true;
- }
else if (stack.Count > 0)
{
- if (cave1 != null && cave1.CanShuffle && GetLocationByCoordsNoOffset((curry, currx)) == null)
+ if (cave1 != null && cave1.CanShuffle && GetLocationAt(current) == null)
{
- map[curry, currx] = Terrain.CAVE;
- cave1.Y = curry;
- cave1.Xpos = currx;
+ map[current] = Terrain.CAVE;
+ cave1.Pos = current;
cave1.CanShuffle = false;
canPlaceCave = false;
- SealDeadEnd(curry, currx);
+ SealDeadEnd(current, RNG);
+ placedLocations.Add(cave1);
}
- else if (cave2 != null && cave2.CanShuffle && GetLocationByCoordsNoOffset((curry, currx)) == null && canPlaceCave)
+ else if (cave2 != null && cave2.CanShuffle && GetLocationAt(current) == null && canPlaceCave)
{
- map[curry, currx] = Terrain.CAVE;
- cave2.Y = curry;
- cave2.Xpos = currx;
+ map[current] = Terrain.CAVE;
+ cave2.Pos = current;
cave2.CanShuffle = false;
- SealDeadEnd(curry, currx);
-
+ SealDeadEnd(current, RNG);
+ placedLocations.Add(cave2);
}
- (int, int)n2 = stack.Pop();
- currx = n2.Item1;
- curry = n2.Item2;
+ current = stack.Pop();
}
}
//place palace 4
-
bool canPlace = false;
-
- int palace4x = RNG.Next(15) + 3;
- int palace4y = RNG.Next(MapRows - 6) + 3;
- while (!canPlace)
+ IntVector2 palace4Pos;
+ do
{
- palace4x = RNG.Next(15) + 3;
- palace4y = RNG.Next(MapRows - 6) + 3;
+ palace4Pos = IntVector2.Random(RNG, 3, MapColumns - 4, 3, MapRows - 4);
+ if (palace4Pos.X % 2 == 0) { palace4Pos += IntVector2.EAST; }
+ if (palace4Pos.Y % 2 == 0) { palace4Pos += IntVector2.SOUTH; }
canPlace = true;
- if (map[palace4y, palace4x] != Terrain.ROAD)
+ if (LocationsIn3x3Area(palace4Pos).Any())
{
canPlace = false;
}
-
- for (int i = -1; i < 2; i++)
+ } while (!canPlace);
+ locationAtPalace4.Pos = palace4Pos;
+ map[palace4Pos] = Terrain.PALACE;
+ foreach (var dir in IntVector2.DIRECTIONS)
{
- for (int j = -1; j < 2; j++)
- {
- if (GetLocationByCoordsNoOffset((palace4y + i, palace4x + j)) != null)
- {
- canPlace = false;
+ map[palace4Pos + dir] = Terrain.ROAD;
}
- }
- }
- }
- locationAtPalace4.Xpos = palace4x;
- locationAtPalace4.Y = palace4y;
- map[palace4y, palace4x] = Terrain.PALACE;
- map[palace4y + 1, palace4x] = Terrain.ROAD;
- map[palace4y - 1, palace4x] = Terrain.ROAD;
- map[palace4y, palace4x + 1] = Terrain.ROAD;
- map[palace4y, palace4x - 1] = Terrain.ROAD;
- map[palace4y + 1, palace4x + 1] = Terrain.ROAD;
- map[palace4y - 1, palace4x - 1] = Terrain.ROAD;
- map[palace4y - 1, palace4x + 1] = Terrain.ROAD;
- map[palace4y + 1, palace4x - 1] = Terrain.ROAD;
+ placedLocations.Add(locationAtPalace4);
//draw a river
- int riverStartY;
- do
- {
- riverStartY = RNG.Next((MapRows - 5) / 2) * 2 + 3;
- }
- while (riverStartY == starty);
-
- int riverEndY = RNG.Next((MapRows - 5) / 2) * 2 + 3;
bool openWest, openEast;
do
{
openWest = RNG.Next(2) == 1;
openEast = RNG.Next(2) == 1;
} while (!openWest && !openEast);
- int riverEndX = Math.Min(MapColumns - 2, 21);
- Debug.Assert(riverEndX % 2 == 1); // even number loops forever
- Debug.Assert(riverEndY % 2 == 1);
- DrawRiver(riverStartY, 1, riverEndY, riverEndX, openWest, openEast);
- //Place raft
- Direction raftDirection = Direction.EAST;
- if (raft != null)
+ int riverEndX = MapColumns - 2;
+ int riverPivotX, riverStartY, riverEndY;
+ do
{
+ riverStartY = RNG.Next((MapRows - 5) / 2) * 2 + 3;
+ } while (riverStartY == starty || Math.Abs(palace4Pos.Y - riverStartY) < 2);
- raftDirection = (Direction)RNG.Next(4);
-
- int raftX = 0;
- int raftY = 0;
- if (raftDirection == Direction.NORTH)
+ do
{
+ riverEndY = RNG.Next((MapRows - 5) / 2) * 2 + 3;
+ } while (Math.Abs(riverStartY - riverEndY) < 2 || Math.Abs(palace4Pos.Y - riverEndY) < 2);
- raftX = RNG.Next(2, MapColumns - 1);
- while (raftY != 2)
- {
- raftX = RNG.Next(2, MapColumns - 1);
- raftY = 0;
- while (raftY < MapRows && map[raftY, raftX] != Terrain.ROAD)
- {
- raftY++;
- }
- }
- raftY--;
- map[raftY, raftX] = Terrain.BRIDGE;
- raft.Y = raftY;
- raft.Xpos = raftX;
- raftY--;
-
- while (raftY >= 0)
- {
- if (map[raftY, raftX] != Terrain.PALACE && map[raftY, raftX] != Terrain.CAVE)
- {
- map[raftY, raftX] = Terrain.WALKABLEWATER;
-
- }
- raftY--;
- }
- }
- else if (raftDirection == Direction.SOUTH)
- {
- raftY = MapRows - 1;
- raftX = RNG.Next(2, MapColumns - 1);
- while (raftY != MapRows - 3)
+ do
{
- raftY = MapRows - 1;
- raftX = RNG.Next(2, MapColumns - 1);
- while (raftY > 0 && map[raftY, raftX] != Terrain.ROAD)
- {
- raftY--;
- }
- }
- raftY++;
- map[raftY, raftX] = Terrain.BRIDGE;
- raft.Y = raftY;
- raft.Xpos = raftX;
- raftY++;
+ riverPivotX = RNG.Next(1, riverEndX / 2) * 2 + 1; //3,5,7,9,11,13,15,17,19
+ } while (Math.Abs(palace4Pos.X - riverPivotX) < 2);
- while (raftY < MapRows)
- {
- if (map[raftY, raftX] != Terrain.PALACE && map[raftY, raftX] != Terrain.CAVE)
- {
- map[raftY, raftX] = Terrain.WALKABLEWATER;
+ Debug.Assert(riverEndX % 2 == 1); // even number loops forever
+ Debug.Assert(riverEndY % 2 == 1);
+ DrawRiver(riverStartY, 1, riverEndY, riverEndX, riverPivotX, openWest, openEast);
- }
- raftY++;
- }
- }
- else if (raftDirection == Direction.WEST)
+ //Pick raft & bridge edges
+ Direction raftDirEnum = (Direction)RNG.Next(4);
+ Direction bridgeDirEnum;
+ do
{
- while (raftX != 2)
- {
- raftX = 0;
- raftY = RNG.Next(2, MapRows - 2);
- while (raftX < MapColumns && map[raftY, raftX] != Terrain.ROAD)
- {
- raftX++;
- }
- }
-
- raftX--;
- map[raftY, raftX] = Terrain.BRIDGE;
- raft.Y = raftY;
- raft.Xpos = raftX;
- raftX--;
+ bridgeDirEnum = (Direction)RNG.Next(4);
+ } while (bridgeDirEnum == raftDirEnum);
- while (raftX >= 0)
+ //Place raft
+ if (raft != null)
{
- if (map[raftY, raftX] != Terrain.PALACE && map[raftY, raftX] != Terrain.CAVE)
+ IntVector2 raftDirVec = raftDirEnum.ToIntVector2();
+ IntVector2 raftPos, nextToRaft;
+ do
{
- map[raftY, raftX] = Terrain.WALKABLEWATER;
-
- }
- raftX--;
- }
- }
- else
+ raftPos = raftDirEnum switch
{
- while (raftX != MapColumns - 3)
- {
- raftX = MapColumns - 1;
- raftY = RNG.Next(2, MapRows - 2);
- while (raftX > 0 && map[raftY, raftX] != Terrain.ROAD)
- {
- raftX--;
- }
- }
- raftX++;
- map[raftY, raftX] = Terrain.BRIDGE;
- raft.Y = raftY;
- raft.Xpos = raftX;
- raftX++;
- while (raftX < MapColumns)
- {
- if (map[raftY, raftX] != Terrain.PALACE && map[raftY, raftX] != Terrain.CAVE)
- {
- map[raftY, raftX] = Terrain.WALKABLEWATER;
-
+ Direction.NORTH => new IntVector2(RNG.Next(2, MapColumns - 2), 1),
+ Direction.SOUTH => new IntVector2(RNG.Next(2, MapColumns - 2), MapRows - 2),
+ Direction.WEST => new IntVector2(1, RNG.Next(2, MapRows - 2)),
+ Direction.EAST => new IntVector2(MapColumns - 2, RNG.Next(2, MapRows - 2)),
+ _ => throw new ArgumentException("Invalid direction: " + raftDirEnum)
+ };
+ nextToRaft = raftPos - raftDirVec;
+ } while (map[raftPos] is not Terrain.MOUNTAIN || map[nextToRaft] is not Terrain.ROAD);
+
+ map[raftPos] = Terrain.BRIDGE;
+ raft.Pos = raftPos;
}
- raftX++;
- }
- }
- }
//Place bridge
- Direction bridgeDirection;
- do
- {
- bridgeDirection = (Direction)RNG.Next(4);
- } while (bridgeDirection == raftDirection);
-
- //TODO: refactor this so it's not replicating the same code 4 times
if (bridge != null)
{
- int bridgeX = 0;
- int bridgeY = 0;
+ IntVector2 bridgeDirVec = bridgeDirEnum.ToIntVector2();
+ IntVector2 bridgePos, nextToBridge;
- if (bridgeDirection == Direction.NORTH)
+ do
{
- bridgeX = RNG.Next(2, MapColumns - 1);
- while (bridgeY < MapRows && map[bridgeY, bridgeX] != Terrain.ROAD)
- {
- //the bridge can't intersect another bridge, either because it is a mini-bridge
- //or because that bridge is actually the raft tile.
- if (map[bridgeY, bridgeX] == Terrain.BRIDGE)
- {
- return false;
- }
- bridgeY++;
- }
-
- //If the bridge spawns on the edge of the map, and it also corresponds to where the river is, it creates a
- //giant bridge across the entire map because it never finds road.
- if(bridgeY == MapRows)
- {
- return false;
- }
-
- bridgeY--;
-
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- bridge.Y = bridgeY;
- bridge.Xpos = bridgeX;
- while (bridgeY >= 0)
- {
- if (map[bridgeY, bridgeX] == Terrain.PALACE
- || map[bridgeY, bridgeX] == Terrain.CAVE
- || locationAtPalace4.Xpos == bridgeX && locationAtPalace4.Y == bridgeY
- || cave1 != null && cave1.Xpos == bridgeX && cave1.Y == bridgeY
- || cave2 != null && cave2.Xpos == bridgeX && cave2.Y == bridgeY)
- {
- return false;
- }
- else
- {
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- }
- bridgeY--;
- }
- }
- else if (bridgeDirection == Direction.SOUTH)
- {
- bridgeY = MapRows - 1;
- bridgeX = RNG.Next(2, MapColumns - 1);
- while (bridgeY > 0 && map[bridgeY, bridgeX] != Terrain.ROAD)
- {
- if (map[bridgeY, bridgeX] == Terrain.BRIDGE)
- {
- return false;
- }
- bridgeY--;
- }
-
- if (bridgeY == 0)
- {
- return false;
- }
-
- bridgeY++;
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- bridge.Y = bridgeY;
- bridge.Xpos = bridgeX;
-
- while (bridgeY < MapRows)
- {
- if (map[bridgeY, bridgeX] == Terrain.PALACE
- || map[bridgeY, bridgeX] == Terrain.CAVE
- || locationAtPalace4.Xpos == bridgeX && locationAtPalace4.Y == bridgeY
- || cave1 != null && cave1.Xpos == bridgeX && cave1.Y == bridgeY
- || cave2 != null && cave2.Xpos == bridgeX && cave2.Y == bridgeY)
- {
- return false;
- }
- else
- {
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- }
- bridgeY++;
- }
- }
- else if (bridgeDirection == Direction.WEST)
- {
- bridgeY = RNG.Next(2, MapRows - 2);
- while(bridgeY == riverEndY || bridgeY == riverStartY)
- {
- bridgeY = RNG.Next(2, MapRows - 2);
- }
- while (bridgeX < MapColumns && map[bridgeY, bridgeX] != Terrain.ROAD)
- {
- if (map[bridgeY, bridgeX] == Terrain.BRIDGE)
- {
- return false;
- }
- bridgeX++;
- }
- if (bridgeX == MapColumns)
- {
- return false;
- }
-
- bridgeX--;
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- bridge.Y = bridgeY;
- bridge.Xpos = bridgeX;
-
- while (bridgeX >= 0)
- {
- if (map[bridgeY, bridgeX] == Terrain.PALACE
- || map[bridgeY, bridgeX] == Terrain.CAVE
- || locationAtPalace4.Xpos == bridgeX && locationAtPalace4.Y == bridgeY
- || cave1 != null && cave1.Xpos == bridgeX && cave1.Y == bridgeY
- || cave2 != null && cave2.Xpos == bridgeX && cave2.Y == bridgeY)
- {
- return false;
- }
- else
- {
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- }
- bridgeX--;
- }
- }
- else
- {
- bridgeX = MapColumns + 3;
- bridgeY = RNG.Next(2, MapRows - 2);
- while (bridgeY == riverEndY || bridgeY == riverStartY)
- {
- if (map[bridgeY, bridgeX] == Terrain.BRIDGE)
- {
- return false;
- }
- bridgeY = RNG.Next(2, MapRows - 2);
- }
- while (bridgeX > 0 && map[bridgeY, bridgeX] != Terrain.ROAD)
- {
- bridgeX--;
- }
- if (bridgeX == 0)
- {
- return false;
- }
-
- bridgeX++;
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- bridge.Y = bridgeY;
- bridge.Xpos = bridgeX;
-
- while (bridgeX < MapColumns)
- {
- if (map[bridgeY, bridgeX] == Terrain.PALACE
- || map[bridgeY, bridgeX] == Terrain.CAVE
- || locationAtPalace4.Xpos == bridgeX && locationAtPalace4.Y == bridgeY
- || cave1 != null && cave1.Xpos == bridgeX && cave1.Y == bridgeY
- || cave2 != null && cave2.Xpos == bridgeX && cave2.Y == bridgeY)
- {
- return false;
- }
- else
- {
- map[bridgeY, bridgeX] = Terrain.BRIDGE;
- }
- bridgeX++;
- }
- }
+ bridgePos = bridgeDirEnum switch
+ {
+ Direction.NORTH => new IntVector2(RNG.Next(2, MapColumns - 2), 1),
+ Direction.SOUTH => new IntVector2(RNG.Next(2, MapColumns - 2), MapRows - 2),
+ Direction.WEST => new IntVector2(1, RNG.Next(2, MapRows - 2)),
+ Direction.EAST => new IntVector2(MapColumns - 2, RNG.Next(2, MapRows - 2)),
+ _ => throw new ArgumentException("Invalid direction: " + bridgeDirEnum)
+ };
+ nextToBridge = bridgePos - bridgeDirVec;
+ } while (map[bridgePos] is not Terrain.MOUNTAIN || map[nextToBridge] is not Terrain.ROAD);
+
+ IntVector2 waterByBridge = bridgePos + bridgeDirVec;
+ map[bridgePos] = Terrain.BRIDGE;
+ bridge.Pos = bridgePos;
+ map[waterByBridge] = Terrain.BRIDGE;
}
-
foreach (Location location in AllLocations)
{
if (location.TerrainType == Terrain.ROAD)
{
- int x = 0;
- int y = 0;
- if (location != magicContainerDrop && location != childDrop)
- {
- do
- {
- x = RNG.Next(19) + 2;
- y = RNG.Next(MapRows - 4) + 2;
- } while (map[y, x] != Terrain.ROAD
- || !((map[y, x + 1] == Terrain.MOUNTAIN && map[y, x - 1] == Terrain.MOUNTAIN)
- || (map[y + 1, x] == Terrain.MOUNTAIN && map[y - 1, x] == Terrain.MOUNTAIN))
- || GetLocationByCoordsNoOffset((y, x + 1)) != null
- || GetLocationByCoordsNoOffset((y, x - 1)) != null
- || GetLocationByCoordsNoOffset((y + 1, x)) != null
- || GetLocationByCoordsNoOffset((y - 1, x)) != null
- || GetLocationByCoordsNoOffset((y, x)) != null);
- }
- else
+ while (true)
{
- do
+ var pos = IntVector2.Random(RNG, 2, MapColumns - 2, 2, MapRows - 2);
+ if (ValidMazeDropPosition(pos))
{
- x = RNG.Next(19) + 2;
- y = RNG.Next(MapRows - 4) + 2;
- } while (map[y, x] != Terrain.ROAD
- || GetLocationByCoordsNoOffset((y, x + 1)) != null
- || GetLocationByCoordsNoOffset((y, x - 1)) != null
- || GetLocationByCoordsNoOffset((y + 1, x)) != null
- || GetLocationByCoordsNoOffset((y - 1, x)) != null
- || GetLocationByCoordsNoOffset((y, x)) != null);
+ location.Pos = pos;
+ break;
+ }
}
-
- location.Xpos = x;
- location.Y = y;
}
}
@@ -715,58 +412,37 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
return true;
}
- private void SealDeadEnd(int curry, int currx)
+ private void SealDeadEnd(IntVector2 pos, Random r)
{
+ var cardinalOrder = IntVector2.CARDINALS.ToArray();
+ r.Shuffle(cardinalOrder);
bool foundRoad = false;
- if(map[curry+1, currx] == Terrain.ROAD)
+ foreach (IntVector2 dir in cardinalOrder)
{
- foundRoad = true;
- }
+ IntVector2 neighbor = pos + dir;
- if(map[curry-1, currx] == Terrain.ROAD)
+ if (map[neighbor] != Terrain.ROAD)
{
- if(foundRoad)
- {
- map[curry - 1, currx] = Terrain.MOUNTAIN;
- }
- else
- {
- foundRoad = true;
+ continue;
}
- }
-
- if (map[curry, currx - 1] == Terrain.ROAD)
- {
if (foundRoad)
{
- map[curry, currx - 1] = Terrain.MOUNTAIN;
+ map[neighbor] = Terrain.MOUNTAIN;
}
else
{
foundRoad = true;
}
}
-
- if (map[curry, currx+1 ] == Terrain.ROAD)
- {
- if (foundRoad)
- {
- map[curry, currx+1] = Terrain.MOUNTAIN;
- }
- else
- {
- foundRoad = true;
}
- }
- }
private bool MoreToVisit(bool[,] v)
{
- for (int i = 0; i < v.GetLength(0); i++)
+ for (int y = 0; y < v.GetLength(0); y++)
{
- for (int j = 1; j < v.GetLength(1) - 1; j++)
+ for (int x = 1; x < v.GetLength(1) - 1; x++)
{
- if (v[i, j] == false)
+ if (v[y, x] == false)
{
return true;
}
@@ -775,129 +451,84 @@ private bool MoreToVisit(bool[,] v)
return false;
}
- private List<(int, int)> GetListOfNeighbors(int currx, int curry, bool[,] v)
+ private List GetPositionsTwoTilesAway(IntVector2 current, bool[,] visited, Random r)
{
- List<(int, int)> x = [];
+ List neighbors = [];
- if (currx - 2 > 1 && v[curry, currx - 2] == false)
+ foreach (IntVector2 dir in IntVector2.CARDINALS)
{
- x.Add((currx - 2, curry));
- }
-
- if (currx + 2 < MapColumns && v[curry, currx + 2] == false)
+ IntVector2 candidate = current + 2 * dir;
+ if (!WithinMapBounds(candidate, 2))
{
- x.Add((currx + 2, curry));
+ continue;
}
-
- if (curry - 2 > 1 && v[curry - 2, currx] == false)
+ if (!visited[candidate.Y, candidate.X])
{
- x.Add((currx, curry - 2));
+ neighbors.Add(candidate);
+ }
}
- if (curry + 2 < MapRows && v[curry + 2, currx] == false)
- {
- x.Add((currx, curry + 2));
+ return neighbors;
}
- return x;
- }
- private void DrawRiver(int fromY, int fromX, int toY, int toX, bool openWest, bool openEast)
+ private void DrawRiver(int fromY, int fromX, int toY, int toX, int xPivot, bool openWest, bool openEast)
{
- //3,5,7,9,11,13,15,17,19
- int xPivot = 1 + RNG.Next(fromX, toX / 2) * 2;
Terrain terrain = Terrain.WALKABLEWATER;
- int startX = fromX;
- int startY = fromY;
- while (fromX != toX)
- {
- if (fromX == 21 || (fromX != xPivot || fromY == toY) && fromX != toX)
- {
- int deltaX = toX - fromX;
- int move = (RNG.Next(Math.Abs(deltaX / 2)) + 1) * 2;
+ IntVector2 startPos = new(fromX, fromY);
+ IntVector2 pos = startPos;
+ IntVector2 endPos = new(toX, toY);
+ IntVector2 horizontalStep = endPos.X > pos.X ? IntVector2.EAST : IntVector2.WEST;
+ IntVector2 verticalStep = endPos.Y > pos.Y ? IntVector2.SOUTH : IntVector2.NORTH;
- while (Math.Abs(move) > 0 && !(fromX == xPivot && fromY != toY) && !(fromX == toX && fromY == toY))
+ while (pos.X != endPos.X)
{
- //Move 2 tiles at a time
- for (int i = 0; i < 2; i++)
+ if (pos.X == xPivot && pos.Y != endPos.Y)
{
- if ((fromX != toX || fromY != toY) && GetLocationByCoordsNoOffset((fromY, fromX)) == null)
- {
- if (map[fromY, fromX] == Terrain.MOUNTAIN)
- {
- map[fromY, fromX] = terrain;
- }
- else if (map[fromY, fromX] == Terrain.ROAD && ((deltaX > 0 && (map[fromY, fromX + 1] == Terrain.MOUNTAIN)) || (deltaX < 0 && map[fromY, fromX - 1] == Terrain.MOUNTAIN)))
- {
- map[fromY, fromX] = Terrain.BRIDGE;
+ PaintRiverTile(pos, terrain, horizontalStep);
+ pos += verticalStep;
}
- else if (map[fromY, fromX] != Terrain.PALACE && map[fromY, fromX] != Terrain.BRIDGE && map[fromY, fromX] != Terrain.CAVE)
+ else
{
- map[fromY, fromX] = terrain;
+ PaintRiverTile(pos, terrain, verticalStep);
+ pos += horizontalStep;
}
+ map[startPos] = openWest ? Terrain.WALKABLEWATER : Terrain.MOUNTAIN;
+ map[endPos] = openEast ? Terrain.WALKABLEWATER : Terrain.MOUNTAIN;
}
- if (deltaX > 0 && fromX < MapColumns)
- {
- fromX++;
-
- }
- else if (fromX > 0)
- {
- fromX--;
-
}
- move--;
- }
- }
- }
- else if (fromY != toY)
+ private void PaintRiverTile(IntVector2 pos, Terrain terrain, IntVector2 perpendicular)
{
- int diff = toY - fromY;
- int move = (RNG.Next(Math.Abs(diff / 2)) + 1) * 2;
- while (Math.Abs(move) > 0 && !(fromX == toX && fromY == toY))
+ if (GetLocationAt(pos) != null)
{
- for (int i = 0; i < 2; i++)
- {
- if ((fromX != toX || fromY != (toY)) && GetLocationByCoordsNoOffset((fromY, fromX)) == null)
- {
- if (map[fromY, fromX] == Terrain.MOUNTAIN)
- {
- map[fromY, fromX] = terrain;
+ return;
}
- else if(map[fromY, fromX] == Terrain.ROAD && ((diff > 0 && (map[fromY + 1, fromX] == Terrain.MOUNTAIN)) || (diff < 0 && map[fromY - 1, fromX] == Terrain.MOUNTAIN)))
+ Terrain current = map[pos];
+ if (current == Terrain.MOUNTAIN)
{
- map[fromY, fromX] = Terrain.BRIDGE;
+ map[pos] = terrain;
}
- else if (map[fromY, fromX] != Terrain.PALACE && map[fromY, fromX] != Terrain.BRIDGE && map[fromY, fromX] != Terrain.CAVE)
+ else if (current == Terrain.ROAD)
{
- map[fromY, fromX] = terrain;
- }
- }
- if (diff > 0 && fromY < MapRows - 1)
+ IntVector2 leftOf = pos - perpendicular;
+ IntVector2 rightOf = pos + perpendicular;
+ if (map[leftOf] != Terrain.MOUNTAIN && map[rightOf] != Terrain.MOUNTAIN)
{
- fromY++;
-
+ map[pos] = Terrain.BRIDGE;
}
- else if (fromY > 0)
+ else
{
- fromY--;
-
+ map[pos] = terrain;
}
- move--;
}
- }
- }
- else
+ else if (current != Terrain.PALACE && current != Terrain.BRIDGE && current != Terrain.CAVE)
{
- throw new ImpossibleException("Logic error drawing Maze Island River");
+ map[pos] = terrain;
}
- map[startY, startX] = openWest ? Terrain.WALKABLEWATER : Terrain.MOUNTAIN;
- map[toY, toX] = openEast ? Terrain.WALKABLEWATER : Terrain.MOUNTAIN;
}
- }
- public override void UpdateVisit(List requireables)
+ public override void UpdateVisit(IReadOnlySet requireables)
{
bool changed = true;
while (changed)
@@ -982,11 +613,6 @@ public override string GetName()
public override IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto)
{
- /*
- public Location childDrop;
- public Location magicContainerDrop;
- public Location locationAtPalace4;
- */
HashSet requiredLocations = new()
{
childDrop,
diff --git a/RandomizerCore/Overworld/OverworldMap.cs b/RandomizerCore/Overworld/OverworldMap.cs
new file mode 100644
index 000000000..8760e4b91
--- /dev/null
+++ b/RandomizerCore/Overworld/OverworldMap.cs
@@ -0,0 +1,42 @@
+namespace Z2Randomizer.RandomizerCore.Overworld;
+
+public class OverworldMap
+{
+ private Terrain[,] inner;
+
+ public OverworldMap(int mapRows, int mapColumns)
+ {
+ inner = new Terrain[mapRows, mapColumns];
+ }
+
+ public OverworldMap(Terrain[,] map)
+ {
+ inner = map;
+ }
+
+ public Terrain this[int y, int x]
+ {
+ get => inner[y, x];
+
+ set
+ {
+ // if (y == 27 && x == 20) Debugger.Break();
+ inner[y, x] = value;
+ }
+ }
+
+ public Terrain this[IntVector2 pos]
+ {
+ get => inner[pos.Y, pos.X];
+
+ set
+ {
+ inner[pos.Y, pos.X] = value;
+ }
+ }
+
+ public int GetLength(int dimension)
+ {
+ return inner.GetLength(dimension);
+ }
+}
diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs
index 2d206da32..80e5612d8 100644
--- a/RandomizerCore/Overworld/WestHyrule.cs
+++ b/RandomizerCore/Overworld/WestHyrule.cs
@@ -301,7 +301,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains),
}
//Climate filtering
- climate = Climates.Create(props.WestClimate);
+ climate = Climates.Create(continentId, props.WestClimate);
climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit());
climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER);
//climate.DisallowTerrain(Terrain.LAVA);
@@ -375,7 +375,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
{
Debug.Assert(MapRows == 75);
Debug.Assert(MapColumns == 64);
- map = rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns);
+ map = new OverworldMap(rom.ReadVanillaMap(rom, VANILLA_MAP_ADDR, MapRows, MapColumns));
if (biome == Biome.VANILLA_SHUFFLE)
{
areasByLocation = new SortedDictionary>
@@ -393,7 +393,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
foreach (Location location in AllLocations)
{
// section uses tuples with the Y+30 offset
- areasByLocation[section[location.CoordsY30Offset]].Add(GetLocationByPos(location.Pos)!);
+ areasByLocation[section[location.CoordsY30Offset]].Add(GetLocationAt(location.Pos)!);
}
ChooseConn("parapa", connections, true);
ChooseConn("lifesouth", connections, true);
@@ -433,7 +433,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom)
locationAtSariaSouth.CanShuffle = false;
locationAtSariaNorth.CanShuffle = false;
- map = new Terrain[MapRows, MapColumns];
+ map = new OverworldMap(MapRows, MapColumns);
//blank the whole map to start
for (int i = 0; i < MapRows; i++)
@@ -1062,7 +1062,7 @@ private bool MakeCaldera(Terrain water, bool useSaneCaves)
startx += deltax;
starty += deltay;
}
- int caveCount = RNG.Next(2) + 1;
+ int caveCount = RNG.Next(2) + 1; // 1 = one-way Caldera, 2 = passthru Caldera
Location cave1l, cave1r;
Location? cave2l = null, cave2r = null;
@@ -1385,27 +1385,10 @@ private void DrawMountains()
}
- public override void UpdateVisit(List requireables)
+ public override void UpdateVisit(IReadOnlySet requireables)
{
visitation[northPalace.Y, northPalace.Xpos] = true;
- UpdateReachable(requireables);
-
- foreach (Location location in AllLocations)
- {
- if (location.Y > 0 && visitation[location.Y, location.Xpos])
- {
- if (location.AccessRequirements.AreSatisfiedBy(requireables))
- {
- location.Reachable = true;
- if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables))
- {
- Location connectedLocation = connections[location];
- connectedLocation.Reachable = true;
- visitation[connectedLocation.Y, connectedLocation.Xpos] = true;
- }
- }
- }
- }
+ base.UpdateVisit(requireables);
}
protected override List GetPathingStarts()
diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs
index 853d35cc4..a7a09261a 100644
--- a/RandomizerCore/Overworld/World.cs
+++ b/RandomizerCore/Overworld/World.cs
@@ -8,8 +8,6 @@
using NLog;
using Z2Randomizer.RandomizerCore.Enemy;
-//using System.Runtime.InteropServices.WindowsRuntime;
-
namespace Z2Randomizer.RandomizerCore.Overworld;
public abstract class World
@@ -26,7 +24,7 @@ public abstract class World
public IReadOnlyList overworldEncounterMapDuplicate { get; protected set; } = [];
public IReadOnlyList nonEncounterMaps { get; protected set; }
protected SortedDictionary<(int, int), Location> locsByCoords;
- public Terrain[,] map;
+ public OverworldMap map;
public const int MAP_ROWS_FULL = 75;
public const int MAP_COLS_FULL = 64;
private const int MAX_LOCATION_PLACEMENT_ATTEMPTS = 5000;
@@ -330,11 +328,46 @@ protected Location GetLocation(int locIdx)
protected Location GetLocation(Continent cont, int locIdx)
=> GetLocation(LocationIDUtils.FromIndex(cont, locIdx));
- protected Location? GetLocationByPos(IntVector2 pos)
+ ///
+ /// Returns the (first) location at the specified position,
+ /// or null if no location exists there.
+ ///
+ protected Location? GetLocationAt(IntVector2 pos)
{
return AllLocations.FirstOrDefault(i => i.Pos == pos);
}
+ ///
+ /// Returns any locations found within the 3x3 area centered on
+ /// , including diagonal neighbors and the
+ /// center position itself.
+ ///
+ protected IEnumerable LocationsIn3x3Area(IntVector2 center)
+ {
+ return AllLocations.Where(i => i.Pos.X >= center.X - 1 && i.Pos.X <= center.X + 1 &&
+ i.Pos.Y >= center.Y - 1 && i.Pos.Y <= center.Y + 1);
+ }
+
+ ///
+ /// Returns any locations found at or one
+ /// tile away in a cardinal direction. (Diagonal positions are not included.)
+ ///
+ /// TLDR; Finds any location within a "plus sign" formation.
+ ///
+ protected IEnumerable LocationsAtOrOrthogonalTo(IntVector2 center)
+ {
+ return AllLocations.Where(i => (i.Pos - center).ManhattanLength <= 1);
+ }
+
+ ///
+ /// Returns any locations found exactly one tile away from
+ /// in any cardinal direction. (Diagonal positions are not included.)
+ ///
+ protected IEnumerable LocationsOrthogonalTo(IntVector2 center)
+ {
+ return AllLocations.Where(i => (i.Pos - center).ManhattanLength == 1);
+ }
+
protected Location? GetLocationByCoordsNoOffset((int, int) coords)
{
IntVector2 pos = new(coords.Item2, coords.Item1); // y,x -> x,y
@@ -356,7 +389,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves)
{
return PlaceLocations(riverTerrain, saneCaves, null, -1);
}
- protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hiddenKasutoLocation, int hiddenPalaceX)
+ protected bool PlaceLocations(Terrain crossingTerrain, bool saneCaves, Location? hiddenKasutoLocation, int hiddenPalaceX)
{
int placementAttempt = 0;
foreach (Location location in AllLocations.Where(loc => loc.AppearsOnMap))
@@ -397,29 +430,23 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi
//If the location is a cave, connect it
if (location.TerrainType == Terrain.CAVE)
{
- List caveDirections = new() { Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST };
- Direction direction = caveDirections[RNG.Next(4)];
Terrain entranceTerrain = climate.GetRandomTerrain(RNG, walkableTerrains);
if (saneCaves && connections.ContainsKey(location))
{
PlaceCaveCount++;
map[y, x] = Terrain.NONE;
- while (!PlaceSaneCave(direction, riverTerrain, location))
+ if (!PlaceSaneCave(location, crossingTerrain))
{
- caveDirections.Remove(direction);
- if (caveDirections.Count == 0)
- {
- //Debug.WriteLine(GetMapDebug());
- return false;
- }
- direction = caveDirections[RNG.Next(caveDirections.Count)];
+ return false;
}
}
else
{
+ List caveDirections = new() { Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST };
+ Direction direction = caveDirections[RNG.Next(4)];
PlaceCaveCount++;
- PlaceCave(x, y, direction, entranceTerrain);
+ PlaceCave(new IntVector2(x, y), direction.ToIntVector2(), entranceTerrain);
location.Xpos = x;
location.Y = y;
location.CanShuffle = false;
@@ -475,26 +502,20 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi
}
///
- /// Returns true iff
+ /// Returns true iff there is any crossingTerrain in the rectangle with corners (x1,y1) and (x2,y2)
///
- ///
- ///
- ///
- ///
- ///
- ///
- protected bool CrossingWater(int x1, int x2, int y1, int y2, Terrain riverTerrain)
+ protected bool CrossingTerrainInArea(IntVector2 p1, IntVector2 p2, Terrain crossingTerrain)
{
- int smallx = Math.Min(x1, x2);
- int largex = Math.Max(x1, x2);
-
- int smally = Math.Min(y1, y2);
- int largey = Math.Max(y1, y2);
- for (int y = smally; y < largey; y++)
+ int minX = Math.Min(p1.X, p2.X);
+ int maxX = Math.Max(p1.X, p2.X);
+ int minY = Math.Min(p1.Y, p2.Y);
+ int maxY = Math.Max(p1.Y, p2.Y);
+ for (int y = minY; y <= maxY; y++)
{
- for (int x = smallx; x < largex; x++)
+ for (int x = minX; x <= maxX; x++)
{
- if (y > 0 && y < MapRows && x > 0 && x < MapColumns && map[y, x] == riverTerrain)
+ var pos = new IntVector2(x, y);
+ if (WithinMapBounds(pos, 0) && map[pos] == crossingTerrain)
{
return true;
}
@@ -507,188 +528,118 @@ protected bool CrossingWater(int x1, int x2, int y1, int y2, Terrain riverTerrai
/// Places a cave onto the map, setting the tile to the cave, and ensuring the 3 tiles facing the entrance are
/// the passable type indicated by entranceTerrain
///
- /// X coordinate of the cave entrance
- /// Y coordinate of the cave entrance
- /// Direction the cave's entrance is facing.
- /// i.e. the direction you enter from, not the direction you press to enter it.
- /// Terrain type to create for the approach.
- protected void PlaceCave(int x, int y, Direction direction, Terrain entranceTerrain)
- {
- map[y, x] = Terrain.CAVE;
- if (direction == Direction.NORTH)
- {
- map[y + 1, x] = entranceTerrain;
- map[y + 1, x + 1] = entranceTerrain;
- map[y + 1, x - 1] = entranceTerrain;
- map[y, x - 1] = Terrain.MOUNTAIN;
- map[y, x + 1] = Terrain.MOUNTAIN;
- map[y - 1, x - 1] = Terrain.MOUNTAIN;
- map[y - 1, x] = Terrain.MOUNTAIN;
- map[y - 1, x + 1] = Terrain.MOUNTAIN;
- }
- else if (direction == Direction.EAST)
- {
- map[y + 1, x] = Terrain.MOUNTAIN;
- map[y + 1, x + 1] = Terrain.MOUNTAIN;
- map[y + 1, x - 1] = entranceTerrain;
- map[y, x - 1] = entranceTerrain;
- map[y, x + 1] = Terrain.MOUNTAIN;
- map[y - 1, x - 1] = entranceTerrain;
- map[y - 1, x] = Terrain.MOUNTAIN;
- map[y - 1, x + 1] = Terrain.MOUNTAIN;
- }
- else if (direction == Direction.SOUTH)
- {
- map[y + 1, x] = Terrain.MOUNTAIN;
- map[y + 1, x + 1] = Terrain.MOUNTAIN;
- map[y + 1, x - 1] = Terrain.MOUNTAIN;
- map[y, x - 1] = Terrain.MOUNTAIN;
- map[y, x + 1] = Terrain.MOUNTAIN;
- map[y - 1, x - 1] = entranceTerrain;
- map[y - 1, x] = entranceTerrain;
- map[y - 1, x + 1] = entranceTerrain;
- }
- else if (direction == Direction.WEST)
+ /// X,Y coordinate of the cave entrance
+ /// The direction you press to enter the cave.
+ /// Walkable Terrain type to set in front of the entrance.
+ protected void PlaceCave(IntVector2 pos, IntVector2 forward, Terrain entranceTerrain)
+ {
+ IntVector2 side = forward.Perpendicular();
+
+ foreach (var d in IntVector2.DIRECTIONS)
{
- map[y + 1, x] = Terrain.MOUNTAIN;
- map[y + 1, x + 1] = entranceTerrain;
- map[y + 1, x - 1] = Terrain.MOUNTAIN;
- map[y, x - 1] = Terrain.MOUNTAIN;
- map[y, x + 1] = entranceTerrain;
- map[y - 1, x - 1] = Terrain.MOUNTAIN;
- map[y - 1, x] = Terrain.MOUNTAIN;
- map[y - 1, x + 1] = entranceTerrain;
+ map[pos + d] = Terrain.MOUNTAIN;
}
+
+ map[pos - forward] = entranceTerrain;
+ map[pos - forward + side] = entranceTerrain;
+ map[pos - forward - side] = entranceTerrain;
+
+ map[pos] = Terrain.CAVE;
}
- protected bool PlaceSaneCave(Direction direction, Terrain riverTerrain, Location location)
+ public (IntVector2 pos, IntVector2 directionIn)? PickCavePositionAndDirection()
{
- int x, y;
- if ((location.MapPage == 0 || location.IsFallInHole) && !location.ForceEnterRight)
+ IntVector2 pos = IntVector2.ZERO;
+ for (int tries = 0; ; tries++)
{
- if (direction == Direction.NORTH)
+ pos = IntVector2.Random(RNG, 5, MapColumns - 5, 5, MapRows - 5);
+ if (AllTerrainIn3x3Equals(pos, Terrain.NONE))
{
- direction = Direction.SOUTH;
+ break;
}
+ if (tries == 1000) { return null; }
+ }
- if (direction == Direction.WEST)
+ IntVector2 minDistToEdge = new(Math.Min(MapColumns / 2 - 1, 15),
+ Math.Min(MapRows / 2 - 1, 15));
+ IntVector2 directionIn = IntVector2.ZERO;
+ for (int tries = 0; ; tries++)
+ {
+ directionIn = IntVector2.CARDINALS.Sample(RNG);
+ // check if the cave is too close to the edge in the direction it's going
+ if (WithinMapBounds(pos + directionIn.ComponentMultiply(minDistToEdge)))
{
- direction = Direction.EAST;
+ break;
}
+ if (tries == 1000) { return null; }
}
- else
+
+ return (pos, directionIn);
+ }
+
+ protected IntVector2? PickMatchingSaneCavePosition(IntVector2 cave1pos, IntVector2 direction, Func rollSpacing, Func additionalCheck)
+ {
+ IntVector2 pos;
+ IntVector2 perp = direction.Perpendicular();
+ for (int tries = 0; tries < 100; tries++)
{
- if (direction == Direction.SOUTH)
+ var forwardSteps = rollSpacing();
+ int lateralSteps = RNG.Next(-3, 4);
+ pos = cave1pos + forwardSteps * direction + lateralSteps * perp;
+ if (!WithinMapBounds(pos, 1) || !AllTerrainIn3x3Equals(pos, Terrain.NONE))
{
- direction = Direction.NORTH;
+ continue;
}
-
- if (direction == Direction.EAST)
+ if (!additionalCheck(pos))
{
- direction = Direction.WEST;
+ continue;
}
+
+ return pos;
}
- //Place the exit cave less than 5 rows or columns from the edge of the map, on an unoccupied square
- //That is also not adjacent to any other location.
- do
- {
- x = RNG.Next(MapColumns - 2) + 1;
- y = RNG.Next(MapRows - 2) + 1;
- } while (x < 5 || y < 5 || x > MapColumns - 5 || y > MapRows - 5 || map[y, x] != Terrain.NONE || map[y - 1, x] != Terrain.NONE || map[y + 1, x] != Terrain.NONE || map[y + 1, x + 1] != Terrain.NONE || map[y, x + 1] != Terrain.NONE || map[y - 1, x + 1] != Terrain.NONE || map[y + 1, x - 1] != Terrain.NONE || map[y, x - 1] != Terrain.NONE || map[y - 1, x - 1] != Terrain.NONE);
+ return null;
+ }
- int minDistX = Math.Min(MapColumns / 2 - 1, 15);
- int minDistY = Math.Min(MapRows / 2 - 1, 15);
- while ((direction == Direction.NORTH && y < minDistY) || (direction == Direction.EAST && x > MapColumns - minDistX) || (direction == Direction.SOUTH && y > MapRows - minDistY) || (direction == Direction.WEST && x < minDistX))
+ protected bool PlaceSaneCave(Location location, Terrain crossingTerrain)
+ {
+ if (!(PickCavePositionAndDirection() is var (cave1pos, cave1dir)))
{
- direction = (Direction)RNG.Next(4);
+ return false;
}
- int otherx, othery;
- int tries = 0;
- bool crossing;
- do
+
+ Func rollSpacing = biome switch
{
- //6-18
- int range = 12;
- int offset = 6;
- if (biome == Biome.ISLANDS || biome == Biome.MOUNTAINOUS)
- {
- //10-20
- range = 10;
- offset = 10;
- }
- else if (biome == Biome.VOLCANO || biome == Biome.CALDERA)
- {
- //5-20
- range = 15;
- offset = 5;
- }
- crossing = true;
- if (direction == Direction.NORTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y - (RNG.Next(range) + offset);
- }
- else if (direction == Direction.EAST)
- {
- otherx = x + (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- else if (direction == Direction.SOUTH)
- {
- otherx = x + (RNG.Next(7) - 3);
- othery = y + (RNG.Next(range) + offset);
- }
- else //west
- {
- otherx = x - (RNG.Next(range) + offset);
- othery = y + (RNG.Next(7) - 3);
- }
- if (biome != Biome.VOLCANO)
- {
- if (!CrossingWater(x, otherx, y, othery, riverTerrain))
- {
- crossing = false;
- }
- }
- if (tries++ >= 100)
- {
- //Debug.WriteLine(GetMapDebug());
- return false;
- }
- } while (
- !crossing
- || otherx <= 1
- || otherx >= MapColumns - 1
- || othery <= 1
- || othery >= MapRows - 1
- || map[othery, otherx] != Terrain.NONE
- || map[othery - 1, otherx] != Terrain.NONE
- || map[othery + 1, otherx] != Terrain.NONE
- || map[othery + 1, otherx + 1] != Terrain.NONE
- || map[othery, otherx + 1] != Terrain.NONE
- || map[othery - 1, otherx + 1] != Terrain.NONE
- || map[othery + 1, otherx - 1] != Terrain.NONE
- || map[othery, otherx - 1] != Terrain.NONE
- || map[othery - 1, otherx - 1] != Terrain.NONE);
+ Biome.ISLANDS or Biome.MOUNTAINOUS => () => RNG.Next(10, 20),
+ Biome.VOLCANO or Biome.CALDERA => () => RNG.Next(5, 20),
+ _ => () => RNG.Next(6, 18),
+ };
+ Func crossingCheck = biome switch
+ {
+ Biome.VOLCANO => (_) => true,
+ _ => (IntVector2 pos) => CrossingTerrainInArea(cave1pos, pos, crossingTerrain)
+ };
+ if (!(PickMatchingSaneCavePosition(cave1pos, cave1dir, rollSpacing, crossingCheck) is IntVector2 cave2pos))
+ {
+ return false;
+ }
Location location2 = connections[location];
location.CanShuffle = false;
- location.Xpos = x;
- location.Y = y;
+ location.Pos = cave1pos;
location2.CanShuffle = false;
- location2.Xpos = otherx;
- location2.Y = othery;
- PlaceCave(x, y, direction, climate.GetRandomTerrain(RNG, walkableTerrains));
- PlaceCave(otherx, othery, direction.Reverse(), climate.GetRandomTerrain(RNG, walkableTerrains));
+ location2.Pos = cave2pos;
+ PlaceCave(cave1pos, cave1dir, climate.GetRandomTerrain(RNG, walkableTerrains));
+ PlaceCave(cave2pos, -cave1dir, climate.GetRandomTerrain(RNG, walkableTerrains));
+ AlignCavePositionsLeftToRight(cave1dir, location, location2);
return true;
}
/// swaps the positions of two caves if the cave that is going East/South
/// in the Overworld is not the cave that enters the sideview from the left
- protected static void AlignCavePositionsLeftToRight(Direction location1Direction, Location location1, Location location2)
+ protected static void AlignCavePositionsLeftToRight(IntVector2 location1Direction, Location location1, Location location2)
{
- bool overworldGoingRight = location1Direction == Direction.EAST || location1Direction == Direction.SOUTH;
+ bool overworldGoingRight = location1Direction == IntVector2.EAST || location1Direction == IntVector2.SOUTH;
bool sideviewGoingRight = location1.MapPage < location2.MapPage;
if (overworldGoingRight != sideviewGoingRight)
{
@@ -755,6 +706,14 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
bool rockBlock, bool placeLongBridge, bool placeDaruniaDesert,
bool canWalkOnWater, Biome biome, int deadZoneMinX = 999, int deadZoneMaxX = -1, int deadZoneMinY = 999, int deadZoneMaxY = -1)
{
+ if (!((deadZoneMinX == 999 && deadZoneMaxX == -1 && deadZoneMinY == 999 && deadZoneMaxY == -1)
+ || (deadZoneMinX != 999 && deadZoneMaxX != -1 && deadZoneMinY != 999 && deadZoneMaxY != -1)))
+ {
+ throw new ArgumentException("ConnectIslands dead zone is improperly defined. 0 or 4 values must be specified.");
+ }
+ bool IsDeadZoneSafe(int deadZoneMinX, int deadZoneMaxX, int deadZoneMinY, int deadZoneMaxY, int y, int x)
+ => (x < deadZoneMinX || x > deadZoneMaxX) && (y < deadZoneMinY || y > deadZoneMaxY);
+
//Any of the bridge locations that are being placed need to be reset so their vanilla locations aren't avoided.
if(placeSaria)
{
@@ -791,15 +750,10 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
//Debug.WriteLine(GetMapDebug());
int maxBridgeLength = MAXIMUM_BRIDGE_LENGTH[biome];
//Great lakes and bad boots are more likely to be incompleteable, so extend max bridge length to give them a fighting chance
- if(canWalkOnWater || climate.Name == Climates.Create(ClimateEnum.GREAT_LAKES).Name)
+ if (!canWalkOnWater || climate.Name == Climates.Create(continentId, ClimateEnum.GREAT_LAKES).Name)
{
maxBridgeLength = (int)(maxBridgeLength * 1.5);
}
- if (!((deadZoneMinX == 999 && deadZoneMaxX == -1 && deadZoneMinY == 999 && deadZoneMaxY == -1)
- || (deadZoneMinX != 999 && deadZoneMaxX != -1 && deadZoneMinY != 999 && deadZoneMaxY != -1)))
- {
- throw new ArgumentException("ConnectIslands dead zone is improperly defined. 0 or 4 values must be specified.");
- }
int maxBridgeAttempts = MAXIMUM_BRIDGE_ATTEMPTS[biome];
maxBridgeAttempts *= canWalkOnWater ? 1 : 2;
int[,] globs = GetTerrainGlobs();
@@ -815,17 +769,17 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
//precompute next to water tiles since there are likely more fails than successes and randomly iterating the same
//bad tiles over and over is wasteful
- List<(int, int, Direction)> nextToWaterTiles = [];
+ List<(IntVector2 Pos, Direction)> nextToWaterTiles = [];
for (int y = 1; y < MapRows - 1; y++)
{
for (int x = 1; x < MapColumns - 1; x++)
{
List waterDirections = NextToWaterDirections(x, y, crossingTerrains);
- if((x < deadZoneMinX || x > deadZoneMaxX) && (y < deadZoneMinY || y > deadZoneMaxY))
+ if (IsDeadZoneSafe(deadZoneMinX, deadZoneMaxX, deadZoneMinY, deadZoneMaxY, y, x))
{
foreach(Direction direction in waterDirections)
{
- nextToWaterTiles.Add((x, y, direction));
+ nextToWaterTiles.Add((new IntVector2(x, y), direction));
}
}
}
@@ -834,73 +788,43 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
while (remainingBridges > 0 && tries < maxBridgeAttempts)
{
tries++;
- int x, y;
- (int, int, Direction)? nextToWaterTile = nextToWaterTiles.Sample(RNG);
+ (IntVector2 Pos, Direction Dir)? nextToWaterTile = nextToWaterTiles.Sample(RNG);
//All possible bridges have been evaluated.
if (nextToWaterTile == null)
{
return true;
}
- x = nextToWaterTile.Value.Item1;
- y = nextToWaterTile.Value.Item2;
- int startX = x, startY = y;
-
- Direction waterDirection = nextToWaterTile.Value.Item3;
+ var pos = nextToWaterTile.Value.Pos;
+ IntVector2 start = pos;
+ IntVector2 forward = nextToWaterTile.Value.Dir.ToIntVector2();
+ IntVector2 side = forward.Perpendicular();
+ Direction waterDirection = nextToWaterTile.Value.Dir;
- int deltaX = waterDirection.DeltaX();
- int deltaY = waterDirection.DeltaY();
int length = 0;
- if (IsSingleTile(y, x))
+ if (IsBlockedSingleTile(pos))
{
length = 100;
}
-
- int startMass = globs[y, x];
-
- //if there is a location at or 1 tile adjacent to the bridge start, it's no good.
- if (GetLocationByCoordsNoOffset((y, x)) != null
- || GetLocationByCoordsNoOffset((y, x + 1)) != null
- || GetLocationByCoordsNoOffset((y, x - 1)) != null
- || GetLocationByCoordsNoOffset((y + 1, x)) != null
- || GetLocationByCoordsNoOffset((y - 1, x)) != null)
+ if (LocationsAtOrOrthogonalTo(pos).Any())
{
length = 100;
}
-
- x += deltaX;
- y += deltaY;
- int perpDx = deltaX == 0 ? 1 : 0;
- int perpDy = deltaY == 0 ? 1 : 0;
+ int startMass = globs[pos.Y, pos.X];
+ pos += forward;
//iterate expanding the bridge
- while (x > 0 && x < MapColumns && y > 0 && y < MapRows && crossingTerrains.Contains(map[y, x]))
+ while (WithinMapBounds(pos, 1) && crossingTerrains.Contains(map[pos]))
{
//if we are too close to a location, give up
- if (x + 1 < MapColumns && GetLocationByCoordsNoOffset((y, x + 1)) != null)
- {
- length = 100;
- break;
- }
- if (x - 1 > 0 && GetLocationByCoordsNoOffset((y, x - 1)) != null)
- {
- length = 100;
- break;
- }
-
- if (y + 1 < MapRows && GetLocationByCoordsNoOffset((y + 1, x)) != null)
- {
- length = 100;
- break;
- }
- if (y - 1 > 0 && GetLocationByCoordsNoOffset((y - 1, x)) != null)
+ if (LocationsAtOrOrthogonalTo(pos).Any())
{
length = 100;
break;
}
//if advancing goes off the edge of the map, give up
- if(!(x + deltaX < MapColumns && x + deltaX >= 0 && y + deltaY < MapRows && y + deltaY >= 0))
+ if(!WithinMapBounds(pos + forward))
{
length = 100;
break;
@@ -910,25 +834,32 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
int adjacentRiverTerrainDirectionCount = 0;
//If the tile just before or just after is walkable, this is the first or last tile of the bridge
//When that happens, the adjacent (not forward/back) directions are allowed to be any walkable terrain
- bool isFirstStep = !crossingTerrains.Contains(map[y - deltaY, x - deltaX]);
- bool isLastStep = !crossingTerrains.Contains(map[y + deltaY, x + deltaX]);
+ bool isFirstStep = !crossingTerrains.Contains(map[pos - forward]);
+ Terrain forwardTerrain = map[pos + forward];
+ bool isLastStep = !crossingTerrains.Contains(forwardTerrain);
Terrain[] effectiveCrossingTerrain = isFirstStep || isLastStep ? edgeCrossingTerrains : crossingTerrains;
//Check forward
- if (effectiveCrossingTerrain.Contains(map[y + deltaY, x + deltaX]) || map[y + deltaY, x + deltaX] == Terrain.MOUNTAIN)
+ if (effectiveCrossingTerrain.Contains(forwardTerrain) || forwardTerrain == Terrain.MOUNTAIN)
{
adjacentRiverTerrainDirectionCount++;
}
//Check perpendicular tiles. This is allowed to be any walkable terrain on the first/last tiles of the bridge
- if (x + perpDx < MapColumns && x + perpDx >= 0 && y + perpDy < MapRows && y - perpDy >= 0 &&
- (effectiveCrossingTerrain.Contains(map[y + perpDy, x + perpDx]) || map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN))
+ if (WithinMapBounds(pos + side))
{
- adjacentRiverTerrainDirectionCount++;
+ var rightTerrain = map[pos + side];
+ if (effectiveCrossingTerrain.Contains(rightTerrain) || rightTerrain == Terrain.MOUNTAIN)
+ {
+ adjacentRiverTerrainDirectionCount++;
+ }
}
- if (x - perpDx < MapColumns && x - perpDx >= 0 && y - perpDy < MapRows && y - perpDy >= 0 &&
- (effectiveCrossingTerrain.Contains(map[y - perpDy, x - perpDx]) || map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN))
+ if (WithinMapBounds(pos - side))
{
- adjacentRiverTerrainDirectionCount++;
+ var leftTerrain = map[pos - side];
+ if (effectiveCrossingTerrain.Contains(leftTerrain) || leftTerrain == Terrain.MOUNTAIN)
+ {
+ adjacentRiverTerrainDirectionCount++;
+ }
}
//if all 3 tiles adjacent to the new tile aren't the passing terrain, give up
@@ -936,31 +867,24 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
{
length = 100;
}
- x += deltaX;
- y += deltaY;
+ pos += forward;
length++;
-
}
//no extending from single tile
- if (IsSingleTile(y, x))
+ if (IsBlockedSingleTile(pos))
{
length = 100;
}
int endMass = 0;
- if (y > 0 && x > 0 && y < MapRows - 1 && x < MapColumns - 1)
+ if (WithinMapBounds(pos, 1))
{
- if (GetLocationByCoordsNoOffset((y, x)) != null
- || GetLocationByCoordsNoOffset((y, x + 1)) != null
- || GetLocationByCoordsNoOffset((y, x - 1)) != null
- || GetLocationByCoordsNoOffset((y + 1, x)) != null
- || GetLocationByCoordsNoOffset((y - 1, x)) != null)
+ if (LocationsAtOrOrthogonalTo(pos).Any())
{
length = 100;
- //Debug.WriteLine(GetGlobDebug(globs));
}
- endMass = globs[y, x];
+ endMass = globs[pos.Y, pos.X];
}
//if we're ending, it has to be on a different chunk of terrain so the bridge doesn't just cross a bay
@@ -976,35 +900,28 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
}
if (
(placeSaria && length < maxBridgeLength || (length < maxBridgeLength && length > MINIMUM_BRIDGE_LENGTH))
- && x > 0
- && x < MapColumns - 1
- && y > 0
- && y < MapRows - 1
- && walkableTerrains.Contains(map[y, x])
- && map[y, x] != riverTerrain)
+ && WithinMapBounds(pos, 1)
+ && walkableTerrains.Contains(map[pos])
+ && map[pos] != riverTerrain)
{
- Terrain terrain = map[y, x];
+ Terrain terrain = map[pos];
globConnections.Add((startMass, endMass));
globConnections.Add((endMass, startMass));
if (placeSaria)
{
//Saria doesn't need to worry about sideways entrance since it's not a passthrough
- map[y, x] = Terrain.TOWN;
+ map[pos] = Terrain.TOWN;
Location location = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH);
- location.Y = y;
- location.Xpos = x;
- x -= deltaX;
- y -= deltaY;
- while (crossingTerrains.Contains(map[y, x]))
+ location.Pos = pos;
+ pos -= forward;
+ while (crossingTerrains.Contains(map[pos]))
{
- x -= deltaX;
- y -= deltaY;
+ pos -= forward;
}
- map[y, x] = Terrain.TOWN;
+ map[pos] = Terrain.TOWN;
location = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH);
- location.Y = y;
- location.Xpos = x;
+ location.Pos = pos;
placeSaria = false;
}
else if (placeLongBridge)
@@ -1012,83 +929,64 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
Location bridge1 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_WEST);
Location bridge2 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_EAST);
- if (!walkableTerrains.Contains(map[y - deltaY, x - deltaX]))
+ pos -= forward;
+ if (!walkableTerrains.Contains(map[pos]))
{
- map[y - deltaY, x - deltaX] = Terrain.BRIDGE;
+ map[pos] = Terrain.BRIDGE;
}
- while (!crossingTerrains.Contains(map[y + perpDy, x + perpDx]) && !crossingTerrains.Contains(map[y - perpDy, x - perpDx]))
+ while (!crossingTerrains.Contains(map[pos + side]) &&
+ !crossingTerrains.Contains(map[pos - side]))
{
- if ((deltaY < 0 && y > startY) || (deltaY > 0 && y < startY) || (deltaX < 0 && x > startX) || (deltaX > 0 && x < startX))
+ if (start.IsBehind(pos, forward))
{
logger.Warn("Unable to roll back bridge location with jagged entrance");
return false;
}
- x -= deltaX;
- y -= deltaY;
+ pos -= forward;
}
- if (crossingTerrains.Contains(map[y + deltaY, x + deltaX]))
+
+ if (crossingTerrains.Contains(map[pos + forward]))
{
- x += deltaX;
- y += deltaY;
+ pos += forward;
}
- if (deltaX > 0 || deltaY > 0)
+ if (forward.X > 0 || forward.Y > 0)
{
- bridge2.Xpos = x;
- bridge2.Y = y;
+ bridge2.Pos = pos;
}
else
{
- bridge1.Xpos = x;
- bridge1.Y = y;
+ bridge1.Pos = pos;
}
- if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater())
- && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater())
- {
- map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx];
- }
- if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater())
- && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater())
- {
- map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx];
- }
+ NormalizeBridgeSideTerrain(map, pos, side);
- while ((deltaX != 0 && x != startX) || (deltaY != 0 && y != startY))
+ while (pos != start)
{
- map[y, x] = Terrain.BRIDGE;
- x -= deltaX;
- y -= deltaY;
+ map[pos] = Terrain.BRIDGE;
+ pos -= forward;
}
- if (crossingTerrains.Contains(map[y,x]))
- {
- map[y, x] = map[y - deltaY, x - deltaX];
- }
- x += deltaX;
- y += deltaY;
- if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater())
- && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater())
+ if (crossingTerrains.Contains(map[pos]))
{
- map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx];
+ map[pos] = map[pos - forward];
}
- if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater())
- && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater())
- {
- map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx];
- }
- map[y, x] = Terrain.BRIDGE;
- if (deltaX > 0 || deltaY > 0)
+
+ pos += forward;
+
+ NormalizeBridgeSideTerrain(map, pos, side);
+ map[pos] = Terrain.BRIDGE;
+
+ if (forward.X > 0 || forward.Y > 0)
{
- bridge1.Xpos = x;
- bridge1.Y = y;
+ bridge1.Pos = pos;
}
else
{
- bridge2.Xpos = x;
- bridge2.Y = y;
+ bridge2.Pos = pos;
}
+
placeLongBridge = false;
bridge1.CanShuffle = false;
bridge2.CanShuffle = false;
@@ -1099,115 +997,84 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
Location bridge2 = GetLocation(LocationID.EAST_TRAP_DESERT1);
if (bridge1.CanShuffle && bridge2.CanShuffle)
{
- if (!walkableTerrains.Contains(map[y - deltaY, x - deltaX]))
+ pos -= forward;
+ if (!walkableTerrains.Contains(map[pos]))
{
- map[y - deltaY, x - deltaX] = Terrain.DESERT;
+ map[pos] = Terrain.DESERT;
}
- //Walk backwards until the first tile flanked on both sides by the river
- while (!crossingTerrains.Contains(map[y + perpDy, x + perpDx]) && !crossingTerrains.Contains(map[y - perpDy, x - perpDx]))
+
+ while (!crossingTerrains.Contains(map[pos + side]) &&
+ !crossingTerrains.Contains(map[pos - side]))
{
- if ((deltaY < 0 && y > startY) || (deltaY > 0 && y < startY) || (deltaX < 0 && x > startX) || (deltaX > 0 && x < startX))
+ if (start.IsBehind(pos, forward))
{
logger.Warn("Unable to roll back bridge location with jagged entrance");
return false;
}
- x -= deltaX;
- y -= deltaY;
+ pos -= forward;
}
- //That's where the first bridge encounter is
- if (deltaX > 0 || deltaY > 0)
+ // first bridge encounter
+ if (forward.X > 0 || forward.Y > 0)
{
- bridge2.Xpos = x;
- bridge2.Y = y;
+ bridge2.Pos = pos;
}
else
{
- bridge1.Xpos = x;
- bridge1.Y = y;
+ bridge1.Pos = pos;
}
- if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater())
- && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater())
- {
- map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx];
- }
- if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater())
- && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater())
- {
- map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx];
- }
+ NormalizeBridgeSideTerrain(map, pos, side);
- //Keep walking back placing deserts until the path opens up
- while ((deltaX != 0 && x != startX) || (deltaY != 0 && y != startY))
+ while (pos != start)
{
- map[y, x] = Terrain.DESERT;
- x -= deltaX;
- y -= deltaY;
+ map[pos] = Terrain.DESERT;
+ pos -= forward;
}
- //If you're going into a C-shape, you'll have a dead stub of water. fill it in
- if (map[y, x].IsWater())
+
+ if (map[pos].IsWater())
{
- map[y, x] = map[y - deltaY, x - deltaX];
+ map[pos] = map[pos - forward];
}
- //now we're past the opening, so go the other way 1
- x += deltaX;
- y += deltaY;
+ pos += forward;
- //If exactly one of the tiles adjacent to the bridge is a potentially impassable, demolish it so you can't get stuck in it.
- if ((map[y + perpDy, x + perpDx] == Terrain.MOUNTAIN || map[y + perpDy, x + perpDx].IsWater())
- && map[y - perpDy, x - perpDx] != Terrain.MOUNTAIN && !map[y - perpDy, x - perpDx].IsWater())
- {
- map[y - perpDy, x - perpDx] = map[y + perpDy, x + perpDx];
- }
- if ((map[y - perpDy, x - perpDx] == Terrain.MOUNTAIN || map[y - perpDy, x - perpDx].IsWater())
- && map[y + perpDy, x + perpDx] != Terrain.MOUNTAIN && !map[y + perpDy, x + perpDx].IsWater())
- {
- map[y + perpDy, x + perpDx] = map[y - perpDy, x - perpDx];
- }
- map[y, x] = Terrain.DESERT;
- if (deltaX > 0 || deltaY > 0)
+ NormalizeBridgeSideTerrain(map, pos, side);
+
+ map[pos] = Terrain.DESERT;
+
+ if (forward.X > 0 || forward.Y > 0)
{
- bridge1.Xpos = x;
- bridge1.Y = y;
+ bridge1.Pos = pos;
}
else
{
- bridge2.Xpos = x;
- bridge2.Y = y;
+ bridge2.Pos = pos;
}
+
bridge1.CanShuffle = false;
bridge2.CanShuffle = false;
}
- placeDaruniaDesert = false;
+ placeDaruniaDesert = false;
}
else
{
- x -= deltaX;
- y -= deltaY;
+ pos -= forward;
int curr = 0;
if (TerrainCycle == 2)
{
-
- x += deltaX;
- y += deltaY;
- map[y, x] = Terrain.ROAD;
- x -= deltaX;
- y -= deltaY;
- while (crossingTerrains.Contains(map[y, x]))
+ map[pos + forward] = Terrain.ROAD;
+ while (crossingTerrains.Contains(map[pos]))
{
- map[y, x] = Terrain.WALKABLEWATER;
- x -= deltaX;
- y -= deltaY;
+ map[pos] = Terrain.WALKABLEWATER;
+ pos -= forward;
}
- map[y, x] = Terrain.ROAD;
-
+ map[pos] = Terrain.ROAD;
}
else
{
- while (crossingTerrains.Contains(map[y, x]))
+ while (crossingTerrains.Contains(map[pos]))
{
if (biome == Biome.MOUNTAINOUS || biome == Biome.VANILLALIKE)
@@ -1224,20 +1091,20 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
}
}
- map[y, x] = terrain;
+ map[pos] = terrain;
bool placed = false;
if (curr == length / 2)
{
List locs = Locations[terrain];
if (riverDevil)
{
- map[y, x] = Terrain.RIVER_DEVIL;
+ map[pos] = Terrain.RIVER_DEVIL;
riverDevil = false;
placed = true;
}
else if (rockBlock)
{
- map[y, x] = Terrain.ROCK;
+ map[pos] = Terrain.ROCK;
rockBlock = false;
placed = true;
}
@@ -1245,8 +1112,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
{
if (location.CanShuffle && !placed)
{
- location.Y = y;
- location.Xpos = x;
+ location.Pos = pos;
location.CanShuffle = false;
break;
}
@@ -1258,20 +1124,20 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
{
if (TerrainCycle == 0)
{
- map[y, x] = Terrain.ROAD;
+ map[pos] = Terrain.ROAD;
if (curr == length / 2)
{
bool placed = false;
if (riverDevil)
{
- map[y, x] = Terrain.RIVER_DEVIL;
+ map[pos] = Terrain.RIVER_DEVIL;
riverDevil = false;
placed = true;
}
else if (rockBlock)
{
- map[y, x] = Terrain.ROCK;
+ map[pos] = Terrain.ROCK;
rockBlock = false;
placed = true;
}
@@ -1279,8 +1145,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
{
if (location.CanShuffle && !placed)
{
- location.Y = y;
- location.Xpos = x;
+ location.Pos = pos;
location.CanShuffle = false;
break;
}
@@ -1295,20 +1160,20 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
|| riverTerrain == Terrain.MOUNTAIN
|| (riverTerrain != Terrain.WALKABLEWATER && curr != length / 2 + 1))
{
- map[y, x] = Terrain.BRIDGE;
+ map[pos] = Terrain.BRIDGE;
}
bool placed = false;
if (curr == length / 2)
{
if (riverDevil)
{
- map[y, x] = Terrain.RIVER_DEVIL;
+ map[pos] = Terrain.RIVER_DEVIL;
riverDevil = false;
placed = true;
}
else if (rockBlock)
{
- map[y, x] = Terrain.ROCK;
+ map[pos] = Terrain.ROCK;
rockBlock = false;
placed = true;
}
@@ -1316,8 +1181,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
{
if (location.CanShuffle && !placed)
{
- location.Y = y;
- location.Xpos = x;
+ location.Pos = pos;
location.CanShuffle = false;
break;
}
@@ -1326,8 +1190,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
}
}
curr++;
- x -= deltaX;
- y -= deltaY;
+ pos -= forward;
}
}
TerrainCycle++;
@@ -1345,7 +1208,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr
remainingBridges--;
}
- nextToWaterTiles.Remove((startX, startY, waterDirection));
+ nextToWaterTiles.Remove((start, waterDirection));
}
return !placeSaria;
@@ -1424,6 +1287,25 @@ private string PrintTerrainGlobMap(int[,] mass)
return sb.ToString();
}
+ private static void NormalizeBridgeSideTerrain(OverworldMap map, IntVector2 pos, IntVector2 side)
+ {
+ IntVector2 left = pos - side;
+ IntVector2 right = pos + side;
+
+ bool leftBlocked = map[left] == Terrain.MOUNTAIN || map[left].IsWater();
+ bool rightBlocked = map[right] == Terrain.MOUNTAIN || map[right].IsWater();
+
+ if (rightBlocked && !leftBlocked)
+ {
+ map[left] = map[right];
+ }
+
+ if (leftBlocked && !rightBlocked)
+ {
+ map[right] = map[left];
+ }
+ }
+
protected List NextToWaterDirections(int x, int y, Terrain[] crossingTerrains)
{
List directions = [];
@@ -1453,38 +1335,22 @@ protected List NextToWaterDirections(int x, int y, Terrain[] crossing
}
///
- /// Returns true IFF this is an isolated single tile. i.e. The terrain type one space in each orthoginal direction is not this terrain type.
+ /// Returns true IFF this is an isolated single tile. i.e. the
+ /// terrain type one space in each of the 4 orthogonal directions
+ /// is not walkable terrain.
///
- ///
- ///
- ///
- private bool IsSingleTile(int y, int x)
+ private bool IsBlockedSingleTile(IntVector2 pos)
{
- int count = 0;
- if (x < MapColumns && x > 0)
+ return IntVector2.CARDINALS.All(d =>
{
- if (y + 1 < MapRows && !walkableTerrains.Contains(map[y + 1, x]))
- {
- count++;
- }
- if (y - 1 > 0 && !walkableTerrains.Contains(map[y - 1, x]))
- {
- count++;
- }
- }
- if (y < MapRows && y > 0)
- {
- if (x + 1 < MapColumns && !walkableTerrains.Contains(map[y, x + 1]))
- {
- count++;
- }
- if (x - 1 > 0 && !walkableTerrains.Contains(map[y, x - 1]))
- {
- count++;
- }
- }
- return count == 4;
+ var checkPos = pos + d;
+ // out of bounds is effectively not walkable (blocked).
+ if (!WithinMapBounds(checkPos)) { return true; }
+
+ var checkTerrain = map[checkPos];
+ return !walkableTerrains.Contains(checkTerrain);
+ });
}
/// used for GrowTerrain optimization
@@ -1534,7 +1400,7 @@ private PlacedTerrainPreCalc[] GrowTerrainGetPlacedTerrains(Climate climate)
protected bool GrowTerrain(Climate climate)
{
TerrainGrowthAttempts++;
- Terrain[,] mapCopy = new Terrain[MapRows, MapColumns];
+ var mapCopy = new OverworldMap(MapRows, MapColumns);
PlacedTerrainPreCalc[] placedTerrains = GrowTerrainGetPlacedTerrains(climate);
const double EPSILON = 1e-9;
double distance, minDistance;
@@ -1606,16 +1472,12 @@ public static int DistanceSquared(int x1, int y1, int x2, int y2)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool AllTerrainIn3x3Equals(int x, int y, Terrain t)
{
- return
- map[y - 1, x - 1] == t &&
- map[y - 1, x ] == t &&
- map[y - 1, x + 1] == t &&
- map[y, x - 1] == t &&
- map[y, x ] == t &&
- map[y, x + 1] == t &&
- map[y + 1, x - 1] == t &&
- map[y + 1, x ] == t &&
- map[y + 1, x + 1] == t;
+ return AllTerrainIn3x3Equals(new IntVector2(x, y), t);
+ }
+
+ protected bool AllTerrainIn3x3Equals(IntVector2 pos, Terrain t)
+ {
+ return IntVector2.DIRECTIONS.All(d => map[pos + d] == t) && map[pos] == t;
}
protected void PlaceRandomTerrain(Climate climate, int seedCountMaximum = 500)
@@ -1849,8 +1711,8 @@ protected bool DrawOcean(Direction direction, Terrain oceanTerrain)
}
return true;
}
-
- protected void UpdateReachable(List requireables)
+
+ protected void UpdateReachable(IReadOnlySet requireables)
{
List starts = GetPathingStarts();
@@ -1882,7 +1744,7 @@ protected virtual void OnUpdateReachableTrigger()
}
//This signature has gotten out of control, consider a refactor
- protected void UpdateReachable(ref bool[,] covered, int start_y, int start_x, List requireables)
+ protected void UpdateReachable(ref bool[,] covered, int start_y, int start_x, IReadOnlySet requireables)
{
Stack<(int, int)> to_visit = new();
// push the initial coord to the visitation stack
@@ -1965,7 +1827,7 @@ protected bool DrawBridge(Direction direction)
return DrawBridge(RNG, map, bridge, walkableTerrains, direction);
}
- public static bool DrawBridge(Random r, Terrain[,] map, Location? bridge, List walkableTerrains, Direction direction)
+ public static bool DrawBridge(Random r, OverworldMap map, Location? bridge, List walkableTerrains, Direction direction)
{
if (bridge == null) { throw new Exception("Unable to draw unloaded bridge"); }
int x = 0;
@@ -2006,7 +1868,7 @@ protected bool DrawRaft(Direction direction)
return DrawRaft(RNG, map, raft, walkableTerrains, direction);
}
- public static bool DrawRaft(Random r, Terrain[,] map, Location? raft, List walkableTerrains, Direction direction)
+ public static bool DrawRaft(Random r, OverworldMap map, Location? raft, List walkableTerrains, Direction direction)
{
if (raft == null) { throw new Exception("Unable to draw unloaded raft"); }
int x = 0;
@@ -2018,7 +1880,8 @@ public static bool DrawRaft(Random r, Terrain[,] map, Location? raft, List 100) {
+ if (tries > 100)
+ {
return false;
}
@@ -2042,7 +1905,7 @@ public static bool DrawRaft(Random r, Terrain[,] map, Location? raft, List walkableTerrains, int x, int y)
+ protected static bool IsValidEndTile(OverworldMap map, List walkableTerrains, int x, int y)
{
int mapRows = map.GetLength(0);
int mapCols = map.GetLength(1);
@@ -2122,7 +1985,7 @@ protected static bool IsValidEndTile(Terrain[,] map, List walkableTerra
return walkableTerrains.Contains(map[y, x]);
}
- public static void PlaceBridge(Terrain[,] map, Location bridge, int x, int y, Direction direction)
+ public static void PlaceBridge(OverworldMap map, Location bridge, int x, int y, Direction direction)
{
int mapRows = map.GetLength(0);
int mapCols = map.GetLength(1);
@@ -2163,7 +2026,7 @@ public static void PlaceBridge(Terrain[,] map, Location bridge, int x, int y, Di
}
}
- public static void PlaceRaft(Terrain[,] map, Location raft, int x, int y)
+ public static void PlaceRaft(OverworldMap map, Location raft, int x, int y)
{
map[y, x] = Terrain.BRIDGE;
raft.Xpos = x;
@@ -2294,400 +2157,176 @@ public List GetContinentConnections()
protected void DrawRiver(bool canWalkOnWaterWithBoots)
{
- Terrain water = Terrain.WATER;
- if (canWalkOnWaterWithBoots)
- {
- water = Terrain.WALKABLEWATER;
- }
- int dirr = RNG.Next(4);
- int dirr2 = dirr;
- while (dirr == dirr2)
- {
- dirr2 = RNG.Next(4);
- }
+ Terrain water = canWalkOnWaterWithBoots ? Terrain.WALKABLEWATER : Terrain.WATER;
- int deltax = 0;
- int deltay = 0;
- int startx = 0;
- int starty = 0;
- if (dirr == 0) //north
- {
- deltay = 1;
- startx = RNG.Next(MapColumns / 3, (MapColumns / 3) * 2);
- starty = 0;
- }
- else if (dirr == 1) //east
- {
- deltax = -1;
- startx = MapColumns - 1;
- starty = RNG.Next(MapRows / 3, (MapRows / 3) * 2);
- }
- else if (dirr == 2) //south
- {
- deltay = -1;
- startx = RNG.Next(MapColumns / 3, (MapColumns / 3) * 2);
- starty = MapRows - 1;
- }
- else //west
+ Direction direction1 = DirectionExtensions.RandomCardinal(RNG);
+ Direction direction2;
+ do
{
- deltax = 1;
- startx = 0;
- starty = RNG.Next(MapRows / 3, (MapRows / 3) * 2);
- }
+ direction2 = DirectionExtensions.RandomCardinal(RNG);
+ } while (direction1 == direction2);
- int stopping = RNG.Next(MapColumns / 3, (MapColumns / 3) * 2);
- if (deltay != 0)
+ (IntVector2 Delta, IntVector2 Start) RiverFromDir(Direction direction)
{
- stopping = RNG.Next(MapRows / 3, (MapRows / 3) * 2);
+ int rx = RNG.Next(MapColumns / 3, (MapColumns / 3) * 2);
+ int ry = RNG.Next(MapRows / 3, (MapRows / 3) * 2);
+ IntVector2 start = direction switch
+ {
+ Direction.SOUTH => new(rx, 0),
+ Direction.WEST => new(MapColumns - 1, ry),
+ Direction.NORTH => new(rx, MapRows - 1),
+ Direction.EAST => new(0, ry),
+ _ => throw new Exception("Invalid Direction")
+ };
+ IntVector2 delta = direction.ToIntVector2();
+ return (start, delta);
}
- int curr = 0;
- while (curr < stopping)
- {
- if (map[starty, startx] == Terrain.NONE)
+ IntVector2 AdjustAndPaint(IntVector2 pos, IntVector2 delta, IntVector2 sideDir)
+ {
+ if (map[pos] != Terrain.NONE) { return pos; }
+ map[pos] = water;
+ int minAdj = (sideDir.X != 0 && pos.X == 1) || (sideDir.Y != 0 && pos.Y == 1) ? 0 : -1;
+ int maxAdj = (sideDir.X != 0 && pos.X == MapColumns - 2) || (sideDir.Y != 0 && pos.Y == MapRows - 2) ? 0 : 1;
+ int adjust = RNG.Next(minAdj, maxAdj + 1);
+ IntVector2 adjusted = pos + adjust * sideDir;
+ if (WithinMapBounds(adjusted) && !IsReserved(adjusted))
{
- map[starty, startx] = water;
- int adjust = RNG.Next(-1, 2);
- if ((deltax == 0 && startx == 1) || (deltay == 0 && starty == 1))
- {
- adjust = RNG.Next(0, 2);
- }
- else if ((deltax == 0 && startx == MapColumns - 2) || (deltay == 0 && starty == MapRows - 2))
- {
- adjust = RNG.Next(-1, 1);
- }
-
- if (adjust < 0)
- {
- if (deltax != 0)
- {
- starty--;
- }
- else
- {
- startx--;
- }
- }
- else if (adjust > 0)
- {
- if (deltax != 0)
- {
- starty++;
- }
- else
- {
- startx++;
- }
- }
- map[starty, startx] = water;
+ map[adjusted] = water;
}
-
- startx += deltax;
- starty += deltay;
- curr++;
+ return adjusted;
}
- deltay = 0;
- deltax = 0;
- if (dirr2 == 0) //north
- {
- deltay = 1;
- }
- else if (dirr2 == 1) //east
- {
- deltax = -1;
- }
- else if (dirr2 == 2) //south
- {
- deltay = -1;
- }
- else //west
+
+ var (start, delta) = RiverFromDir(direction1);
+ IntVector2 sideDir = delta.Perpendicular();
+ int stopping = delta.Y != 0 ? RNG.Next(MapRows / 3, (MapRows / 3) * 2) : RNG.Next(MapColumns / 3, (MapColumns / 3) * 2);
+
+ for (int i = 0; i < stopping; i++)
{
- deltax = 1;
+ start = AdjustAndPaint(start, delta, sideDir);
+ start += delta;
}
- while (startx > 0 && startx < MapColumns && starty > 0 && starty < MapRows)
- {
- if (map[starty, startx] == Terrain.NONE)
- {
- map[starty, startx] = water;
- int adjust = RNG.Next(-1, 2);
- if ((deltax == 0 && startx == 1) || (deltay == 0 && starty == 1))
- {
- adjust = RNG.Next(0, 2);
- }
- else if ((deltax == 0 && startx == MapColumns - 2) || (deltay == 0 && starty == MapRows - 2))
- {
- adjust = RNG.Next(-1, 1);
- }
- if (adjust < 0)
- {
- if (deltax != 0)
- {
- starty--;
- }
- else
- {
- startx--;
- }
- }
- else if (adjust > 0)
- {
- if (deltax != 0)
- {
- starty++;
- }
- else
- {
- startx++;
- }
- }
- map[starty, startx] = water;
- }
- startx += deltax;
- starty += deltay;
+ var (_, delta2) = RiverFromDir(direction2);
+ delta = delta2;
+ sideDir = delta.Perpendicular();
+
+ while (WithinMapBounds(start, 0))
+ {
+ start = AdjustAndPaint(start, delta, sideDir);
+ start += delta;
}
}
public void DrawCanyon(Terrain riverT)
{
+ IntVector2 forward = isHorizontal ? IntVector2.EAST : IntVector2.SOUTH;
+ IntVector2 side = isHorizontal ? IntVector2.SOUTH : IntVector2.EAST;
+ int forwardLen = isHorizontal ? MapColumns : MapRows;
+ int sideLen = isHorizontal ? MapRows : MapColumns;
+ int minDist = Math.Min(sideLen / 2 - 1, 15);
int drawLeft = RNG.Next(0, 5);
int drawRight = RNG.Next(0, 5);
Terrain tleft = climate.GetRandomTerrain(RNG, walkableTerrains);
Terrain tright = climate.GetRandomTerrain(RNG, walkableTerrains);
+ int riverSide = RNG.Next(minDist, sideLen - minDist);
- if (isHorizontal)
+ for (int f = 0; f < forwardLen; f++)
{
- int minDistY = Math.Min(MapRows / 2 - 1, 15);
- int rivery = RNG.Next(minDistY, MapRows - minDistY);
- for (int x = 0; x < MapColumns; x++)
- {
- drawLeft++;
- drawRight++;
- map[rivery, x] = riverT;
- map[rivery + 1, x] = riverT;
- int adjust = RNG.Next(-3, 3);
- int leftM = RNG.Next(14, 17);
- if (rivery - leftM > 0)
- {
- map[rivery - leftM + 3, x] = tleft;
- }
- if (drawLeft % 5 == 0)
- {
- tleft = climate.GetRandomTerrain(RNG, walkableTerrains); ;
- }
- for (int i = rivery - leftM; i >= 0; i--)
- {
- map[i, x] = Terrain.MOUNTAIN;
- }
-
- int rightM = RNG.Next(14, 17);
+ drawLeft++;
+ drawRight++;
+ IntVector2 basePos = f * forward;
+ map[basePos + riverSide * side] = riverT;
+ map[basePos + (riverSide + 1) * side] = riverT;
- if (rivery + rightM < MapRows)
- {
- map[rivery + rightM - 3, x] = tright;
- }
-
- if (drawRight % 5 == 0)
- {
- tright = climate.GetRandomTerrain(RNG, walkableTerrains); ;
- }
- for (int i = rivery + 1 + rightM; i < MapRows; i++)
- {
- map[i, x] = Terrain.MOUNTAIN;
- }
- while (rivery + adjust + 1 > MapRows - minDistY || rivery + adjust < minDistY)
- {
- adjust = RNG.Next(-1, 2);
- }
- if (adjust > 0)
- {
- int curr = 0;
- while (curr < adjust)
- {
- map[rivery, x] = riverT;
- rivery++;
- curr++;
- }
- }
- else
- {
- int curr = 0;
- while (curr > adjust)
- {
- map[rivery, x] = riverT;
- rivery--;
- curr--;
- }
- }
+ int adjust = RNG.Next(-3, isHorizontal ? 3 : 4);
+ int leftM = RNG.Next(14, 17);
+ if (riverSide - leftM > 0)
+ {
+ map[basePos + (riverSide - leftM + 3) * side] = tleft;
}
- }
- else
- {
- int minDistX = Math.Min(MapColumns / 2 - 1, 15);
- int riverx = RNG.Next(minDistX, MapColumns - minDistX);
- for (int y = 0; y < MapRows; y++)
- {
- drawLeft++;
- drawRight++;
- map[y, riverx] = riverT;
- map[y, riverx + 1] = riverT;
- int adjust = RNG.Next(-3, 4);
- int leftM = RNG.Next(14, 17);
- if (riverx - leftM > 0)
- {
- map[y, riverx - leftM + 3] = tleft;
- }
- if (drawLeft % 5 == 0)
- {
- tleft = climate.GetRandomTerrain(RNG, walkableTerrains); ;
- }
- for (int i = riverx - leftM; i >= 0; i--)
- {
- map[y, i] = Terrain.MOUNTAIN;
- }
-
- int rightM = RNG.Next(14, 17);
-
- if (riverx + rightM < MapColumns)
- {
- map[y, riverx + rightM - 3] = tright;
- }
-
- if (drawRight % 5 == 0)
- {
- tright = climate.GetRandomTerrain(RNG, walkableTerrains);
- }
- for (int i = riverx + 1 + rightM; i < MapColumns; i++)
- {
- map[y, i] = Terrain.MOUNTAIN;
- }
- while (riverx + adjust + 1 > MapColumns - minDistX || riverx + adjust < minDistX)
- {
- adjust = RNG.Next(-1, 2);
- }
- if (adjust > 0)
- {
- int curr = 0;
- while (curr < adjust)
- {
- map[y, riverx] = riverT;
- riverx++;
- curr++;
- }
- }
- else
- {
- int curr = 0;
- while (curr > adjust)
- {
- map[y, riverx] = riverT;
- riverx--;
- curr--;
- }
- }
+ if (drawLeft % 5 == 0)
+ {
+ tleft = climate.GetRandomTerrain(RNG, walkableTerrains);
}
-
- }
- }
-
- public void DrawCenterMountain()
- {
- int top = (MapRows - 35) / 2; //20
- int bottom = MapRows - top; //55
- if (isHorizontal)
- {
- //Block out a stripe of mountains where the caldera is going to be
- for (int i = 0; i < MapRows; i++)
+ for (int i = riverSide - leftM; i >= 0; i--)
{
- if (i < top || i > bottom)
- {
- for (int j = 0; j < MapColumns; j++)
- {
- map[i, j] = Terrain.MOUNTAIN;
- }
- }
+ map[basePos + i * side] = Terrain.MOUNTAIN;
}
-
- for (int y = 0; y < 8; y++)
+ int rightM = RNG.Next(14, 17);
+ if (riverSide + rightM < sideLen)
{
- int xstart = MapColumns / 2 - (3 + y); //29 to
- int xend = MapColumns / 2 + (3 + y);
- //map[20 + i, jstart - 1] = Terrain.lava;
- //map[20 + i, jend] = Terrain.lava;
- for (int x = xstart; x < xend; x++)
- {
- map[top + y, x] = Terrain.MOUNTAIN;
- }
+ map[basePos + (riverSide + rightM - 3) * side] = tright;
}
- for (int i = 0; i < 19; i++)
+ if (drawRight % 5 == 0)
{
- //map[28 + i, MAP_COLS / 2 - 11] = Terrain.lava;
- //map[28 + i, MAP_COLS / 2 - 10 + 21] = Terrain.lava;
-
- for (int j = 0; j < 20; j++)
- {
- map[top + 8 + i, MapColumns / 2 - 10 + j] = Terrain.MOUNTAIN;
- }
+ tright = climate.GetRandomTerrain(RNG, walkableTerrains);
}
- for (int i = 0; i < 8; i++)
+ for (int i = riverSide + 1 + rightM; i < sideLen; i++)
{
- int jstart = MapColumns / 2 - (3 + (6 - i));
- int jend = MapColumns / 2 + (3 + (6 - i));
- //map[47 + i, jstart - 1] = Terrain.lava;
- //map[47 + i, jend] = Terrain.lava;
- for (int j = jstart; j < jend; j++)
- {
- map[top + 27 + i, j] = Terrain.MOUNTAIN;
- }
+ map[basePos + i * side] = Terrain.MOUNTAIN;
+ }
+ while (riverSide + adjust + 1 > sideLen - minDist || riverSide + adjust < minDist)
+ {
+ adjust = RNG.Next(-1, 2);
+ }
+ int oldSide = riverSide;
+ riverSide += adjust;
+ for (int s = oldSide; adjust > 0 ? s < riverSide : s > riverSide; s += Math.Sign(adjust))
+ {
+ map[basePos + s * side] = riverT;
}
}
- else
+ }
+
+ public void DrawCenterMountain()
+ {
+ IntVector2 forward = isHorizontal ? IntVector2.SOUTH : IntVector2.EAST;
+ IntVector2 side = isHorizontal ? IntVector2.EAST : IntVector2.SOUTH;
+ int forwardLen = isHorizontal ? MapRows : MapColumns;
+ int sideLen = isHorizontal ? MapColumns : MapRows;
+ int top = (forwardLen - 35) / 2;
+ int bottom = forwardLen - top;
+ int sideCenter = sideLen / 2;
+
+ // Block out stripes outside caldera zone
+ for (int f = 0; f < forwardLen; f++)
{
- top = (MapColumns - 35) / 2;
- bottom = MapColumns - top;
- for (int i = 0; i < MapColumns; i++)
+ if (f < top || f > bottom)
{
- if (i < top || i > bottom)
+ for (int s = 0; s < sideLen; s++)
{
- for (int j = 0; j < MapRows; j++)
- {
- map[j, i] = Terrain.MOUNTAIN;
- }
+ map[f * forward + s * side] = Terrain.MOUNTAIN;
}
}
+ }
- for (int i = 0; i < 8; i++)
+ // Top triangle (widening)
+ for (int y = 0; y < 8; y++)
+ {
+ int half = 3 + y;
+ for (int s = sideCenter - half; s < sideCenter + half; s++)
{
- int jstart = MapRows / 2 - (3 + i);
- int jend = MapRows / 2 + (3 + i);
- //map[20 + i, jstart - 1] = Terrain.lava;
- //map[20 + i, jend] = Terrain.lava;
- for (int j = jstart; j < jend; j++)
- {
-
- map[j, top + i] = Terrain.MOUNTAIN;
- }
+ map[(top + y) * forward + s * side] = Terrain.MOUNTAIN;
}
- for (int i = 0; i < 19; i++)
- {
- //map[28 + i, MAP_COLS / 2 - 11] = Terrain.lava;
- //map[28 + i, MAP_COLS / 2 - 10 + 21] = Terrain.lava;
+ }
- for (int j = 0; j < 20; j++)
- {
- map[MapRows / 2 - 10 + j, top + 8 + i] = Terrain.MOUNTAIN;
- }
+ // Middle rectangle
+ for (int i = 0; i < 19; i++)
+ {
+ for (int s = sideCenter - 10; s < sideCenter + 10; s++)
+ {
+ map[(top + 8 + i) * forward + s * side] = Terrain.MOUNTAIN;
}
- for (int i = 0; i < 8; i++)
+ }
+
+ // Bottom triangle (narrowing)
+ for (int i = 0; i < 8; i++)
+ {
+ int half = 3 + (6 - i);
+ for (int s = sideCenter - half; s < sideCenter + half; s++)
{
- int jstart = MapRows / 2 - (3 + (6 - i));
- int jend = MapRows / 2 + (3 + (6 - i));
- //map[47 + i, jstart - 1] = Terrain.lava;
- //map[47 + i, jend] = Terrain.lava;
- for (int j = jstart; j < jend; j++)
- {
- map[j, top + 27 + i] = Terrain.MOUNTAIN;
- }
+ map[(top + 27 + i) * forward + s * side] = Terrain.MOUNTAIN;
}
}
}
@@ -2704,8 +2343,8 @@ protected bool WithinMapBounds(IntVector2 pos)
}
///
- /// Useful variant to test if something is too close to the
- /// edge of the map.
+ /// Useful variant to verify that a coordinate is some margin away
+ /// from the edge of the map.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool WithinMapBounds(IntVector2 pos, int margin)
@@ -2915,7 +2554,7 @@ bool checkForExistingLocation(IntVector2 pos)
{
if (WithinMapBounds(pos))
{
- var loc = GetLocationByPos(pos);
+ var loc = GetLocationAt(pos);
return loc != null;
}
return false;
@@ -2971,6 +2610,11 @@ public void PlaceCaveBlocker(Location cave, IntVector2 dir, Terrain blockerTerra
cave.Pos = newCavePos;
}
+ protected virtual bool IsReserved(IntVector2 pos)
+ {
+ return false;
+ }
+
///
/// Check if the position is suitable as a trap tile. It should
/// - Be the center tile of a 3-tile long path.
@@ -3010,7 +2654,7 @@ public void PlaceCaveBlocker(Location cave, IntVector2 dir, Terrain blockerTerra
for (int i = -1; i <= 1; i++)
{
var p = pos + i * dir;
- var l = GetLocationByPos(p);
+ var l = GetLocationAt(p);
if (l != null)
{
return null;
@@ -3020,6 +2664,46 @@ public void PlaceCaveBlocker(Location cave, IntVector2 dir, Terrain blockerTerra
return dir;
}
+ ///
+ /// Check if the position is suitable as a MI drop tile. It should
+ /// - Be on a walkable path.
+ /// - There must be no adjacent special locations.
+ /// - At most 2 adjacent tiles should be passable.
+ ///
+ public bool ValidMazeDropPosition(IntVector2 pos)
+ {
+ bool isPassable(IntVector2 pos) => TRAP_PATH_VALID_TERRAIN.Contains(map[pos.Y, pos.X]);
+
+ if (!WithinMapBounds(pos, 1)) { return false; }
+ if (!isPassable(pos)) { return false; }
+
+ var dx = IntVector2.EAST;
+ var dy = IntVector2.SOUTH;
+ bool h1 = isPassable(pos + dx);
+ bool h2 = isPassable(pos - dx);
+ bool v1 = isPassable(pos + dy);
+ bool v2 = isPassable(pos - dy);
+ int passableCount = (h1 ? 1 : 0) + (h2 ? 1 : 0) + (v1 ? 1 : 0) + (v2 ? 1 : 0);
+
+ if (passableCount == 0 || passableCount > 2)
+ {
+ return false;
+ }
+
+ // expensive Location check last
+ foreach (var dir in IntVector2.CARDINALS)
+ {
+ var adjacent = pos + dir;
+ var l = GetLocationAt(adjacent);
+ if (l != null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public string GetGlobDebug(int[,] globs)
{
StringBuilder debug = new();
@@ -3214,7 +2898,30 @@ public void SynchronizeLinkedLocations()
}
}
- public abstract void UpdateVisit(List requireables);
+ ///
+ /// Updates the visitation matrix and location reachability
+ ///
+ public virtual void UpdateVisit(IReadOnlySet requireables)
+ {
+ UpdateReachable(requireables);
+
+ foreach (Location location in AllLocations)
+ {
+ if (location.Y > 0 && visitation[location.Y, location.Xpos])
+ {
+ if (location.AccessRequirements.AreSatisfiedBy(requireables))
+ {
+ location.Reachable = true;
+ if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables))
+ {
+ Location connectedLocation = connections[location];
+ connectedLocation.Reachable = true;
+ visitation[connectedLocation.Y, connectedLocation.Xpos] = true;
+ }
+ }
+ }
+ }
+ }
public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto);
diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs
index 4c345ea36..3fac62c65 100644
--- a/RandomizerCore/ROM.cs
+++ b/RandomizerCore/ROM.cs
@@ -924,17 +924,18 @@ jmp LoadNewLevelCap
nop
LoadNewLevelCapReturn: ; $9F7F
-.org $A89E
+.reloc
LoadNewLevelCap:
lda $0777,X ; the instruction we overwrote with jmp
cmp LevelCaps,X
jmp LoadNewLevelCapReturn
""");
- a.Org(0xa8a7);
+ a.Reloc();
a.Label("LevelCaps");
a.Byt((byte)atkMax);
a.Byt((byte)magicMax);
a.Byt((byte)lifeMax);
+ a.Export("LevelCaps");
}
///
@@ -953,6 +954,8 @@ public void ChangeLevelUpCancelling(Assembler asm)
a.Code("""
; This is called when the game checks if it should skip the cancel option
; in the level up menu or not. It runs once for each stat (X).
+.import LevelCaps
+
.segment "PRG0"
.org $A0D4
jmp CheckIfStatMaxed
@@ -976,8 +979,6 @@ asl a
asl a
jmp CheckStatNormally
""");
- a.Org(0xa8a7);
- a.Label("LevelCaps");
}
public void UseExtendedBanksForPalaceRooms(Assembler a)
@@ -2048,7 +2049,7 @@ public void DashSpell(Assembler asm)
a.Byt(dash);
}
- public void CombineFireSpell(Assembler asm, List? customSpellOrder, Random RNG)
+ public void CombineFireSpell(Assembler asm, Collectable linkedSpell, List? customSpellOrder)
{
// These are the bit flags for each spell. The old implementation
// changed this table directly. However as this table is also used in
@@ -2058,8 +2059,7 @@ public void CombineFireSpell(Assembler asm, List? customSpellOrder,
byte[] spellBytes = GetBytes(0xdcb, 8);
int fireSpellIndex = 4;
- int r = RNG.Next(7);
- int linkedSpellIndex = r > 3 ? r + 1 : r;
+ int linkedSpellIndex = linkedSpell.VanillaSpellOrder();
byte combinedSpellBits = (byte)(spellBytes[linkedSpellIndex] | spellBytes[fireSpellIndex]);
spellBytes[fireSpellIndex] = combinedSpellBits;
@@ -2086,67 +2086,116 @@ public void MoveAfterGem()
Put(0x11af5, new byte[] { 0x47, 0x9b, 0x56, 0x9b, 0x35, 0x9b });
}
- public void ElevatorBossFix(bool bossItem)
+ public void HandleRandomBossDrop(Assembler asm)
{
- /*
- * Notes:
- *
- * Screen lock set at bank 4 BE99 (0x13ea9)
- * Screen lock tbird set at bank 5 A363 (0x16373)
- * Screen lock released at bank 7 E7A9 (0x1e7b9)
- *
- * Jump to subroutine at all three locations above:
- *
- * 20 40 F3
- *
- * Subroutine at 1F350:
- *
- * Load accumulator with 1 (A9 01)
- * EOR 728 (4D 28 07)
- * store 728 (8D 28 07)
- * load accumulator with 13 (A9 13)
- * compare a2 with 0x13 (C5 A2)
- * branch if not equal to the end (D0 0A)
- * Load accumulator with 1 (a9 01)
- * EOR b6 (45 b6)
- * store b6 (85 b6)
- * load accumulator with a0 (a9 a0)
- * Set 2a to 0xa0 (85 2a)
- * return (60)
- */
+ var a = asm.Module();
+ a.Code(/* lang=s */"""
+.include "z2r.inc"
+.import ElevatorBossFix
- // jsr $f340
- var jsrF340 = new byte[] { 0x20, 0x40, 0xF3 };
- Put(0x13ea9, jsrF340);
- Put(0x16373, jsrF340);
- Put(0x13230, jsrF340);
+.segment "PRG7"
+.org $e79a
+ ; Branch if scroll frozen
+ lda ScrollFrozen
+ beq +
+ ; freeze scroll
+ lda #0
+ jsr ElevatorBossFix
+ ; branch if the music is already playing
+ lda $07fb
+ bne +
+ ; otherwise resume the previous track (palace theme)
+ lda #2
+ sta $eb
+ +
+ ; Write the "grab item" sound effect to the sfx queue
+ lda #8
+ sta Z2Square1SoundQueue
+ ; Branch if the item we are getting is NOT a key
+ cpy #8
+ bne +
+ ; increment number of keys and carry on
+ inc Keys
+ jmp $e797 ; always overwritten by full_item_shuffle anyway
+ +
+ ; Otherwise continue to $E7BB which is the start of the get item code
+ .assert * = $E7BB
+
+; Patch a few locations to make sure the music returns to normal after getting an item
+.org $e80c
+ jsr DontSwitchMusicIfInPalace1
+ nop
- if (!bossItem)
- {
- Put(0x1e7b9, jsrF340);
- }
- else
- {
- Put(0x1e7b1, jsrF340);
- }
+.org $e84b
+ jsr DontSwitchMusicIfInPalace2
+ nop
- /*
- * Patched function at $f340
- * lda #1
- * eor $0728 ; _728_FreezeScrolling un freeze scrolling if frozen, otherwise freeze it.
- * sta $0728 ; _728_FreezeScrolling
- * lda #$13 ; check if the enemy id is 0x13
- * cmp $a1 ; enemy slot "6" (index 0) current ID
- * bne + ; * + 10
- * lda #1 ; if it is equal, then flip $b6 (holds the enemy status?)
- * eor $b6
- * sta $b6
- * lda #$a0 ; and set the enemy y position to 0xa0
- * sta $2a
- * +
- * rts
- */
- Put(0x1F350, new byte[] { 0xa9, 0x01, 0x4d, 0x28, 0x07, 0x8d, 0x28, 0x07, 0xa9, 0x13, 0xc5, 0xa1, 0xd0, 0x0a, 0xa9, 0x01, 0x45, 0xb6, 0x85, 0xb6, 0xa9, 0xa0, 0x85, 0x2a, 0x60 });
+.reloc
+DontSwitchMusicIfInPalace1:
+ lda $eb
+ cmp #$02
+ beq +
+ ; Restore track 16
+ lda #$10
+ sta $eb
++ rts
+
+DontSwitchMusicIfInPalace2:
+ lda $eb
+ cmp #$02
+ beq +
+ ; Restore track 0
+ lda #$00
+ sta $eb
++ rts
+""");
+ }
+
+ public void ElevatorBossFix(Assembler asm, bool randomBossItem)
+ {
+ var a = asm.Module();
+ a.Assign("RANDOM_BOSS_ITEM", randomBossItem ? 1 : 0);
+ a.Code(/* lang=s */"""
+.include "z2r.inc"
+
+.segment "PRG4"
+.org $b220
+ jsr ElevatorBossFix
+
+.org $be99 ; Screen lock set at bank 4 BE99 (0x13ea9)
+ jsr ElevatorBossFix
+
+; Screen lock tbird set at bank 5 A363 (0x16373)
+.segment "PRG5"
+.org $a363
+ jsr ElevatorBossFix
+
+.segment "PRG7"
+
+; Screen lock released at bank 7 E7A9 (0x1e7b9)
+.if !RANDOM_BOSS_ITEM
+ .org $e7a9
+ jsr ElevatorBossFix
+.endif
+
+.org $f340 ; this should probably be free'd and realloc'd
+ElevatorBossFix:
+ lda #$01
+ eor ScrollFrozen ; unfreeze scrolling if frozen, otherwise freeze it
+ sta ScrollFrozen
+ lda #$13
+ cmp Enemy0Type
+ bne @Exit
+ lda #$01
+ eor Enemy0Status
+ sta Enemy0Status
+ lda #$a0
+ sta Enemy0YPositionLo
+ @Exit:
+ rts
+.export ElevatorBossFix
+
+""");
}
public void AdjustGpProjectileDamage()
@@ -2619,6 +2668,169 @@ public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto
}
}
+ public void SetEncounterRate(Assembler asm, RandomizerProperties props, Random r)
+ {
+ List encounterRates = [props.EncounterRates, props.EncounterRates, props.EncounterRates, props.EncounterRates];
+ List randomCandidates = Enums.GetShufflableList();
+ encounterRates = [.. encounterRates.Select(val => val is EncounterRate.RANDOM ? randomCandidates.Sample(r) : val)];
+
+ bool allNoEncounters = encounterRates.All(val => val is EncounterRate.NONE);
+ bool anyHalfEncounters = encounterRates.Any(val => val is EncounterRate.HALF);
+ bool allNormalEncounters = encounterRates.All(val => val is EncounterRate.NORMAL);
+ bool differentRates = encounterRates.Distinct().Count() > 1;
+ Debug.Assert(allNoEncounters || anyHalfEncounters || allNormalEncounters || differentRates);
+
+ var a = asm.Module();
+ a.Set("NO_ENCOUNTERS", allNoEncounters ? 1 : 0);
+ a.Set("HAS_HALF_ENCOUNTERS", anyHalfEncounters ? 1 : 0);
+ a.Set("NORMAL_ENCOUNTERS", allNormalEncounters ? 1 : 0);
+ a.Set("VANILLA_WEST", props.WestBiome.UsesVanillaMap() ? 1 : 0);
+ a.Set("ENCOUNTER_RATE_PER_CONTINENT", differentRates ? 1 : 0);
+
+ if (differentRates)
+ {
+ byte[] encounterTable = [.. encounterRates.Select(o => o.GetAsmByte())];
+ a.Segment("PRG0");
+ a.Reloc();
+ a.Label("EncounterRateRegionTable");
+ a.Byt(encounterTable);
+ }
+
+ a.Code(/* lang=s */"""
+.include "z2r.inc"
+
+.segment "PRG0"
+
+EncounterTerrainTimerTable = $823f
+OverworldEncounterTick = $8284
+VanillaRegionCheck = $8287
+CheckStepCounter = $828f
+CheckTimer = $8293
+SpawnEncounter = $8298
+SetTerrainEncounterTimer = $82b4
+HammerTileTable = $84ad
+OverworldMainJsr = $8563
+SetInitialEncounterTimer = $8879
+
+.if NO_ENCOUNTERS
+ .org OverworldEncounterTick
+ rts
+ FREE_UNTIL HammerTileTable
+.endif
+
+.if ENCOUNTER_RATE_PER_CONTINENT
+ .org OverworldEncounterTick
+ jmp OverworldEncounterTickHook
+
+ .reloc
+ OverworldEncounterTickHook:
+ ldy RegionNumber
+ lda EncounterRateRegionTable,y
+ cmp #1
+ beq @none ; A == 1
+ bcc @normal ; A < 1
+ @half: ; A > 1
+ .if HAS_HALF_ENCOUNTERS
+ jmp ExtraEncounterStepCheck
+ .endif
+ @normal:
+ .if VANILLA_WEST
+ tya
+ jmp VanillaRegionCheck ; preserve special vanilla West behavior where steps are not counted in the north
+ .else
+ jmp CheckStepCounter
+ .endif
+ @none:
+ rts
+
+ .org SetInitialEncounterTimer + 2
+ jsr SetInitialEncounterTimerHook
+
+ .reloc
+ SetInitialEncounterTimerHook:
+ ldx RegionNumber
+ lda EncounterRateRegionTable,x
+ beq @normal
+ @half:
+ lda #02
+ sta OverworldStepCounterHi
+ lda #$10
+ bne @done
+ @normal:
+ lda #$08
+ @done:
+ sta EncounterSpawnTimer
+ rts
+
+ .org SetTerrainEncounterTimer
+ jsr SetTerrainEncounterTimerHook
+
+ .reloc
+ SetTerrainEncounterTimerHook:
+ ldx RegionNumber
+ lda EncounterRateRegionTable,x
+ beq @normal
+ @half:
+ lda EncounterTerrainTimerTable,y
+ asl ; multiply timer duration by 2
+ rts
+ @normal:
+ lda EncounterTerrainTimerTable,y
+ rts
+.else ; not EncounterRatePerContinent
+ .if NORMAL_ENCOUNTERS
+ .if !VANILLA_WEST
+ .org OverworldEncounterTick
+ FREE_UNTIL CheckStepCounter
+ .org OverworldMainJsr
+ jsr CheckStepCounter ; overwriting jsr OverworldEncounterTick
+ .endif
+ .endif
+
+ .if HAS_HALF_ENCOUNTERS
+ .org EncounterTerrainTimerTable + 1
+ .byt $40 ; grass
+ .byt $30 ; desert
+ .byt $30 ; forest
+ .byt $40 ; swamp
+ .byt $12 ; graveyard
+ .byt $06 ; lava
+
+ .org OverworldEncounterTick
+ FREE_UNTIL CheckTimer
+ .org OverworldMainJsr
+ jsr ExtraEncounterStepCheck ; overwriting jsr OverworldEncounterTick
+
+ .org SetInitialEncounterTimer
+ lda #$10
+ jsr SetInitialEncounterTimerHook2
+
+ .reloc
+ SetInitialEncounterTimerHook2:
+ sta EncounterSpawnTimer
+ lda #02
+ sta OverworldStepCounterHi
+ rts
+ .endif
+.endif
+
+.if HAS_HALF_ENCOUNTERS
+ .reloc
+ ExtraEncounterStepCheck:
+ lda OverworldStepCounter
+ bne @exit
+ dec OverworldStepCounterHi
+ bne @exit
+ lda #02
+ sta OverworldStepCounterHi
+ jmp SpawnEncounter
+ @exit:
+ jmp CheckTimer
+.endif
+
+""");
+ }
+
public void UpdateItem(Collectable item, Room room)
{
int sideviewPtrAddr = room.GetSideviewPtrRomAddr();
diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs
index 89559ed0d..225398a0b 100644
--- a/RandomizerCore/RandomizerConfiguration.cs
+++ b/RandomizerCore/RandomizerConfiguration.cs
@@ -4,8 +4,6 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
using System.Runtime.CompilerServices;
using NLog;
using Z2Randomizer.RandomizerCore.Flags;
@@ -77,6 +75,16 @@ public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged
Collectable.THUNDER_SPELL
];
+ [IgnoreInFlags]
+ private readonly static Collectable[] POSSIBLE_LINKED_FIRE_SPELLS = [
+ Collectable.SHIELD_SPELL,
+ Collectable.JUMP_SPELL,
+ Collectable.LIFE_SPELL,
+ Collectable.FAIRY_SPELL,
+ Collectable.REFLECT_SPELL,
+ Collectable.SPELL_SPELL,
+ Collectable.THUNDER_SPELL
+ ];
//Start Configuration
[Reactive]
@@ -274,10 +282,10 @@ public sealed partial class RandomizerConfiguration() : INotifyPropertyChanged
private Biome mazeBiome = Biome.VANILLA;
[Reactive]
- private ClimateEnum westClimate = ClimateEnum.CLASSIC;
+ private ClimateEnum westClimate = ClimateEnum.VANILLA_WEIGHTED;
[Reactive]
- private ClimateEnum eastClimate = ClimateEnum.CLASSIC;
+ private ClimateEnum eastClimate = ClimateEnum.VANILLA_WEIGHTED;
[Reactive]
private ClimateEnum dmClimate = ClimateEnum.CLASSIC;
@@ -337,6 +345,11 @@ private bool palaceStylesAreNotAllVanillaOrShuffled()
return false;
}
+ private bool roomSelectionEnabled()
+ {
+ return palaceStylesAreNotAllVanillaOrShuffled();
+ }
+
private bool palaceStylesAnyMetastyleSelected()
{
foreach (var style in (List)[normalPalaceStyle, gpStyle])
@@ -355,26 +368,28 @@ private bool palaceStylesAnyMetastyleSelected()
public bool randomStylesAllowVanillaIncluded() => palaceStylesAnyMetastyleSelected();
[Reactive]
+ [ConditionallyIncludeInFlags]
private bool? includeVanillaRooms = true;
+ public bool includeVanillaRoomsIncluded() => roomSelectionEnabled();
[Reactive]
[ConditionallyIncludeInFlags]
private bool? includev4_0Rooms = false;
- public bool includev4_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled();
+ public bool includev4_0RoomsIncluded() => roomSelectionEnabled();
[Reactive]
[ConditionallyIncludeInFlags]
private bool? includev5_0Rooms = false;
- public bool includev5_0RoomsIncluded() => palaceStylesAreNotAllVanillaOrShuffled();
+ public bool includev5_0RoomsIncluded() => roomSelectionEnabled();
[Reactive]
[ConditionallyIncludeInFlags]
private bool blockingRoomsInAnyPalace = false;
- public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanillaOrShuffled();
+ public bool blockingRoomsInAnyPalaceIncluded() => palaceStylesAreNotAllVanilla();
[Reactive]
- private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ENTRANCE;
- public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanillaOrShuffled();
+ private PalaceDropStyle palaceDropStyle = PalaceDropStyle.ANY_EXIT;
+ public bool palaceDropStyleIncluded() => palaceStylesAreNotAllVanilla();
[Reactive]
[ConditionallyIncludeInFlags]
@@ -384,12 +399,12 @@ private bool palaceStylesAnyMetastyleSelected()
[Reactive]
[ConditionallyIncludeInFlags]
private bool includeExpertRooms = false;
- public bool includeExpertRoomsIncluded() => palaceStylesAreNotAllVanilla();
+ public bool includeExpertRoomsIncluded() => roomSelectionEnabled();
[Reactive]
[ConditionallyIncludeInFlags]
private BossRoomsExitType bossRoomsExitType = BossRoomsExitType.OVERWORLD;
- public bool bossRoomsExitTypeIncluded() => palaceStylesAreNotAllVanillaOrShuffled();
+ public bool bossRoomsExitTypeIncluded() => roomSelectionEnabled();
[Reactive]
[ConditionallyIncludeInFlags]
@@ -433,7 +448,6 @@ private bool palaceStylesAnyMetastyleSelected()
[Minimum(0)]
[Maximum(6)]
[ConditionallyIncludeInFlags]
- [DefaultValue(6)]
private int palacesToCompleteMax = 6;
public bool palacesToCompleteMaxIncluded() => palacesToCompleteMin != 6;
@@ -556,7 +570,7 @@ private bool palaceStylesAnyMetastyleSelected()
private bool shuffleXPStolenAmount = false;
[Reactive]
- private bool shuffleSwordImmunity = false;
+ private SwordImmunityOption swordImmunityOption = SwordImmunityOption.VANILLA;
[Reactive]
[DifficultyOnly]
@@ -729,7 +743,7 @@ private bool palaceStylesAnyMetastyleSelected()
[Reactive]
[IgnoreInFlags]
- private bool removeFlashing = false;
+ private bool removeFlashing = true;
[Reactive]
[IgnoreInFlags]
@@ -794,7 +808,7 @@ private bool palaceStylesAnyMetastyleSelected()
private RiverDevilBlockerOption riverDevilBlockerOption = RiverDevilBlockerOption.PATH;
[Reactive]
- private bool? eastRocks = false;
+ private bool? eastRocks = true;
[Reactive]
private bool generateSpoiler = false;
@@ -812,9 +826,16 @@ private bool palaceStylesAnyMetastyleSelected()
private string? seed;
// public string Seed { get => seed ?? ""; set => SetField(ref seed, value); }
+ [IgnoreInFlags]
+ private bool _inDeserializeFlags = false;
+
public void DeserializeFlags(string flags)
{
+ // avoid emitting property changed for Flags during deserialization
+ _inDeserializeFlags = true;
Deserialize(flags?.Trim() ?? "");
+ _inDeserializeFlags = false;
+ OnPropertyChanged("Flags");
}
public String SerializeFlags()
{
@@ -991,38 +1012,43 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true)
} while (!properties.HasEnoughSpaceToAllocateItems());
//Handle Fire
+ Collectable RollLinkedFireSpell()
+ {
+ return POSSIBLE_LINKED_FIRE_SPELLS[r.Next(POSSIBLE_LINKED_FIRE_SPELLS.Length)];
+ }
switch (fireOption)
{
case FireOption.NORMAL:
- properties.CombineFire = false;
+ properties.LinkedFireSpell = null;
properties.ReplaceFireWithDash = false;
break;
case FireOption.PAIR_WITH_RANDOM:
- properties.CombineFire = true;
+ properties.LinkedFireSpell = RollLinkedFireSpell();
properties.ReplaceFireWithDash = false;
break;
case FireOption.REPLACE_WITH_DASH:
- properties.CombineFire = false;
+ properties.LinkedFireSpell = null;
properties.ReplaceFireWithDash = true;
break;
case FireOption.RANDOM:
switch (r.Next(3))
{
case 0:
- properties.CombineFire = false;
+ properties.LinkedFireSpell = null;
properties.ReplaceFireWithDash = false;
break;
case 1:
- properties.CombineFire = true;
+ properties.LinkedFireSpell = RollLinkedFireSpell();
properties.ReplaceFireWithDash = false;
break;
case 2:
- properties.CombineFire = false;
+ properties.LinkedFireSpell = null;
properties.ReplaceFireWithDash = true;
break;
-
}
break;
+ default:
+ throw new Exception("Illegal Fire option");
}
ResolveStartingTechniques(properties, r, includeDifficulty);
@@ -1294,7 +1320,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true)
properties.ShuffleBossHP = includeDifficulty ? shuffleBossHP : EnemyLifeOption.VANILLA;
properties.ShuffleEnemyStealExp = shuffleXPStealers;
properties.ShuffleStealExpAmt = shuffleXPStolenAmount;
- properties.ShuffleSwordImmunity = shuffleSwordImmunity;
+ properties.SwordImmunityOption = swordImmunityOption;
properties.ShuffleOverworldEnemies = shuffleOverworldEnemies ?? GetIndeterminateFlagValue(r);
properties.ShufflePalaceEnemies = shufflePalaceEnemies ?? GetIndeterminateFlagValue(r);
properties.MixLargeAndSmallEnemies = mixLargeAndSmallEnemies ?? GetIndeterminateFlagValue(r);
@@ -1507,7 +1533,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true)
if (properties.ReplaceFireWithDash)
{
- properties.CombineFire = false;
+ Debug.Assert(properties.LinkedFireSpell == null);
}
//If spells are in the shuffle pool, shuffle spells means nothing, so diable it
@@ -1962,28 +1988,4 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
-
- ///
- /// Should be called with the field to reset as the expression body:
- /// SetFieldToDefault(() => SettingToReset);
- ///
- public void SetFieldToDefault(Expression> fieldExpr)
- {
- if (fieldExpr.Body is not MemberExpression member || member.Member is not FieldInfo field)
- {
- throw new ArgumentException("Expression must reference a field");
- }
-
- var target = ((ConstantExpression)member.Expression!).Value;
-
- var attr = field.GetCustomAttribute();
- if (attr != null)
- {
- field.SetValue(target, attr.Value);
- }
- else
- {
- field.SetValue(target, default);
- }
- }
}
diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj
index afe3c142d..78116bdc6 100644
--- a/RandomizerCore/RandomizerCore.csproj
+++ b/RandomizerCore/RandomizerCore.csproj
@@ -57,7 +57,7 @@
-
+
diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs
index d947cf240..96ec6a1e3 100644
--- a/RandomizerCore/RandomizerProperties.cs
+++ b/RandomizerCore/RandomizerProperties.cs
@@ -48,7 +48,7 @@ public class RandomizerProperties
public bool StartReflect { get; set; }
public bool StartSpell { get; set; }
public bool StartThunder { get; set; }
- public bool CombineFire { get; set; }
+ public Collectable? LinkedFireSpell { get; set; }
public bool ReplaceFireWithDash { get; set; }
public StartingResourceLimit StartSpellsLimit { get; set; }
@@ -144,7 +144,7 @@ public class RandomizerProperties
public EnemyLifeOption ShuffleBossHP { get; set; }
public bool ShuffleEnemyStealExp { get; set; }
public bool ShuffleStealExpAmt { get; set; }
- public bool ShuffleSwordImmunity { get; set; }
+ public SwordImmunityOption SwordImmunityOption;
public bool ShuffleOverworldEnemies { get; set; }
public bool ShufflePalaceEnemies { get; set; }
public bool MixLargeAndSmallEnemies { get; set; }
@@ -221,6 +221,7 @@ public class RandomizerProperties
public bool JumpAlwaysOn { get; set; }
public bool DashAlwaysOn { get; set; }
public bool FastCast { get; set; }
+
public BeamSprites BeamSprite { get; set; }
public bool DisableMusic { get; set; }
public bool RandomizeMusic { get; set; }
diff --git a/RandomizerCore/Requirements.cs b/RandomizerCore/Requirements.cs
index 54534f6d1..e358d7a2c 100644
--- a/RandomizerCore/Requirements.cs
+++ b/RandomizerCore/Requirements.cs
@@ -100,24 +100,23 @@ public override string ToString()
return Serialize();
}
- public bool AreSatisfiedBy(IEnumerable requireables, bool enforceImplicitRequirements = true)
+ public bool AreSatisfiedBy(IReadOnlySet requireables, bool enforceImplicitRequirements = true)
{
if(IndividualRequirements.Length + CompositeRequirements.Length == 0)
{
return true;
}
var individualRequirementsSatisfied = false;
- var requirementTypes = requireables as RequirementType[] ?? requireables.ToArray();
foreach (var requirement in IndividualRequirements)
{
- if (requirementTypes.Contains(requirement))
+ if (requireables.Contains(requirement))
{
individualRequirementsSatisfied = true;
if (enforceImplicitRequirements && ImplicitRequirements.ContainsKey(requirement))
{
foreach(RequirementType implicitRequirement in ImplicitRequirements[requirement])
{
- if(!requirementTypes.Contains(implicitRequirement))
+ if(!requireables.Contains(implicitRequirement))
{
individualRequirementsSatisfied = false;
continue;
@@ -158,14 +157,13 @@ public bool AreSatisfiedBy(IEnumerable requireables, bool enfor
return individualRequirementsSatisfied || compositeRequirementSatisfied;
}
- public bool AreSatisfiedBy(IEnumerable requireables, StatRandomizer statRoll)
+ public bool AreSatisfiedBy(IReadOnlySet requireables, StatRandomizer statRoll)
{
if (IndividualRequirements.Length + CompositeRequirements.Length == 0)
{
return true;
}
var individualRequirementsSatisfied = false;
- var requirementTypes = requireables as RequirementType[] ?? requireables.ToArray();
statRoll.AssertHasRandomized();
@@ -177,7 +175,7 @@ bool StatAdjustedContainerRequirementSatisfied(RequirementType requirement)
var requiredLevel = ImplicitMagicLevelRequirements[requirement];
var magicCost = statRoll.GetSpellCost(collectable, requiredLevel);
var containerRequirement = MagicContainerRequirementFromCost(magicCost);
- return requirementTypes.Contains(containerRequirement);
+ return requireables.Contains(containerRequirement);
}
else
{
@@ -187,7 +185,7 @@ bool StatAdjustedContainerRequirementSatisfied(RequirementType requirement)
foreach (var requirement in IndividualRequirements)
{
- if (requirementTypes.Contains(requirement))
+ if (requireables.Contains(requirement))
{
individualRequirementsSatisfied = true;
if (!StatAdjustedContainerRequirementSatisfied(requirement))
diff --git a/RandomizerCore/RomMap.cs b/RandomizerCore/RomMap.cs
index c653641fe..a2740f886 100644
--- a/RandomizerCore/RomMap.cs
+++ b/RandomizerCore/RomMap.cs
@@ -127,6 +127,12 @@ class RomMap
// (0x15454, 0x158aa), // Dark Link
];
+ public const int BOSS_DROP_COLLECTABLE = 0x1de29;
+ public const int PBAG_XP_TABLE = 0x1e800;
+ public const int SMALL_DROP_TABLE = 0x1e880;
+ public const int LARGE_DROP_TABLE = 0x1e888;
+ public const int ENEMY_DROP_FREQUENCY = 0x1e8b0;
+
public const int WEST_PALETTE_TABLE = 0x401e;
public const int EAST_PALETTE_TABLE = 0x801e;
public const int TOWN_PALETTE_TABLE = 0xc01e;
diff --git a/RandomizerCore/Shuffler.cs b/RandomizerCore/Shuffler.cs
index 7f70b4d3a..8ea6bfb03 100644
--- a/RandomizerCore/Shuffler.cs
+++ b/RandomizerCore/Shuffler.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using js65;
+using System.Collections.Generic;
using NLog;
using Z2Randomizer.RandomizerCore.Sidescroll;
@@ -16,33 +13,9 @@ public class Shuffler
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
-
- private static readonly List bossRooms = new List { 13, 34, 41 }; //break this up by palace group
- private static readonly List bossRooms2 = new List { 14, 28, 58 }; //break this up by palace group
- private static readonly List bossRooms3 = new List { 53, 54 };
-
- private readonly int[] drops = { 0x8a, 0x8b, 0x8c, 0x8d, 0x90, 0x91, 0x92, 0x88 };//items that can be dropped
-
-
-
- //instance variables
- private RandomizerProperties props;
- //private ROM ROMData;
- //private Character link;
- //private Random R1;
- //public Random R { get => R1; set => R1 = value; }
- public RandomizerProperties Props { get => props; set => props = value; }
-
-
-
- public Shuffler(RandomizerProperties props)
- {
- this.props = props;
- }
-
//None of these methods should have a reference to the ROM and write their own output.
//All of these should output their results, and then those results should be written to the output state.
- public void ShufflePalacePalettes(ROM ROMData, Random r)
+ public static void ShufflePalacePalettes(ROM ROMData, Random r)
{
List brickList = new List();
List curtainList = new List();
@@ -70,213 +43,4 @@ public void ShufflePalacePalettes(ROM ROMData, Random r)
ROMData.WritePalacePalettes(brickList, curtainList, bRows, binRows);
}
-
- public void ShuffleDrops(ROM ROMData, Random r)
- {
- List small = [];
- List large = [];
-
-
- if (props.Smallbluejar)
- {
- small.Add(0x90);
- }
- if (props.Smallredjar)
- {
- small.Add(0x91);
- }
- if (props.Small50)
- {
- small.Add(0x8a);
- }
- if (props.Small100)
- {
- small.Add(0x8b);
- }
- if (props.Small200)
- {
- small.Add(0x8c);
- }
- if (props.Small500)
- {
- small.Add(0x8d);
- }
- if (props.Small1up)
- {
- small.Add(0x92);
- }
- if (props.Smallkey)
- {
- small.Add(0x88);
- }
- if (props.Largebluejar)
- {
- large.Add(0x90);
- }
- if (props.Largeredjar)
- {
- large.Add(0x91);
- }
- if (props.Large50)
- {
- large.Add(0x8a);
- }
- if (props.Large100)
- {
- large.Add(0x8b);
- }
- if (props.Large200)
- {
- large.Add(0x8c);
- }
- if (props.Large500)
- {
- large.Add(0x8d);
- }
- if (props.Large1up)
- {
- large.Add(0x92);
- }
- if (props.Largekey)
- {
- large.Add(0x88);
- }
-
- // drops are kept vanilla if nothing is selected & RandomizeDrops is off
- if (small.Count > 0)
- {
- // shuffle order
- for (int i = 0; i < small.Count; i++)
- {
- int swap = r.Next(small.Count);
- (small[i], small[swap]) = (small[swap], small[i]);
- }
- // the game uses 8 drop items, fill the rest with copies at random
- for (int i = 0; i < 8; i++)
- {
- if (i < small.Count())
- {
- ROMData.Put(0x1E880 + i, (byte)small[i]);
- }
- else
- {
- ROMData.Put(0x1E880 + i, (byte)small[r.Next(small.Count())]);
- }
- }
- }
- if (large.Count > 0)
- {
- // shuffle order
- for (int i = 0; i < large.Count; i++)
- {
- int swap = r.Next(large.Count);
- (large[i], large[swap]) = (large[swap], large[i]);
- }
- // the game uses 8 drop items, fill the rest with copies at random
- for (int i = 0; i < 8; i++)
- {
- if (i < large.Count())
- {
- ROMData.Put(0x1E888 + i, (byte)large[i]);
- }
- else
- {
- ROMData.Put(0x1E888 + i, (byte)large[r.Next(large.Count())]);
- }
- }
- }
- }
-
- public void ShufflePbagAmounts(ROM ROMData, Random r)
- {
- /*
- * 0 - 0
- * 1 - 2
- * 2 - 3
- * 3 - 5
- * 4 - 10
- * 5 - 20
- * 6 - 30
- * 7 - 50
- * 8 - 70
- * 9 - 100
- * 10 - 150
- * 11 - 200
- * 12 - 300
- * 13 - 500
- * 14 - 700
- * 15 - 1000
- */
- if (props.ShufflePbagXp)
- {
- ROMData.Put(0x1e800, (byte)r.Next(5, 10));
- ROMData.Put(0x1e801, (byte)r.Next(7, 12));
- ROMData.Put(0x1e802, (byte)r.Next(9, 14));
- ROMData.Put(0x1e803, (byte)r.Next(11, 16));
- }
- }
-
- public void ShuffleBossDrop(ROM ROMData, Random r, Assembler a)
- {
- int drop = drops[r.Next(drops.Count())];
- ROMData.Put(0x1de29, (byte)(drop - 0x80));
-
- a.Module().Code("""
-.segment "PRG7"
-.org $E79A
- ; Branch if scroll frozen
- lda $0728
- beq +
- ; freeze scroll
- lda #0
- sta $0728
- ; branch if the music is already playing
- lda $07fb
- bne +
- ; otherwise resume the previous track (palace theme)
- lda #2
- sta $eb
- +
- ; Write the "grab item" sound effect to the sfx queue
- lda #8
- sta $ef
- ; Branch if the item we are getting is NOT a key
- cpy #8
- bne +
- ; increment number of keys and carry on
- inc $0793
- jmp $e797
- +
- ; Otherwise continue to $E7BB which is the start of the get item code
- .assert * = $E7BB
-
-; Patch a few locations to make sure the music returns to normal after getting an item
-.org $e80c
- jsr DontSwitchMusicIfInPalace1
- nop
-
-.org $e84b
- jsr DontSwitchMusicIfInPalace2
- nop
-
-.reloc
-DontSwitchMusicIfInPalace1:
- lda $eb
- cmp #$02
- beq +
- ; Restore track 16
- lda #$10
- sta $eb
-+ rts
-
-DontSwitchMusicIfInPalace2:
- lda $eb
- cmp #$02
- beq +
- ; Restore track 0
- lda #$00
- sta $eb
-+ rts
-""");
- }
}
diff --git a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs b/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs
index 57c6545ce..c08c6403b 100644
--- a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs
@@ -13,7 +13,7 @@ internal class ChaosPalaceGenerator : PalaceGenerator
private const int CONNECTION_ATTEMPT_LIMIT = 200;
private static int debug = 0;
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
debug++;
bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber);
diff --git a/RandomizerCore/Sidescroll/Palace.cs b/RandomizerCore/Sidescroll/Palace.cs
index 26647f65b..8fd139317 100644
--- a/RandomizerCore/Sidescroll/Palace.cs
+++ b/RandomizerCore/Sidescroll/Palace.cs
@@ -1037,7 +1037,7 @@ public void RandomizeEnemies(RandomizerProperties props, Random r)
}
}
- public bool CanClearAllRooms(IEnumerable requireables, Collectable palaceItem)
+ public bool CanClearAllRooms(IReadOnlySet requireables, Collectable palaceItem)
{
//If the palace's item can be reached with the current items, it can be used to clear the rest of the palace.
RequirementType? palaceItemRequirement = palaceItem.AsRequirement();
@@ -1047,8 +1047,8 @@ public bool CanClearAllRooms(IEnumerable requireables, Collecta
//the shuffle will eventually put the item into the reachable place.
if (CanReachAnItemRoom(requireables))
{
- requireables = new List(requireables);
- ((List)requireables).Add((RequirementType)palaceItemRequirement);
+ HashSet palaceRequireables = [.. requireables, palaceItemRequirement.Value];
+ requireables = palaceRequireables;
}
else return false;
}
@@ -1135,7 +1135,7 @@ public bool HasDisallowedDrop(bool palacesContinueAfterBoss, PalaceDropStyle dro
return false;
}
- public bool CanReachAnItemRoom(IEnumerable requireables)
+ public bool CanReachAnItemRoom(IReadOnlySet requireables)
{
List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) };
List coveredRooms = new();
@@ -1175,10 +1175,9 @@ public bool CanReachAnItemRoom(IEnumerable requireables)
return false;
}
- public List GetGettableItems(IEnumerable initialRequireables)
+ public List GetGettableItems(IReadOnlySet initialRequireables)
{
- List requireables = [];
- requireables.AddRange(initialRequireables);
+ HashSet requireables = new(initialRequireables);
List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) };
List coveredRooms = [];
List previousGettableItems = [];
diff --git a/RandomizerCore/Sidescroll/PalaceGenerator.cs b/RandomizerCore/Sidescroll/PalaceGenerator.cs
index 707ba44c2..a7d8253ff 100644
--- a/RandomizerCore/Sidescroll/PalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/PalaceGenerator.cs
@@ -16,7 +16,7 @@ public abstract class PalaceGenerator
protected static readonly IEqualityComparer byteArrayEqualityComparer = new Util.StandardByteArrayEqualityComparer();
- internal abstract Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber);
+ internal abstract Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt);
protected static bool AllowDuplicatePrevention(RandomizerProperties props, int palaceNumber)
{
diff --git a/RandomizerCore/Sidescroll/PalaceRooms.cs b/RandomizerCore/Sidescroll/PalaceRooms.cs
index 1f3b3de5c..10f32d2cc 100644
--- a/RandomizerCore/Sidescroll/PalaceRooms.cs
+++ b/RandomizerCore/Sidescroll/PalaceRooms.cs
@@ -10,7 +10,7 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll;
public partial class PalaceRooms
{
private readonly Dictionary> roomsByGroup = new();
-
+ public IReadOnlyList RoomsByGroup(RoomGroup group) => roomsByGroup.GetValueOrDefault(group, []);
private readonly Dictionary roomsByName = new();
public static readonly string roomsMD5 = "JCa3OsnJhIe/fZ5yrx/+mA==";
diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palaces.cs
index 91de75a27..b91a4ca76 100644
--- a/RandomizerCore/Sidescroll/Palaces.cs
+++ b/RandomizerCore/Sidescroll/Palaces.cs
@@ -20,22 +20,25 @@ public class Palaces
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- private static readonly RequirementType[] VANILLA_P1_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P1_ALLOWED_BLOCKERS = [
RequirementType.KEY, ..RequirementTypeExtensions.UpToXContainers(5)];
- private static readonly RequirementType[] VANILLA_P2_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P2_ALLOWED_BLOCKERS = [
RequirementType.KEY, RequirementType.JUMP, RequirementType.GLOVE, ..RequirementTypeExtensions.UpToXContainers(6) ];
- private static readonly RequirementType[] VANILLA_P3_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P3_ALLOWED_BLOCKERS = [
RequirementType.KEY, RequirementType.DOWNSTAB, RequirementType.UPSTAB, RequirementType.GLOVE, ..RequirementTypeExtensions.UpToXContainers(6) ];
- private static readonly RequirementType[] VANILLA_P4_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P4_ALLOWED_BLOCKERS = [
RequirementType.KEY, RequirementType.FAIRY, RequirementType.JUMP, ..RequirementTypeExtensions.UpToXContainers(7) ];
- private static readonly RequirementType[] VANILLA_P5_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P5_ALLOWED_BLOCKERS = [
RequirementType.KEY, RequirementType.FAIRY, RequirementType.JUMP, ..RequirementTypeExtensions.UpToXContainers(7) ];
- private static readonly RequirementType[] VANILLA_P6_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P6_ALLOWED_BLOCKERS = [
RequirementType.KEY, RequirementType.FAIRY, RequirementType.JUMP, RequirementType.GLOVE, ..RequirementTypeExtensions.UpToXContainers(8) ];
- private static readonly RequirementType[] VANILLA_P7_ALLOWED_BLOCKERS = [
+ private static readonly RequirementType[] VANILLA_P7_ALLOWED_BLOCKERS = [
RequirementType.FAIRY, RequirementType.UPSTAB, RequirementType.DOWNSTAB, RequirementType.JUMP, RequirementType.GLOVE, ..RequirementTypeExtensions.UpToXContainers(8)];
- public static readonly RequirementType[][] ALLOWED_BLOCKERS_BY_PALACE = [
+ public static readonly RequirementType[] ALL_PALACE_ALLOWED_BLOCKERS = [
+ RequirementType.JUMP, RequirementType.FAIRY, RequirementType.UPSTAB, RequirementType.DOWNSTAB, RequirementType.JUMP, RequirementType.KEY, RequirementType.DASH, RequirementType.GLOVE, ..RequirementTypeExtensions.UpToXContainers(8)];
+
+ public static readonly RequirementType[][] ALLOWED_BLOCKERS_BY_PALACE = [
VANILLA_P1_ALLOWED_BLOCKERS,
VANILLA_P2_ALLOWED_BLOCKERS,
VANILLA_P3_ALLOWED_BLOCKERS,
@@ -91,9 +94,10 @@ public async Task> CreatePalaces(Random r, RandomizerProperties pro
roomPool = new(palaceRooms, currentPalace, props);
}
Palace palace;
+ int attempts = 0;
do
{
- palace = await palaceGenerator.GeneratePalace(props, roomPool, r, sizes[currentPalace - 1], currentPalace);
+ palace = await palaceGenerator.GeneratePalace(props, roomPool, r, sizes[currentPalace - 1], currentPalace, attempts++);
} while (!palace.IsValid);
palace.BossRoom!.Enemies = (byte[])roomPool.VanillaBossRoom.Enemies.Clone();
PalaceGenerator.DebugCheckDuplicates(props, palace);
@@ -304,7 +308,7 @@ private static bool AtLeastOnePalaceCanHaveGlove(RandomizerProperties props, Lis
{
return true;
}
- List requireables =
+ HashSet requireables =
[
RequirementType.KEY,
RequirementType.UPSTAB,
@@ -329,12 +333,12 @@ private static bool CanGetGlove(RandomizerProperties props, Palace palace2)
{
if (!props.ShufflePalaceItems)
{
- List requireables = [..RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)];
+ HashSet requireables = [.. RequirementTypeExtensions.UpToXContainers(props.StartMagicContainers)];
//If shuffle overworld items is on, we assume you can get all the items / spells
//as all progression items will eventually shuffle into spots that work
if (props.ShuffleOverworldItems)
{
- requireables = [RequirementType.KEY, ..RequirementTypeExtensions.UpToXContainers(8)];
+ requireables = [RequirementType.KEY, .. RequirementTypeExtensions.UpToXContainers(8)];
}
//Otherwise if it's vanilla items we can't get the magic key, because we could need glove for boots for flute to get to new kasuto
@@ -357,7 +361,7 @@ private static bool CanGetRaft(RandomizerProperties props, bool raftIsRequired,
//or it will send the logic into an uinrecoverable nosedive since the palaces can't re-generate
if (!props.ShufflePalaceItems && raftIsRequired)
{
- List requireables;
+ HashSet requireables;
//If shuffle overworld items is on, we assume you can get all the items / spells
//as all progression items will eventually shuffle into spots that work
if (props.ShuffleOverworldItems)
diff --git a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs b/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs
index 16fc9749e..a431c836d 100644
--- a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs
@@ -8,12 +8,12 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll;
public class ReconstructedLoopyPalaceGenerator(CancellationToken ct) : ReconstructedPalaceGenerator(ct)
{
- internal override Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
rooms.RemoveRooms(room => room.HasDrop);
rooms.RemoveRooms(room => !room.IsEntrance && !room.IsBossRoom && !room.HasItem
&& RoomExitTypeExtensions.DEADENDS.Contains(room.CategorizeExits()));
- return base.GeneratePalace(props, rooms, r, roomCount, palaceNumber);
+ return base.GeneratePalace(props, rooms, r, roomCount, palaceNumber, attempt);
}
public override void Consolidate(List openRooms, RandomizerProperties props, int palaceNumber)
diff --git a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs b/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs
index 4ad33d683..bcb4bc77c 100644
--- a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs
@@ -14,8 +14,15 @@ public class ReconstructedPalaceGenerator(CancellationToken ct) : PalaceGenerato
static int debug = 0;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
+ if (roomCount > 40 && attempt > 500)
+ {
+ // eventually try smaller palaces to avoid deadlock when
+ // room pool + room count together create unconsolidatable palaces
+ roomCount = Math.Max(40, roomCount - (attempt / 500));
+ }
+
int tries = 0;
debug++;
@@ -210,8 +217,9 @@ internal override async Task GeneratePalace(RandomizerProperties props,
/*
if(palace.Number == 7 && palace.AllRooms.Count == roomCount && palace.AllRooms.Count(i => i.CountOpenExits() > 0) < 4)
{
- Debug.WriteLine("");
+ Debug.WriteLine("Unable to consolidate Reconstructed rooms:");
palace.AllRooms.Where(i => i.CountOpenExits() > 0).ToList().ForEach(i => Debug.WriteLine(i.OpenExitsDebug()));
+ Debug.WriteLine("");
}
*/
palace.IsValid = false;
diff --git a/RandomizerCore/Sidescroll/Room.cs b/RandomizerCore/Sidescroll/Room.cs
index dd6fce0ec..e0a98c2b9 100644
--- a/RandomizerCore/Sidescroll/Room.cs
+++ b/RandomizerCore/Sidescroll/Room.cs
@@ -423,6 +423,24 @@ public void AdjustEntrance(int palaceItemRoomCount, Random r)
SideView = edit.Finalize();
}
+ public bool HasTag(string tag) => Tags != null && Tags.Contains(tag);
+
+ /// Aggregating PalaceNumber matching logic to determine if this room is appropriate for palaceNumber.
+ public bool InPoolForPalace(int palaceNumber)
+ {
+ if (PalaceNumber == null)
+ {
+ if (IsBossRoom)
+ {
+ return palaceNumber < 6; // Barba room is not generic
+ }
+ Debug.Assert(!IsThunderBirdRoom, "Thunderbird rooms must have palaceNumber=7");
+ return palaceNumber < 7; // Any non-GP palace is the default meaning of null
+ }
+
+ return PalaceNumber == palaceNumber;
+ }
+
public string DebugString()
{
StringBuilder sb = new();
@@ -454,7 +472,7 @@ public string PrintUnsatisfiedExits()
}
return sb.ToString();
}
- public bool IsTraversable(IEnumerable requireables)
+ public bool IsTraversable(IReadOnlySet requireables)
{
return Requirements.AreSatisfiedBy(requireables);
}
@@ -743,6 +761,15 @@ public bool IsOpen()
|| HasDownExit && Down == null;
}
+ public bool HasExitInDirection(Direction direction) => direction switch
+ {
+ Direction.NORTH => HasUpExit,
+ Direction.SOUTH => HasDownExit,
+ Direction.WEST => HasLeftExit,
+ Direction.EAST => HasRightExit,
+ _ => false
+ };
+
public void UpdateSideviewItem(Collectable collectable)
{
if (PalaceNumber == 7)
diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/RoomPool.cs
index 2bddd9d39..813e697c3 100644
--- a/RandomizerCore/Sidescroll/RoomPool.cs
+++ b/RandomizerCore/Sidescroll/RoomPool.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -7,164 +7,121 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll;
public class RoomPool
{
- public List NormalRooms { get; set; } = [];
- public List Entrances { get; set; } = [];
- public List BossRooms { get; set; } = [];
- public List TbirdRooms { get; set; } = [];
- public List ItemRooms { get; set; } = [];
- public Room VanillaBossRoom { get; set; }
+ public List NormalRooms { get; protected set; } = [];
+ public List Entrances { get; protected set; } = [];
+ public List BossRooms { get; protected set; } = [];
+ public List TbirdRooms { get; protected set; } = [];
+ public List ItemRooms { get; protected set; } = [];
+ public Room VanillaBossRoom { get; protected set; } = null!;
public Dictionary LinkedRooms { get; } = [];
- public Dictionary> ItemRoomsByDirection { get; set; } = [];
+ public Dictionary> ItemRoomsByDirection { get; protected set; } = [];
/// keeping this private so callers do not access the keys in non-deterministic ways
private Dictionary> ItemRoomsByShape { get; set; } = [];
- public Dictionary DefaultStubsByDirection { get; set; } = [];
- public Room DefaultUpEntrance { get; }
- public Room DefaultDownBossRoom { get; }
+ public Dictionary DefaultStubsByDirection { get; protected set; } = [];
+ public Room DefaultUpEntrance { get; protected set; } = null!;
+ public Room DefaultDownBossRoom { get; protected set; } = null!;
-#pragma warning disable CS8618
protected RoomPool() { }
-#pragma warning restore CS8618
protected static readonly IEqualityComparer byteArrayEqualityComparer = new Util.StandardByteArrayEqualityComparer();
public RoomPool(PalaceRooms palaceRooms, int palaceNumber, RandomizerProperties props)
{
- Dictionary> roomDirectionWeightsByDirection = [];
- roomDirectionWeightsByDirection.Add(Direction.NORTH, []);
- roomDirectionWeightsByDirection.Add(Direction.SOUTH, []);
- roomDirectionWeightsByDirection.Add(Direction.WEST, []);
- roomDirectionWeightsByDirection.Add(Direction.EAST, []);
+ var allRooms = GatherRoomsFromProps(palaceRooms, palaceNumber, props);
+ ApplyPropertyExclusions(props);
+ GatherLinkedRooms(allRooms, palaceRooms);
+ SplitRooms(allRooms, palaceNumber);
+ FinalizePool(palaceRooms, palaceNumber, props);
+ }
+
+ static List GatherRoomsFromProps(PalaceRooms palaceRooms, int palaceNumber, RandomizerProperties props)
+ {
+ var roomSet = new List();
- if (props.AllowVanillaRooms
//4.4 GP room pool is too shallow to create proper palaces from right now, so if you pick 4.4 only,
//GP also has vanilla rooms added.
- || (palaceNumber == 7 && !props.AllowVanillaRooms && !props.AllowV4Rooms && props.AllowV5_0Rooms))
+ bool allowVanilla = props.AllowVanillaRooms
+ || (palaceNumber == 7 && !props.AllowVanillaRooms && !props.AllowV4Rooms && props.AllowV5_0Rooms);
+
+ if (allowVanilla)
{
- AddRoomGroup(palaceRooms, RoomGroup.VANILLA, palaceNumber, roomDirectionWeightsByDirection);
+ AddGroup(roomSet, palaceRooms, RoomGroup.VANILLA);
}
if (props.AllowV4Rooms)
{
- AddRoomGroup(palaceRooms, RoomGroup.V4_0, palaceNumber, roomDirectionWeightsByDirection);
+ AddGroup(roomSet, palaceRooms, RoomGroup.V4_0);
}
if (props.AllowV5_0Rooms)
{
- AddRoomGroup(palaceRooms, RoomGroup.V5_0, palaceNumber, roomDirectionWeightsByDirection);
- }
-
- //If we are using these categorized exits to cap paths, there needs to always be a path of each type
- //Since vanilla and 4.0 don't normally contain up/down elevator deadends, we add some dummy ones
- DefaultStubsByDirection.Add(RoomExitType.DEADEND_EXIT_DOWN, palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.STUBS).Where(i => i.HasDownExit).First());
- DefaultStubsByDirection.Add(RoomExitType.DEADEND_EXIT_UP, palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.STUBS).Where(i => i.HasUpExit).First());
- foreach (var direction in DirectionExtensions.ITEM_ROOM_ORIENTATIONS)
- {
- if (roomDirectionWeightsByDirection[direction].Count > 0)
- {
- ItemRoomsByDirection[direction] = new TableWeightedRandom(roomDirectionWeightsByDirection[direction]);
- }
- }
-
- List<(RoomExitType, Room)> linkedRoomShapes = [];
- foreach (var pair in ItemRoomsByShape)
- {
- foreach(Room room in pair.Value)
- {
- if(room.LinkedRoomName != null)
- {
- RoomExitType newShape = room.CategorizeExits().Merge(LinkedRooms[room.LinkedRoomName].CategorizeExits());
- linkedRoomShapes.Add((newShape, room));
- }
- }
- }
- foreach((RoomExitType shape, Room room) in linkedRoomShapes)
- {
- ItemRoomsByShape[room.CategorizeExits()].Remove(room);
- var ls = ItemRoomsByShape.GetValueOrDefault(shape, []);
- ls.Add(room);
- ItemRoomsByShape[shape] = ls;
+ AddGroup(roomSet, palaceRooms, RoomGroup.V5_0);
}
- //If we're using a room set that has no entraces, we still need to have something, so add the vanilla entrances.
- if (Entrances.Count == 0)
- {
- Entrances.AddRange(palaceRooms.Entrances(RoomGroup.VANILLA).Where(i => i.PalaceNumber == palaceNumber).ToList());
- }
+ return roomSet.ToList();
+ }
- //same with boss rooms
- VanillaBossRoom = palaceRooms.VanillaBossRoom(palaceNumber);
- if (BossRooms.Count == 0)
+ private static void AddGroup(List roomSet, PalaceRooms palaceRooms, RoomGroup group)
+ {
+ foreach (var room in palaceRooms.RoomsByGroup(group))
{
- BossRooms.Add(VanillaBossRoom);
+ roomSet.Add(room);
}
+ }
- //for tower, we need a default Up entrance and Down boss room in case the pool doesn't contain them
- DefaultUpEntrance = palaceRooms.Entrances(RoomGroup.V5_0)
- .FirstOrDefault(i => i.IsEntrance && i.CategorizeExits() == RoomExitType.DEADEND_EXIT_UP && i.PalaceNumber == palaceNumber)!;
- Debug.Assert(DefaultUpEntrance != null);
- //in the 4.0 boss rooms, P6/7 have their own rooms, but 1-5 are generic
- if(palaceNumber >= 6)
+ private void ApplyPropertyExclusions(RandomizerProperties props)
+ {
+ if (props.RemoveLongDeadEnds)
{
- DefaultDownBossRoom = palaceRooms.BossRooms(RoomGroup.V4_0)
- .First(i => i.IsBossRoom && i.CategorizeExits() == RoomExitType.DEADEND_EXIT_DOWN && i.PalaceNumber == palaceNumber);
+ RemoveRooms(r => r.HasTag("LongDeadEnd"));
}
- else
+ if (!props.IncludeExpertRooms)
{
- DefaultDownBossRoom = palaceRooms.BossRooms(RoomGroup.V4_0)
- .First(i => i.IsBossRoom && i.CategorizeExits() == RoomExitType.DEADEND_EXIT_DOWN && i.PalaceNumber == null);
+ RemoveRooms(r => r.HasTag("Expert"));
}
+ }
- if (palaceNumber == 7)
+ private void SplitRooms(List allRooms, int palaceNumber)
+ {
+ foreach (var room in allRooms)
{
- if (props.AllowVanillaRooms
- //4.4 GP room pool is too shallow to create proper palaces from right now, so if you pick 4.4 only,
- //GP also has vanilla rooms added.
- || (!props.AllowVanillaRooms && !props.AllowV4Rooms && props.AllowV5_0Rooms))
+ if (!room.InPoolForPalace(palaceNumber))
{
- NormalRooms.AddRange(palaceRooms.GpRoomsByGroup(RoomGroup.VANILLA));
+ continue;
}
- if (props.AllowV4Rooms)
+ if (room.IsEntrance)
{
- NormalRooms.AddRange(palaceRooms.GpRoomsByGroup(RoomGroup.V4_0));
+ Entrances.Add(room);
}
-
- if (props.AllowV5_0Rooms)
+ else if (room.HasItem)
{
- NormalRooms.AddRange(palaceRooms.GpRoomsByGroup(RoomGroup.V5_0));
+ ItemRooms.Add(room);
}
- }
- else
+ else if (room.IsBossRoom)
{
- if (props.AllowVanillaRooms)
- {
- NormalRooms.AddRange(palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.VANILLA));
+ BossRooms.Add(room);
}
-
- if (props.AllowV4Rooms)
+ else if (room.IsThunderBirdRoom)
{
- NormalRooms.AddRange(palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.V4_0));
+ TbirdRooms.Add(room);
}
-
- if (props.AllowV5_0Rooms)
+ else
{
- NormalRooms.AddRange(palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.V5_0));
+ NormalRooms.Add(room);
}
}
+ }
- if (!props.BlockersAnywhere)
- {
- RequirementType[] allowedBlockers = Palaces.ALLOWED_BLOCKERS_BY_PALACE[palaceNumber - 1];
- RemoveRooms(room => !room.IsTraversable(allowedBlockers));
- }
-
- if (props.RemoveLongDeadEnds)
- {
- RemoveRooms(room => room.Tags != null && room.Tags.Contains("LongDeadEnd"));
- }
- if (!props.IncludeExpertRooms)
+ private void GatherLinkedRooms(List allRooms, PalaceRooms palaceRooms)
+ {
+ foreach (var room in allRooms)
{
- RemoveRooms(room => room.Tags != null && room.Tags.Contains("Expert"));
+ if (room.Enabled && room.LinkedRoomName != null)
+ {
+ LinkedRooms[room.LinkedRoomName] = palaceRooms.GetRoomByName(room.LinkedRoomName)!;
+ LinkedRooms[room.Name] = room;
+ }
}
}
@@ -201,33 +158,86 @@ public RoomPool(RoomPool target)
}
}
- private void AddRoomGroup(PalaceRooms palaceRooms, RoomGroup group, int palaceNumber, Dictionary> roomDirectionWeightsByDirection)
+ private void RemoveBlockedRooms(int palaceNumber, RandomizerProperties props)
{
- Entrances.AddRange(palaceRooms.Entrances(group).Where(room => (room.PalaceNumber == null && palaceNumber < 7) || room.PalaceNumber == palaceNumber));
- ItemRooms.AddRange(palaceRooms.ItemRooms(group).Where(room => (room.PalaceNumber == null && palaceNumber < 7) || room.PalaceNumber == palaceNumber));
- BossRooms.AddRange(palaceRooms.BossRooms(group).Where(room => (room.PalaceNumber == null && palaceNumber < 6) || room.PalaceNumber == palaceNumber));
- TbirdRooms.AddRange(palaceRooms.ThunderBirdRooms(group).Where(room => (room.PalaceNumber == null && palaceNumber == 7) || room.PalaceNumber == palaceNumber));
- foreach (var pair in palaceRooms.LinkedRooms(group))
+ var palaceBlockers = !props.BlockersAnywhere ? Palaces.ALLOWED_BLOCKERS_BY_PALACE[palaceNumber - 1] : Palaces.ALL_PALACE_ALLOWED_BLOCKERS;
+ HashSet allowedBlockers = [.. palaceBlockers];
+ if (!props.ReplaceFireWithDash)
{
- LinkedRooms[pair.Key] = pair.Value;
+ allowedBlockers.Remove(RequirementType.DASH);
}
+ RemoveRooms(room => !room.IsTraversable(allowedBlockers));
+ }
+
+ void FinalizePool(PalaceRooms palaceRooms, int palaceNumber, RandomizerProperties props)
+ {
+ DefaultStubsByDirection.Add(RoomExitType.DEADEND_EXIT_DOWN, palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.STUBS).First(i => i.HasDownExit));
+ DefaultStubsByDirection.Add(RoomExitType.DEADEND_EXIT_UP, palaceRooms.NormalPalaceRoomsByGroup(RoomGroup.STUBS).First(i => i.HasUpExit));
+
foreach (var direction in DirectionExtensions.ITEM_ROOM_ORIENTATIONS)
{
- foreach (var room in palaceRooms.ItemRoomsByDirection(group, direction))
+ var weightedRooms = ItemRooms
+ .Where(room => room.HasExitInDirection(direction))
+ .Select(room => (room, 5 - RoomExitCount(room))) // (room, weight) pair where weight is higher with less exits
+ .ToList();
+
+ if (weightedRooms.Count > 0)
{
- // give item rooms lower weights if they have more exits
- bool[] hasExits = [room.HasUpExit, room.HasDownExit, room.HasLeftExit, room.HasRightExit, room.IsDropZone];
- roomDirectionWeightsByDirection[direction].Add((room, 5 - hasExits.Count(i => i)));
+ ItemRoomsByDirection[direction] = new TableWeightedRandom(weightedRooms);
}
}
- var exitTypes = palaceRooms.ItemRooms(group).Select(r => r.CategorizeExits()).Distinct();
+
+ var exitTypes = ItemRooms.Select(r => r.CategorizeExits()).Distinct();
foreach (var shape in exitTypes)
{
- var newRooms = palaceRooms.ItemRoomsByShape(group, shape);
+ var shapeRooms = ItemRooms.Where(r => r.CategorizeExits() == shape).ToList();
var ls = ItemRoomsByShape.GetValueOrDefault(shape, []);
- ls.AddRange(newRooms);
+ ls.AddRange(shapeRooms);
ItemRoomsByShape[shape] = ls;
}
+
+ // this should probably be done per-room before creating the collections above instead of rebuilding the lists after
+ var linkedRoomShapes = ItemRoomsByShape
+ .SelectMany(pair => pair.Value, (shape, room) => (shape, room))
+ .Where(r => r.room.LinkedRoomName != null)
+ .Select(r => (GetMergedExitType(r.room), r.room))
+ .ToList();
+
+ foreach ((RoomExitType newShape, Room room) in linkedRoomShapes)
+ {
+ var originalShape = room.CategorizeExits();
+ ItemRoomsByShape[originalShape].Remove(room);
+ var ls = ItemRoomsByShape.GetValueOrDefault(newShape, []);
+ ls.Add(room);
+ ItemRoomsByShape[newShape] = ls;
+ }
+
+ if (Entrances.Count == 0)
+ {
+ Entrances.AddRange(palaceRooms.Entrances(RoomGroup.VANILLA).Where(i => i.PalaceNumber == palaceNumber));
+ }
+
+ VanillaBossRoom = palaceRooms.VanillaBossRoom(palaceNumber);
+ if (BossRooms.Count == 0)
+ {
+ BossRooms.Add(VanillaBossRoom);
+ }
+
+ DefaultUpEntrance = palaceRooms.Entrances(RoomGroup.V5_0)
+ .FirstOrDefault(i => i.IsEntrance && i.CategorizeExits() == RoomExitType.DEADEND_EXIT_UP && i.PalaceNumber == palaceNumber)!;
+ Debug.Assert(DefaultUpEntrance != null);
+
+ DefaultDownBossRoom = palaceRooms.BossRooms(RoomGroup.V4_0)
+ .First(i => i.IsBossRoom && i.CategorizeExits() == RoomExitType.DEADEND_EXIT_DOWN
+ && (palaceNumber >= 6 ? i.PalaceNumber == palaceNumber : i.PalaceNumber == null));
+
+ RemoveBlockedRooms(palaceNumber, props);
+ }
+
+ static int RoomExitCount(Room room)
+ {
+ bool[] exits = [room.HasUpExit, room.HasDownExit, room.HasLeftExit, room.HasRightExit, room.IsDropZone];
+ return exits.Count(i => i);
}
public IEnumerable GetItemRoomShapes()
@@ -271,8 +281,23 @@ public void RemoveRoom(Room room)
Entrances.Remove(room);
BossRooms.Remove(room);
TbirdRooms.Remove(room);
+ RemoveFromItemRooms(room);
+ }
+
+ public void RemoveRooms(Predicate removalCondition)
+ {
+ NormalRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
+ Entrances.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
+ BossRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
+ TbirdRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
+ ItemRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
+ RemoveFromItemRooms(removalCondition);
+ }
+
+ void RemoveFromItemRooms(Room room)
+ {
ItemRooms.Remove(room);
- foreach(Direction direction in ItemRoomsByDirection.Keys)
+ foreach (Direction direction in ItemRoomsByDirection.Keys)
{
var originalTable = ItemRoomsByDirection[direction];
var newTable = (TableWeightedRandom)originalTable.Subtract(room);
@@ -287,19 +312,13 @@ public void RemoveRoom(Room room)
}
}
- public void RemoveRooms(Predicate removalCondition)
+ void RemoveFromItemRooms(Predicate removalCondition)
{
- NormalRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
- Entrances.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
- BossRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
- TbirdRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
- ItemRooms.RemoveAll(room => RoomMatchesIncludingLinked(room, removalCondition));
foreach (Direction direction in ItemRoomsByDirection.Keys)
{
var originalTable = ItemRoomsByDirection[direction];
var newTable = originalTable;
- var keysCopy = newTable.Keys().ToList();
- foreach (Room room in keysCopy)
+ foreach (Room room in originalTable.Keys().ToList())
{
if (RoomMatchesIncludingLinked(room, removalCondition))
{
@@ -330,9 +349,10 @@ private bool RoomMatchesIncludingLinked(Room room, Predicate match)
}
else
{
- throw new Exception($"Linked room \"{room.LinkedRoomName}\" is references but is not in LinkedRooms pool.");
+ throw new Exception($"Linked room \"{room.LinkedRoomName}\" is referenced but is not in LinkedRooms collection.");
}
}
+
return false;
}
@@ -341,34 +361,30 @@ public Dictionary> CategorizeNormalRoomExits(bool linkR
Dictionary> categorizedRooms = new Dictionary>(NormalRooms.Count);
foreach(Room room in NormalRooms)
{
- RoomExitType type = room.CategorizeExits();
- if(room.LinkedRoomName != null)
- {
- type = type.Merge(LinkedRooms[room.LinkedRoomName].CategorizeExits());
- }
+ var type = GetMergedExitType(room);
if(!categorizedRooms.TryGetValue(type, out List? value))
{
value = new List(NormalRooms.Count);
categorizedRooms[type] = value;
}
-
value.Add(room);
}
-
return categorizedRooms;
}
- public List GetNormalRoomsForExitType(RoomExitType exitType, bool linkRooms = false)
+ RoomExitType GetMergedExitType(Room room)
{
- return NormalRooms.Where(room =>
+ var type = room.CategorizeExits();
+ if (room.LinkedRoomName != null && LinkedRooms.TryGetValue(room.LinkedRoomName, out var linked))
{
- var roomType = room.CategorizeExits();
- if (room.LinkedRoomName != null)
- {
- roomType = roomType.Merge(LinkedRooms[room.LinkedRoomName].CategorizeExits());
- }
- return roomType == exitType;
- }).ToList();
+ type = type.Merge(linked.CategorizeExits());
+ }
+ return type;
+ }
+
+ public List GetNormalRoomsForExitType(RoomExitType exitType, bool linkRooms = false)
+ {
+ return NormalRooms.Where(room => GetMergedExitType(room) == exitType).ToList();
}
public void RefillNormalRoomsForExitType(RoomPool rooms, RoomExitType exitType)
diff --git a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs
index 2a4ee2304..1e78ac710 100644
--- a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs
@@ -26,7 +26,7 @@ public class SequentialPlacementCoordinatePalaceGenerator : CoordinatePalaceGene
private int palaceNumber;
private RoomPool? roomPool;
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNum)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNum, int attempt)
{
this.roomCount = roomCount;
this.palaceNumber = palaceNum;
diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs
index 28c1d7d43..d333497d1 100644
--- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs
@@ -13,7 +13,7 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll;
public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGenerator()
{
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber);
Palace palace = new(palaceNumber);
diff --git a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs b/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs
index 113f9b6e1..22bf62307 100644
--- a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs
@@ -10,7 +10,7 @@ public class VanillaPalaceGenerator() : PalaceGenerator
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
VanillaRoomPool roomPool = (VanillaRoomPool)rooms;
if(roomPool.BossRooms.Count != 1
diff --git a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs b/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs
index 6e192bc1e..d7a47d55e 100644
--- a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs
+++ b/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs
@@ -5,9 +5,9 @@ namespace Z2Randomizer.RandomizerCore.Sidescroll;
public class VanillaShufflePalaceGenerator() : VanillaPalaceGenerator()
{
- internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber)
+ internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber, int attempt)
{
- Palace palace = await base.GeneratePalace(props, rooms, r, roomCount, palaceNumber);
+ Palace palace = await base.GeneratePalace(props, rooms, r, roomCount, palaceNumber, attempt);
palace.ResetRooms();
palace.ShuffleRooms(r);
diff --git a/RandomizerCore/SmallItem.cs b/RandomizerCore/SmallItem.cs
index 0077e82e5..6a40cd1a0 100644
--- a/RandomizerCore/SmallItem.cs
+++ b/RandomizerCore/SmallItem.cs
@@ -1,36 +1,36 @@
using System;
-namespace Z2Randomizer.RandomizerCore
+namespace Z2Randomizer.RandomizerCore;
+
+/// Subset of Collectables that can be used as enemy drops. The enum
+/// values match their Collectable counterparts with 7th bit set.
+public enum SmallItem : byte
{
- //private readonly int[] drops = { 0x8a, 0x8b, 0x8c, 0x8d, 0x90, 0x91, 0x92, 0x88 }
- public enum SmallItem
- {
- BLUE_JAR = 0x90,
- RED_JAR = 0x91,
- SMALL_BAG = 0x8a,
- MEDIUM_BAG = 0x8b,
- LARGE_BAG = 0x8c,
- XL_BAG = 0x8d,
- ONE_UP = 0x92,
- KEY = 0x88
- }
+ KEY = 0x88,
+ SMALL_BAG = 0x8a, // 50 P
+ MEDIUM_BAG = 0x8b, // 100 P
+ LARGE_BAG = 0x8c, // 200 P
+ XL_BAG = 0x8d, // 500 P
+ BLUE_JAR = 0x90,
+ RED_JAR = 0x91,
+ ONEUP = 0x92,
+}
- static class SmallItemExtensions
+public static class SmallItemExtensions
+{
+ public static Collectable ToCollectable(this SmallItem drop)
{
- public static SmallItem Random(this SmallItem s, Random random)
+ return drop switch
{
- return random.Next(8) switch
- {
- 0 => SmallItem.BLUE_JAR,
- 1 => SmallItem.RED_JAR,
- 2 => SmallItem.SMALL_BAG,
- 3 => SmallItem.MEDIUM_BAG,
- 4 => SmallItem.LARGE_BAG,
- 5 => SmallItem.XL_BAG,
- 6 => SmallItem.ONE_UP,
- 7 => SmallItem.KEY,
- _ => throw new ArgumentException("Invalid smallItem")
- };
- }
+ SmallItem.KEY => Collectable.KEY,
+ SmallItem.SMALL_BAG => Collectable.SMALL_BAG,
+ SmallItem.MEDIUM_BAG => Collectable.MEDIUM_BAG,
+ SmallItem.LARGE_BAG => Collectable.LARGE_BAG,
+ SmallItem.XL_BAG => Collectable.XL_BAG,
+ SmallItem.BLUE_JAR => Collectable.BLUE_JAR,
+ SmallItem.RED_JAR => Collectable.RED_JAR,
+ SmallItem.ONEUP => Collectable.ONEUP,
+ _ => throw new ArgumentOutOfRangeException(nameof(drop), drop, null),
+ };
}
-}
\ No newline at end of file
+}
diff --git a/RandomizerCore/Spoiler.cs b/RandomizerCore/Spoiler.cs
index cd937a7b0..2c4e7e1fa 100644
--- a/RandomizerCore/Spoiler.cs
+++ b/RandomizerCore/Spoiler.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq;
using System.Runtime.InteropServices;
using Z2Randomizer.RandomizerCore.Overworld;
@@ -10,64 +9,26 @@ namespace Z2Randomizer.RandomizerCore;
public class Spoiler
{
- public readonly Dictionary terrainTileChrAddrs = new()
- {
- { Terrain.TOWN, 0x115c0 },
- { Terrain.CAVE, 0x11f40 },
- { Terrain.PALACE, 0x11600 },
- { Terrain.BRIDGE, 0x115a0 },
- { Terrain.DESERT, 0x116c0 },
- { Terrain.GRASS, 0x116d0 },
- { Terrain.FOREST, 0x11680 },
- { Terrain.SWAMP, 0x116f0 },
- { Terrain.GRAVE, 0x11700 },
- { Terrain.ROAD, 0x11fe0},
- { Terrain.LAVA, 0x116e0 },
- { Terrain.MOUNTAIN, 0x11640 },
- { Terrain.WATER, 0x116e0 },
- { Terrain.PREPLACED_WATER, 0x116e0 },
- { Terrain.WALKABLEWATER, 0x116e0 },
- { Terrain.PREPLACED_WATER_WALKABLE, 0x116e0 },
- { Terrain.ROCK, 0x11560 },
- { Terrain.RIVER_DEVIL, 0x11400 },
- };
-
- public readonly Dictionary terrainPalettePrgAddrs = new()
- {
- { Terrain.TOWN, 0x1c463 },
- { Terrain.CAVE, 0x1c45f },
- { Terrain.PALACE, 0x1c463 },
- { Terrain.BRIDGE, 0x1c45f },
- { Terrain.DESERT, 0x1c467 },
- { Terrain.GRASS, 0x1c45b },
- { Terrain.FOREST, 0x1c45b },
- { Terrain.SWAMP, 0x1c45b },
- { Terrain.GRAVE, 0x1c45f },
- { Terrain.ROAD, 0x1c45f },
- { Terrain.LAVA, 0x1c45f },
- { Terrain.MOUNTAIN, 0x1c45f },
- { Terrain.WATER, 0x100aa },
- { Terrain.PREPLACED_WATER, 0x100aa },
- { Terrain.WALKABLEWATER, 0x1c467 },
- { Terrain.PREPLACED_WATER_WALKABLE, 0x1c467 },
- { Terrain.ROCK, 0x1c45f },
- { Terrain.RIVER_DEVIL, 0x1c45f },
- };
-
+ private ROM rom;
+ private byte[] itemPalette;
public Dictionary terrainTiles;
public Spoiler(ROM rom)
{
+ this.rom = rom;
+ itemPalette = rom.GetBytes(Palettes.ORANGE, 4);
+ itemPalette[0] = 0x00;
+
Dictionary palettes = new();
terrainTiles = new();
- foreach (var kvp in terrainTileChrAddrs)
+ foreach (var kvp in CHR.TERRAIN_TILE_ADDRS)
{
Terrain t = kvp.Key;
var chrAddr = kvp.Value;
if (!palettes.TryGetValue(t, out var palette))
{
- palette = rom.GetBytes(ROM.RomHdrSize + terrainPalettePrgAddrs[t], 4);
+ palette = rom.GetBytes(ROM.RomHdrSize + Palettes.TERRAIN_ADDRS[t], 4);
palette[0] = 0x0f;
palettes[t] = palette;
}
@@ -97,8 +58,8 @@ public Spoiler(ROM rom)
break;
case Terrain.GRAVE:
// we need to combine the grave 8x16 tile and two 8x8 road tiles
- byte[] tileData1 = rom.ReadSprite(ROM.ChrRomOffset + terrainTileChrAddrs[Terrain.GRAVE], 1, 2, palette);
- byte[] tileData2 = rom.ReadSprite(ROM.ChrRomOffset + terrainTileChrAddrs[Terrain.ROAD], 1, 1, palette);
+ byte[] tileData1 = rom.ReadSprite(ROM.ChrRomOffset + CHR.TERRAIN_TILE_ADDRS[Terrain.GRAVE], 1, 2, palette);
+ byte[] tileData2 = rom.ReadSprite(ROM.ChrRomOffset + CHR.TERRAIN_TILE_ADDRS[Terrain.ROAD], 1, 1, palette);
byte[] fullTileData = new byte[16 * 16 * 4];
InsertTile(fullTileData, 16, 16, tileData1, 8, 16, 0, 0);
InsertTile(fullTileData, 16, 16, tileData2, 8, 8, 8, 0);
@@ -163,6 +124,18 @@ private void DrawWorld(SKCanvas canvas, World world, int startDrawX, int startDr
canvas.DrawBitmap(tile, (startDrawX - offsetX) * 16 + x * 16, startDrawY * 16 + y * 16);
}
}
+
+ int[,] itemCountAtPos = new int[world.MapColumns, world.MapRows];
+ foreach (var loc in world.AllLocations)
+ {
+ foreach (var col in loc.Collectables)
+ {
+ var parts = CHR.COLLECTABLE_TILES[col];
+ var itemCount = itemCountAtPos[loc.Xpos, loc.Y];
+ DrawSpriteParts(rom, canvas, parts, (startDrawX - offsetX) * 16 + loc.Xpos * 16 + itemCount * 8, startDrawY * 16 + loc.Y * 16 + itemCount * 8, itemPalette);
+ itemCountAtPos[loc.Xpos, loc.Y]++;
+ }
+ }
}
public static SKBitmap LoadChr(ROM rom, int chrAddr, int tilesWide, int tilesHigh, byte[] palette)
@@ -171,10 +144,21 @@ public static SKBitmap LoadChr(ROM rom, int chrAddr, int tilesWide, int tilesHig
return MakeSpriteBitmap(tileData, tilesWide * 8, tilesHigh * 8);
}
+ public static SKBitmap LoadChrTransparent(ROM rom, int chrAddr, int tilesWide, int tilesHigh, byte[] palette)
+ {
+ int stride = tilesWide * 8 * 4;
+ byte[] tileData = rom!.ReadSprite(ROM.ChrRomOffset + chrAddr, tilesWide, tilesHigh, palette);
+ for (var i = 3; i < tileData.Length; i += 4)
+ {
+ tileData[i] = 0xff;
+ }
+ return MakeSpriteBitmap(tileData, tilesWide * 8, tilesHigh * 8);
+ }
+
public static SKBitmap LoadChrFillPattern(ROM rom, int chrAddr,
- int tilesWide, int tilesHigh,
- int targetTileWidth, int targetTileHeight,
- byte[] palette)
+ int tilesWide, int tilesHigh,
+ int targetTileWidth, int targetTileHeight,
+ byte[] palette)
{
const int tileSize = 8; // Each tile is 8x8 pixels
const int colorDepthBytes = 4; // RGBA
@@ -241,6 +225,23 @@ public static void InsertTile(byte[] dest, int destWidth, int destHeight,
}
}
+ public static void DrawSpriteParts(ROM rom, SKCanvas canvas, SpriteTile[] parts, int startX, int startY, byte[] palette)
+ {
+ foreach (var part in parts)
+ {
+ /*
+ SKBitmap tile = part.Alpha ? LoadChrTransparent(rom, part.Addr, part.W, part.H, palette)
+ : LoadChr(rom, part.Addr, part.W, part.H, palette);
+ */
+ SKBitmap tile = LoadChr(rom, part.Addr, part.W, part.H, palette);
+ foreach (var p in part.Placement)
+ {
+ SKBitmap drawTile = p.FlipH ? FlipTileHorizontally(tile) : tile;
+ canvas.DrawBitmap(drawTile, startX + p.X * 8, startY + p.Y * 8);
+ }
+ }
+ }
+
public static SKBitmap MakeSpriteBitmap(byte[] tileData, int w, int h)
{
SKBitmap tile = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Unpremul);
@@ -257,4 +258,18 @@ public static SKBitmap MakeSpriteBitmap(byte[] tileData, int w, int h)
}
finally { handle.Free(); }
}
+
+ public static SKBitmap FlipTileHorizontally(SKBitmap tile)
+ {
+ SKBitmap res = new SKBitmap(tile.Width, tile.Height);
+ for (int x = 0; x < tile.Width; x++)
+ {
+ var mirrorX = tile.Width - x - 1;
+ for (int y = 0; y < tile.Height; y++)
+ {
+ res.SetPixel(mirrorX, y, tile.GetPixel(x, y));
+ }
+ }
+ return res;
+ }
}
diff --git a/RandomizerCore/StatRandomizer.cs b/RandomizerCore/StatRandomizer.cs
index c931968fb..955a2e91e 100644
--- a/RandomizerCore/StatRandomizer.cs
+++ b/RandomizerCore/StatRandomizer.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Text;
using Z2Randomizer.RandomizerCore.Enemy;
namespace Z2Randomizer.RandomizerCore;
@@ -365,58 +366,55 @@ protected void RandomizeAttackEffectiveness(Random r, AttackEffectiveness attack
}
byte[] newTable = new byte[8];
- for (int i = 0; i < 8; i++)
+
+ FixedBytesAttribute? fixedBytes = attackEffectiveness.GetFixedBytes();
+ if (fixedBytes != null)
+ {
+ Debug.Assert(fixedBytes.Values.Length == 8);
+ newTable = fixedBytes.Values;
+ }
+ else
{
- int nextVal;
- byte vanilla = AttackEffectivenessTable[i];
- switch (attackEffectiveness)
+ RandomRangeDoubleAttribute? range = attackEffectiveness.GetRandomRangeDouble();
+ Debug.Assert(range != null);
+
+ for (int i = 0; i < 8; i++)
{
- case AttackEffectiveness.LOW:
- //the naieve approach here gives a curve of 1,2,2,4,5,6 which is weird, or a different
- //irregular curve in digshake's old approach. Just use a linear increase for the first 6 levels on low
- if (i < 6)
- {
- nextVal = i + 1;
- }
- else
- {
- nextVal = (int)Math.Round(vanilla * .5, MidpointRounding.ToPositiveInfinity);
- }
- break;
- case AttackEffectiveness.AVERAGE_LOW:
- nextVal = RandomInRange(r, vanilla * .5, vanilla);
+ int nextVal;
+ byte vanilla = AttackEffectivenessTable[i];
+
+ int min = (int)(vanilla * range.Low);
+ int max = (int)(vanilla * range.High);
+ nextVal = r.Next(min, max);
+
+ if (attackEffectiveness == AttackEffectiveness.AVERAGE_LOW)
+ {
if (i == 1)
{
nextVal = Math.Max(nextVal, 2); // set minimum 2 damage at level 2
}
- break;
- case AttackEffectiveness.AVERAGE:
- nextVal = RandomInRange(r, vanilla * .667, vanilla * 1.5);
+ }
+ else if (attackEffectiveness == AttackEffectiveness.AVERAGE)
+ {
if (i == 0)
{
nextVal = Math.Max(nextVal, 2); // set minimum 2 damage at start
}
- break;
- case AttackEffectiveness.AVERAGE_HIGH:
- nextVal = RandomInRange(r, vanilla, vanilla * 1.5);
- break;
- case AttackEffectiveness.HIGH:
- nextVal = (int)Math.Round(vanilla * 1.5);
- break;
- default:
- throw new NotImplementedException("Invalid Attack Effectiveness");
- }
- if (i > 0)
- {
- byte lastValue = newTable[i - 1];
- if (nextVal < lastValue)
+ }
+
+ if (i > 0)
{
- nextVal = lastValue; // levelling up should never be worse
+ byte lastValue = newTable[i - 1];
+ if (nextVal < lastValue)
+ {
+ nextVal = lastValue; // levelling up should never be worse
+ }
}
- }
- newTable[i] = (byte)nextVal;
+ newTable[i] = (byte)nextVal;
+ }
}
+
AttackEffectivenessTable = newTable;
}
@@ -437,6 +435,9 @@ protected void RandomizeLifeEffectiveness(Random r, LifeEffectiveness statEffect
return;
}
+ RandomRangeDoubleAttribute? range = statEffectiveness.GetRandomRangeDouble();
+ Debug.Assert(range != null);
+
byte[] newTable = new byte[LIFE_EFFECTIVENESS_ROWS * 8];
// The values we are randomizing are actually *enemy damage* values
@@ -447,25 +448,11 @@ protected void RandomizeLifeEffectiveness(Random r, LifeEffectiveness statEffect
int index = damageCode * 8 + level;
byte nextVal;
byte vanilla = (byte)(LifeEffectivenessTable[index] >> 1);
- int min = (int)(vanilla * .75);
- int max = Math.Min((int)(vanilla * 1.5), 120);
- switch (statEffectiveness)
- {
- case LifeEffectiveness.AVERAGE_LOW:
- nextVal = (byte)r.Next(vanilla, max);
- break;
- case LifeEffectiveness.AVERAGE:
- nextVal = (byte)r.Next(min, max);
- break;
- case LifeEffectiveness.AVERAGE_HIGH:
- nextVal = (byte)r.Next(min, vanilla);
- break;
- case LifeEffectiveness.HIGH:
- nextVal = (byte)(vanilla * .5);
- break;
- default:
- throw new NotImplementedException("Invalid Life Effectiveness");
- }
+ int min = (int)(vanilla * range.Low);
+ int max = (int)(vanilla * range.High);
+ nextVal = (byte)r.Next(min, max);
+ nextVal = Math.Min(nextVal, (byte)120);
+
if (level > 0)
{
byte lastVal = (byte)(newTable[index - 1] >> 1);
@@ -493,6 +480,9 @@ protected void RandomizeMagicEffectiveness(Random r, MagicEffectiveness statEffe
return;
}
+ RandomRangeDoubleAttribute? range = statEffectiveness.GetRandomRangeDouble();
+ Debug.Assert(range != null);
+
byte[] newTable = new byte[MAGIC_EFFECTIVENESS_ROWS * 8];
for (int level = 0; level < 8; level++)
@@ -501,29 +491,12 @@ protected void RandomizeMagicEffectiveness(Random r, MagicEffectiveness statEffe
{
int index = spellIndex * 8 + level;
byte nextVal;
- byte vanilla = (byte)(MagicEffectivenessTable[index] >> 1);
- int min = (int)(vanilla * .5);
- int max = Math.Min((int)(vanilla * 1.5), 120);
- switch (statEffectiveness)
- {
- case MagicEffectiveness.HIGH_COST:
- nextVal = (byte)max;
- break;
- case MagicEffectiveness.AVERAGE_HIGH_COST:
- nextVal = (byte)r.Next(vanilla, max);
- break;
- case MagicEffectiveness.AVERAGE:
+ byte baseVal = (byte)(MagicEffectivenessTable[index] >> 1);
+ int min = (int)(baseVal * range.Low);
+ int max = (int)(baseVal * range.High);
nextVal = (byte)r.Next(min, max);
- break;
- case MagicEffectiveness.AVERAGE_LOW_COST:
- nextVal = (byte)r.Next(min, vanilla);
- break;
- case MagicEffectiveness.LOW_COST:
- nextVal = (byte)min;
- break;
- default:
- throw new Exception("Invalid Magic Effectiveness");
- }
+ nextVal = Math.Min(nextVal, (byte)120);
+
if (level > 0)
{
byte lastVal = (byte)(newTable[index - 1] >> 1);
@@ -619,10 +592,28 @@ protected void RandomizeEnemyAttributes