diff --git a/worlds/tmc/__init__.py b/worlds/tmc/__init__.py index f3257ef3cbb7..d671d7b9a281 100644 --- a/worlds/tmc/__init__.py +++ b/worlds/tmc/__init__.py @@ -41,6 +41,7 @@ MinishCapOptions, NonElementDungeons, PedReward, + FusionAccess, ShuffleElements, get_option_data, ) @@ -133,6 +134,16 @@ def generate_early(self) -> None: 10, 10 - options.cucco_rounds.value, -1)]) enabled_pools.update([f"goron:{round_num}" for round_num in range(1, options.goron_sets.value + 1)]) + # Option correction for fusions: + if options.gold_fusion_access.value in {FusionAccess.option_open, FusionAccess.option_closed}: + options.clouds_kinstone_multiplier.value = 0 + options.swamp_kinstone_multiplier.value = 0 + options.falls_kinstone_multiplier.value = 0 + elif options.gold_fusion_access == FusionAccess.option_combined: + options.swamp_kinstone_multiplier.value = 0 + options.falls_kinstone_multiplier.value = 0 + + # Default dhc_access to closed when it's been set to ped with goal vaati disabled. # There's too many flags to manage to allow DHC to open after ped completes and vaati is slain. if options.goal.value == Goal.option_pedestal and options.dhc_access.value == DHCAccess.option_pedestal: diff --git a/worlds/tmc/data/basepatch.bsdiff b/worlds/tmc/data/basepatch.bsdiff index 2f7412ccbe6b..0ef8b97c7fe0 100644 Binary files a/worlds/tmc/data/basepatch.bsdiff and b/worlds/tmc/data/basepatch.bsdiff differ diff --git a/worlds/tmc/items.py b/worlds/tmc/items.py index 82e1be06ed63..53f076755e2e 100644 --- a/worlds/tmc/items.py +++ b/worlds/tmc/items.py @@ -1,8 +1,9 @@ from dataclasses import dataclass +import math from typing import TYPE_CHECKING from BaseClasses import ItemClassification -from .options import DHCAccess, DungeonItem, Goal, ShuffleElements, DungeonWarp, PedReward, DungeonCompasses, DungeonMaps +from .options import DHCAccess, DungeonItem, Goal, ShuffleElements, DungeonWarp, PedReward, DungeonCompasses, DungeonMaps, FusionAccess, MinishCapOptions from .constants import TMCItem, TMCLocation, MinishCapItem if TYPE_CHECKING: @@ -58,7 +59,7 @@ def pool_bottles() -> list[str]: return [TMCItem.EMPTY_BOTTLE] * 4 -def pool_baseitems() -> list[str]: +def pool_baseitems(world: "MinishCapWorld") -> list[str]: return [ *[TMCItem.BOMB_BAG] * 4, TMCItem.REMOTE_BOMB, @@ -103,7 +104,7 @@ def pool_baseitems() -> list[str]: TMCItem.GREEN_BOOK, TMCItem.BLUE_BOOK, - *(pool_kinstone_gold()), + *(pool_kinstone_gold(world)), ] @@ -193,33 +194,49 @@ def pool_smallkeys(world: "MinishCapWorld") -> list[str]: return keys -def pool_kinstone_gold() -> list[str]: - return [*[TMCItem.KINSTONE_GOLD_CLOUD] * 5, *[TMCItem.KINSTONE_GOLD_SWAMP] * 3, TMCItem.KINSTONE_GOLD_FALLS] +def pool_kinstone_gold(world: "MinishCapWorld") -> list[str]: + options = world.options + kinstones = [] + if options.gold_fusion_access.value == FusionAccess.option_combined: + return [TMCItem.KINSTONE_GOLD_CLOUD] * math.ceil(9 / options.clouds_kinstone_multiplier) + # assuming FusionAccess.option_vanilla + kinstones.extend([TMCItem.KINSTONE_GOLD_CLOUD] * math.ceil(5 / options.clouds_kinstone_multiplier)) + kinstones.extend([TMCItem.KINSTONE_GOLD_SWAMP] * math.ceil(3 / options.swamp_kinstone_multiplier)) + kinstones.append(TMCItem.KINSTONE_GOLD_FALLS) + return kinstones -def pool_kinstone_red() -> list[str]: +def pool_kinstone_red(options: MinishCapOptions) -> list[str]: return [*[TMCItem.KINSTONE_RED_W] * 9, *[TMCItem.KINSTONE_RED_ANGLE] * 7, *[TMCItem.KINSTONE_RED_E] * 8] -def pool_kinstone_blue() -> list[str]: +def pool_kinstone_blue(options: MinishCapOptions) -> list[str]: return [*[TMCItem.KINSTONE_BLUE_L] * 9, *[TMCItem.KINSTONE_BLUE_6] * 9] -def pool_kinstone_green() -> list[str]: +def pool_kinstone_green(options: MinishCapOptions) -> list[str]: return [*[TMCItem.KINSTONE_GREEN_ANGLE] * 17, *[TMCItem.KINSTONE_GREEN_SQUARE] * 16, *[TMCItem.KINSTONE_GREEN_P] * 16] def get_item_pool(world: "MinishCapWorld") -> list[MinishCapItem]: player = world.player multiworld = world.multiworld - item_pool = pool_baseitems() - - if world.options.early_weapon.value: - weapon_pool = [TMCItem.PROGRESSIVE_SWORD, TMCItem.SMITHS_SWORD] - if world.options.weapon_bomb.value in {1, 2}: - weapon_pool.extend([TMCItem.BOMB_BAG]) - if world.options.weapon_bow.value: - weapon_pool.extend([TMCItem.PROGRESSIVE_BOW, TMCItem.BOW]) + options = world.options + item_pool = pool_baseitems(world) + + if options.early_weapon.value: + weapon_pool = [] + if options.progressive_sword.value: + weapon_pool.append(TMCItem.PROGRESSIVE_SWORD) + else: + weapon_pool.append(TMCItem.SMITHS_SWORD) + if options.weapon_bomb.value in {1, 2}: + weapon_pool.append(TMCItem.BOMB_BAG) + if options.weapon_bow.value: + if options.progressive_bow.value: + weapon_pool.append(TMCItem.PROGRESSIVE_BOW) + else: + weapon_pool.append(TMCItem.BOW) weapon_choice = world.random.choice(weapon_pool) multiworld.local_early_items[player][weapon_choice] = 1 @@ -230,31 +247,31 @@ def get_item_pool(world: "MinishCapWorld") -> list[MinishCapItem]: item_pool.extend(pool_scroll(world)) item_pool.extend(pool_health(world)) - if world.options.dungeon_big_keys.value == DungeonItem.option_anywhere: + if options.dungeon_big_keys.value == DungeonItem.option_anywhere: item_pool.extend(pool_bigkeys(world)) item_pool.append(TMCItem.BIG_KEY_TOD) - if world.options.dungeon_small_keys.value == DungeonItem.option_anywhere: + if options.dungeon_small_keys.value == DungeonItem.option_anywhere: item_pool.extend(pool_smallkeys(world)) - if world.options.dungeon_compasses.value == DungeonItem.option_anywhere: + if options.dungeon_compasses.value == DungeonItem.option_anywhere: item_pool.extend(pool_compass(world)) - if world.options.dungeon_maps.value == DungeonItem.option_anywhere: + if options.dungeon_maps.value == DungeonItem.option_anywhere: item_pool.extend(pool_dungeonmaps(world)) - if world.options.figurine_amount > 0: - item_pool.extend([TMCItem.FIGURINE] * world.options.figurine_amount.value) + if options.figurine_amount > 0: + item_pool.extend([TMCItem.FIGURINE] * options.figurine_amount.value) - if world.options.ped_reward == PedReward.option_dhc_big_key: + if options.ped_reward == PedReward.option_dhc_big_key: world.get_location(TMCLocation.PEDESTAL_REQUIREMENT_REWARD).place_locked_item(world.create_item(TMCItem.BIG_KEY_DHC)) # ToD is stupid, need to place the big key manually - if world.options.dungeon_big_keys.value == DungeonItem.option_own_dungeon and \ - TMCItem.BIG_KEY_TOD not in world.options.start_inventory_from_pool.value.keys() and \ - world.options.dungeon_warp_tod.value == DungeonWarp.option_none: + if options.dungeon_big_keys.value == DungeonItem.option_own_dungeon and \ + TMCItem.BIG_KEY_TOD not in options.start_inventory_from_pool.value.keys() and \ + options.dungeon_warp_tod.value == DungeonWarp.option_none: location = world.random.choice([TMCLocation.DROPLETS_ENTRANCE_B2_EAST_ICEBLOCK, TMCLocation.DROPLETS_ENTRANCE_B2_WEST_ICEBLOCK]) world.get_location(location).place_locked_item(world.create_item(TMCItem.BIG_KEY_TOD)) - if not world.options.random_bottle_contents.value: + if not options.random_bottle_contents.value: item_pool.extend(pool_bottles()) else: selected_bottles = [] @@ -265,7 +282,7 @@ def get_item_pool(world: "MinishCapWorld") -> list[MinishCapItem]: item_pool.extend(selected_bottles) - if world.options.shuffle_elements.value is ShuffleElements.option_anywhere: + if options.shuffle_elements.value is ShuffleElements.option_anywhere: item_pool.extend(pool_elements()) return [world.create_item(item) for item in item_pool] diff --git a/worlds/tmc/options.py b/worlds/tmc/options.py index 03b2a3b02245..255133336615 100644 --- a/worlds/tmc/options.py +++ b/worlds/tmc/options.py @@ -110,18 +110,19 @@ class GreenFusionAccess(Choice): class GoldFusionAccess(Choice): """How/when are the Gold Kinstone Fusions accessible? - - Closed: Gold Kinstones aren't in the item pool and none of their fusions are accessible - Vanilla: Gold Kinstones are added to the item pool and their fusions must be completed as normal - Combined: Same as Vanilla except all Gold Fusions are changed to request only 'Kinstone Cloud Tops' - - Open: Gold Kinstones aren't in the item pool and all of their fusions are accessible """ + # - Closed: Gold Kinstones aren't in the item pool and none of their fusions are accessible + # - Open: Gold Kinstones aren't in the item pool and all of their fusions are accessible - visibility = Visibility.none + display_name = "Gold Fusion Access" rich_text_doc = True + value: int # option_closed = 0 option_vanilla = 1 - # option_combined = 2 + option_combined = 2 # option_open = 3 default = option_vanilla @@ -847,6 +848,106 @@ class RemoteItems(Toggle): rich_text_doc = True +class CloudKinstoneMultiplier(Range): + """How many Cloud Kinstones should be added to your bag each time you get 1? + If gold combined kinstones are enabled, this setting will be used for the all gold kinstones. + This also reduces the number of kinstones in the pool to match the amount required rounded up. + Ex, When set to 2 without combined kinstones, there will be 3 Clouds Kinstones in the pool that each give you 2 Clouds Kinstones.""" + display_name = "Cloud Kinstone Multiplier" + rich_text_doc = True + + default = 1 + range_start = 1 + range_end = 9 + + +class SwampKinstoneMultiplier(Range): + """How many Swamp Kinstones should be added to your bag each time you get 1? + This setting is ignored if using combined gold kinstones""" + display_name = "Swamp Kinstone Multiplier" + rich_text_doc = True + + default = 1 + range_start = 1 + range_end = 3 + + +class FallsKinstoneMultiplier(Range): + """How many Falls Kinstones should be added to your bag each time you get 1? + This setting is ignored if using combined gold kinstones""" + # This is only here for easy access when writing overriding for open/closed fusions and writing to the rom + visibility = Visibility.none + display_name = "Falls Kinstone Multiplier" + rich_text_doc = True + + default = 1 + range_start = 1 + range_end = 1 + + +class DWSKeyMultiplier(Range): + """How many Small Keys (DWS) should be added to your inventory each time you get 1?""" + display_name = "DWS Key Multiplier" + + default = 1 + range_start = 1 + range_end = 4 + + +class CoFKeyMultiplier(Range): + """How many Small Keys (CoF) should be added to your inventory each time you get 1?""" + display_name = "CoF Key Multiplier" + + default = 1 + range_start = 1 + range_end = 2 + + +class FoWKeyMultiplier(Range): + """How many Small Keys (FoW) should be added to your inventory each time you get 1?""" + display_name = "FoW Key Multiplier" + + default = 1 + range_start = 1 + range_end = 4 + + +class ToDKeyMultiplier(Range): + """How many Small Keys (ToD) should be added to your inventory each time you get 1?""" + display_name = "ToD Key Multiplier" + + default = 1 + range_start = 1 + range_end = 4 + + +class RCKeyMultiplier(Range): + """How many Small Keys (RC) should be added to your inventory each time you get 1?""" + display_name = "RC Key Multiplier" + + default = 1 + range_start = 1 + range_end = 3 + + +class PoWKeyMultiplier(Range): + """How many Small Keys (PoW) should be added to your inventory each time you get 1?""" + display_name = "PoW Key Multiplier" + + default = 1 + range_start = 1 + range_end = 6 + + +class DHCKeyMultiplier(Range): + """How many Small Keys (DHC) should be added to your inventory each time you get 1?""" + display_name = "DHC Key Multiplier" + + default = 1 + range_start = 1 + range_end = 5 + + @dataclass class MinishCapOptions(PerGameCommonOptions): # AP settings / DL settings @@ -902,6 +1003,17 @@ class MinishCapOptions(PerGameCommonOptions): starting_hearts: StartingHearts heart_containers: HeartContainerAmount piece_of_hearts: PieceOfHeartAmount + # Multipliers + clouds_kinstone_multiplier: CloudKinstoneMultiplier + swamp_kinstone_multiplier: SwampKinstoneMultiplier + falls_kinstone_multiplier: FallsKinstoneMultiplier + dws_key_multiplier: DWSKeyMultiplier + cof_key_multiplier: CoFKeyMultiplier + fow_key_multiplier: FoWKeyMultiplier + tod_key_multiplier: ToDKeyMultiplier + rc_key_multiplier: RCKeyMultiplier + pow_key_multiplier: PoWKeyMultiplier + dhc_key_multiplier: DHCKeyMultiplier # Logic Settings dungeon_warp_dws: WarpDWS dungeon_warp_cof: WarpCoF @@ -1047,6 +1159,16 @@ def get_option_data(world: "MinishCapWorld"): "wind_crest_castor", "wind_crest_south_field", "wind_crest_minish_woods", + "clouds_kinstone_multiplier", + "swamp_kinstone_multiplier", + "falls_kinstone_multiplier", + "dws_key_multiplier", + "cof_key_multiplier", + "fow_key_multiplier", + "tod_key_multiplier", + "rc_key_multiplier", + "pow_key_multiplier", + "dhc_key_multiplier", "tricks", ] """The yaml options that'll be transfered into slot_data for the tracker""" @@ -1054,10 +1176,10 @@ def get_option_data(world: "MinishCapWorld"): OPTION_GROUPS = [ OptionGroup("Goal", [Goal, DHCAccess, PedElements, PedSword, PedDungeons, PedFigurines, FigurineAmount]), - # OptionGroup( - # "Fusions", - # [GoldFusionAccess, RedFusionAccess, GreenFusionAccess, BlueFusionAccess], - # ), + OptionGroup( + "Fusions", + [GoldFusionAccess] # , RedFusionAccess, GreenFusionAccess, BlueFusionAccess], + ), OptionGroup( "Dungeon Shuffle", [ @@ -1116,6 +1238,21 @@ def get_option_data(world: "MinishCapWorld"): WindCrestMinish, ], ), + OptionGroup( + "Item Multipliers", + [ + CloudKinstoneMultiplier, + SwampKinstoneMultiplier, + + DWSKeyMultiplier, + CoFKeyMultiplier, + FoWKeyMultiplier, + ToDKeyMultiplier, + RCKeyMultiplier, + PoWKeyMultiplier, + DHCKeyMultiplier + ] + ), OptionGroup( "Quality of Life", [GoronJPPrices, OcarinaOnSelect, BootsOnL, BootsAsMinish, BigOctorokManipulation, ReplicaToDBossDoor], diff --git a/worlds/tmc/rom.py b/worlds/tmc/rom.py index bfdd6d32c401..d3a5484ef416 100644 --- a/worlds/tmc/rom.py +++ b/worlds/tmc/rom.py @@ -300,6 +300,17 @@ def write_tokens(world: "MinishCapWorld", patch: MinishCapProcedurePatch) -> Non for request in no_fusions_requests: patch.write_token(APTokenTypes.WRITE, request, bytes([0xF2])) + # Combined Kinstones + if options.gold_fusion_access.value == FusionAccess.option_combined: + patch.write_token(APTokenTypes.WRITE, 0x0C9415, bytes([8])) + patch.write_token(APTokenTypes.WRITE, 0x0C9419, bytes([1])) + patch.write_token(APTokenTypes.WRITE, 0x0C941D, bytes([8])) + patch.write_token(APTokenTypes.WRITE, 0x0C9421, bytes([1])) + patch.write_token(APTokenTypes.WRITE, 0x0C9425, bytes([8])) + patch.write_token(APTokenTypes.WRITE, 0x0C9429, bytes([1])) + patch.write_token(APTokenTypes.WRITE, 0x0C942D, bytes([8])) + patch.write_token(APTokenTypes.WRITE, 0x0C9431, bytes([1])) + # Cucco/Goron Rounds cucco_complete = int(world.options.cucco_rounds.value == 0) cucco_skipped = 10 - world.options.cucco_rounds.value if world.options.cucco_rounds.value > 0 else 9 @@ -316,6 +327,38 @@ def write_tokens(world: "MinishCapWorld", patch: MinishCapProcedurePatch) -> Non patch.write_token(APTokenTypes.WRITE, flag_group_by_name[TMCFlagGroup.LINKS_CURRENT_HEALTH], bytes([starting_hp])) patch.write_token(APTokenTypes.WRITE, flag_group_by_name[TMCFlagGroup.LINKS_MAX_HEALTH], bytes([starting_hp])) + # Multipliers + patch.write_token(APTokenTypes.WRITE, 0xFF0530, bytes([ + options.clouds_kinstone_multiplier.value, + options.clouds_kinstone_multiplier.value, + options.clouds_kinstone_multiplier.value, + options.clouds_kinstone_multiplier.value, + options.clouds_kinstone_multiplier.value, + options.swamp_kinstone_multiplier.value, + options.swamp_kinstone_multiplier.value, + options.swamp_kinstone_multiplier.value, + options.falls_kinstone_multiplier.value, + 0, # Red W + 0, # Red V + 0, # Red E + 0, # Blue L + 0, # Blue S + 0, # Green C + 0, # Green G + 0, # Green P + 0xFF, # spacers for an ALIGN 4 + 0xFF, # spacers for an ALIGN 4 + 0xFF, # spacers for an ALIGN 4 + 1, # universal key + options.dws_key_multiplier.value, + options.cof_key_multiplier.value, + options.fow_key_multiplier.value, + options.tod_key_multiplier.value, + options.pow_key_multiplier.value, + options.dhc_key_multiplier.value, + options.rc_key_multiplier.value, + ])) + # Patch Items into Locations for location_name, loc in location_table_by_name.items(): if loc.rom_addr is None: diff --git a/worlds/tmc/rules.py b/worlds/tmc/rules.py index c3844e768131..03d4e93fbade 100644 --- a/worlds/tmc/rules.py +++ b/worlds/tmc/rules.py @@ -1,3 +1,4 @@ +import math from collections.abc import Callable from typing import TYPE_CHECKING @@ -5,7 +6,7 @@ from worlds.generic.Rules import CollectionRule, add_rule from .constants import TMCEvent, TMCItem, TMCLocation, TMCRegion, TMCTricks -from .options import Biggoron, DHCAccess, DungeonItem, DungeonWarp, Goal, MinishCapOptions +from .options import Biggoron, DHCAccess, DungeonItem, DungeonWarp, Goal, MinishCapOptions, FusionAccess if TYPE_CHECKING: from . import MinishCapWorld @@ -124,7 +125,7 @@ def __init__(self, world: "MinishCapWorld") -> None: (TMCRegion.CASTOR_WILDS, TMCRegion.WESTERN_WOODS): self.logic_or([self.has_any([TMCItem.PEGASUS_BOOTS, TMCItem.ROCS_CAPE]), self.has_bow()]), (TMCRegion.CASTOR_WILDS, TMCRegion.WIND_RUINS): - self.logic_and([self.has(TMCItem.KINSTONE_GOLD_SWAMP, 3), + self.logic_and([self.swamp_fusion_cost(), self.logic_or([self.has(TMCItem.ROCS_CAPE), self.logic_and([self.has(TMCItem.PEGASUS_BOOTS), self.logic_or([self.swamp_crest(), self.has_bow(), @@ -141,14 +142,14 @@ def __init__(self, world: "MinishCapWorld") -> None: self.logic_and([self.has_weapon(), self.has(TMCItem.SMALL_KEY_RC, 3), self.has(TMCItem.LANTERN)]), (TMCRegion.FALLS_ENTRANCE, TMCRegion.MIDDLE_FALLS): - self.logic_and([self.has(TMCItem.KINSTONE_GOLD_FALLS), self.dark_room()]), + self.logic_and([self.falls_fusion_cost(), self.dark_room()]), (TMCRegion.MIDDLE_FALLS, TMCRegion.FALLS_ENTRANCE): self.has(TMCItem.FLIPPERS), (TMCRegion.MIDDLE_FALLS, TMCRegion.UPPER_FALLS): self.has(TMCItem.GRIP_RING), (TMCRegion.UPPER_FALLS, TMCRegion.MIDDLE_FALLS): self.has(TMCItem.GRIP_RING), (TMCRegion.UPPER_FALLS, TMCRegion.CLOUDS): self.has(TMCItem.GRIP_RING), (TMCRegion.CLOUDS, TMCRegion.UPPER_FALLS): self.has(TMCItem.GRIP_RING), (TMCRegion.CLOUDS, TMCRegion.WIND_TRIBE): - self.logic_and([self.has(TMCItem.KINSTONE_GOLD_CLOUD, 5), + self.logic_and([self.cloud_fusion_cost(), self.has_any([TMCItem.MOLE_MITTS, TMCItem.ROCS_CAPE])]), (TMCRegion.WIND_TRIBE, TMCRegion.CLOUDS): None, (TMCRegion.WIND_TRIBE, TMCRegion.DUNGEON_POW_ENTRANCE): None, @@ -1554,6 +1555,23 @@ def cost(self, price: int) -> CollectionRule: return self.no_access() return self.has(TMCItem.BIG_WALLET, needed_wallets) + def cloud_fusion_cost(self) -> CollectionRule: + return self.logic_option(self.options.gold_fusion_access.value == FusionAccess.option_combined, + self.has(TMCItem.KINSTONE_GOLD_CLOUD, math.ceil(9 / self.options.clouds_kinstone_multiplier)), + self.has(TMCItem.KINSTONE_GOLD_CLOUD, math.ceil(5 / self.options.clouds_kinstone_multiplier))) + + def swamp_fusion_cost(self) -> CollectionRule: + if self.options.gold_fusion_access.value == FusionAccess.option_combined: + return self.has(TMCItem.KINSTONE_GOLD_CLOUD, math.ceil(9 / self.options.clouds_kinstone_multiplier)) + return self.has(TMCItem.KINSTONE_GOLD_SWAMP, math.ceil(3 / self.options.swamp_kinstone_multiplier)) + + def falls_fusion_cost(self) -> CollectionRule: + if self.options.gold_fusion_access.value == FusionAccess.option_combined: + if self.options.wind_crest_clouds.value: + return self.has(TMCItem.KINSTONE_GOLD_CLOUD, math.ceil(9 / self.options.clouds_kinstone_multiplier)) + return self.has(TMCItem.KINSTONE_GOLD_CLOUD, math.ceil(4 / self.options.clouds_kinstone_multiplier)) + return self.has(TMCItem.KINSTONE_GOLD_FALLS, 1) + def blow_dust(self) -> CollectionRule: return self.logic_option(TMCTricks.BOMB_DUST in self.world.options.tricks, self.has_any([TMCItem.GUST_JAR, TMCItem.BOMB_BAG]),