diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index f25f2ada..d08e6a3e 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,5 +1,6 @@ from BaseClasses import MultiWorld from ..AutoWorld import LogicMixin +from .Options import EnergyCore from typing import Set # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? @@ -8,9 +9,9 @@ from . import pyevermizer # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] -# Logic.items are all items excluding non-progression items and duplicates +# Logic.items are all items and extra items excluding non-progression items and duplicates item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items()) +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) if item.name not in item_names and not item_names.add(item.name)] @@ -47,4 +48,9 @@ class SecretOfEvermoreLogic(LogicMixin): """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ + if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call + w = world.worlds[player] + if w.energy_core == EnergyCore.option_fragments: + progress = pyevermizer.P_CORE_FRAGMENT + count = w.required_fragments return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 0c24399d..4ec0ce2b 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -37,6 +37,32 @@ class Difficulty(EvermizerFlags, Choice): flags = ['e', 'n', 'h', 'x'] +class EnergyCore(EvermizerFlags, Choice): + """How to obtain the Energy Core""" + display_name = "Energy Core" + option_vanilla = 0 + option_shuffle = 1 + option_fragments = 2 + default = 1 + flags = ['z', '', 'Z'] + + +class RequiredFragments(Range): + """Required fragment count for Energy Core = Fragments""" + display_name = "Required Fragments" + range_start = 1 + range_end = 99 + default = 10 + + +class AvailableFragments(Range): + """Placed fragment count for Energy Core = Fragments""" + display_name = "Available Fragments" + range_start = 1 + range_end = 99 + default = 11 + + class MoneyModifier(Range): """Money multiplier in %""" display_name = "Money Modifier" @@ -186,10 +212,15 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} soe_options: typing.Dict[str, type(Option)] = { "difficulty": Difficulty, + "energy_core": EnergyCore, + "required_fragments": RequiredFragments, + "available_fragments": AvailableFragments, "money_modifier": MoneyModifier, "exp_modifier": ExpModifier, "fix_sequence": FixSequence, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f00ebbe1..d708d6d7 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -16,7 +16,7 @@ except ImportError: from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin -from .Options import soe_options +from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -52,7 +52,6 @@ Item grouping currently supports * Ingredients - Matches all ingredient drops * Alchemy - Matches all alchemy formulas * Weapons - Matches all weapons but Bazooka, Bone Crusher, Neutron Blade -* Bazooka - Matches all bazookas (currently only one) * Traps - Matches all traps """ @@ -63,12 +62,14 @@ _id_offset: typing.Dict[int, int] = { pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 # TODO: sniff 64500..64799 - pyevermizer.CHECK_TRAP: _id_base + 900, # npc 64900..64999 + pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899 + pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999 } # cache native evermizer items and locations _items = pyevermizer.get_items() _traps = pyevermizer.get_traps() +_extras = pyevermizer.get_extra_items() # items that are not placed by default _locations = pyevermizer.get_locations() # fix up texts for AP for _loc in _locations: @@ -104,7 +105,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: name_to_id = {} id_to_raw = {} - for item in itertools.chain(_items, _traps): + for item in itertools.chain(_items, _extras, _traps): if item.name in name_to_id: continue ap_id = _id_offset[item.type] + item.index @@ -127,7 +128,6 @@ def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]: groups['Alchemy'] = set(item.name for item in _items if item.type == pyevermizer.CHECK_ALCHEMY) groups['Weapons'] = {'Spider Claw', 'Horn Spear', 'Gladiator Sword', 'Bronze Axe', 'Bronze Spear', 'Crusader Sword', 'Lance (Weapon)', 'Knight Basher', 'Atom Smasher', 'Laser Lance'} - groups['Bazooka'] = {'Bazooka+Shells / Shining Armor / 5k Gold'} groups['Traps'] = {trap.name for trap in _traps} return groups @@ -136,7 +136,8 @@ class SoEWebWorld(WebWorld): theme = 'jungle' tutorials = [Tutorial( "Multiworld Setup Guide", - "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.", + "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related" + " software.", "English", "multiworld_en.md", "multiworld/en", @@ -153,9 +154,9 @@ class SoEWorld(World): options = soe_options topology_present = False remote_items = False - data_version = 2 + data_version = 3 web = SoEWebWorld() - required_client_version = (0, 2, 6) + required_client_version = (0, 3, 3) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -165,6 +166,9 @@ class SoEWorld(World): evermizer_seed: int connect_name: str + energy_core: int + available_fragments: int + required_fragments: int _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] @@ -172,6 +176,14 @@ class SoEWorld(World): self.connect_name_available_event = threading.Event() super(SoEWorld, self).__init__(*args, **kwargs) + def generate_early(self) -> None: + # store option values that change logic + self.energy_core = self.world.energy_core[self.player].value + self.required_fragments = self.world.required_fragments[self.player].value + if self.required_fragments > self.world.available_fragments[self.player].value: + self.world.available_fragments[self.player].value = self.required_fragments + self.available_fragments = self.world.available_fragments[self.player].value + def create_event(self, event: str) -> Item: return SoEItem(event, ItemClassification.progression, None, self.player) @@ -182,6 +194,8 @@ class SoEWorld(World): classification = ItemClassification.trap elif item.progression: classification = ItemClassification.progression + elif item.useful: + classification = ItemClassification.useful else: classification = ItemClassification.filler @@ -208,9 +222,33 @@ class SoEWorld(World): self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) def create_items(self): - # add items to the pool - items = list(map(lambda item: self.create_item(item), _items)) + # add regular items to the pool + exclusions: typing.List[str] = [] + if self.energy_core != EnergyCore.option_shuffle: + exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below + items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions))) + # remove one pair of wings that will be placed in generate_basic + items.remove(self.create_item("Wings")) + + def is_ingredient(item): + for ingredient in _ingredients: + if _match_item_name(item, ingredient): + return True + return False + + # add energy core fragments to the pool + ingredients = [n for n, item in enumerate(items) if is_ingredient(item)] + if self.energy_core == EnergyCore.option_fragments: + items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core + for _ in range(self.available_fragments - 1): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = self.create_item("Energy Core Fragment") + + # add traps to the pool trap_count = self.world.trap_count[self.player].value trap_chances = {} trap_names = {} @@ -232,13 +270,12 @@ class SoEWorld(World): return self.create_item(trap_names[t]) v -= c - while trap_count > 0: - r = self.world.random.randrange(len(items)) - for ingredient in _ingredients: - if _match_item_name(items[r], ingredient): - items[r] = create_trap() - trap_count -= 1 - break + for _ in range(trap_count): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = create_trap() self.world.itempool += items @@ -271,7 +308,10 @@ class SoEWorld(World): wings_location = self.world.random.choice(self._halls_ne_chest_names) wings_item = self.create_item('Wings') self.world.get_location(wings_location, self.player).place_locked_item(wings_item) - self.world.itempool.remove(wings_item) + # place energy core at vanilla location for vanilla mode + if self.energy_core == EnergyCore.option_vanilla: + energy_core = self.create_item('Energy Core') + self.world.get_location('Energy Core #285', self.player).place_locked_item(energy_core) # generate stuff for later self.evermizer_seed = self.world.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? @@ -286,9 +326,12 @@ class SoEWorld(World): try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value - switches = [] + switches: typing.List[str] = [] if self.world.death_link[self.player].value: switches.append("--death-link") + if self.energy_core == EnergyCore.option_fragments: + switches.extend(('--available-fragments', str(self.available_fragments), + '--required-fragments', str(self.required_fragments))) rom_file = get_base_rom_path() out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_' f'{self.world.get_file_safe_player_name(self.player)}') diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 8dfdc0de..7f6a11e4 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2.tar.gz#egg=pyevermizer; python_version == '3.11' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11'