diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 52bff89d..b86dc86d 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -1,22 +1,17 @@ from BaseClasses import CollectionState -from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level from typing import Dict from random import Random - def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, - required_star_count: int, player: int) -> bool: - # Check if the ramps in the overworld are set correctly - if level_name in ramp_logic: - (ramp_reqs, level_reqs) = ramp_logic[level_name] + required_star_count: int, allow_ramp_tricks: bool, player: int) -> bool: - for req in level_reqs: - if not state.has(req + " Level Complete", player): - return False # This level needs another to be beaten first - - for req in ramp_reqs: - if not state.has(req + " Ramp", player): - return False # The player doesn't have the pre-requisite ramp button + # Must have correct ramp buttons and pre-requisite levels, or tricks to sequence break + overworld_region = overworld_region_by_level[level_name] + overworld_logic = overworld_region_logic[overworld_region] + visited = list() + if not overworld_logic(state, player, allow_ramp_tricks, visited): + return False # Kevin Levels Need to have the corresponding items if level_name.startswith("K"): @@ -81,8 +76,9 @@ def is_item_progression(item_name, level_mapping, include_kevin): if item_name.endswith("Emote"): return False - if "Kevin" in item_name or "Ramp" in item_name: - return True # always progression + for item_identifier in ["Kevin", "Ramp", "Dash"]: + if item_identifier in item_name: + return True # These things are always progression because they can have overworld implications def item_in_logic(shortname, _item_name): for star in range(0, 3): @@ -214,28 +210,128 @@ def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: return len(exclusive) == 0 and len(additive) == 0 +def can_reach_main(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.main in visited: + return False + visited.append(OverworldRegion.main) -# If key missing, doesn't require a ramp to access (or the logic is handled by a preceeding level) -# -# If empty, a ramp is required to access, but the ramp button is garunteed accessible -# -# If populated, ramp(s) are required to access and the button requires all levels in the -# list to be compelted before it can be pressed -# -ramp_logic = { - "1-5": (["Yellow"], []), - "2-2": (["Green"], []), - "3-1": (["Blue"], []), - "5-2": (["Purple"], []), - "6-1": (["Pink"], []), - "6-2": (["Red", "Purple"], ["5-1"]), # 5-1 spawns blue button, blue button gets you to red button - "Kevin-1": (["Dark Green"], []), - "Kevin-7": (["Purple"], ["5-1"]), # 5-1 spawns blue button, - # press blue button, - # climb blue ramp, - # jump the gap, - # climb wood ramps - "Kevin-8": (["Red", "Blue"], ["5-1", "6-2"]), # Same as above, but 6-2 spawns the ramp to K8 + return True + +def can_reach_yellow_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.yellow_island in visited: + return False + visited.append(OverworldRegion.yellow_island) + + return state.has("Yellow Ramp", player) + +def can_reach_dark_green_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.dark_green_mountain in visited: + return False + visited.append(OverworldRegion.dark_green_mountain) + + return state.has_all({"Dark Green Ramp", "Kevin-1"}, player) + +def can_reach_out_of_bounds(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.out_of_bounds in visited: + return False + visited.append(OverworldRegion.out_of_bounds) + + return allow_tricks and state.has("Progressive Dash", player) and can_reach_dark_green_mountain(state, player, allow_tricks, visited) + +def can_reach_stonehenge_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.stonehenge_mountain in visited: + return False + visited.append(OverworldRegion.stonehenge_mountain) + + if state.has("Blue Ramp", player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_sky_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.sky_shelf in visited: + return False + visited.append(OverworldRegion.sky_shelf) + + if state.has("Green Ramp", player): + return True + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if allow_tricks and can_reach_pink_island(state, player, allow_tricks, visited) and state.has("Progressive Dash", player): + return True + + if can_reach_tip_of_the_map(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_pink_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.pink_island in visited: + return False + visited.append(OverworldRegion.pink_island) + + if state.has("Pink Ramp", player): + return True + + if allow_tricks and state.has("Progressive Dash", player) and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_tip_of_the_map(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.tip_of_the_map in visited: + return False + visited.append(OverworldRegion.tip_of_the_map) + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + if allow_tricks and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_mars_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.mars_shelf in visited: + return False + visited.append(OverworldRegion.mars_shelf) + + tip_of_the_map = can_reach_tip_of_the_map(state, player, allow_tricks, visited) + + if tip_of_the_map and allow_tricks: + return True + + if tip_of_the_map and state.has_all({"6-1 Level Complete", "Red Ramp"}, player): + return True + + return False + +def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.kevin_eight_island in visited: + return False + visited.append(OverworldRegion.kevin_eight_island) + + return can_reach_mars_shelf(state, player, allow_tricks, visited) + + +overworld_region_logic = { + OverworldRegion.main : can_reach_main , + OverworldRegion.yellow_island : can_reach_yellow_island , + OverworldRegion.sky_shelf : can_reach_sky_shelf , + OverworldRegion.stonehenge_mountain: can_reach_stonehenge_mountain, + OverworldRegion.tip_of_the_map : can_reach_tip_of_the_map , + OverworldRegion.pink_island : can_reach_pink_island , + OverworldRegion.mars_shelf : can_reach_mars_shelf , + OverworldRegion.dark_green_mountain: can_reach_dark_green_mountain, + OverworldRegion.kevin_eight_island : can_reach_kevin_eight_island , } horde_logic = { # Additive diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index 400796af..c8596340 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,6 +1,6 @@ from enum import IntEnum from typing import TypedDict -from Options import DefaultOnToggle, Range, Choice +from Options import Toggle, DefaultOnToggle, Range, Choice class LocationBalancingMode(IntEnum): @@ -21,6 +21,12 @@ class OC2OnToggle(DefaultOnToggle): return bool(self.value) +class OC2Toggle(Toggle): + @property + def result(self) -> bool: + return bool(self.value) + + class LocationBalancing(Choice): """Location balancing affects the density of progression items found in your world relative to other wordlds. This setting changes nothing for solo games. @@ -36,6 +42,10 @@ class LocationBalancing(Choice): option_full = LocationBalancingMode.full.value default = LocationBalancingMode.compromise.value +class RampTricks(OC2Toggle): + """If enabled, generated games may require sequence breaks on the overworld map. This includes crossing small gaps and escaping out of bounds.""" + display_name = "Overworld Tricks" + class DeathLink(Choice): """DeathLink is an opt-in feature for Multiworlds where individual death events are propogated to all games with DeathLink enabled. @@ -66,7 +76,7 @@ class AlwaysPreserveCookingProgress(OC2OnToggle): display_name = "Preserve Cooking/Mixing Progress" -class DisplayLeaderboardScores(OC2OnToggle): +class DisplayLeaderboardScores(OC2Toggle): """Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players.""" display_name = "Display Leaderboard Scores" @@ -153,6 +163,7 @@ class StarThresholdScale(Range): overcooked_options = { # generator options "location_balancing": LocationBalancing, + "ramp_tricks": RampTricks, # deathlink "deathlink": DeathLink, diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index 007be13c..e0d23eb6 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -372,3 +372,62 @@ level_id_to_shortname = { (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , } + +class OverworldRegion(IntEnum): + main = 0 + yellow_island = 1 + sky_shelf = 2 + stonehenge_mountain = 3 + tip_of_the_map = 4 + pink_island = 5 + mars_shelf = 6 + dark_green_mountain = 7 + kevin_eight_island = 8 + out_of_bounds = 9 + +overworld_region_by_level = { + "1-1": OverworldRegion.main, + "1-2": OverworldRegion.main, + "1-3": OverworldRegion.main, + "1-4": OverworldRegion.main, + "1-5": OverworldRegion.yellow_island, + "1-6": OverworldRegion.yellow_island, + "2-1": OverworldRegion.main, + "2-2": OverworldRegion.sky_shelf, + "2-3": OverworldRegion.sky_shelf, + "2-4": OverworldRegion.main, + "2-5": OverworldRegion.main, + "2-6": OverworldRegion.main, + "3-1": OverworldRegion.main, + "3-2": OverworldRegion.main, + "3-3": OverworldRegion.main, + "3-4": OverworldRegion.main, + "3-5": OverworldRegion.main, + "3-6": OverworldRegion.main, + "4-1": OverworldRegion.main, + "4-2": OverworldRegion.main, + "4-3": OverworldRegion.main, + "4-4": OverworldRegion.main, + "4-5": OverworldRegion.main, + "4-6": OverworldRegion.main, + "5-1": OverworldRegion.main, + "5-2": OverworldRegion.sky_shelf, + "5-3": OverworldRegion.main, + "5-4": OverworldRegion.tip_of_the_map, + "5-5": OverworldRegion.tip_of_the_map, + "5-6": OverworldRegion.tip_of_the_map, + "6-1": OverworldRegion.pink_island, + "6-2": OverworldRegion.tip_of_the_map, + "6-3": OverworldRegion.tip_of_the_map, + "6-4": OverworldRegion.sky_shelf, + "6-5": OverworldRegion.mars_shelf, + "6-6": OverworldRegion.mars_shelf, + "Kevin-1": OverworldRegion.dark_green_mountain, + "Kevin-2": OverworldRegion.main, + "Kevin-3": OverworldRegion.main, + "Kevin-4": OverworldRegion.main, + "Kevin-5": OverworldRegion.main, + "Kevin-6": OverworldRegion.main, + "Kevin-7": OverworldRegion.tip_of_the_map, + "Kevin-8": OverworldRegion.kevin_eight_island, +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 63d87648..f481b3e5 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -322,7 +322,7 @@ class Overcooked2World(World): level_access_rule: Callable[[CollectionState], bool] = \ lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ - has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player) + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player) self.connect_regions("Overworld", level.level_name, level_access_rule) # Level --> Overworld diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index d724f02f..8738e3ed 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -82,3 +82,13 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. + +## Overworld Sequence Breaking + +In the world's settings, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes: + +- Dashing across gaps + +- "Wiggling" up ledges + +- Going out of bounds [See Video](https://youtu.be/VdOGhi6XPu4) diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py index a6b5a4dc..4cb12d9d 100644 --- a/worlds/overcooked2/test/TestOvercooked2.py +++ b/worlds/overcooked2/test/TestOvercooked2.py @@ -1,10 +1,12 @@ import unittest - from random import Random +from worlds.AutoWorld import AutoWorldRegister +from test.general import setup_solo_multiworld + from worlds.overcooked2.Items import * -from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC -from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC +from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory from worlds.overcooked2.Locations import oc2_location_name_to_id @@ -170,3 +172,43 @@ class Overcooked2Test(unittest.TestCase): count += 1 self.assertEqual(count, len(level_id_range), f"Number of levels in {dlc.name} has discrepancy between level_id range and directory") + + def testOverworldRegion(self): + # OverworldRegion + # overworld_region_by_level + # overworld_region_logic + + # Test for duplicates + regions_list = [x for x in OverworldRegion] + regions_set = set(regions_list) + self.assertEqual(len(regions_list), len(regions_set), f"Duplicate values in OverworldRegion") + + # Test all levels represented + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + for shortname in shortnames: + if " " in shortname: + shortname = shortname.split(" ")[1] + shortname = shortname.replace("K-", "Kevin-") + self.assertIn(shortname, overworld_region_by_level) + + for region in overworld_region_by_level.values(): + # Test all regions valid + self.assertIn(region, regions_list) + + # Test Region Coverage + self.assertIn(region, overworld_region_logic) + + # Test all regions valid + for region in overworld_region_logic: + self.assertIn(region, regions_set) + + self.assertIn("Overcooked! 2", AutoWorldRegister.world_types.keys()) + world_type = AutoWorldRegister.world_types["Overcooked! 2"] + world = setup_solo_multiworld(world_type) + state = world.get_all_state(False) + + # Test region logic + for logic in overworld_region_logic.values(): + for allow_tricks in [False, True]: + result = logic(state, 1, allow_tricks, list()) + self.assertIn(result, [False, True])