From f7b688de388af9554af497798c9c9f4efc3ee2c8 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:42:04 -0400 Subject: [PATCH] Orbsanity (#32) * My big dumb shortcut: a 2000 item array. * A better idea: bundle orbs as a numerical option and make array variable size. * Have Item/Region generation respect the chosen Orbsanity bundle size. Fix trade logic. * Separate Global/Local Orbsanity options. TODO - re-introduce orb factory for per-level option. * Per-level Orbsanity implemented w/ orb bundle factory. * Implement Orbsanity for client, fix some things up for regions. * Fix location name/id mappings. * Fix client orb collection on connection. * Fix minor Deathlink bug, add Update instructions. --- worlds/jakanddaxter/Client.py | 24 ++- worlds/jakanddaxter/Items.py | 23 ++- worlds/jakanddaxter/JakAndDaxterOptions.py | 73 ++++++-- worlds/jakanddaxter/Locations.py | 4 +- worlds/jakanddaxter/Regions.py | 62 ++++--- worlds/jakanddaxter/Rules.py | 82 ++++++++- worlds/jakanddaxter/__init__.py | 58 ++++-- worlds/jakanddaxter/client/MemoryReader.py | 59 +++++-- worlds/jakanddaxter/client/ReplClient.py | 30 +++- worlds/jakanddaxter/docs/setup_en.md | 18 +- worlds/jakanddaxter/locs/OrbLocations.py | 165 ++++++++++-------- worlds/jakanddaxter/regs/BoggySwampRegions.py | 23 ++- worlds/jakanddaxter/regs/FireCanyonRegions.py | 21 ++- .../regs/ForbiddenJungleRegions.py | 22 ++- worlds/jakanddaxter/regs/GeyserRockRegions.py | 21 ++- .../regs/GolAndMaiasCitadelRegions.py | 22 ++- worlds/jakanddaxter/regs/LavaTubeRegions.py | 21 ++- .../regs/LostPrecursorCityRegions.py | 22 ++- .../jakanddaxter/regs/MistyIslandRegions.py | 22 ++- .../jakanddaxter/regs/MountainPassRegions.py | 21 ++- .../regs/PrecursorBasinRegions.py | 21 ++- worlds/jakanddaxter/regs/RegionBase.py | 35 +++- .../jakanddaxter/regs/RockVillageRegions.py | 35 +++- .../regs/SandoverVillageRegions.py | 30 +++- .../jakanddaxter/regs/SentinelBeachRegions.py | 22 ++- .../jakanddaxter/regs/SnowyMountainRegions.py | 22 ++- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 22 ++- .../regs/VolcanicCraterRegions.py | 34 +++- 28 files changed, 820 insertions(+), 194 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 397f63e7ca..51b26eb53a 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -107,6 +107,16 @@ class JakAndDaxterContext(CommonContext): await self.send_connect() def on_package(self, cmd: str, args: dict): + + if cmd == "Connected": + slot_data = args["slot_data"] + if slot_data["enable_orbsanity"] == 1: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["level_orbsanity_bundle_size"]) + elif slot_data["enable_orbsanity"] == 2: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["global_orbsanity_bundle_size"]) + else: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], 1) + if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): logger.debug(f"index: {str(index)}, item: {str(item)}") @@ -137,7 +147,8 @@ class JakAndDaxterContext(CommonContext): async def ap_inform_deathlink(self): if self.memr.deathlink_enabled: - death_text = self.memr.cause_of_death.replace("Jak", self.player_names[self.slot]) + player = self.player_names[self.slot] if self.slot is not None else "Jak" + death_text = self.memr.cause_of_death.replace("Jak", player) await self.send_death(death_text) logger.info(death_text) @@ -155,6 +166,14 @@ class JakAndDaxterContext(CommonContext): def on_deathlink_toggle(self): create_task_log_exception(self.ap_inform_deathlink_toggle()) + async def repl_reset_orbsanity(self): + if self.memr.orbsanity_enabled: + self.memr.reset_orbsanity = False + self.repl.reset_orbsanity() + + def on_orbsanity_check(self): + create_task_log_exception(self.repl_reset_orbsanity()) + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -165,7 +184,8 @@ class JakAndDaxterContext(CommonContext): await self.memr.main_tick(self.on_location_check, self.on_finish_check, self.on_deathlink_check, - self.on_deathlink_toggle) + self.on_deathlink_toggle, + self.on_orbsanity_check) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 4f94e7b85c..28574b14ca 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -39,8 +39,29 @@ scout_item_table = { } # Orbs are also generic and interchangeable. +# These items are only used by Orbsanity, and only one of these +# items will be used corresponding to the chosen bundle size. orb_item_table = { 1: "Precursor Orb", + 2: "Bundle of 2 Precursor Orbs", + 4: "Bundle of 4 Precursor Orbs", + 5: "Bundle of 5 Precursor Orbs", + 8: "Bundle of 8 Precursor Orbs", + 10: "Bundle of 10 Precursor Orbs", + 16: "Bundle of 16 Precursor Orbs", + 20: "Bundle of 20 Precursor Orbs", + 25: "Bundle of 25 Precursor Orbs", + 40: "Bundle of 40 Precursor Orbs", + 50: "Bundle of 50 Precursor Orbs", + 80: "Bundle of 80 Precursor Orbs", + 100: "Bundle of 100 Precursor Orbs", + 125: "Bundle of 125 Precursor Orbs", + 200: "Bundle of 200 Precursor Orbs", + 250: "Bundle of 250 Precursor Orbs", + 400: "Bundle of 400 Precursor Orbs", + 500: "Bundle of 500 Precursor Orbs", + 1000: "Bundle of 1000 Precursor Orbs", + 2000: "Bundle of 2000 Precursor Orbs", } # These are special items representing unique unlocks in the world. Notice that their Item ID equals their @@ -85,8 +106,8 @@ move_item_table = { item_table = { **{Cells.to_ap_id(k): cell_item_table[k] for k in cell_item_table}, **{Scouts.to_ap_id(k): scout_item_table[k] for k in scout_item_table}, - **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, **{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table}, **{Caches.to_ap_id(k): move_item_table[k] for k in move_item_table}, + **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, jak1_max: "Green Eco Pill" # Filler item. } diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index de5581f4c9..5391b7b094 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,23 +1,74 @@ -import os from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions +from Options import Toggle, PerGameCommonOptions, Choice class EnableMoveRandomizer(Toggle): - """Enable to include movement options as items in the randomizer. - Jak is only able to run, swim, and single jump, until you find his other moves. - Adds 11 items to the pool.""" + """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump, + until you find his other moves. This adds 11 items to the pool.""" display_name = "Enable Move Randomizer" -# class EnableOrbsanity(Toggle): -# """Enable to include Precursor Orbs as an ordered list of progressive checks. -# Each orb you collect triggers the next release in the list. -# Adds 2000 items to the pool.""" -# display_name = "Enable Orbsanity" +class EnableOrbsanity(Choice): + """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect + the chosen number of orbs, you will trigger the next release in the list. "Per Level" means these lists are + generated and populated for each level in the game (Geyser Rock, Sandover Village, etc.). "Global" means there is + only one list for the entire game. + + This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle. + For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, + you will add 8 items to the pool.""" + display_name = "Enable Orbsanity" + option_off = 0 + option_per_level = 1 + option_global = 2 + default = 0 + + +class GlobalOrbsanityBundleSize(Choice): + """Set the size of the bundle for Global Orbsanity. + This only applies if "Enable Orbsanity" is set to "Global." + There are 2000 orbs in the game, so your bundle size must be a factor of 2000.""" + display_name = "Global Orbsanity Bundle Size" + option_1_orb = 1 + option_2_orbs = 2 + option_4_orbs = 4 + option_5_orbs = 5 + option_8_orbs = 8 + option_10_orbs = 10 + option_16_orbs = 16 + option_20_orbs = 20 + option_25_orbs = 25 + option_40_orbs = 40 + option_50_orbs = 50 + option_80_orbs = 80 + option_100_orbs = 100 + option_125_orbs = 125 + option_200_orbs = 200 + option_250_orbs = 250 + option_400_orbs = 400 + option_500_orbs = 500 + option_1000_orbs = 1000 + option_2000_orbs = 2000 + default = 1 + + +class PerLevelOrbsanityBundleSize(Choice): + """Set the size of the bundle for Per Level Orbsanity. + This only applies if "Enable Orbsanity" is set to "Per Level." + There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.""" + display_name = "Per Level Orbsanity Bundle Size" + option_1_orb = 1 + option_2_orbs = 2 + option_5_orbs = 5 + option_10_orbs = 10 + option_25_orbs = 25 + option_50_orbs = 50 + default = 1 @dataclass class JakAndDaxterOptions(PerGameCommonOptions): enable_move_randomizer: EnableMoveRandomizer - # enable_orbsanity: EnableOrbsanity + enable_orbsanity: EnableOrbsanity + global_orbsanity_bundle_size: GlobalOrbsanityBundleSize + level_orbsanity_bundle_size: PerLevelOrbsanityBundleSize diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 3cea4f2ace..ed0273060e 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,6 +1,7 @@ from BaseClasses import Location from .GameID import jak1_name -from .locs import (CellLocations as Cells, +from .locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -48,4 +49,5 @@ location_table = { **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}, **{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}, **{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable}, + **{Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable} } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 4e4ca73b32..168640b9e2 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,8 +1,9 @@ -import typing from BaseClasses import MultiWorld -from .Items import item_table from .JakAndDaxterOptions import JakAndDaxterOptions -from .locs import (CellLocations as Cells, +from .Items import item_table +from .Rules import can_reach_orbs +from .locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts) from .regs.RegionBase import JakAndDaxterRegion from .regs import (GeyserRockRegions as GeyserRock, @@ -30,7 +31,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: multiworld.regions.append(menu) # Build the special "Free 7 Scout Flies" Region. This is a virtual region always accessible to Menu. - # The Power Cells within it are automatically checked when you receive the 7th scout fly for the corresponding cell. + # The Locations within are automatically checked when you receive the 7th scout fly for the corresponding cell. free7 = JakAndDaxterRegion("'Free 7 Scout Flies' Power Cells", player, multiworld) free7.add_cell_locations(Cells.loc7SF_cellTable.keys()) for scout_fly_cell in free7.locations: @@ -39,27 +40,46 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address)) scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) multiworld.regions.append(free7) + menu.connect(free7) + + # If Global Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Menu. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 2: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld) + + bundle_size = options.global_orbsanity_bundle_size.value + bundle_count = int(2000 / bundle_size) + for bundle_index in range(bundle_count): + + # Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16. + orbs.add_orb_locations(16, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + menu.connect(orbs) # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules. - [gr] = GeyserRock.build_regions("Geyser Rock", player, multiworld) - [sv] = SandoverVillage.build_regions("Sandover Village", player, multiworld) - [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", player, multiworld) - [sb] = SentinelBeach.build_regions("Sentinel Beach", player, multiworld) - [mi] = MistyIsland.build_regions("Misty Island", player, multiworld) - [fc] = FireCanyon.build_regions("Fire Canyon", player, multiworld) - [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", player, multiworld) - [pb] = PrecursorBasin.build_regions("Precursor Basin", player, multiworld) - [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", player, multiworld) - [bs] = BoggySwamp.build_regions("Boggy Swamp", player, multiworld) - [mp, mpr] = MountainPass.build_regions("Mountain Pass", player, multiworld) - [vc] = VolcanicCrater.build_regions("Volcanic Crater", player, multiworld) - [sc] = SpiderCave.build_regions("Spider Cave", player, multiworld) - [sm] = SnowyMountain.build_regions("Snowy Mountain", player, multiworld) - [lt] = LavaTube.build_regions("Lava Tube", player, multiworld) - [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", player, multiworld) + [gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player) + [sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player) + [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player) + [sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player) + [mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player) + [fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player) + [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", multiworld, options, player) + [pb] = PrecursorBasin.build_regions("Precursor Basin", multiworld, options, player) + [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", multiworld, options, player) + [bs] = BoggySwamp.build_regions("Boggy Swamp", multiworld, options, player) + [mp, mpr] = MountainPass.build_regions("Mountain Pass", multiworld, options, player) + [vc] = VolcanicCrater.build_regions("Volcanic Crater", multiworld, options, player) + [sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player) + [sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player) + [lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player) + [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player) # Define the interconnecting rules. - menu.connect(free7) menu.connect(gr) gr.connect(sv) # Geyser Rock modified to let you leave at any time. sv.connect(fj) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 9ff3d29d3e..ad7c6dda1f 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,9 +1,51 @@ +import math import typing from BaseClasses import MultiWorld, CollectionState from .JakAndDaxterOptions import JakAndDaxterOptions +from .Items import orb_item_table from .locs import CellLocations as Cells from .Locations import location_table -from .Regions import JakAndDaxterRegion +from .regs.RegionBase import JakAndDaxterRegion + + +def can_reach_orbs(state: CollectionState, + player: int, + multiworld: MultiWorld, + options: JakAndDaxterOptions, + level_name: str = None) -> int: + + # Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable. + # Per Level Orbsanity needs to know if you can reach orbs *in a particular level.* + if options.enable_orbsanity.value in [0, 2]: + return can_reach_orbs_global(state, player, multiworld) + else: + return can_reach_orbs_level(state, player, multiworld, level_name) + + +def can_reach_orbs_global(state: CollectionState, + player: int, + multiworld: MultiWorld) -> int: + + accessible_orbs = 0 + for region in multiworld.get_regions(player): + if state.can_reach(region, "Region", player): + accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + + return accessible_orbs + + +def can_reach_orbs_level(state: CollectionState, + player: int, + multiworld: MultiWorld, + level_name: str) -> int: + + accessible_orbs = 0 + regions = [typing.cast(JakAndDaxterRegion, reg) for reg in multiworld.get_regions(player)] + for region in regions: + if region.level_name == level_name and state.can_reach(region, "Region", player): + accessible_orbs += region.orb_count + + return accessible_orbs # TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the @@ -11,14 +53,28 @@ from .Regions import JakAndDaxterRegion def can_trade(state: CollectionState, player: int, multiworld: MultiWorld, + options: JakAndDaxterOptions, required_orbs: int, required_previous_trade: int = None) -> bool: - accessible_orbs = 0 - for region in multiworld.get_regions(player): - if state.can_reach(region, "Region", player): - accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + if options.enable_orbsanity.value == 1: + bundle_size = options.level_orbsanity_bundle_size.value + return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) + elif options.enable_orbsanity.value == 2: + bundle_size = options.global_orbsanity_bundle_size.value + return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) + else: + return can_trade_regular(state, player, multiworld, required_orbs, required_previous_trade) + +def can_trade_regular(state: CollectionState, + player: int, + multiworld: MultiWorld, + required_orbs: int, + required_previous_trade: int = None) -> bool: + + # We know that Orbsanity is off, so count orbs globally. + accessible_orbs = can_reach_orbs_global(state, player, multiworld) if required_previous_trade: name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] return (accessible_orbs >= required_orbs @@ -27,6 +83,22 @@ def can_trade(state: CollectionState, return accessible_orbs >= required_orbs +def can_trade_orbsanity(state: CollectionState, + player: int, + orb_bundle_size: int, + required_orbs: int, + required_previous_trade: int = None) -> bool: + + required_count = math.ceil(required_orbs / orb_bundle_size) + orb_bundle_name = orb_item_table[orb_bundle_size] + if required_previous_trade: + name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] + return (state.has(orb_bundle_name, player, required_count) + and state.can_reach(name_of_previous_trade, "Location", player=player)) + else: + return state.has(orb_bundle_name, player, required_count) + + def can_free_scout_flies(state: CollectionState, player: int) -> bool: return (state.has("Jump Dive", player) or (state.has("Crouch", player) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 7660bdd39d..ad7555397b 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,4 +1,4 @@ -import typing +from typing import Dict, Any, ClassVar import settings from Utils import local_path, visualize_regions @@ -66,14 +66,13 @@ class JakAndDaxterWorld(World): required_client_version = (0, 4, 6) # Options - settings: typing.ClassVar[JakAndDaxterSettings] + settings: ClassVar[JakAndDaxterSettings] options_dataclass = JakAndDaxterOptions options: JakAndDaxterOptions # Web world web = JakAndDaxterWebWorld() - # Items and Locations # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. # Remember, the game ID and various offsets for each item type have already been calculated. item_name_to_id = {item_table[k]: k for k in item_table} @@ -91,15 +90,22 @@ class JakAndDaxterWorld(World): if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, } - # Regions and Rules # This will also set Locations, Location access rules, Region access rules, etc. - def create_regions(self): + def create_regions(self) -> None: create_regions(self.multiworld, self.options, self.player) # visualize_regions(self.multiworld.get_region("Menu", self.player), "jak.puml") + # Helper function to get the correct orb bundle size. + def get_orb_bundle_size(self) -> int: + if self.options.enable_orbsanity.value == 1: + return self.options.level_orbsanity_bundle_size.value + elif self.options.enable_orbsanity.value == 2: + return self.options.global_orbsanity_bundle_size.value + else: + return 0 + # Helper function to reuse some nasty if/else trees. - @staticmethod - def item_type_helper(item) -> (int, ItemClassification): + def item_type_helper(self, item) -> (int, ItemClassification): # Make 101 Power Cells. if item in range(jak1_id, jak1_id + Scouts.fly_offset): classification = ItemClassification.progression_skip_balancing @@ -120,10 +126,11 @@ class JakAndDaxterWorld(World): classification = ItemClassification.progression count = 1 - # TODO - Make 2000 Precursor Orbs, ONLY IF Orbsanity is enabled. + # Make N Precursor Orb bundles, where N is 2000 / bundle size. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): classification = ItemClassification.progression_skip_balancing - count = 0 + size = self.get_orb_bundle_size() + count = int(2000 / size) if size > 0 else 0 # Don't divide by zero! # Under normal circumstances, we will create 0 filler items. # We will manually create filler items as needed. @@ -137,19 +144,30 @@ class JakAndDaxterWorld(World): return count, classification - def create_items(self): - for item_id in item_table: + def create_items(self) -> None: + for item_name in self.item_name_to_id: + item_id = self.item_name_to_id[item_name] # Handle Move Randomizer option. # If it is OFF, put all moves in your starting inventory instead of the item pool, # then fill the item pool with a corresponding amount of filler items. - if not self.options.enable_move_randomizer and item_table[item_id] in self.item_name_groups["Moves"]: - self.multiworld.push_precollected(self.create_item(item_table[item_id])) + if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer: + self.multiworld.push_precollected(self.create_item(item_name)) self.multiworld.itempool += [self.create_item(self.get_filler_item_name())] - else: - count, classification = self.item_type_helper(item_id) - self.multiworld.itempool += [JakAndDaxterItem(item_table[item_id], classification, item_id, self.player) - for _ in range(count)] + continue + + # Handle Orbsanity option. + # If it is OFF, don't add any orbs to the item pool. + # If it is ON, only add the orb bundle that matches the choice in options. + if (item_name in self.item_name_groups["Precursor Orbs"] + and ((self.options.enable_orbsanity.value == 0 + or Orbs.to_game_id(item_id) != self.get_orb_bundle_size()))): + continue + + # In every other scenario, do this. + count, classification = self.item_type_helper(item_id) + self.multiworld.itempool += [JakAndDaxterItem(item_name, classification, item_id, self.player) + for _ in range(count)] def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] @@ -158,3 +176,9 @@ class JakAndDaxterWorld(World): def get_filler_item_name(self) -> str: return "Green Eco Pill" + + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("enable_move_randomizer", + "enable_orbsanity", + "global_orbsanity_bundle_size", + "level_orbsanity_bundle_size") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 8fd54b8b32..a0787071bf 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,5 +1,5 @@ import random -import typing +from typing import ByteString, List, Callable import json import pymem from pymem import pattern @@ -7,7 +7,8 @@ from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinA from dataclasses import dataclass from CommonClient import logger -from ..locs import (CellLocations as Cells, +from ..locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -65,6 +66,12 @@ orb_caches_checked_offset = offsets.define(sizeof_uint32, 16) moves_received_offset = offsets.define(sizeof_uint8, 16) moverando_enabled_offset = offsets.define(sizeof_uint8) +# Orbsanity information. +orbsanity_option_offset = offsets.define(sizeof_uint8) +orbsanity_bundle_offset = offsets.define(sizeof_uint32) +collected_bundle_level_offset = offsets.define(sizeof_uint8) +collected_bundle_count_offset = offsets.define(sizeof_uint32) + # The End. end_marker_offset = offsets.define(sizeof_uint8, 4) @@ -111,7 +118,7 @@ def autopsy(died: int) -> str: class JakAndDaxterMemoryReader: - marker: typing.ByteString + marker: ByteString goal_address = None connected: bool = False initiated_connect: bool = False @@ -128,15 +135,20 @@ class JakAndDaxterMemoryReader: send_deathlink: bool = False cause_of_death: str = "" - def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): + # Orbsanity handling + orbsanity_enabled: bool = False + reset_orbsanity: bool = False + + def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker self.connect() async def main_tick(self, - location_callback: typing.Callable, - finish_callback: typing.Callable, - deathlink_callback: typing.Callable, - deathlink_toggle: typing.Callable): + location_callback: Callable, + finish_callback: Callable, + deathlink_callback: Callable, + deathlink_toggle: Callable, + orbsanity_callback: Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -171,6 +183,9 @@ class JakAndDaxterMemoryReader: if self.send_deathlink: deathlink_callback() + if self.reset_orbsanity: + orbsanity_callback() + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -207,7 +222,7 @@ class JakAndDaxterMemoryReader: logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index]) if self.outbox_index else "None")) - def read_memory(self) -> typing.List[int]: + def read_memory(self) -> List[int]: try: next_cell_index = self.read_goal_address(0, sizeof_uint64) next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64) @@ -262,8 +277,30 @@ class JakAndDaxterMemoryReader: logger.debug("Checked orb cache: " + str(next_cache)) # Listen for any changes to this setting. - moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8) - self.moverando_enabled = bool(moverando_flag) + # moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8) + # self.moverando_enabled = bool(moverando_flag) + + orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8) + orbsanity_bundle = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) + self.orbsanity_enabled = orbsanity_option > 0 + + # Treat these values like the Deathlink flag. They need to be reset once they are checked. + collected_bundle_level = self.read_goal_address(collected_bundle_level_offset, sizeof_uint8) + collected_bundle_count = self.read_goal_address(collected_bundle_count_offset, sizeof_uint32) + + if orbsanity_option > 0 and collected_bundle_count > 0: + # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. + # e.g. {25, 50, 75, 100, 125...} + for k in range(orbsanity_bundle, + orbsanity_bundle + collected_bundle_count, # Range max is non-inclusive. + orbsanity_bundle): + + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(collected_bundle_level, k, orbsanity_bundle)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + + # self.reset_orbsanity = True except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index c1fe78d3b2..a646b55311 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -257,15 +257,15 @@ class JakAndDaxterReplClient: return ok def receive_precursor_orb(self, ap_id: int) -> bool: - orb_id = Orbs.to_game_id(ap_id) + orb_amount = Orbs.to_game_id(ap_id) ok = self.send_form("(send-event " "*target* \'get-archipelago " "(pickup-type money) " - "(the float " + str(orb_id) + "))") + "(the float " + str(orb_amount) + "))") if ok: - logger.debug(f"Received a Precursor Orb!") + logger.debug(f"Received {orb_amount} Precursor Orbs!") else: - logger.error(f"Unable to receive a Precursor Orb!") + logger.error(f"Unable to receive {orb_amount} Precursor Orbs!") return ok # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. @@ -308,6 +308,28 @@ class JakAndDaxterReplClient: logger.error(f"Unable to reset deathlink flag!") return ok + def setup_orbsanity(self, option: int, bundle: int) -> bool: + ok = self.send_form(f"(ap-setup-orbs! (the uint {option}) (the uint {bundle}))") + if ok: + logger.debug(f"Set up orbsanity: Option {option}, Bundle {bundle}!") + else: + logger.error(f"Unable to set up orbsanity: Option {option}, Bundle {bundle}!") + return ok + + def reset_orbsanity(self) -> bool: + ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-level) 0)") + if ok: + logger.debug(f"Reset level ID for collected orbsanity bundle!") + else: + logger.error(f"Unable to reset level ID for collected orbsanity bundle!") + + ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-count) 0)") + if ok: + logger.debug(f"Reset orb count for collected orbsanity bundle!") + else: + logger.error(f"Unable to reset orb count for collected orbsanity bundle!") + return ok + def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index cdd2bcad7b..7199666f6a 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -29,11 +29,11 @@ At this time, this method of setup works on Windows only, but Linux support is a - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` should have *all* the same files as - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data`, if it doesn't, copy those files over manually. - And then `Recompile` if you needed to copy the files over. -- **DO NOT LAUNCH THE GAME FROM THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). +- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). ***Archipelago Launcher*** -- Copy the `jakanddaxter.apworld` file into your `Archipelago/lib/worlds` directory. +- Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. - Run the Archipelago Launcher. - From the left-most list, click `Generate Template Options`. @@ -44,6 +44,20 @@ At this time, this method of setup works on Windows only, but Linux support is a - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. - You can sort by Date Modified to make it easy to find. +## Updates and New Releases + +***OpenGOAL Mod Launcher*** + +- Run the Mod Launcher and click `ArchipelaGOAL` in the mod list. +- Click `Launch` to download and install any new updates that have been released. +- You can verify your version once you reach the title screen menu by navigating to `Options > Game Options > Miscellaneous > Speedrunner Mode`. +- Turn on `Speedrunner Mode` and exit the menu. You should see the installed version number in the bottom left corner. Then turn `Speedrunner Mode` back off. +- Once you've verified your version, you can close the game. Remember, this is just for downloading updates. **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** + +***Archipelago Launcher*** + +- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. + ## Starting a Game ***New Game*** diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 6e45a300d0..1853730c33 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from ..GameID import jak1_id # Precursor Orbs are not necessarily given ID's by the game. @@ -7,14 +9,13 @@ from ..GameID import jak1_id # so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs. # In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die, -# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. +# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. These orbs that spawn +# from parent actors DON'T have an Actor ID themselves - the parent object keeps track of how many of its orbs +# have been picked up. -# These orbs that spawn from parent actors DON'T have an Actor ID themselves - the parent object keeps -# track of how many of its orbs have been picked up. If you pick up only some of its orbs, it -# will respawn when you leave the area, and only drop the remaining number of orbs when activated/killed. -# Once all the orbs are picked up, the actor will permanently "retire" and never spawn again. -# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering -# these ID-less orbs may need to be a future enhancement. TODO ^^ +# In order to deal with this mess, we're creating a factory class that will generate Orb Locations for us. +# This will be compatible with both Global Orbsanity and Per-Level Orbsanity, allowing us to create any +# number of Locations depending on the bundle size chosen, while also guaranteeing that each has a unique address. # We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792). orb_offset = 32768 @@ -32,68 +33,96 @@ def to_game_id(ap_id: int) -> int: return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. -# The ID's you see below correspond directly to that orb's Actor ID in the game. +# Use this when the Memory Reader learns that you checked a specific bundle. +# Offset each level by 200 orbs (max number in any level), {200, 400, ...} +# then divide orb count by bundle size, {201, 202, ...} +# then subtract 1. {200, 201, ...} +def find_address(level_index: int, orb_count: int, bundle_size: int) -> int: + result = (level_index * 200) + (orb_count // bundle_size) - 1 + return result -# Geyser Rock -locGR_orbTable = { + +# Use this when assigning addresses during region generation. +def create_address(level_index: int, bundle_index: int) -> int: + result = (level_index * 200) + bundle_index + return result + + +# What follows is our method of generating all the name/ID pairs for location_name_to_id. +# Remember that not every bundle will be used in the actual seed, we just need this as a static map of strings to ints. +level_info = { + "": { + "level_index": 16, # Global + "orbs": 2000 + }, + "Geyser Rock": { + "level_index": 0, + "orbs": 50 + }, + "Sandover Village": { + "level_index": 1, + "orbs": 50 + }, + "Sentinel Beach": { + "level_index": 2, + "orbs": 150 + }, + "Forbidden Jungle": { + "level_index": 3, + "orbs": 150 + }, + "Misty Island": { + "level_index": 4, + "orbs": 150 + }, + "Fire Canyon": { + "level_index": 5, + "orbs": 50 + }, + "Rock Village": { + "level_index": 6, + "orbs": 50 + }, + "Lost Precursor City": { + "level_index": 7, + "orbs": 200 + }, + "Boggy Swamp": { + "level_index": 8, + "orbs": 200 + }, + "Precursor Basin": { + "level_index": 9, + "orbs": 200 + }, + "Mountain Pass": { + "level_index": 10, + "orbs": 50 + }, + "Volcanic Crater": { + "level_index": 11, + "orbs": 50 + }, + "Snowy Mountain": { + "level_index": 12, + "orbs": 200 + }, + "Spider Cave": { + "level_index": 13, + "orbs": 200 + }, + "Lava Tube": { + "level_index": 14, + "orbs": 50 + }, + "Gol and Maia's Citadel": { + "level_index": 15, + "orbs": 200 + } } -# Sandover Village -locSV_orbTable = { -} - -# Forbidden Jungle -locFJ_orbTable = { -} - -# Sentinel Beach -locSB_orbTable = { -} - -# Misty Island -locMI_orbTable = { -} - -# Fire Canyon -locFC_orbTable = { -} - -# Rock Village -locRV_orbTable = { -} - -# Precursor Basin -locPB_orbTable = { -} - -# Lost Precursor City -locLPC_orbTable = { -} - -# Boggy Swamp -locBS_orbTable = { -} - -# Mountain Pass -locMP_orbTable = { -} - -# Volcanic Crater -locVC_orbTable = { -} - -# Spider Cave -locSC_orbTable = { -} - -# Snowy Mountain -locSM_orbTable = { -} - -# Lava Tube -locLT_orbTable = { -} - -# Gol and Maias Citadel -locGMC_orbTable = { +loc_orbBundleTable = { + create_address(level_info[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip() + for name in level_info + for index in range(level_info[name]["orbs"]) } diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index 9f5600f5ab..e6b621e38d 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) @@ -20,6 +21,7 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ or (state.has("Punch", p) and state.has("Punch Uppercut", p))) # Orb crates and fly box in this area can be gotten with yellow eco and goggles. + # Start with the first yellow eco cluster near first_bats and work your way backward toward the entrance. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) main_area.add_fly_locations([43]) @@ -151,4 +153,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(last_tar_pit) multiworld.regions.append(fourth_tether) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(8, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py index b77d28b7d6..473248d33c 100644 --- a/worlds/jakanddaxter/regs/FireCanyonRegions.py +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(5, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py index 33e49a9d04..ca48cf2c8b 100644 --- a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25) @@ -80,4 +81,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(temple_int_pre_blue) multiworld.regions.append(temple_int_post_blue) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(3, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index ed4c4daaf3..938fe51aed 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([92, 93]) @@ -23,4 +25,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) multiworld.regions.append(cliff) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(0, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index da3dabfe77..14db2b19e6 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -1,11 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs # God help me... here we go. -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) @@ -112,4 +113,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(rotating_tower) multiworld.regions.append(final_boss) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(15, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area, final_boss] diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py index d8c8a7ec41..04fb16af98 100644 --- a/worlds/jakanddaxter/regs/LavaTubeRegions.py +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(14, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index de5251a787..102283bf86 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # Just the starting area. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4) @@ -127,4 +128,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(second_slide) multiworld.regions.append(helix_room) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(7, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 259e9c5c23..c40601d794 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9) @@ -113,4 +114,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(lower_approach) multiworld.regions.append(arena) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(4, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index b1eaea1019..f6e2419689 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This is basically just Klaww. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) @@ -30,5 +32,22 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(race) multiworld.regions.append(shortcut) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(10, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + # Return race required for inter-level connections. return [main_area, race] diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py index 7b1ea8a883..0e24c1b774 100644 --- a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(9, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py index 40e4cb9273..42534e462a 100644 --- a/worlds/jakanddaxter/regs/RegionBase.py +++ b/worlds/jakanddaxter/regs/RegionBase.py @@ -3,7 +3,8 @@ from BaseClasses import MultiWorld, Region from ..GameID import jak1_name from ..JakAndDaxterOptions import JakAndDaxterOptions from ..Locations import JakAndDaxterLocation, location_table -from ..locs import (CellLocations as Cells, +from ..locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -31,7 +32,8 @@ class JakAndDaxterRegion(Region): Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Cells.to_ap_id(loc), access_rule) + ap_id = Cells.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_fly_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -39,7 +41,8 @@ class JakAndDaxterRegion(Region): Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Scouts.to_ap_id(loc), access_rule) + ap_id = Scouts.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_special_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -49,7 +52,8 @@ class JakAndDaxterRegion(Region): Power Cell Locations, so you get 2 unlocks for these rather than 1. """ for loc in locations: - self.add_jak_locations(Specials.to_ap_id(loc), access_rule) + ap_id = Specials.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_cache_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -57,13 +61,28 @@ class JakAndDaxterRegion(Region): Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Caches.to_ap_id(loc), access_rule) + ap_id = Caches.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_jak_locations(self, ap_id: int, access_rule: Callable = None): + def add_orb_locations(self, level_index: int, bundle_index: int, bundle_size: int, access_rule: Callable = None): """ - Helper function to add Locations. Not to be used directly. + Adds Orb Bundle Locations to this region equal to `bundle_count`. Used only when Per-Level Orbsanity is enabled. + The orb factory class will handle AP ID enumeration. """ - location = JakAndDaxterLocation(self.player, location_table[ap_id], ap_id, self) + bundle_address = Orbs.create_address(level_index, bundle_index) + location = JakAndDaxterLocation(self.player, + f"{self.level_name} Orb Bundle {bundle_index + 1}".strip(), + Orbs.to_ap_id(bundle_address), + self) + if access_rule: + location.access_rule = access_rule + self.locations.append(location) + + def add_jak_locations(self, ap_id: int, name: str, access_rule: Callable = None): + """ + Helper function to add Locations. Not to be used directly. + """ + location = JakAndDaxterLocation(self.player, name, ap_id, self) if access_rule: location.access_rule = access_rule self.locations.append(location) diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 09b0858ff3..d17ac0cb78 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -1,23 +1,24 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) main_area.add_cell_locations([31], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([32], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([33], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([34], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([35], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 34)) + can_trade(state, player, multiworld, options, 1530, 34)) # These 2 scout fly boxes can be broken by running with nearby blue eco. main_area.add_fly_locations([196684, 262220]) @@ -33,8 +34,9 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ orb_cache.add_cache_locations([10945], access_rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player))) + # Fly here can be gotten with Yellow Eco from Boggy, goggles, and no extra movement options (see fly ID 43). pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7) - pontoon_bridge.add_fly_locations([393292], access_rule=lambda state: can_free_scout_flies(state, player)) + pontoon_bridge.add_fly_locations([393292]) klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0) @@ -59,5 +61,22 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(pontoon_bridge) multiworld.regions.append(klaww_cliff) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(6, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + # Return klaww_cliff required for inter-level connections. return [main_area, pontoon_bridge, klaww_cliff] diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index ac4d904a45..1a0c3ccb00 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -1,19 +1,20 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26) # Yakows requires no combat. main_area.add_cell_locations([10]) main_area.add_cell_locations([11], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([12], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. main_area.add_fly_locations([262219, 327755, 131147, 65611]) @@ -32,9 +33,9 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) oracle_platforms.add_cell_locations([13], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) oracle_platforms.add_cell_locations([14], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 13)) + can_trade(state, player, multiworld, options, 1530, 13)) oracle_platforms.add_fly_locations([393291], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -68,4 +69,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(yakow_cliff) multiworld.regions.append(oracle_platforms) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(1, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index 0e85dc0573..f573f4ee92 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128) main_area.add_cell_locations([18, 21, 22]) @@ -82,4 +83,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(blue_ridge) multiworld.regions.append(cannon_tower) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(2, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index dbd92c297c..b6fd711171 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -1,11 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs # God help me... here we go. -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # We need a few helper functions. def can_cross_main_gap(state: CollectionState, p: int) -> bool: @@ -189,4 +190,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(fort_interior_base) multiworld.regions.append(fort_interior_course_end) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(12, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index 3d9e1093e1..84af260a09 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63) @@ -107,4 +108,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(spider_tunnel) multiworld.regions.append(spider_tunnel_crates) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(13, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index 48241d647c..4f8bd8a2eb 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -1,26 +1,27 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # No area is inaccessible in VC even with only running and jumping. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([96], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([97], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 96)) + can_trade(state, player, multiworld, options, 1530, 96)) main_area.add_cell_locations([98], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 97)) + can_trade(state, player, multiworld, options, 1530, 97)) main_area.add_cell_locations([99], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 98)) + can_trade(state, player, multiworld, options, 1530, 98)) main_area.add_cell_locations([100], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([101], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 100)) + can_trade(state, player, multiworld, options, 1530, 100)) # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). @@ -35,4 +36,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(11, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area]