From e0410eabdd57f3071fd789f8bd43f9188071bbb2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:03:30 -0400 Subject: [PATCH] More Options, More Docs, More Tests (#51) * Reorder cell counts, require punch for Klaww. * Friendlier friendly friendlies. * Removed custom_worlds references from docs/setup guide, focused OpenGOAL Launcher language. * Increased breadth of unit tests. --- worlds/jakanddaxter/Client.py | 8 +-- worlds/jakanddaxter/JakAndDaxterOptions.py | 19 +++++- worlds/jakanddaxter/Rules.py | 16 +++-- worlds/jakanddaxter/__init__.py | 32 +++++++--- worlds/jakanddaxter/docs/setup_en.md | 38 +++--------- .../jakanddaxter/regs/MountainPassRegions.py | 6 ++ worlds/jakanddaxter/test/test_moverando.py | 23 +++++++ worlds/jakanddaxter/test/test_orbsanity.py | 61 +++++++++++++++++++ .../test/test_orderedcellcounts.py | 35 +++++++++++ worlds/jakanddaxter/test/test_trades.py | 45 ++++++++++++++ 10 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 worlds/jakanddaxter/test/test_moverando.py create mode 100644 worlds/jakanddaxter/test/test_orbsanity.py create mode 100644 worlds/jakanddaxter/test/test_orderedcellcounts.py create mode 100644 worlds/jakanddaxter/test/test_trades.py diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 3aff8b2222..23ead2baf7 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -122,12 +122,6 @@ class JakAndDaxterContext(CommonContext): else: orbsanity_bundle = 1 - # Keep compatibility with 0.0.8 at least for now - TODO: Remove this. - if "completion_condition" in slot_data: - goal_id = slot_data["completion_condition"] - else: - goal_id = slot_data["jak_completion_condition"] - create_task_log_exception( self.repl.setup_options(orbsanity_option, orbsanity_bundle, @@ -136,7 +130,7 @@ class JakAndDaxterContext(CommonContext): slot_data["lava_tube_cell_count"], slot_data["citizen_orb_trade_amount"], slot_data["oracle_orb_trade_amount"], - goal_id)) + slot_data["jak_completion_condition"])) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 55bfea4946..35303c2938 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range +from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle class EnableMoveRandomizer(Toggle): @@ -114,6 +114,21 @@ class LavaTubeCellCount(Range): default = 72 +class EnableOrderedCellCounts(DefaultOnToggle): + """Enable to reorder the Cell Count options in ascending order. This is useful if you choose to randomize + those options. + + For example, if Fire Canyon Cell Count, Mountain Pass Cell Count, and Lava Tube Cell Count are 60, 30, and 40 + respectively, they will be reordered to 30, 40, and 60 respectively.""" + display_name = "Enable Ordered Cell Counts" + + +class RequirePunchForKlaww(DefaultOnToggle): + """Enable to force the Punch move to come before Klaww. Disabling this setting may require Jak to fight Klaww + and Gol and Maia by shooting yellow eco through his goggles. This only applies if "Enable Move Randomizer" is ON.""" + display_name = "Require Punch For Klaww" + + # 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.). @@ -166,6 +181,8 @@ class JakAndDaxterOptions(PerGameCommonOptions): fire_canyon_cell_count: FireCanyonCellCount mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount + enable_ordered_cell_counts: EnableOrderedCellCounts + require_punch_for_klaww: RequirePunchForKlaww citizen_orb_trade_amount: CitizenOrbTradeAmount oracle_orb_trade_amount: OracleOrbTradeAmount jak_completion_condition: CompletionCondition diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 9b8852d850..c9b36cae50 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -175,8 +175,11 @@ def enforce_multiplayer_limits(world: JakAndDaxterWorld): f"{options.oracle_orb_trade_amount.value}).\n") if friendly_message != "": - raise OptionError(f"{world.player_name}: Please adjust the following Options for a multiplayer game.\n" - f"{friendly_message}") + raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n" + f"Please adjust the following Options for a multiplayer game. \n" + f"{friendly_message}" + f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. " + f"(Use at your own risk!)") def enforce_singleplayer_limits(world: JakAndDaxterWorld): @@ -202,10 +205,11 @@ def enforce_singleplayer_limits(world: JakAndDaxterWorld): f"{options.lava_tube_cell_count.value}).\n") if friendly_message != "": - raise OptionError(f"The options you have chosen may result in seed generation failures." - f"Please adjust the following Options for a singleplayer game.\n" - f"{friendly_message} " - f"Or set 'enforce_friendly_options' in host.yaml to false. (Use at your own risk!)") + raise OptionError(f"The options you have chosen may result in seed generation failures. \n" + f"Please adjust the following Options for a singleplayer game. \n" + f"{friendly_message}" + f"Or set 'enforce_friendly_options' in your host.yaml to false. " + f"(Use at your own risk!)") def verify_orb_trade_amounts(world: JakAndDaxterWorld): diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 08b7f9b998..86b4b8ceb5 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -129,6 +129,25 @@ class JakAndDaxterWorld(World): # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: + + # 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. + + # Order the thresholds ascending and set the options values to the new order. + # TODO - How does this affect region access rules and other things? + try: + if self.options.enable_ordered_cell_counts: + self.power_cell_thresholds.sort() + self.options.fire_canyon_cell_count.value = self.power_cell_thresholds[0] + self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1] + self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2] + except IndexError: + pass # Skip if not possible. + # For the fairness of other players in a multiworld game, enforce some friendly limitations on our options, # so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen. enforce_friendly_options = Utils.get_settings()["jakanddaxter_options"]["enforce_friendly_options"] @@ -157,13 +176,6 @@ class JakAndDaxterWorld(World): 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 set_orb_trade_rule(self) @@ -182,9 +194,9 @@ class JakAndDaxterWorld(World): 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. + # Make 101 Power Cells. 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 - Option-driven region creation would be an enormous refactor. if item in range(jak1_id, jak1_id + Scouts.fly_offset): # If for some unholy reason we don't have the list of power cell thresholds, have a fallback plan. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index c01de29afc..f74b85cd0f 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -8,26 +8,10 @@ At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux. -## Installation - -### Archipelago Launcher - -- 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`. -- Select `Jak and Daxter The Precursor Legacy.yaml`. -- In the text file that opens, enter the name you want and remember it for later. -- Save this file in `Archipelago/players`. You can now close the file. -- Back in the Archipelago Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. -- If you plan to host the game yourself, from the left-most list, click `Host`. - - 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. - -### OpenGOAL Launcher +## Installation via OpenGOAL Launcher - Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - - You must set up a vanilla installation of Jak and Daxter before you can install mods for it. + - **You must set up a vanilla installation of Jak and Daxter before you can install mods for it.** - Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. @@ -57,20 +41,14 @@ jakanddaxter_options: - Save the file and close it. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). -## Updates and New Releases - -### Archipelago Launcher - -- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - -### OpenGOAL Launcher +## Updates and New Releases via OpenGOAL Launcher If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`. -- Under `Available Mods`, click `ArchipelaGOAL`. +- Under `Installed Mods`, click `ArchipelaGOAL`. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. - **After the update is installed, you must click `Advanced`, then click `Compile` to make the update take effect.** @@ -111,16 +89,16 @@ You may start the game via the Text Client, but it never loads in the title scre -- Compilation Error! -- ``` -If this happens, run the OpenGOAL Launcher. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. +If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. - Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`. - Paste the `iso_data` folder you copied earlier. - Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Compile`. ### The Text Client Says "The process has died" @@ -133,7 +111,7 @@ If at any point the text client says `The process has died`, you will - Then enter the following commands into the text client to reconnect everything to the game. - `/repl connect` - `/memr connect` -- Once these are done, you can enter `/repl status` and `/memr status` to verify. +- Once these are done, you can enter `/repl status` and `/memr status` in the text client to verify. ### The Game Freezes On The Same Two Frames, But The Music Is Still Playing diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index 019b717c41..23314a0dc4 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -4,6 +4,7 @@ from .RegionBase import JakAndDaxterRegion from .. import EnableOrbsanity, JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts +from worlds.generic.Rules import add_rule def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: @@ -15,6 +16,11 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_cell_locations([86]) + # Some folks prefer firing Yellow Eco from the hip, so optionally put this rule before Klaww. Klaww is the only + # location in main_area, so he's at index 0. + if world.options.require_punch_for_klaww: + add_rule(main_area.locations[0], lambda state: state.has("Punch", player)) + race = JakAndDaxterRegion("Race", player, multiworld, level_name, 50) race.add_cell_locations([87]) diff --git a/worlds/jakanddaxter/test/test_moverando.py b/worlds/jakanddaxter/test/test_moverando.py new file mode 100644 index 0000000000..dddc9f4201 --- /dev/null +++ b/worlds/jakanddaxter/test/test_moverando.py @@ -0,0 +1,23 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class MoveRandoTest(JakAndDaxterTestBase): + options = { + "enable_move_randomizer": True + } + + def test_move_items_in_pool(self): + for move in move_item_table: + self.assertIn(move_item_table[move], {item.name for item in self.multiworld.itempool}) + + def test_cannot_reach_without_move(self): + self.assertAccessDependency( + ["GR: Climb Up The Cliff"], + [["Double Jump"], ["Crouch"]], + only_check_listed=True) diff --git a/worlds/jakanddaxter/test/test_orbsanity.py b/worlds/jakanddaxter/test/test_orbsanity.py new file mode 100644 index 0000000000..4f84256be1 --- /dev/null +++ b/worlds/jakanddaxter/test/test_orbsanity.py @@ -0,0 +1,61 @@ +from . import JakAndDaxterTestBase +from ..Items import orb_item_table + + +class NoOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 0, # Off + "level_orbsanity_bundle_size": 25, + "global_orbsanity_bundle_size": 16 + } + + def test_orb_bundles_not_exist_in_pool(self): + for bundle in orb_item_table: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(0, count) + + bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(0, count) + + +class PerLevelOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 1, # Per Level + "level_orbsanity_bundle_size": 25 + } + + def test_orb_bundles_exist_in_pool(self): + for bundle in orb_item_table: + if bundle == self.options["level_orbsanity_bundle_size"]: + self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + else: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(80, count) + + +class GlobalOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, # Global + "global_orbsanity_bundle_size": 16 + } + + def test_orb_bundles_exist_in_pool(self): + for bundle in orb_item_table: + if bundle == self.options["global_orbsanity_bundle_size"]: + self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + else: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(125, count) diff --git a/worlds/jakanddaxter/test/test_orderedcellcounts.py b/worlds/jakanddaxter/test/test_orderedcellcounts.py new file mode 100644 index 0000000000..a457beaa4a --- /dev/null +++ b/worlds/jakanddaxter/test/test_orderedcellcounts.py @@ -0,0 +1,35 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class ReorderedCellCountsTest(JakAndDaxterTestBase): + options = { + "enable_ordered_cell_counts": True, + "fire_canyon_cell_count": 20, + "mountain_pass_cell_count": 15, + "lava_tube_cell_count": 10, + } + + def test_reordered_cell_counts(self): + self.world.generate_early() + self.assertLessEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count) + self.assertLessEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count) + + +class UnorderedCellCountsTest(JakAndDaxterTestBase): + options = { + "enable_ordered_cell_counts": False, + "fire_canyon_cell_count": 20, + "mountain_pass_cell_count": 15, + "lava_tube_cell_count": 10, + } + + def test_unordered_cell_counts(self): + self.world.generate_early() + self.assertGreaterEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count) + self.assertGreaterEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count) diff --git a/worlds/jakanddaxter/test/test_trades.py b/worlds/jakanddaxter/test/test_trades.py new file mode 100644 index 0000000000..4a9331307f --- /dev/null +++ b/worlds/jakanddaxter/test/test_trades.py @@ -0,0 +1,45 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class TradesCostNothingTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, + "global_orbsanity_bundle_size": 5, + "citizen_orb_trade_amount": 0, + "oracle_orb_trade_amount": 0 + } + + def test_orb_items_are_filler(self): + self.collect_all_but("") + self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items) + + def test_trades_are_accessible(self): + self.assertTrue(self.multiworld + .get_location("SV: Bring 90 Orbs To The Mayor", self.player) + .can_reach(self.multiworld.state)) + + +class TradesCostEverythingTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, + "global_orbsanity_bundle_size": 5, + "citizen_orb_trade_amount": 222, + "oracle_orb_trade_amount": 0 + } + + def test_orb_items_are_progression(self): + self.collect_all_but("") + self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player]) + self.assertEqual(400, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"]) + + def test_trades_are_accessible(self): + self.collect_all_but("") + self.assertTrue(self.multiworld + .get_location("SV: Bring 90 Orbs To The Mayor", self.player) + .can_reach(self.multiworld.state)) \ No newline at end of file