From 898fa203ad852edeb4a9339d3291c51ff8206548 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:48:13 -0400 Subject: [PATCH] Smz3 updated to version 11.3 (#886) --- worlds/smz3/Options.py | 86 +++- worlds/smz3/TotalSMZ3/Config.py | 105 ++--- worlds/smz3/TotalSMZ3/Item.py | 30 +- worlds/smz3/TotalSMZ3/Patch.py | 424 +++++++++--------- worlds/smz3/TotalSMZ3/Region.py | 21 +- .../Regions/SuperMetroid/Brinstar/Kraid.py | 2 +- .../Regions/SuperMetroid/Brinstar/Pink.py | 2 +- .../Regions/SuperMetroid/Crateria/East.py | 4 +- .../Regions/SuperMetroid/Maridia/Inner.py | 19 +- .../Regions/SuperMetroid/NorfairLower/East.py | 6 +- .../Regions/SuperMetroid/NorfairLower/West.py | 29 +- .../SuperMetroid/NorfairUpper/Crocomire.py | 9 +- .../Regions/SuperMetroid/WreckedShip.py | 7 +- .../Regions/Zelda/DarkWorld/NorthEast.py | 10 +- .../Regions/Zelda/DarkWorld/NorthWest.py | 2 +- .../Regions/Zelda/DarkWorld/South.py | 2 +- .../TotalSMZ3/Regions/Zelda/GanonsTower.py | 26 +- .../smz3/TotalSMZ3/Regions/Zelda/IcePalace.py | 3 +- .../Regions/Zelda/LightWorld/NorthEast.py | 2 +- .../Regions/Zelda/LightWorld/NorthWest.py | 4 +- .../TotalSMZ3/Regions/Zelda/MiseryMire.py | 8 +- .../TotalSMZ3/Regions/Zelda/SwampPalace.py | 1 + .../TotalSMZ3/Regions/Zelda/TurtleRock.py | 8 +- worlds/smz3/TotalSMZ3/Text/Dialog.py | 103 +++-- .../smz3/TotalSMZ3/Text/Scripts/General.yaml | 73 ++- .../TotalSMZ3/Text/Scripts/StringTable.yaml | 29 +- worlds/smz3/TotalSMZ3/Text/StringTable.py | 16 +- worlds/smz3/TotalSMZ3/Text/Texts.py | 6 +- worlds/smz3/TotalSMZ3/World.py | 73 +-- worlds/smz3/TotalSMZ3/WorldState.py | 170 +++++++ worlds/smz3/__init__.py | 109 ++++- worlds/smz3/data/zsm.ips | Bin 1460427 -> 1470833 bytes 32 files changed, 907 insertions(+), 482 deletions(-) create mode 100644 worlds/smz3/TotalSMZ3/WorldState.py diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index ad59bc6d42..2bbddf7ab3 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Option +from Options import Choice, Option, Toggle, DefaultOnToggle, Range class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -45,6 +45,22 @@ class MorphLocation(Choice): option_Original = 2 default = 0 + +class Goal(Choice): + """This option decides what goal is required to finish the randomizer. + Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses. + Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in + Ganon's Tower and Ganon can be defeat as soon you have the required + crystals to make Ganon vulnerable. For keysanity, this mode also removes + the Crateria Boss Key requirement from Tourian to allow faster access. + All Dungeons and Defeat Mother Brain - Similar to "Defeat Ganon and Mother Brain", but also requires all dungeons + to be beaten including Castle Tower and Agahnim.""" + display_name = "Goal" + option_DefeatBoth = 0 + option_FastGanonDefeatMotherBrain = 1 + option_AllDungeonsDefeatMotherBrain = 2 + default = 0 + class KeyShuffle(Choice): """This option decides how dungeon items such as keys are shuffled. None - A Link to the Past dungeon items can only be placed inside the @@ -55,9 +71,75 @@ class KeyShuffle(Choice): option_Keysanity = 1 default = 0 +class OpenTower(Range): + """The amount of crystals required to be able to enter Ganon's Tower. + If this is set to Random, the amount can be found in-game on a sign next to Ganon's Tower.""" + display_name = "Open Tower" + range_start = 0 + range_end = 7 + default = 7 + +class GanonVulnerable(Range): + """The amount of crystals required to be able to harm Ganon. The amount can be found + in-game on a sign near the top of the Pyramid.""" + display_name = "Ganon Vulnerable" + range_start = 0 + range_end = 7 + default = 7 + +class OpenTourian(Range): + """The amount of boss tokens required to enter Tourian. The amount can be found in-game + on a sign above the door leading to the Tourian entrance.""" + display_name = "Open Tourian" + range_start = 0 + range_end = 4 + default = 4 + +class SpinJumpsAnimation(Toggle): + """Enable separate space/screw jump animations""" + display_name = "Spin Jumps Animation" + +class HeartBeepSpeed(Choice): + """Sets the speed of the heart beep sound in A Link to the Past.""" + display_name = "Heart Beep Speed" + option_Off = 0 + option_Quarter = 1 + option_Half = 2 + option_Normal = 3 + option_Double = 4 + alias_false = 0 + default = 3 + +class HeartColor(Choice): + """Changes the color of the hearts in the HUD for A Link to the Past.""" + display_name = "Heart Color" + option_Red = 0 + option_Green = 1 + option_Blue = 2 + option_Yellow = 3 + default = 0 + +class QuickSwap(Toggle): + """When enabled, lets you switch items in ALTTP with L/R""" + display_name = "Quick Swap" + +class EnergyBeep(DefaultOnToggle): + """Toggles the low health energy beep in Super Metroid.""" + display_name = "Energy Beep" + + smz3_options: typing.Dict[str, type(Option)] = { "sm_logic": SMLogic, "sword_location": SwordLocation, "morph_location": MorphLocation, - "key_shuffle": KeyShuffle + "goal": Goal, + "key_shuffle": KeyShuffle, + "open_tower": OpenTower, + "ganon_vulnerable": GanonVulnerable, + "open_tourian": OpenTourian, + "spin_jumps_animation": SpinJumpsAnimation, + "heart_beep_speed": HeartBeepSpeed, + "heart_color": HeartColor, + "quick_swap": QuickSwap, + "energy_beep": EnergyBeep } diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index bfcd541b98..23dde1c88e 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -26,16 +26,42 @@ class MorphLocation(Enum): class Goal(Enum): DefeatBoth = 0 + FastGanonDefeatMotherBrain = 1 + AllDungeonsDefeatMotherBrain = 2 class KeyShuffle(Enum): Null = 0 Keysanity = 1 -class GanonInvincible(Enum): - Never = 0 - BeforeCrystals = 1 - BeforeAllDungeons = 2 - Always = 3 +class OpenTower(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class GanonVulnerable(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class OpenTourian(Enum): + Random = -1 + NoBosses = 0 + OneBoss = 1 + TwoBosses = 2 + ThreeBosses = 3 + FourBosses = 4 class Config: GameMode: GameMode = GameMode.Multiworld @@ -45,63 +71,20 @@ class Config: MorphLocation: MorphLocation = MorphLocation.Randomized Goal: Goal = Goal.DefeatBoth KeyShuffle: KeyShuffle = KeyShuffle.Null - Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False - GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals - def __init__(self, options: Dict[str, str]): - self.GameMode = self.ParseOption(options, GameMode.Multiworld) - self.Z3Logic = self.ParseOption(options, Z3Logic.Normal) - self.SMLogic = self.ParseOption(options, SMLogic.Normal) - self.SwordLocation = self.ParseOption(options, SwordLocation.Randomized) - self.MorphLocation = self.ParseOption(options, MorphLocation.Randomized) - self.Goal = self.ParseOption(options, Goal.DefeatBoth) - self.GanonInvincible = self.ParseOption(options, GanonInvincible.BeforeCrystals) - self.KeyShuffle = self.ParseOption(options, KeyShuffle.Null) - self.Keysanity = self.KeyShuffle != KeyShuffle.Null - self.Race = self.ParseOptionWith(options, "Race", False) + OpenTower: OpenTower = OpenTower.SevenCrystals + GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals + OpenTourian: OpenTourian = OpenTourian.FourBosses - def ParseOption(self, options:Dict[str, str], defaultValue:Enum): - enumKey = defaultValue.__class__.__name__.lower() - if (enumKey in options): - return defaultValue.__class__[options[enumKey]] - return defaultValue + @property + def SingleWorld(self) -> bool: + return self.GameMode == GameMode.Normal + + @property + def Multiworld(self) -> bool: + return self.GameMode == GameMode.Multiworld - def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool): - if (option.lower() in options): - return options[option.lower()] - return defaultValue - - """ public static RandomizerOption GetRandomizerOption(string description, string defaultOption = "") where T : Enum { - var enumType = typeof(T); - var values = Enum.GetValues(enumType).Cast(); - - return new RandomizerOption { - Key = enumType.Name.ToLower(), - Description = description, - Type = RandomizerOptionType.Dropdown, - Default = string.IsNullOrEmpty(defaultOption) ? GetDefaultValue().ToLString() : defaultOption, - Values = values.ToDictionary(k => k.ToLString(), v => v.GetDescription()) - }; - } - - public static RandomizerOption GetRandomizerOption(string name, string description, bool defaultOption = false) { - return new RandomizerOption { - Key = name.ToLower(), - Description = description, - Type = RandomizerOptionType.Checkbox, - Default = defaultOption.ToString().ToLower(), - Values = new Dictionary() - }; - } - - public static TEnum GetDefaultValue() where TEnum : Enum { - Type t = typeof(TEnum); - var attributes = (DefaultValueAttribute[])t.GetCustomAttributes(typeof(DefaultValueAttribute), false); - if ((attributes?.Length ?? 0) > 0) { - return (TEnum)attributes.First().Value; - } - else { - return default; - } - } """ + @property + def Keysanity(self) -> bool: + return self.KeyShuffle != KeyShuffle.Null \ No newline at end of file diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py index bad16ce9d0..2aced8bfac 100644 --- a/worlds/smz3/TotalSMZ3/Item.py +++ b/worlds/smz3/TotalSMZ3/Item.py @@ -130,6 +130,11 @@ class ItemType(Enum): CardLowerNorfairL1 = 0xDE CardLowerNorfairBoss = 0xDF + SmMapBrinstar = 0xCA + SmMapWreckedShip = 0xCB + SmMapMaridia = 0xCC + SmMapLowerNorfair = 0xCD + Missile = 0xC2 Super = 0xC3 PowerBomb = 0xC4 @@ -174,6 +179,7 @@ class Item: map = re.compile("^Map") compass = re.compile("^Compass") keycard = re.compile("^Card") + smMap = re.compile("^SmMap") def IsDungeonItem(self): return self.dungeon.match(self.Type.name) def IsBigKey(self): return self.bigKey.match(self.Type.name) @@ -181,6 +187,7 @@ class Item: def IsMap(self): return self.map.match(self.Type.name) def IsCompass(self): return self.compass.match(self.Type.name) def IsKeycard(self): return self.keycard.match(self.Type.name) + def IsSmMap(self): return self.smMap.match(self.Type.name) def Is(self, type: ItemType, world): return self.Type == type and self.World == world @@ -313,7 +320,7 @@ class Item: Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5)) Item.AddRange(itemPool, 2, Item(ItemType.OneRupee)) Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees)) - Item.AddRange(itemPool, 25 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) + Item.AddRange(itemPool, 21 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees)) Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees)) @@ -421,6 +428,21 @@ class Item: return itemPool + @staticmethod + def CreateSmMaps(world): + itemPool = [ + Item(ItemType.SmMapBrinstar, world), + Item(ItemType.SmMapWreckedShip, world), + Item(ItemType.SmMapMaridia, world), + Item(ItemType.SmMapLowerNorfair, world) + ] + + for item in itemPool: + item.Progression = True + item.World = world + + return itemPool + @staticmethod def Get(items, itemType:ItemType): item = next((i for i in items if i.Type == itemType), None) @@ -725,7 +747,7 @@ class Progression: def CanAccessMiseryMirePortal(self, config: Config): if (config.SMLogic == SMLogic.Normal): - return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and (self.Gravity and self.SpaceJump) and self.CanUsePowerBombs() + return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and self.Gravity and self.SpaceJump and self.CanUsePowerBombs() else: return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \ (self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \ @@ -769,11 +791,11 @@ class Progression: if (world.Config.SMLogic == SMLogic.Normal): return self.MoonPearl and self.Flippers and \ self.Gravity and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) else: return self.MoonPearl and self.Flippers and \ (self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) # Start of AP integration items_start_id = 84000 diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index d029e58473..2b8d278d49 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -6,7 +6,7 @@ import typing from BaseClasses import Location from worlds.smz3.TotalSMZ3.Item import Item, ItemType from worlds.smz3.TotalSMZ3.Location import LocationType -from worlds.smz3.TotalSMZ3.Region import IMedallionAccess, IReward, RewardType, SMRegion, Z3Region +from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera @@ -18,10 +18,14 @@ from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable from worlds.smz3.TotalSMZ3.World import World -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible +from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal from worlds.smz3.TotalSMZ3.Text.Texts import Texts from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog @@ -30,6 +34,11 @@ class KeycardPlaque: Level2 = 0xe1 Boss = 0xe2 Null = 0x00 + Zero = 0xe3 + One = 0xe4 + Two = 0xe5 + Three = 0xe6 + Four = 0xe7 class KeycardDoors: Left = 0xd414 @@ -73,8 +82,8 @@ class DropPrize(Enum): Fairy = 0xE3 class Patch: - Major = 0 - Minor = 1 + Major = 11 + Minor = 3 allWorlds: List[World] myWorld: World seedGuid: str @@ -105,13 +114,16 @@ class Patch: self.WriteDiggingGameRng() - self.WritePrizeShuffle() + self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes) self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - self.WriteGanonInvicible(config.GanonInvincible) + self.WriteGanonInvicible(config.Goal) + self.WritePreOpenPyramid(config.Goal) + self.WriteCrystalsNeeded(self.myWorld.TowerCrystals, self.myWorld.GanonCrystals) + self.WriteBossesNeeded(self.myWorld.TourianBossTokens) self.WriteRngBlock() self.WriteSaveAndQuitFromBossRoom() @@ -135,26 +147,27 @@ class Patch: return {patch[0]:patch[1] for patch in self.patches} def WriteMedallions(self): + from worlds.smz3.TotalSMZ3.WorldState import Medallion turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock)) miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire)) turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ] miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ] - if turtleRock.Medallion == ItemType.Bombos: + if turtleRock.Medallion == Medallion.Bombos: turtleRockValues = [0x00, 0x51, 0x10, 0x00] - elif turtleRock.Medallion == ItemType.Ether: + elif turtleRock.Medallion == Medallion.Ether: turtleRockValues = [0x01, 0x51, 0x18, 0x00] - elif turtleRock.Medallion == ItemType.Quake: + elif turtleRock.Medallion == Medallion.Quake: turtleRockValues = [0x02, 0x14, 0xEF, 0xC4] else: raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion") - if miseryMire.Medallion == ItemType.Bombos: + if miseryMire.Medallion == Medallion.Bombos: miseryMireValues = [0x00, 0x51, 0x00, 0x00] - elif miseryMire.Medallion == ItemType.Ether: + elif miseryMire.Medallion == Medallion.Ether: miseryMireValues = [0x01, 0x13, 0x9F, 0xF1] - elif miseryMire.Medallion == ItemType.Quake: + elif miseryMire.Medallion == Medallion.Quake: miseryMireValues = [0x02, 0x51, 0x08, 0x00] else: raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion") @@ -174,12 +187,19 @@ class Patch: self.rnd.shuffle(pendantsBlueRed) pendantRewards = pendantsGreen + pendantsBlueRed + bossTokens = [ 1, 2, 3, 4 ] + regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed] pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen] + bossRegions = [region for region in regions if region.Reward == RewardType.BossTokenKraid] + \ + [region for region in regions if region.Reward == RewardType.BossTokenPhantoon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenDraygon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenRidley] self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues) self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues) + self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues) def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable): addresses = [self.RewardAddresses(region) for region in regions] @@ -189,17 +209,22 @@ class Patch: def RewardAddresses(self, region: IReward): regionType = { - EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ], - DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ], - TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ], - PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ], - SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ], - SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ], - ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ], - IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ], - MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ], - TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ] + EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100], + DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ], + TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ], + PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ], + SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ], + SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ], + ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ], + IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ], + MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ], + TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708, 0x30D109 ], + Kraid : [ 0xF26002, 0xF26004, 0xF26005, 0xF26000, 0xF26006, 0xF26007, 0x82FD36 ], + WreckedShip : [ 0xF2600A, 0xF2600C, 0xF2600D, 0xF26008, 0xF2600E, 0xF2600F, 0x82FE26 ], + Inner : [ 0xF26012, 0xF26014, 0xF26015, 0xF26010, 0xF26016, 0xF26017, 0x82FE76 ], + East : [ 0xF2601A, 0xF2601C, 0xF2601D, 0xF26018, 0xF2601E, 0xF2601F, 0x82FDD6 ] } + result = regionType.get(type(region), None) if result is None: raise exception(f"Region {region} should not be a dungeon reward region") @@ -208,13 +233,13 @@ class Patch: def CrystalValues(self, crystal: int): crystalMap = { - 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ], - 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ], - 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ], - 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ], - 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ], - 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ], - 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ], + 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ], + 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ], + 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ], + 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ], + 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ], + 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ], + 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ], } result = crystalMap.get(crystal, None) if result is None: @@ -224,15 +249,28 @@ class Patch: def PendantValues(self, pendant: int): pendantMap = { - 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ], - 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ], - 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ], + 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ], + 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ], + 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ] } result = pendantMap.get(pendant, None) if result is None: raise exception(f"Tried using {pendant} as a pendant number") else: return result + + def BossTokenValues(self, token: int): + tokenMap = { + 1 : [ 0x01, 0x38, 0x40, 0x80, 0x69, 0x80, 0x15 ], + 2 : [ 0x02, 0x34, 0x42, 0x80, 0x69, 0x81, 0x16 ], + 3 : [ 0x04, 0x34, 0x44, 0x80, 0x69, 0x82, 0x17 ], + 4 : [ 0x08, 0x32, 0x46, 0x80, 0x69, 0x83, 0x18 ] + } + result = tokenMap.get(token, None) + if result is None: + raise exception(f"Tried using {token} as a boss token number") + else: + return result def WriteSMLocations(self, locations: List[Location]): def GetSMItemPLM(location:Location): @@ -259,7 +297,7 @@ class Patch: ItemType.SpaceJump : 0xEF1B, ItemType.ScrewAttack : 0xEF1F } - plmId = 0xEFE0 if self.myWorld.Config.GameMode == GameMode.Multiworld else \ + plmId = 0xEFE0 if self.myWorld.Config.Multiworld else \ itemMap.get(location.APLocation.item.item.Type, 0xEFE0) if (plmId == 0xEFE0): plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0 @@ -268,7 +306,7 @@ class Patch: return plmId for location in locations: - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location)))) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -283,18 +321,14 @@ class Patch: self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - dialog = Dialog.Simple(text) if (location.Type == LocationType.Pedestal): self.stringTable.SetPedestalText(text) - self.patches.append((Snes(0x308300), dialog)) elif (location.Type == LocationType.Ether): self.stringTable.SetEtherText(text) - self.patches.append((Snes(0x308F00), dialog)) elif (location.Type == LocationType.Bombos): self.stringTable.SetBombosText(text) - self.patches.append((Snes(0x309000), dialog)) - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), [(location.Id - 256)])) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -305,11 +339,11 @@ class Patch: item = location.APLocation.item.item itemDungeon = None if item.IsKey(): - itemDungeon = ItemType.Key if (not item.World.Config.Keysanity or item.Type != ItemType.KeyHC) else ItemType.KeyHC + itemDungeon = ItemType.Key elif item.IsBigKey(): itemDungeon = ItemType.BigKey elif item.IsMap(): - itemDungeon = ItemType.Map if (not item.World.Config.Keysanity or item.Type != ItemType.MapHC) else ItemType.MapHC + itemDungeon = ItemType.Map elif item.IsCompass(): itemDungeon = ItemType.Compass @@ -327,15 +361,11 @@ class Patch: def WriteDungeonMusic(self, keysanity: bool): if (not keysanity): - regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] - music = [] + regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and + region.Reward != None and region.Reward != RewardType.Agahnim] pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]] crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]] - regions = pendantRegions + crystalRegions - music = [ - 0x11, 0x11, 0x11, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, - ] + music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions] self.patches += self.MusicPatches(regions, music) #IEnumerable RandomDungeonMusic() { @@ -366,51 +396,13 @@ class Patch: else: return result - def WritePrizeShuffle(self): - prizePackItems = 56 - treePullItems = 3 - - bytes = [] - drop = 0 - final = 0 - - pool = [ - DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 - DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 - DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 - DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 - DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 - DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 - DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 - DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees - DropPrize.Green, DropPrize.Red,#// from prize crab - DropPrize.Green, #// stunned prize - DropPrize.Red,#// saved fish prize - ] - - prizes = pool - self.rnd.shuffle(prizes) - - #/* prize pack drop order */ - (bytes, prizes) = SplitOff(prizes, prizePackItems) - self.patches.append((Snes(0x6FA78), [byte.value for byte in bytes])) - - #/* tree pull prizes */ - (bytes, prizes) = SplitOff(prizes, treePullItems) - self.patches.append((Snes(0x1DFBD4), [byte.value for byte in bytes])) - - #/* crab prizes */ - (drop, final, prizes) = (prizes[0], prizes[1], prizes[2:]) - self.patches.append((Snes(0x6A9C8), [ drop.value ])) - self.patches.append((Snes(0x6A9C4), [ final.value ])) - - #/* stun prize */ - (drop, prizes) = (prizes[0], prizes[1:]) - self.patches.append((Snes(0x6F993), [ drop.value ])) - - #/* fish prize */ - drop = prizes[0] - self.patches.append((Snes(0x1D82CC), [ drop.value ])) + def WritePrizeShuffle(self, dropPrizes): + self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs])) + self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls])) + self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value])) + self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value])) + self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value])) + self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value])) self.patches += self.EnemyPrizePackDistribution() @@ -524,46 +516,29 @@ class Patch: redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed] sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon) - self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla))) self.stringTable.SetSahasrahlaRevealText(sahasrahla) bombShop = Texts.BombShopReveal(redCrystalDungeons) - self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop))) self.stringTable.SetBombShopRevealText(bombShop) blind = Texts.Blind(self.rnd) - self.patches.append((Snes(0x308800), Dialog.Simple(blind))) self.stringTable.SetBlindText(blind) tavernMan = Texts.TavernMan(self.rnd) - self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan))) self.stringTable.SetTavernManText(tavernMan) ganon = Texts.GanonFirstPhase(self.rnd) - self.patches.append((Snes(0x308600), Dialog.Simple(ganon))) self.stringTable.SetGanonFirstPhaseText(ganon) - #// Todo: Verify these two are correct if ganon invincible patch is ever added - #// ganon_fall_in_alt in v30 - ganonFirstPhaseInvincible = "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - self.patches.append((Snes(0x309100), Dialog.Simple(ganonFirstPhaseInvincible))) - - #// ganon_phase_3_alt in v30 - ganonThirdPhaseInvincible = "Got wax in\nyour ears?\nI cannot die!" - self.patches.append((Snes(0x309200), Dialog.Simple(ganonThirdPhaseInvincible))) - #// --- - silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)] if len(silversLocation) == 0: silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID]) else: - silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.GameMode == GameMode.Multiworld else \ + silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.Multiworld else \ Texts.GanonThirdPhaseSingle(silversLocation[0].Region) - self.patches.append((Snes(0x308700), Dialog.Simple(silvers))) self.stringTable.SetGanonThirdPhaseText(silvers) triforceRoom = Texts.TriforceRoom(self.rnd) - self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom))) self.stringTable.SetTriforceRoomText(triforceRoom) def WriteStringTable(self): @@ -579,26 +554,32 @@ class Patch: return bytearray(name, 'utf8') def WriteSeedData(self): - configField = \ + configField1 = \ ((1 if self.myWorld.Config.Race else 0) << 15) | \ ((1 if self.myWorld.Config.Keysanity else 0) << 13) | \ - ((1 if self.myWorld.Config.GameMode == Config.GameMode.Multiworld else 0) << 12) | \ + ((1 if self.myWorld.Config.Multiworld else 0) << 12) | \ (self.myWorld.Config.Z3Logic.value << 10) | \ (self.myWorld.Config.SMLogic.value << 8) | \ (Patch.Major << 4) | \ (Patch.Minor << 0) + configField2 = \ + ((1 if self.myWorld.Config.SwordLocation else 0) << 14) | \ + ((1 if self.myWorld.Config.MorphLocation else 0) << 12) | \ + ((1 if self.myWorld.Config.Goal else 0) << 8) + self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id))) - self.patches.append((Snes(0x80FF52), getWordArray(configField))) + self.patches.append((Snes(0x80FF52), getWordArray(configField1))) self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed))) + self.patches.append((Snes(0x80FF58), getWordArray(configField2))) #/* Reserve the rest of the space for future use */ - self.patches.append((Snes(0x80FF58), [0x00] * 8)) + self.patches.append((Snes(0x80FF5A), [0x00] * 6)) self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8'))) self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8'))) def WriteCommonFlags(self): #/* Common Combo Configuration flags at [asm]/config.asm */ - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(0xF47000), getWordArray(0x0001))) if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0xF47006), getWordArray(0x0001))) @@ -619,97 +600,104 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass - self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// FreeItemText: db #$01 ; #00 = Off (default) - #$01 = On def WriteSMKeyCardDoors(self): - if (not self.myWorld.Config.Keysanity): - return - - plaquePLm = 0xd410 - - doorList = [ - #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) - #// Crateria - [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet - [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB - [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) - [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 - [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo - - #// Brinstar - [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room - - [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area - [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn - [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank - [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super - - [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid - - #// Upper Norfair - [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice - [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice - - [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain - [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain - [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain - [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain - - [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire - - #// Lower Norfair - [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre - [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" - [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley - - #// Maridia - [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia - [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach - - [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon - [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon - - [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon - - [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) - - #// Wrecked Ship - [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check - [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley - [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley - - [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon - ] - - doorId = 0x0000 + plaquePlm = 0xd410 plmTablePos = 0xf800 - for door in doorList: - doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] - if (door[6] == 0): - #// Write dynamic door - doorData = [] - for x in door[0:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) - plmTablePos += 0x08 - else: - #// Overwrite existing door - doorData = [] - for x in door[1:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + door[6]), doorData)) - if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): - #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself - self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) - #// Plaque data - if (door[4] != KeycardPlaque.Null): - plaqueData = getWordArray(door[0]) + getWordArray(plaquePLm) + getWordArray(door[5]) + getWordArray(door[4]) - self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) - plmTablePos += 0x08 - doorId += 1 + if ( self.myWorld.Config.Keysanity): + doorList = [ + #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) + #// Crateria + [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet + [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB + [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) + [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 + [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo + + #// Brinstar + [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room + + [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area + [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn + [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank + [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super + + [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid + + #// Upper Norfair + [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice + [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice + + [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain + [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain + [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain + [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain + + [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire + + #// Lower Norfair + [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre + [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" + [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley + + #// Maridia + [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia + [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach + + [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon + [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon + + [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon + + [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) + + #// Wrecked Ship + [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check + [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley + [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley + + [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon + ] + + doorId = 0x0000 + for door in doorList: + #/* When "Fast Ganon" is set, don't place the G4 Boss key door to enable faster games */ + if (door[0] == 0x99BD and self.myWorld.Config.Goal == Goal.FastGanonDefeatMotherBrain): + continue + doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] + if (door[6] == 0): + #// Write dynamic door + doorData = [] + for x in door[0:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) + plmTablePos += 0x08 + else: + #// Overwrite existing door + doorData = [] + for x in door[1:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + door[6]), doorData)) + if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): + #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself + self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) + + #// Plaque data + if (door[4] != KeycardPlaque.Null): + plaqueData = getWordArray(door[0]) + getWordArray(plaquePlm) + getWordArray(door[5]) + getWordArray(door[4]) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 + doorId += 1 + + #/* Write plaque showing SM bosses that needs to be killed */ + if (self.myWorld.Config.OpenTourian != OpenTourian.FourBosses): + plaqueData = getWordArray(0xA5ED) + getWordArray(plaquePlm) + getWordArray(0x044F) + getWordArray(KeycardPlaque.Zero + self.myWorld.TourianBossTokens) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])) @@ -745,20 +733,32 @@ class Patch: (Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]), ] - def WriteGanonInvicible(self, invincible: GanonInvincible): + def WritePreOpenPyramid(self, goal: Goal): + if (goal == Goal.FastGanonDefeatMotherBrain): + self.patches.append((Snes(0x30808B), [0x01])) + + def WriteGanonInvicible(self, goal: Goal): #/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */ - invincibleMap = { - GanonInvincible.Never : 0x00, - GanonInvincible.Always : 0x01, - GanonInvincible.BeforeAllDungeons : 0x02, - GanonInvincible.BeforeCrystals : 0x03, - } - value = invincibleMap.get(invincible, None) + valueMap = { + Goal.DefeatBoth : 0x03, + Goal.FastGanonDefeatMotherBrain : 0x04, + Goal.AllDungeonsDefeatMotherBrain : 0x02 + } + value = valueMap.get(goal, None) if (value is None): - raise exception(f"Unknown Ganon invincible value {invincible}") + raise exception(f"Unknown Ganon invincible value {goal}") else: self.patches.append((Snes(0x30803E), [value])) + def WriteBossesNeeded(self, tourianBossTokens): + self.patches.append((Snes(0xF47200), getWordArray(tourianBossTokens))) + + def WriteCrystalsNeeded(self, towerCrystals, ganonCrystals): + self.patches.append((Snes(0x30805E), [towerCrystals])) + self.patches.append((Snes(0x30805F), [ganonCrystals])) + + self.stringTable.SetTowerRequirementText(f"You need {towerCrystals} crystals to enter Ganon's Tower.") + self.stringTable.SetGanonRequirementText(f"You need {ganonCrystals} crystals to defeat Ganon.") def WriteRngBlock(self): #/* Repoint RNG Block */ diff --git a/worlds/smz3/TotalSMZ3/Region.py b/worlds/smz3/TotalSMZ3/Region.py index f352247c80..00e209ce45 100644 --- a/worlds/smz3/TotalSMZ3/Region.py +++ b/worlds/smz3/TotalSMZ3/Region.py @@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType class RewardType(Enum): Null = 0 - Agahnim = 1 - PendantGreen = 2 - PendantNonGreen = 3 - CrystalBlue = 4 - CrystalRed = 5 - GoldenFourBoss = 6 + Agahnim = 1 << 0 + PendantGreen = 1 << 1 + PendantNonGreen = 1 << 2 + CrystalBlue = 1 << 3 + CrystalRed = 1 << 4 + BossTokenKraid = 1 << 5 + BossTokenPhantoon = 1 << 6 + BossTokenDraygon = 1 << 7 + BossTokenRidley = 1 << 8 + + AnyPendant = PendantGreen | PendantNonGreen + AnyCrystal = CrystalBlue | CrystalRed + AnyBossToken = BossTokenKraid | BossTokenPhantoon | BossTokenDraygon | BossTokenRidley class IReward: Reward: RewardType @@ -18,7 +25,7 @@ class IReward: pass class IMedallionAccess: - Medallion: object + Medallion = None class Region: import worlds.smz3.TotalSMZ3.Location as Location diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py index fe3f804da9..2b99081dd3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py @@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward): Name = "Brinstar Kraid" Area = "Brinstar" - Reward = RewardType.GoldenFourBoss + Reward = RewardType.Null def __init__(self, world, config: Config): super().__init__(world, config) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py index 465f885b11..bb1036fb81 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py @@ -40,5 +40,5 @@ class Pink(SMRegion): else: return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \ items.CanUsePowerBombs() or \ - items.CanAccessNorfairUpperPortal() and items.Morph and (items.CanOpenRedDoors() or items.Wave) and \ + items.CanAccessNorfairUpperPortal() and items.Morph and (items.Missile or items.Super or items.Wave ) and \ (items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly()) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py index d223fd82a2..72d10a4496 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py @@ -17,9 +17,9 @@ class East(SMRegion): self.world.CanEnter("Wrecked Ship", items)) if self.Logic == SMLogic.Normal else \ lambda items: items.Morph), Location(self, 2, 0x8F81EE, LocationType.Hidden, "Missile (outside Wrecked Ship top)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 3, 0x8F81F4, LocationType.Visible, "Missile (outside Wrecked Ship middle)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 4, 0x8F8248, LocationType.Visible, "Missile (Crateria moat)", lambda items: True) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py index 280f7e5b28..7de0798bae 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py @@ -9,20 +9,17 @@ class Inner(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 140, 0x8FC4AF, LocationType.Visible, "Super Missile (yellow Maridia)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and self.CanReachAqueduct(items) and + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 141, 0x8FC4B5, LocationType.Visible, "Missile (yellow Maridia super missile)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 142, 0x8FC533, LocationType.Visible, "Missile (yellow Maridia false wall)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 143, 0x8FC559, LocationType.Chozo, "Plasma Beam", lambda items: self.CanDefeatDraygon(items) and (items.ScrewAttack or items.Plasma) and (items.HiJump or items.CanFly()) if self.Logic == SMLogic.Normal else \ lambda items: self.CanDefeatDraygon(items) and @@ -31,17 +28,17 @@ class Inner(SMRegion, IReward): Location(self, 144, 0x8FC5DD, LocationType.Visible, "Missile (left Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 145, 0x8FC5E3, LocationType.Chozo, "Reserve Tank, Maridia", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 146, 0x8FC5EB, LocationType.Visible, "Missile (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump or items.Gravity), Location(self, 147, 0x8FC5F1, LocationType.Visible, "Power Bomb (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump and items.CanSpringBallJump() or items.Gravity), + lambda items: self.CanReachAqueduct(items) and items.Super and (items.Gravity or items.HiJump and items.CanSpringBallJump()), Location(self, 148, 0x8FC603, LocationType.Visible, "Missile (pink Maridia)", lambda items: self.CanReachAqueduct(items) and items.SpeedBooster if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Gravity), diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py index 0cd577f78d..f1a325a12b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py @@ -9,7 +9,7 @@ class East(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 74, 0x8F8FCA, LocationType.Visible, "Missile (lower Norfair above fire flea room)", lambda items: self.CanExit(items)), @@ -30,11 +30,11 @@ class East(SMRegion, IReward): def CanExit(self, items:Progression): if self.Logic == SMLogic.Normal: # /*Bubble Mountain*/ - return items.CardNorfairL2 or ( + return items.Morph and (items.CardNorfairL2 or ( # /* Volcano Room and Blue Gate */ items.Gravity) and items.Wave and ( # /*Spikey Acid Snakes and Croc Escape*/ - items.Grapple or items.SpaceJump) + items.Grapple or items.SpaceJump)) else: # /*Vanilla LN Escape*/ return (items.Morph and (items.CardNorfairL2 or # /*Bubble Mountain*/ diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py index 8740c545e3..4e44d28ca5 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py @@ -17,13 +17,13 @@ class West(SMRegion): items.CanAccessNorfairLowerPortal() and (items.CanFly() or items.CanSpringBallJump() or items.SpeedBooster) and items.Super)), Location(self, 71, 0x8F8E74, LocationType.Hidden, "Super Missile (Gold Torizo)", lambda items: items.CanDestroyBombWalls() and (items.Super or items.Charge) and - (items.CanAccessNorfairLowerPortal() or items.SpaceJump and items.CanUsePowerBombs()) if self.Logic == SMLogic.Normal else \ + (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ lambda items: items.CanDestroyBombWalls() and items.Varia and (items.Super or items.Charge)), Location(self, 79, 0x8F9110, LocationType.Chozo, "Screw Attack", - lambda items: items.CanDestroyBombWalls() and (items.SpaceJump and items.CanUsePowerBombs() or items.CanAccessNorfairLowerPortal()) if self.Logic == SMLogic.Normal else \ - lambda items: items.CanDestroyBombWalls() and (items.Varia or items.CanAccessNorfairLowerPortal())), + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.Varia)), Location(self, 73, 0x8F8F30, LocationType.Visible, "Missile (Mickey Mouse room)", - lambda items: items.CanFly() and items.Morph and items.Super and ( + lambda items: items.Morph and items.Super and items.CanFly() and items.CanUsePowerBombs() and ( # /*Exit to Upper Norfair*/ (items.CardLowerNorfairL1 or # /*Vanilla or Reverse Lava Dive*/ @@ -33,17 +33,20 @@ class West(SMRegion): # /* Volcano Room and Blue Gate */ items.Gravity and items.Wave and # /*Spikey Acid Snakes and Croc Escape*/ - (items.Grapple or items.SpaceJump) or + (items.Grapple or items.SpaceJump) or # /*Exit via GT fight and Portal*/ - (items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge))) if self.Logic == SMLogic.Normal else \ + items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge)) if self.Logic == SMLogic.Normal else \ lambda items: - items.Morph and items.Varia and items.Super and ((items.CanFly() or items.CanSpringBallJump() or - items.SpeedBooster and (items.HiJump and items.CanUsePowerBombs() or items.Charge and items.Ice)) and - # /*Exit to Upper Norfair*/ - (items.CardNorfairL2 or (items.SpeedBooster or items.CanFly() or items.Grapple or items.HiJump and - (items.CanSpringBallJump() or items.Ice))) or - # /*Return to Portal*/ - items.CanUsePowerBombs())) + items.Varia and items.Morph and items.Super and + #/* Climb worst room (from LN East CanEnter) */ + (items.CanFly() or items.HiJump or items.CanSpringBallJump() or items.Ice and items.Charge) and + (items.CanPassBombPassages() or items.ScrewAttack and items.SpaceJump) and ( + #/* Exit to Upper Norfair */ + items.CardNorfairL2 or items.SpeedBooster or items.CanFly() or items.Grapple or + items.HiJump and (items.CanSpringBallJump() or items.Ice) or + #/* Portal (with GGG) */ + items.CanUsePowerBombs() + )) ] # // Todo: account for Croc Speedway once Norfair Upper East also do so, otherwise it would be inconsistent to do so here diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py index 914d07c3be..b38bbe70c6 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py @@ -45,11 +45,10 @@ class Crocomire(SMRegion): # /* Cathedral -> through the floor or Vulcano */ items.CanOpenRedDoors() and (items.CardNorfairL2 if self.Config.Keysanity else items.Super) and (items.CanFly() or items.HiJump or items.SpeedBooster) and - (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave - or + (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and - items.Gravity and items.Wave and (items.CardNorfairL2 or items.Morph)) + items.Varia) and items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and ( + items.Gravity) and items.Wave and (items.CardNorfairL2 or items.Morph) else: return ((items.CanDestroyBombWalls() or items.SpeedBooster) and items.Super and items.Morph or items.CanAccessNorfairUpperPortal()) and ( # /* Ice Beam -> Croc Speedway */ @@ -65,5 +64,5 @@ class Crocomire(SMRegion): (items.Missile or items.Super or items.Wave) # /* Blue Gate */ ) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Varia and items.Super and ( + items.Varia and items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Super and ( items.HasEnergyReserves(2)) and (items.CardNorfairL2 or items.Morph) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py index 053de3d1a6..e83c6f539c 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py @@ -9,12 +9,13 @@ class WreckedShip(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Weight = 4 + self.Reward = RewardType.Null self.Locations = [ Location(self, 128, 0x8FC265, LocationType.Visible, "Missile (Wrecked Ship middle)", lambda items: items.CanPassBombPassages()), Location(self, 129, 0x8FC2E9, LocationType.Chozo, "Reserve Tank, Wrecked Ship", - lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.SpeedBooster and items.CanUsePowerBombs() and + lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Grapple or items.SpaceJump or items.Varia and items.HasEnergyReserves(2) or items.HasEnergyReserves(3)) if self.Logic == SMLogic.Normal else \ lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Varia or items.HasEnergyReserves(2))), @@ -27,7 +28,7 @@ class WreckedShip(SMRegion, IReward): Location(self, 132, 0x8FC337, LocationType.Visible, "Energy Tank, Wrecked Ship", lambda items: self.CanUnlockShip(items) and (items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanUnlockShip(items) and (items.Bombs or items.PowerBomb or items.CanSpringBallJump() or + lambda items: self.CanUnlockShip(items) and (items.Morph and (items.Bombs or items.PowerBomb) or items.CanSpringBallJump() or items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity)), Location(self, 133, 0x8FC357, LocationType.Visible, "Super Missile (Wrecked Ship left)", lambda items: self.CanUnlockShip(items)), diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py index 5bc581c6d4..7ca34cb031 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py @@ -14,15 +14,15 @@ class NorthEast(Z3Region): lambda items: items.MoonPearl and items.CanLiftLight()), Location(self, 256+79, 0x308147, LocationType.Regular, "Pyramid"), Location(self, 256+80, 0x1E980, LocationType.Regular, "Pyramid Fairy - Left", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))), + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))), Location(self, 256+81, 0x1E983, LocationType.Regular, "Pyramid Fairy - Right", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))) + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))) ] def CanEnter(self, items: Progression): - return self.world.CanAquire(items, RewardType.Agahnim) or items.MoonPearl and ( + return self.world.CanAcquire(items, RewardType.Agahnim) or items.MoonPearl and ( items.Hammer and items.CanLiftLight() or items.CanLiftHeavy() and items.Flippers or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py index 57b5ece194..28a318e80d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py @@ -25,7 +25,7 @@ class NorthWest(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and items.Hookshot and (items.Flippers or items.CanLiftLight() or items.Hammer) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py index f43cb8886b..14f4515c6d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py @@ -21,7 +21,7 @@ class South(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and (items.Hammer or items.Hookshot and (items.Flippers or items.CanLiftLight())) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py index 68fbd3b8db..0f21d7e284 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py @@ -34,33 +34,33 @@ class GanonsTower(Z3Region): self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ]) or self.GetLocation("Ganon's Tower - Firesnake Room").ItemIs(ItemType.KeyGT, self.world) else 3)), - Location(self, 256+196, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", + Location(self, 256+230, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+197, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", + Location(self, 256+231, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+198, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", + Location(self, 256+232, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+199, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", + Location(self, 256+233, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left") ])), - Location(self, 256+200, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), - Location(self, 256+201, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), - Location(self, 256+202, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", + Location(self, 256+234, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), + Location(self, 256+235, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), + Location(self, 256+236, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", lambda items: items.Somaria), Location(self, 256+203, 0x1EAE5, LocationType.Regular, "Ganon's Tower - Compass Room - Top Left", lambda items: self.RightSide(items, [ @@ -118,8 +118,9 @@ class GanonsTower(Z3Region): return items.Somaria and items.Firerod and items.KeyGT >= (3 if any(l.ItemIs(ItemType.BigKeyGT, self.world) for l in locations) else 4) def BigKeyRoom(self, items: Progression): - return items.KeyGT >= 3 and self.CanBeatArmos(items) \ - and (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) + return items.KeyGT >= 3 and \ + (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) \ + and self.CanBeatArmos(items) def TowerAscend(self, items: Progression): return items.BigKeyGT and items.KeyGT >= 3 and items.Bow and items.CanLightTorches() @@ -134,13 +135,14 @@ class GanonsTower(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and self.world.CanEnter("Dark World Death Mountain East", items) and \ - self.world.CanAquireAll(items, RewardType.CrystalBlue, RewardType.CrystalRed, RewardType.GoldenFourBoss) + self.world.CanAcquireAtLeast(self.world.TowerCrystals, items, RewardType.AnyCrystal) and \ + self.world.CanAcquireAtLeast(self.world.TourianBossTokens * (self.world.TowerCrystals / 7), items, RewardType.AnyBossToken) def CanFill(self, item: Item): - if (self.Config.GameMode == GameMode.Multiworld): + if (self.Config.Multiworld): if (item.World != self.world or item.Progression): return False - if (self.Config.KeyShuffle == KeyShuffle.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): + if (self.Config.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): return False return super().CanFill(item) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py index 6543017c6f..9b16a08b4d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py @@ -10,6 +10,7 @@ class IcePalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 4 self.RegionItems = [ ItemType.KeyIP, ItemType.BigKeyIP, ItemType.MapIP, ItemType.CompassIP] self.Reward = RewardType.Null self.Locations = [ @@ -43,7 +44,7 @@ class IcePalace(Z3Region, IReward): ] def CanNotWasteKeysBeforeAccessible(self, items: Progression, locations: List[Location]): - return not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) + return self.world.ForwardSearch or not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) def CanEnter(self, items: Progression): return items.MoonPearl and items.Flippers and items.CanLiftHeavy() and items.CanMeltFreezors() diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py index 0edf93f302..c111b07dfd 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py @@ -24,5 +24,5 @@ class NorthEast(Z3Region): Location(self, 256+42, 0x1EA85, LocationType.Regular, "Sahasrahla's Hut - Middle").Weighted(sphereOne), Location(self, 256+43, 0x1EA88, LocationType.Regular, "Sahasrahla's Hut - Right").Weighted(sphereOne), Location(self, 256+44, 0x5F1FC, LocationType.Regular, "Sahasrahla", - lambda items: self.world.CanAquire(items, RewardType.PendantGreen)) + lambda items: self.world.CanAcquire(items, RewardType.PendantGreen)) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py index f35cbad33e..46f830dc8b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py @@ -11,11 +11,11 @@ class NorthWest(Z3Region): sphereOne = -14 self.Locations = [ Location(self, 256+14, 0x589B0, LocationType.Pedestal, "Master Sword Pedestal", - lambda items: self.world.CanAquireAll(items, RewardType.PendantGreen, RewardType.PendantNonGreen)), + lambda items: self.world.CanAcquireAll(items, RewardType.AnyPendant)), Location(self, 256+15, 0x308013, LocationType.Regular, "Mushroom").Weighted(sphereOne), Location(self, 256+16, 0x308000, LocationType.Regular, "Lost Woods Hideout").Weighted(sphereOne), Location(self, 256+17, 0x308001, LocationType.Regular, "Lumberjack Tree", - lambda items: self.world.CanAquire(items, RewardType.Agahnim) and items.Boots), + lambda items: self.world.CanAcquire(items, RewardType.Agahnim) and items.Boots), Location(self, 256+18, 0x1EB3F, LocationType.Regular, "Pegasus Rocks", lambda items: items.Boots), Location(self, 256+19, 0x308004, LocationType.Regular, "Graveyard Ledge", diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py index 855c326f23..b1746184d3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py @@ -10,9 +10,10 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 2 self.RegionItems = [ ItemType.KeyMM, ItemType.BigKeyMM, ItemType.MapMM, ItemType.CompassMM] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+169, 0x1EA5E, LocationType.Regular, "Misery Mire - Main Lobby", lambda items: items.BigKeyMM or items.KeyMM >= 1), @@ -34,8 +35,9 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): # // Need "CanKillManyEnemies" if implementing swordless def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and (items.Boots or items.Hookshot) and \ self.world.CanEnter("Dark World Mire", items) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index 1805e74dca..27b5a1db43 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -10,6 +10,7 @@ class SwampPalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 3 self.RegionItems = [ ItemType.KeySP, ItemType.BigKeySP, ItemType.MapSP, ItemType.CompassSP] self.Reward = RewardType.Null self.Locations = [ diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py index c4d19bcda1..45546e9e99 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py @@ -10,9 +10,10 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 6 self.RegionItems = [ ItemType.KeyTR, ItemType.BigKeyTR, ItemType.MapTR, ItemType.CompassTR] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+177, 0x1EA22, LocationType.Regular, "Turtle Rock - Compass Chest"), Location(self, 256+178, 0x1EA1C, LocationType.Regular, "Turtle Rock - Roller Room - Left", @@ -46,8 +47,9 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): return items.Firerod and items.Icerod def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and items.CanLiftHeavy() and items.Hammer and items.Somaria and \ self.world.CanEnter("Light World Death Mountain East", items) diff --git a/worlds/smz3/TotalSMZ3/Text/Dialog.py b/worlds/smz3/TotalSMZ3/Text/Dialog.py index d465e1e201..92e034af67 100644 --- a/worlds/smz3/TotalSMZ3/Text/Dialog.py +++ b/worlds/smz3/TotalSMZ3/Text/Dialog.py @@ -4,9 +4,7 @@ class Dialog: command = re.compile(r"^\{[^}]*\}") invalid = re.compile(r"(?[0-9])|(?P[A-Z])|(?P[a-z])") @staticmethod def Simple(text: str): @@ -29,19 +27,16 @@ class Dialog: lineIndex += 1 - if (lineIndex % 3 == 0 and lineIndex < len(lines)): - bytes.append(0x7E) - if (lineIndex >= 3 and lineIndex < len(lines)): - bytes.append(0x73) + if (lineIndex < len(lines)): + if (lineIndex % 3 == 0): + bytes.append(0x7E) # pause for input + if (lineIndex >= 3): + bytes.append(0x73) # scroll - bytes.append(0x7F) - if (len(bytes) > maxBytes): - return bytes[:maxBytes - 1].append(0x7F) - - return bytes + return bytes[:maxBytes - 1].append(0x7F) @staticmethod - def Compiled(text: str, pause = True): + def Compiled(text: str): maxBytes = 2046 wrap = 19 @@ -49,6 +44,7 @@ class Dialog: raise Exception("Dialog commands must be placed on separate lines", text) padOut = False + pause = True bytes = [ 0xFB ] lines = Dialog.Wordwrap(text, wrap) @@ -61,33 +57,11 @@ class Dialog: return [ 0xFB, 0xFE, 0x6E, 0x00, 0xFE, 0x6B, 0x04 ] if (match.string == "{INTRO}"): padOut = True + if (match.string == "{NOPAUSE}"): + pause = False + continue - bytesMap = { - "{SPEED0}" : [ 0xFC, 0x00 ], - "{SPEED2}" : [ 0xFC, 0x02 ], - "{SPEED6}" : [ 0xFC, 0x06 ], - "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], - "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], - "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], - "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], - "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], - "{INPUT}" : [ 0xFA ], - "{CHOICE}" : [ 0xFE, 0x68 ], - "{ITEMSELECT}" : [ 0xFE, 0x69 ], - "{CHOICE2}" : [ 0xFE, 0x71 ], - "{CHOICE3}" : [ 0xFE, 0x72 ], - "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], - "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], - "{HARP}" : [ 0xFE, 0x79, 0x2D ], - "{MENU}" : [ 0xFE, 0x6D, 0x00 ], - "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], - "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], - "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], - "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], - "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], - "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], - } - result = bytesMap.get(match.string, None) + result = Dialog.CommandBytesFor(match.string) if (result is None): raise Exception(f"Dialog text contained unknown command {match.string}", text) else: @@ -98,12 +72,10 @@ class Dialog: continue - if (lineIndex == 1): - bytes.append(0xF8); #// row 2 - elif (lineIndex >= 3 and lineIndex < lineCount): - bytes.append(0xF6); #// scroll - elif (lineIndex >= 2): - bytes.append(0xF9); #// row 3 + if (lineIndex > 0): + bytes.append(0xF8 if lineIndex == 1 else #// row 2 + 0xF9 if lineIndex == 2 else #// row 3 + 0xF6) #// scroll #// The first box needs to fill the full width with spaces as the palette is loaded weird. letters = line + (" " * wrap) if padOut and lineIndex < 3 else line @@ -113,10 +85,39 @@ class Dialog: lineIndex += 1 if (pause and lineIndex % 3 == 0 and lineIndex < lineCount): - bytes.append(0xFA) #// wait for input + bytes.append(0xFA) #// pause for input return bytes[:maxBytes] + @staticmethod + def CommandBytesFor(text: str): + bytesMap = { + "{SPEED0}" : [ 0xFC, 0x00 ], + "{SPEED2}" : [ 0xFC, 0x02 ], + "{SPEED6}" : [ 0xFC, 0x06 ], + "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], + "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], + "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], + "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], + "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], + "{INPUT}" : [ 0xFA ], + "{CHOICE}" : [ 0xFE, 0x68 ], + "{ITEMSELECT}" : [ 0xFE, 0x69 ], + "{CHOICE2}" : [ 0xFE, 0x71 ], + "{CHOICE3}" : [ 0xFE, 0x72 ], + "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], + "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], + "{HARP}" : [ 0xFE, 0x79, 0x2D ], + "{MENU}" : [ 0xFE, 0x6D, 0x00 ], + "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], + "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], + "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], + "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], + "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], + "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], + } + return bytesMap.get(text, None) + @staticmethod def Wordwrap(text: str, width: int): result = [] @@ -146,9 +147,13 @@ class Dialog: @staticmethod def LetterToBytes(c: str): - if Dialog.digit.match(c): return [(ord(c) - ord('0') + 0xA0) ] - elif Dialog.uppercaseLetter.match(c): return [ (ord(c) - ord('A') + 0xAA) ] - elif Dialog.lowercaseLetter.match(c): return [ (ord(c) - ord('a') + 0x30) ] + match = Dialog.character.match(c) + if match is None: + value = Dialog.letters.get(c, None) + return value if value else [ 0xFF ] + elif match.group("digit") != None: return [(ord(c) - ord('0') + 0xA0) ] + elif match.group("upper") != None: return [ (ord(c) - ord('A') + 0xAA) ] + elif match.group("lower") != None: return [ (ord(c) - ord('a') + 0x30) ] else: value = Dialog.letters.get(c, None) return value if value else [ 0xFF ] diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml index 12b5271eab..065e7a9e93 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml @@ -377,9 +377,76 @@ Items: THE GREEN BOOMERANG IS THE FASTEST! - Keycard: |- - A key from - the future? + + CardCrateriaL1: |- + An Alien Key! + It says On top + of the world! + CardCrateriaL2: |- + An Alien Key! + It says Lower + the drawbridge + CardCrateriaBoss: |- + An Alien Key! + It says The First + and The Last + CardBrinstarL1: |- + An Alien Key! + It says But wait + there's more! + CardBrinstarL2: |- + An Alien Key! + It says + Green Monkeys + CardBrinstarBoss: |- + An Alien Key! + It says + Metroid DLC + CardNorfairL1: |- + An Alien Key! + It says ice? + In this heat? + CardNorfairL2: |- + An Alien Key! + It says + THE BUBBLES! + CardNorfairBoss: |- + An Alien Key! + It says + Place your bets + CardMaridiaL1: |- + An Alien Key! + It says A + Day at the Beach + CardMaridiaL2: |- + An Alien Key! + It says + That's a Moray + CardMaridiaBoss: |- + An Alien Key! + It says Shrimp + for dinner? + CardWreckedShipL1: |- + An Alien Key! + It says + Gutter Ball + CardWreckedShipBoss: |- + An Alien Key! + It says The + Ghost of Arrghus + CardLowerNorfairL1: |- + An Alien Key! + It says Worst + Key in the Game + CardLowerNorfairBoss: |- + An Alien Key! + It says + This guy again? + + SmMap: |- + You can now + find your way + to the stars! Something: |- A small victory! diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml index d1e5b9c364..918ef3f63d 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml @@ -4,8 +4,8 @@ # The order of the dialog entries is significant - set_cursor: [0xFB, 0xFC, 0x00, 0xF9, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] - set_cursor2: [0xFB, 0xFC, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] -- game_over_menu: { NoPause: "{SPEED0}\nSave and Continue\nSave and Quit\nContinue" } -- var_test: { NoPause: "0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" } +- game_over_menu: "{NOPAUSE}\n{SPEED0}\nSave and Continue\nSave and Quit\nContinue" +- var_test: "{NOPAUSE}\n0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" - follower_no_enter: "Can't you take me some place nice." - choice_1_3: [0xFB, 0xFC, 0x00, 0xF7, 0xE4, 0xF8, 0xFF, 0xF9, 0xFF, 0xFE, 0x71] - choice_2_3: [0xFB, 0xFC, 0x00, 0xF7, 0xFF, 0xF8, 0xE4, 0xF9, 0xFF, 0xFE, 0x71] @@ -290,10 +290,10 @@ # $110 - magic_bat_wake: "You bum! I was sleeping! Where's my magic bolts?" - magic_bat_give_half_magic: "How you like me now?" -- intro_main: { NoPause: "{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" } -- intro_throne_room: { NoPause: "{IBOX}\nLook at this Stalfos on the throne." } -- intro_zelda_cell: { NoPause: "{IBOX}\nIt is your time to shine!" } -- intro_agahnim: { NoPause: "{IBOX}\nAlso, you need to defeat this guy!" } +- intro_main: "{NOPAUSE}\n{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" +- intro_throne_room: "{NOPAUSE}\n{IBOX}\nLook at this Stalfos on the throne." +- intro_zelda_cell: "{NOPAUSE}\n{IBOX}\nIt is your time to shine!" +- intro_agahnim: "{NOPAUSE}\n{IBOX}\nAlso, you need to defeat this guy!" - pickup_purple_chest: "A curious box. Let's take it with us!" - bomb_shop: "30 bombs for 100 rupees. Good deals all day!" - bomb_shop_big_bomb: "30 bombs for 100 rupees, 100 rupees 1 BIG bomb. Good deals all day!" @@ -341,7 +341,6 @@ # $140 - agahnim_defeated: "Arrrgggghhh. Well you're coming with me!" - agahnim_final_meeting: "You have done well to come this far. Now, die!" -# $142 - zora_meeting: "What do you want?\n ≥ Flippers\n _Nothin'\n{CHOICE}" - zora_tells_cost: "Fine! But they aren't cheap. You got 500 rupees?\n ≥ Duh\n _Oh carp\n{CHOICE}" - zora_get_flippers: "Here's some Flippers for you! Swim little fish, swim." @@ -396,14 +395,12 @@ - lost_woods_thief: "Have you seen Andy?\n\nHe was out looking for our prized Ether medallion.\nI wonder when he will be back?" - blinds_hut_dude: "I'm just some dude. This is Blind's hut." - end_triforce: "{SPEED2}\n{MENU}\n{NOBORDER}\n G G" -# $174 - toppi_fallen: "Ouch!\n\nYou Jerk!" - kakariko_tavern_fisherman: "Don't argue\nwith a frozen\nDeadrock.\nHe'll never\nchange his\nposition!" - thief_money: "It's a secret to everyone." - thief_desert_rupee_cave: "So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees." - thief_ice_rupee_cave: "I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am." - telepathic_tile_south_east_darkworld_cave: "~~ Dev cave ~~\n No farming\n required" -# $17A - cukeman: "Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?" - cukeman_2: "You found Shabadoo, huh?\nNiiiiice." - potion_shop_no_cash: "Yo! I'm not running a charity here." @@ -415,19 +412,25 @@ - game_chest_lost_woods: "Pay 100 rupees, open 1 chest. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}" - kakariko_flophouse_man_no_flippers: "I sure do have a lot of beds.\n\nZora is a cheapskate and will try to sell you his trash for 500 rupees…" - kakariko_flophouse_man: "I sure do have a lot of beds.\n\nDid you know if you played the flute in the center of town things could happen?" -- menu_start_2: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" } -- menu_start_3: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" } -- menu_pause: { NoPause: "{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" } +- menu_start_2: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" +- menu_start_3: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" +- menu_pause: "{NOPAUSE}\n{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" - game_digging_choice: "Have 80 Rupees? Want to play digging game?\n ≥Yes\n _No\n{CHOICE}" - game_digging_start: "Okay, use the shovel with Y!" - game_digging_no_cash: "Shovel rental is 80 rupees.\nI have all day" - game_digging_end_time: "Time's up!\nTime for you to go." - game_digging_come_back_later: "Come back later, I have to bury things." - game_digging_no_follower: "Something is following you. I don't like." -- menu_start_4: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" } +- menu_start_4: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" - ganon_fall_in_alt: "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - ganon_phase_3_alt: "Got wax in your ears? I cannot die!" # $190 - sign_east_death_mountain_bridge: "How did you get up here?" - fish_money: "It's a secret to everyone." +- sign_ganons_tower: "You need all 7 crystals to enter." +- sign_ganon: "You need all 7 crystals to beat Ganon." +- ganon_phase_3_no_bow: "You have no bow. Dingus!" +- ganon_phase_3_no_silvers_alt: "You can't best me without silver arrows!" +- ganon_phase_3_no_silvers: "You can't best me without silver arrows!" +- ganon_phase_3_silvers: "Oh no! Silver! My one true weakness!" - end_pad_data: "" diff --git a/worlds/smz3/TotalSMZ3/Text/StringTable.py b/worlds/smz3/TotalSMZ3/Text/StringTable.py index 4c1986993a..13f3f5edb5 100644 --- a/worlds/smz3/TotalSMZ3/Text/StringTable.py +++ b/worlds/smz3/TotalSMZ3/Text/StringTable.py @@ -3,7 +3,7 @@ from typing import Any, List import copy from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog from worlds.smz3.TotalSMZ3.Text.Texts import text_folder -from yaml import load, Loader +from Utils import unsafe_parse_yaml class StringTable: @@ -11,7 +11,7 @@ class StringTable: def ParseEntries(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - content = load(yaml, Loader) + content = unsafe_parse_yaml(yaml) result = [] for entryValue in content: @@ -20,8 +20,6 @@ class StringTable: result.append((key, value)) elif isinstance(value, str): result.append((key, Dialog.Compiled(value))) - elif isinstance(value, dict): - result.append((key, Dialog.Compiled(value["NoPause"], False))) else: raise Exception(f"Did not expect an object of type {type(value)}") return result @@ -47,9 +45,11 @@ class StringTable: def SetGanonThirdPhaseText(self, text: str): self.SetText("ganon_phase_3", text) + self.SetText("ganon_phase_3_no_silvers", text) + self.SetText("ganon_phase_3_no_silvers_alt", text) def SetTriforceRoomText(self, text: str): - self.SetText("end_triforce", "{NOBORDER}\n" + text) + self.SetText("end_triforce", f"{{NOBORDER}}\n{text}") def SetPedestalText(self, text: str): self.SetText("mastersword_pedestal_translated", text) @@ -60,6 +60,12 @@ class StringTable: def SetBombosText(self, text: str): self.SetText("tablet_bombos_book", text) + def SetTowerRequirementText(self, text: str): + self.SetText("sign_ganons_tower", text) + + def SetGanonRequirementText(self, text: str): + self.SetText("sign_ganon", text) + def SetText(self, name: str, text: str): count = 0 for key, value in self.entries: diff --git a/worlds/smz3/TotalSMZ3/Text/Texts.py b/worlds/smz3/TotalSMZ3/Text/Texts.py index 247ff92b1a..dfaeee06da 100644 --- a/worlds/smz3/TotalSMZ3/Text/Texts.py +++ b/worlds/smz3/TotalSMZ3/Text/Texts.py @@ -2,7 +2,7 @@ from worlds.smz3.TotalSMZ3.Region import Region from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Item import Item, ItemType -from yaml import load, Loader +from Utils import unsafe_parse_yaml import random import os @@ -13,7 +13,7 @@ class Texts: def ParseYamlScripts(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - return load(yaml, Loader) + return unsafe_parse_yaml(yaml) @staticmethod def ParseTextScript(resource: str): @@ -75,7 +75,7 @@ class Texts: } if item.IsMap(): name = "Map" elif item.IsCompass(): name = "Compass" - elif item.IsKeycard(): name = "Keycard" + elif item.IsSmMap(): name = "SmMap" else: name = nameMap[item.Type] items = Texts.scripts["Items"] diff --git a/worlds/smz3/TotalSMZ3/World.py b/worlds/smz3/TotalSMZ3/World.py index 14d685167f..722d5858e6 100644 --- a/worlds/smz3/TotalSMZ3/World.py +++ b/worlds/smz3/TotalSMZ3/World.py @@ -54,10 +54,26 @@ class World: Player: str Guid: str Id: int + WorldState = None + + @property + def TowerCrystals(self): + return 7 if self.WorldState is None else self.WorldState.TowerCrystals + + @property + def GanonCrystals(self): + return 7 if self.WorldState is None else self.WorldState.GanonCrystals + + @property + def TourianBossTokens(self): + return 4 if self.WorldState is None else self.WorldState.TourianBossTokens def Items(self): return [l.Item for l in self.Locations if l.Item != None] + ForwardSearch: bool = False + + rewardLookup: Dict[int, List[Region.IReward]] locationLookup: Dict[str, Location.Location] regionLookup: Dict[str, Region.Region] @@ -95,22 +111,22 @@ class World: DarkWorldNorthEast(self, self.Config), DarkWorldSouth(self, self.Config), DarkWorldMire(self, self.Config), - Central(self, self.Config), CrateriaWest(self, self.Config), + Central(self, self.Config), CrateriaEast(self, self.Config), Blue(self, self.Config), Green(self, self.Config), - Kraid(self, self.Config), Pink(self, self.Config), Red(self, self.Config), + Kraid(self, self.Config), + WreckedShip(self, self.Config), Outer(self, self.Config), Inner(self, self.Config), NorfairUpperWest(self, self.Config), NorfairUpperEast(self, self.Config), Crocomire(self, self.Config), NorfairLowerWest(self, self.Config), - NorfairLowerEast(self, self.Config), - WreckedShip(self, self.Config) + NorfairLowerEast(self, self.Config) ] self.Locations = [] @@ -130,37 +146,32 @@ class World: raise Exception(f"World.CanEnter: Invalid region name {regionName}", f'{regionName=}'.partition('=')[0]) return region.CanEnter(items) - def CanAquire(self, items: Item.Progression, reward: Region.RewardType): + def CanAcquire(self, items: Item.Progression, reward: Region.RewardType): return next(iter([region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == reward])).CanComplete(items) - def CanAquireAll(self, items: Item.Progression, *rewards: Region.RewardType): - for region in self.Regions: - if issubclass(type(region), Region.IReward): - if (region.Reward in rewards): - if not region.CanComplete(items): - return False - return True + def CanAcquireAll(self, items: Item.Progression, rewardsMask: Region.RewardType): + return all(region.CanComplete(items) for region in self.rewardLookup[rewardsMask.value]) - # return all(region.CanComplete(items) for region in self.Regions if (isinstance(region, Region.IReward) and region.Reward in rewards)) + def CanAcquireAtLeast(self, amount, items: Item.Progression, rewardsMask: Region.RewardType): + return len([region for region in self.rewardLookup[rewardsMask.value] if region.CanComplete(items)]) >= amount - def Setup(self, rnd: random): - self.SetMedallions(rnd) - self.SetRewards(rnd) + def Setup(self, state): + self.WorldState = state + self.SetMedallions(state.Medallions) + self.SetRewards(state.Rewards) + self.SetRewardLookup() - def SetMedallions(self, rnd: random): - medallionMap = {0: Item.ItemType.Bombos, 1: Item.ItemType.Ether, 2: Item.ItemType.Quake} - regionList = [region for region in self.Regions if isinstance(region, Region.IMedallionAccess)] - for region in regionList: - region.Medallion = medallionMap[rnd.randint(0, 2)] + def SetRewards(self, rewards: List): + regions = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] + for (region, reward) in zip(regions, rewards): + region.Reward = reward - def SetRewards(self, rnd: random): - rewards = [ - Region.RewardType.PendantGreen, Region.RewardType.PendantNonGreen, Region.RewardType.PendantNonGreen, Region.RewardType.CrystalRed, Region.RewardType.CrystalRed, - Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue - ] - rnd.shuffle(rewards) - regionList = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] - for region in regionList: - region.Reward = rewards[0] - rewards.remove(region.Reward) + def SetMedallions(self, medallions: List): + self.GetRegion("Misery Mire").Medallion = medallions[0] + self.GetRegion("Turtle Rock").Medallion = medallions[1] + def SetRewardLookup(self): + #/* Generate a lookup of all possible regions for any given reward combination for faster lookup later */ + self.rewardLookup: Dict[int, Region.IReward] = {} + for i in range(0, 512): + self.rewardLookup[i] = [region for region in self.Regions if isinstance(region, Region.IReward) and (region.Reward.value & i) != 0] diff --git a/worlds/smz3/TotalSMZ3/WorldState.py b/worlds/smz3/TotalSMZ3/WorldState.py new file mode 100644 index 0000000000..c857b539d1 --- /dev/null +++ b/worlds/smz3/TotalSMZ3/WorldState.py @@ -0,0 +1,170 @@ +from enum import Enum +from typing import List +from copy import copy + +from worlds.smz3.TotalSMZ3.Patch import DropPrize +from worlds.smz3.TotalSMZ3.Region import RewardType +from worlds.smz3.TotalSMZ3.Config import OpenTower, GanonVulnerable, OpenTourian + +class Medallion(Enum): + Bombos = 0 + Ether = 1 + Quake = 2 + +class DropPrizeRecord: + Packs: List[DropPrize] + TreePulls: List[DropPrize] + CrabContinous: DropPrize + CrabFinal: DropPrize + Stun: DropPrize + Fish: DropPrize + + def __init__(self, Packs, TreePulls, CrabContinous, CrabFinal, Stun, Fish): + self.Packs = Packs + self.TreePulls = TreePulls + self.CrabContinous = CrabContinous + self.CrabFinal = CrabFinal + self.Stun = Stun + self.Fish = Fish + +class WorldState: + Rewards: List[RewardType] + Medallions: List[Medallion] + + TowerCrystals: int + GanonCrystals: int + TourianBossTokens: int + + DropPrizes: DropPrizeRecord + + def __init__(self, config, rnd): + self.Rewards = self.DistributeRewards(rnd) + self.Medallions = self.GenerateMedallions(rnd) + self.TowerCrystals = rnd.randint(0, 7) if config.OpenTower == OpenTower.Random else config.OpenTower.value + self.GanonCrystals = rnd.randint(0, 7) if config.GanonVulnerable == GanonVulnerable.Random else config.GanonVulnerable.value + self.TourianBossTokens = rnd.randint(0, 4) if config.OpenTourian == OpenTourian.Random else config.OpenTourian.value + self.DropPrizes = self.ShuffleDropPrizes(rnd) + + @staticmethod + def Generate(config, rnd): + return WorldState(config, rnd) + + BaseRewards = [ + RewardType.PendantGreen, RewardType.PendantNonGreen, RewardType.PendantNonGreen, RewardType.CrystalRed, RewardType.CrystalRed, + RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, + RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, + ] + + BossTokens = [ + RewardType.BossTokenKraid, RewardType.BossTokenPhantoon, RewardType.BossTokenDraygon, RewardType.BossTokenRidley + ] + + @staticmethod + def DistributeRewards(rnd): + #// Assign four rewards for SM using a "loot table", randomized result + gen = WorldState.Distribution().Generate(lambda dist: dist.Hit(rnd.randrange(dist.Sum))) + smRewards = [next(gen) for x in range(4)] + + #// Exclude the SM rewards to get the Z3 lineup + z3Rewards = WorldState.BaseRewards[:] + for reward in smRewards: + z3Rewards.remove(reward) + + rnd.shuffle(z3Rewards) + #// Replace "any token" with random specific tokens + rewards = z3Rewards + smRewards + tokens = WorldState.BossTokens[:] + rnd.shuffle(tokens) + rewards = [tokens.pop() if reward == RewardType.AnyBossToken else reward for reward in rewards] + + return rewards + + + class Distribution: + factor = 3 + + def __init__(self, distribution = None, boss = None, blue = None, red = None, pend = None, green = None): + self.Boss = 4 * self.factor + self.Blue = 5 * self.factor + self.Red = 2 * self.factor + self.Pend = 2 + self.Green = 1 + + if (distribution is not None): + self = copy(distribution) + if (boss is not None): + self.Boss = boss + if (blue is not None): + self.Blue = blue + if (red is not None): + self.Red = red + if (pend is not None): + self.Pend = pend + if (green is not None): + self.Green = green + + @property + def Sum(self): + return self.Boss + self.Blue + self.Red + self.Pend + self.Green + + def Hit(self, p): + p -= self.Boss + if (p < 0): return (RewardType.AnyBossToken, WorldState.Distribution(self, boss = self.Boss - WorldState.Distribution.factor)) + p -= self.Blue + if (p - self.Blue < 0): return (RewardType.CrystalBlue, WorldState.Distribution(self, blue = self.Blue - WorldState.Distribution.factor)) + p -= self.Red + if (p - self.Red < 0): return (RewardType.CrystalRed, WorldState.Distribution(self, red = self.Red - WorldState.Distribution.factor)) + p -= self.Pend + if (p - self.Pend < 0): return (RewardType.PendantNonGreen, WorldState.Distribution(self, pend = self.Pend - 1)) + return (RewardType.PendantGreen, WorldState.Distribution(self, green = self.Green - 1)) + + def Generate(self, func): + result = None + while (True): + (result, newSelf) = func(self) + self.Boss = newSelf.Boss + self.Blue = newSelf.Blue + self.Red = newSelf.Red + self.Pend = newSelf.Pend + self.Green = newSelf.Green + yield result + + @staticmethod + def GenerateMedallions(rnd): + return [ + Medallion(rnd.randrange(3)), + Medallion(rnd.randrange(3)), + ] + + BaseDropPrizes = [ + DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 + DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 + DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 + DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 + DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 + DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 + DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 + DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees + DropPrize.Green, DropPrize.Red,#// from prize crab + DropPrize.Green, #// stunned prize + DropPrize.Red,#// saved fish prize + ] + + @staticmethod + def ShuffleDropPrizes(rnd): + nrPackDrops = 8 * 7 + nrTreePullDrops = 3 + + prizes = WorldState.BaseDropPrizes[:] + rnd.shuffle(prizes) + + (packs, prizes) = (prizes[:nrPackDrops], prizes[nrPackDrops:]) + (treePulls, prizes) = (prizes[:nrTreePullDrops], prizes[nrTreePullDrops:]) + (crabContinous, crabFinalDrop, prizes) = (prizes[0], prizes[1], prizes[2:]) + (stun, prizes) = (prizes[0], prizes[1:]) + fish = prizes[0] + return DropPrizeRecord(packs, treePulls, crabContinous, crabFinalDrop, stun, fish) + + @staticmethod + def SplitOff(source, count): + return (source[:count], source[count:]) \ No newline at end of file diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 9a0fcad90e..2849567d33 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -12,9 +12,10 @@ from worlds.smz3.TotalSMZ3.Item import ItemType import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic +from worlds.smz3.TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray +from worlds.smz3.TotalSMZ3.WorldState import WorldState from ..AutoWorld import World, AutoLogicRegister, WebWorld from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch @@ -60,12 +61,12 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 1 + data_version = 2 options = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id - location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config({}), "", 0, "").locationLookup.items()} + location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} web = SMZ3Web() remote_items: bool = False @@ -180,30 +181,32 @@ class SMZ3World(World): base_combined_rom = get_base_rom_bytes() def generate_early(self): - config = Config({}) - config.GameMode = GameMode.Multiworld - config.Z3Logic = Z3Logic.Normal - config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) - config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) - config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) - config.Goal = Goal.DefeatBoth - config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) - config.Keysanity = config.KeyShuffle != KeyShuffle.Null - config.GanonInvincible = GanonInvincible.BeforeCrystals + self.config = Config() + self.config.GameMode = GameMode.Multiworld + self.config.Z3Logic = Z3Logic.Normal + self.config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) + self.config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) + self.config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) + self.config.Goal = Goal(self.world.goal[self.player].value) + self.config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) + self.config.OpenTower = OpenTower(self.world.open_tower[self.player].value) + self.config.GanonVulnerable = GanonVulnerable(self.world.ganon_vulnerable[self.player].value) + self.config.OpenTourian = OpenTourian(self.world.open_tourian[self.player].value) self.local_random = random.Random(self.world.random.randint(0, 1000)) - self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) + self.smz3World = TotalSMZ3World(self.config, self.world.get_player_name(self.player), self.player, self.world.seed_name) self.smz3DungeonItems = [] SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys()) self.world.state.smz3state[self.player] = TotalSMZ3Item.Progression([]) def generate_basic(self): - self.smz3World.Setup(self.world.random) + self.smz3World.Setup(WorldState.Generate(self.config, self.world.random)) self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World) self.dungeon.reverse() self.progression = TotalSMZ3Item.Item.CreateProgressionPool(self.smz3World) self.keyCardsItems = TotalSMZ3Item.Item.CreateKeycards(self.smz3World) + self.SmMapsItems = TotalSMZ3Item.Item.CreateSmMaps(self.smz3World) niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) @@ -211,7 +214,7 @@ class SMZ3World(World): self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): - progressionItems = self.progression + self.dungeon + self.keyCardsItems + progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: progressionItems = self.progression for item in self.keyCardsItems: @@ -352,6 +355,49 @@ class SMZ3World(World): return patch + def SnesCustomization(self, addr: int): + addr = (0x400000 if addr < 0x800000 else 0)| (addr & 0x3FFFFF) + return addr + + def apply_customization(self): + patch = {} + + # smSpinjumps + if (self.world.spin_jumps_animation[self.player].value == 1): + patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) + + # z3HeartBeep + values = [ 0x00, 0x80, 0x40, 0x20, 0x10] + index = self.world.heart_beep_speed[self.player].value + patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) + + # z3HeartColor + values = [ + [0x24, [0x18, 0x00]], + [0x3C, [0x04, 0x17]], + [0x2C, [0xC9, 0x69]], + [0x28, [0xBC, 0x02]] + ] + index = self.world.heart_color[self.player].value + (hud, fileSelect) = values[index if index < len(values) else 0] + for i in range(0, 20, 2): + patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) + patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) + + # z3QuickSwap + patch[0x40004B] = bytearray([0x01 if self.world.quick_swap[self.player].value else 0x00]) + + # smEnergyBeepOff + if (self.world.energy_beep[self.player].value == 0): + for ([addr, value]) in [ + [0x90EA9B, 0x80], + [0x90F337, 0x80], + [0x91E6D5, 0x80] + ]: + patch[self.SnesCustomization(addr)] = bytearray([value]) + + return patch + def generate_output(self, output_directory: str): try: base_combined_rom = get_base_rom_bytes() @@ -368,6 +414,7 @@ class SMZ3World(World): patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) patches.update(self.apply_item_names()) + patches.update(self.apply_customization()) for addr, bytes in patches.items(): offset = 0 for byte in bytes: @@ -463,7 +510,7 @@ class SMZ3World(World): item.item.Progression = False item.location.event = False self.unreachable.append(item.location) - self.JunkFillGT() + self.JunkFillGT(0.5) def get_pre_fill_items(self): if (not self.smz3World.Config.Keysanity): @@ -477,21 +524,34 @@ class SMZ3World(World): def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) - def JunkFillGT(self): + def JunkFillGT(self, factor): + poolLength = len(self.world.itempool) + junkPoolIdx = [i for i in range(0, poolLength) + if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and + self.world.itempool[i].player == self.player] + toRemove = [] for loc in self.locations.values(): + # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT + # which isnt desirable (SMZ3 logic only filters for SMZ3 items). Having progression in GT can only happen in Single Player. + # if len(toRemove) >= int(len(self.locationNamesGT) * factor * self.smz3World.TowerCrystals / 7): + # break if loc.name in self.locationNamesGT and loc.item is None: - poolLength = len(self.world.itempool) + poolLength = len(junkPoolIdx) # start looking at a random starting index and loop at start if no match found start = self.world.random.randint(0, poolLength) for off in range(0, poolLength): i = (start + off) % poolLength - if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) \ - and loc.can_fill(self.world.state, self.world.itempool[i], False): - itemFromPool = self.world.itempool.pop(i) + candidate = self.world.itempool[junkPoolIdx[i]] + if junkPoolIdx[i] not in toRemove and loc.can_fill(self.world.state, candidate, False): + itemFromPool = candidate + toRemove.append(junkPoolIdx[i]) break self.world.push_item(loc, itemFromPool, False) loc.event = False - + toRemove.sort(reverse = True) + for i in toRemove: + self.world.itempool.pop(i) + def FillItemAtLocation(self, itemPool, itemType, location): itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World) if (itemToPlace == None): @@ -524,7 +584,8 @@ class SMZ3World(World): def InitialFillInOwnWorld(self): self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room")) - self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) + if (not self.smz3World.Config.Keysanity): + self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index 6faeeaa2fb4bb4f2b29203806e3d43e91be85d55..2d6027d5e5875bc3fead9306d855e3f0f77ee168 100644 GIT binary patch delta 21133 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 eG2?cuxzMBJgpaWA)gEbH1+^FIJ;u#`{$ delta 16841 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