diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 77db95350b..3aff8b2222 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -160,10 +160,11 @@ class JakAndDaxterContext(CommonContext): async def json_to_game_text(self, args: dict): if "type" in args and args["type"] in {"ItemSend"}: - my_item_name = Optional[str] - my_item_finder = Optional[str] - their_item_name = Optional[str] - their_item_owner = Optional[str] + my_item_name: Optional[str] = None + my_item_finder: Optional[str] = None + their_item_name: Optional[str] = None + their_item_owner: Optional[str] = None + item = args["item"] recipient = args["receiving"] diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 21fb2725b0..55bfea4946 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -29,7 +29,10 @@ class EnableOrbsanity(Choice): class GlobalOrbsanityBundleSize(Choice): """Set the orb bundle size 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.""" + There are 2000 orbs in the game, so your bundle size must be a factor of 2000. + + Multiplayer Minimum: 10 + Multiplayer Maximum: 200""" display_name = "Global Orbsanity Bundle Size" option_1_orb = 1 option_2_orbs = 2 @@ -58,7 +61,9 @@ class GlobalOrbsanityBundleSize(Choice): class PerLevelOrbsanityBundleSize(Choice): """Set the orb bundle size 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.""" + There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50. + + Multiplayer Minimum: 10""" display_name = "Per Level Orbsanity Bundle Size" option_1_orb = 1 option_2_orbs = 2 @@ -71,7 +76,10 @@ class PerLevelOrbsanityBundleSize(Choice): class FireCanyonCellCount(Range): - """Set the number of power cells you need to cross Fire Canyon.""" + """Set the number of power cells you need to cross Fire Canyon. + + Multiplayer Maximum: 30 + Singleplayer Maximum: 34""" display_name = "Fire Canyon Cell Count" range_start = 0 range_end = 100 @@ -81,7 +89,10 @@ class FireCanyonCellCount(Range): class MountainPassCellCount(Range): - """Set the number of power cells you need to reach Klaww and cross Mountain Pass.""" + """Set the number of power cells you need to reach Klaww and cross Mountain Pass. + + Multiplayer Maximum: 60 + Singleplayer Maximum: 63""" display_name = "Mountain Pass Cell Count" range_start = 0 range_end = 100 @@ -91,7 +102,10 @@ class MountainPassCellCount(Range): class LavaTubeCellCount(Range): - """Set the number of power cells you need to cross Lava Tube.""" + """Set the number of power cells you need to cross Lava Tube. + + Multiplayer Maximum: 90 + Singleplayer Maximum: 99""" display_name = "Lava Tube Cell Count" range_start = 0 range_end = 100 @@ -100,12 +114,14 @@ class LavaTubeCellCount(Range): default = 72 -# 222 is the maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). +# 222 is the absolute maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). class CitizenOrbTradeAmount(Range): """Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.). Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). - The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades). + + Multiplayer Maximum: 120""" display_name = "Citizen Orb Trade Amount" range_start = 0 range_end = 222 @@ -113,12 +129,14 @@ class CitizenOrbTradeAmount(Range): default = 90 -# 333 is the maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333). +# 333 is the absolute maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333). class OracleOrbTradeAmount(Range): """Set the number of orbs you need to trade to the Oracles for a power cell. Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). - The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades). + + Multiplayer Maximum: 150""" display_name = "Oracle Orb Trade Amount" range_start = 0 range_end = 333 diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index f5765503f5..91608b7649 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union +from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union, List import Utils import settings @@ -9,7 +9,7 @@ from BaseClasses import (Item, Tutorial, CollectionState) from .GameID import jak1_id, jak1_name, jak1_max -from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity +from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity, CompletionCondition from .Locations import (JakAndDaxterLocation, location_table, cell_location_table, @@ -125,6 +125,7 @@ class JakAndDaxterWorld(World): orb_bundle_item_name: str = "" orb_bundle_size: int = 0 total_trade_orbs: int = 0 + power_cell_thresholds: List[int] = [] # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: @@ -152,6 +153,16 @@ class JakAndDaxterWorld(World): elif self.options.enable_orbsanity == EnableOrbsanity.option_global: self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] + else: + self.orb_bundle_size = 0 + self.orb_bundle_item_name = "" + + # Cache the power cell threshold values for quicker reference. + self.power_cell_thresholds = [] + self.power_cell_thresholds.append(self.options.fire_canyon_cell_count.value) + self.power_cell_thresholds.append(self.options.mountain_pass_cell_count.value) + self.power_cell_thresholds.append(self.options.lava_tube_cell_count.value) + self.power_cell_thresholds.append(100) # The 100 Power Cell Door. # Options drive which trade rules to use, so they need to be setup before we create_regions. from .Rules import set_orb_trade_rule @@ -165,47 +176,68 @@ class JakAndDaxterWorld(World): # from Utils import visualize_regions # visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") - # Helper function to reuse some nasty if/else trees. - def item_type_helper(self, item) -> Tuple[int, ItemClass]: - # Make 101 Power Cells. + # Helper function to reuse some nasty if/else trees. This outputs a list of pairs of item count and classification. + # For instance, not all 101 power cells need to be marked progression if you only need 72 to beat the game. So we + # will have 72 Progression Power Cells, and 29 Filler Power Cells. + def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: + counts_and_classes: List[Tuple[int, ItemClass]] = [] + + # Make 101 Power Cells. Not all of them will be Progression, some will be Filler. We only want AP's Progression + # Fill routine to handle the amount of cells we need to reach the furthest possible region. Even for early + # completion goals, all areas in the game must be reachable or generation will fail. TODO - Enormous refactor. if item in range(jak1_id, jak1_id + Scouts.fly_offset): - classification = ItemClass.progression_skip_balancing - count = 101 + + # If for some unholy reason we don't have the list of power cell thresholds, have a fallback plan. + if self.power_cell_thresholds: + prog_count = max(self.power_cell_thresholds[:3]) + non_prog_count = 101 - prog_count + + if self.options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: + counts_and_classes.append((100, ItemClass.progression_skip_balancing)) + counts_and_classes.append((1, ItemClass.filler)) + else: + counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) + counts_and_classes.append((non_prog_count, ItemClass.filler)) + else: + counts_and_classes.append((101, ItemClass.progression_skip_balancing)) # Make 7 Scout Flies per level. elif item in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset): - classification = ItemClass.progression_skip_balancing - count = 7 + counts_and_classes.append((7, ItemClass.progression_skip_balancing)) # Make only 1 of each Special Item. elif item in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): - classification = ItemClass.progression | ItemClass.useful - count = 1 + counts_and_classes.append((1, ItemClass.progression | ItemClass.useful)) # Make only 1 of each Move Item. elif item in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): - classification = ItemClass.progression | ItemClass.useful - count = 1 + counts_and_classes.append((1, ItemClass.progression | ItemClass.useful)) - # Make N Precursor Orb bundles, where N is 2000 / bundle size. + # Make N Precursor Orb bundles, where N is 2000 // bundle size. Like Power Cells, only a fraction of these will + # be marked as Progression with the remainder as Filler, but they are still entirely fungible. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): - if self.total_trade_orbs == 0: - classification = ItemClass.filler # If you don't need orbs to do trades, they are useless. - else: - classification = ItemClass.progression_skip_balancing - count = 2000 // self.orb_bundle_size if self.orb_bundle_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. + # Don't divide by zero! + if self.orb_bundle_size > 0: + item_count = 2000 // self.orb_bundle_size + + prog_count = -(-self.total_trade_orbs // self.orb_bundle_size) # Lazy ceil using integer division. + non_prog_count = item_count - prog_count + + counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) + counts_and_classes.append((non_prog_count, ItemClass.filler)) + else: + counts_and_classes.append((0, ItemClass.filler)) # No orbs in a bundle means no bundles. + + # Under normal circumstances, we create 0 green eco fillers. We will manually create filler items as needed. elif item == jak1_max: - classification = ItemClass.filler - count = 0 + counts_and_classes.append((0, ItemClass.filler)) # If we try to make items with ID's higher than we've defined, something has gone wrong. else: raise KeyError(f"Tried to fill item pool with unknown ID {item}.") - return count, classification + return counts_and_classes def create_items(self) -> None: for item_name in self.item_name_to_id: @@ -227,14 +259,15 @@ class JakAndDaxterWorld(World): or item_name != self.orb_bundle_item_name)): 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)] + # In every other scenario, do this. Not all items with the same name will have the same classification. + counts_and_classes = self.item_type_helper(item_id) + for (count, classification) in counts_and_classes: + 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] - _, classification = self.item_type_helper(item_id) + _, classification = self.item_type_helper(item_id)[0] # Use first tuple (will likely be the most important). return JakAndDaxterItem(name, classification, item_id, self.player) def get_filler_item_name(self) -> str: @@ -252,6 +285,17 @@ class JakAndDaxterWorld(World): if item.name == self.orb_bundle_item_name: state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size # Give a bundle of Trade Orbs + # Scout Flies ALSO do not unlock anything that contains more Reachable Orbs, NOR do they give you more + # tradeable orbs. So let's just pass on them. + elif item.name in self.item_name_groups["Scout Flies"]: + pass + + # Power Cells DO unlock new regions that contain more Reachable Orbs - the connector levels and new + # hub levels - BUT they only do that when you have a number of them equal to one of the threshold values. + elif (item.name == "Power Cell" + and state.count("Power Cell", self.player) not in self.power_cell_thresholds): + pass + # However, every other item that changes the CollectionState should set the cache to stale, because they # likely made it possible to reach more orb locations (level unlocks, region unlocks, etc.). else: @@ -265,10 +309,22 @@ class JakAndDaxterWorld(World): # Do the same thing we did in collect, except subtract trade orbs instead of add. if item.name == self.orb_bundle_item_name: state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size # Take a bundle of Trade Orbs + + # Ditto Scout Flies. + elif item.name in self.item_name_groups["Scout Flies"]: + pass + + # Ditto Power Cells, but check count + 1, because we potentially crossed the threshold in the opposite + # direction. E.g. we've removed the 20th power cell, our count is now 19, so we should stale the cache. + elif (item.name == "Power Cell" + and state.count("Power Cell", self.player) + 1 not in self.power_cell_thresholds): + pass + + # Ditto everything else. else: state.prog_items[self.player]["Reachable Orbs Fresh"] = False - # TODO - 3.8 compatibility, remove this block when no longer required. + # TODO - Python 3.8 compatibility, remove this block when no longer required. if state.prog_items[self.player]["Tradeable Orbs"] < 1: del state.prog_items[self.player]["Tradeable Orbs"] if state.prog_items[self.player]["Reachable Orbs"] < 1: diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 73009f0ad4..06c254de9c 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -210,33 +210,33 @@ class JakAndDaxterReplClient: # I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate. @staticmethod def sanitize_game_text(text: str) -> str: - if text is None: - return "\"NONE\"" - result = "".join(c for c in text if (c in {"-", " "} or c.isalnum())) result = result[:32].upper() return f"\"{result}\"" # Pushes a JsonMessageData object to the json message queue to be processed during the repl main_tick def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_item_owner): - self.json_message_queue.put(self.JsonMessageData(my_item_name, my_item_finder, - their_item_name, their_item_owner)) + self.json_message_queue.put(JsonMessageData(my_item_name, my_item_finder, their_item_name, their_item_owner)) # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). # So for the game to constantly display this information in the HUD, we have to write it # to a memory address as a char*. async def write_game_text(self, data: JsonMessageData): logger.debug(f"Sending info to in-game display!") - await self.send_form(f"(begin " - f" (charp<-string (-> *ap-info-jak1* my-item-name) " - f" {self.sanitize_game_text(data.my_item_name)}) " - f" (charp<-string (-> *ap-info-jak1* my-item-finder) " - f" {self.sanitize_game_text(data.my_item_finder)}) " - f" (charp<-string (-> *ap-info-jak1* their-item-name) " - f" {self.sanitize_game_text(data.their_item_name)}) " - f" (charp<-string (-> *ap-info-jak1* their-item-owner) " - f" {self.sanitize_game_text(data.their_item_owner)}) " - f" (none))", print_ok=False) + body = "" + if data.my_item_name: + body += (f" (charp<-string (-> *ap-info-jak1* my-item-name)" + f" {self.sanitize_game_text(data.my_item_name)})") + if data.my_item_finder: + body += (f" (charp<-string (-> *ap-info-jak1* my-item-finder)" + f" {self.sanitize_game_text(data.my_item_finder)})") + if data.their_item_name: + body += (f" (charp<-string (-> *ap-info-jak1* their-item-name)" + f" {self.sanitize_game_text(data.their_item_name)})") + if data.their_item_owner: + body += (f" (charp<-string (-> *ap-info-jak1* their-item-owner)" + f" {self.sanitize_game_text(data.their_item_owner)})") + await self.send_form(f"(begin {body} (none))", print_ok=False) async def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index e85d93fc6f..bfcbce606c 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -2,7 +2,7 @@ from typing import List from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from .. import EnableOrbsanity, JakAndDaxterWorld, CompletionCondition from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level @@ -57,8 +57,6 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) - final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0) - # Jump Dive required for a lot of buttons, prepare yourself. main_area.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) or state.has_all({"Roll", "Roll Jump"}, player)) @@ -101,9 +99,6 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte final_boss.connect(rotating_tower) # Take elevator back down. - # Final door. Need 100 power cells. - final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100)) - multiworld.regions.append(main_area) multiworld.regions.append(robot_scaffolding) multiworld.regions.append(jump_pad_room) @@ -111,7 +106,6 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte multiworld.regions.append(bunny_room) multiworld.regions.append(rotating_tower) multiworld.regions.append(final_boss) - multiworld.regions.append(final_door) # 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. @@ -127,4 +121,13 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte multiworld.regions.append(orbs) main_area.connect(orbs) - return [main_area, final_boss, final_door] + # Final door. Need 100 power cells. + if options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: + final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0) + final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100)) + + multiworld.regions.append(final_door) + + return [main_area, final_boss, final_door] + else: + return [main_area, final_boss, None]