diff --git a/README.md b/README.md index 9615b0fb..65f1792d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Currently, the following games are supported: * Dark Souls 3 * Super Mario World * Pokémon Red and Blue +* Overcooked! 2 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/test/overcooked2/TestOvercooked2.py b/test/overcooked2/TestOvercooked2.py new file mode 100644 index 00000000..ec9efd1f --- /dev/null +++ b/test/overcooked2/TestOvercooked2.py @@ -0,0 +1,142 @@ +import unittest +import json + +from random import Random + +from worlds.overcooked2.Items import * +from worlds.overcooked2.Overcooked2Levels import Overcooked2Level, level_id_to_shortname +from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Locations import oc2_location_name_to_id + + +class Overcooked2Test(unittest.TestCase): + def testItems(self): + self.assertEqual(len(item_name_to_id), len(item_id_to_name)) + self.assertEqual(len(item_name_to_id), len(item_table)) + + previous_item = None + for item_name in item_table.keys(): + item: Item = item_table[item_name] + self.assertGreaterEqual(item.code, oc2_base_id, "Overcooked Item ID out of range") + self.assertLessEqual(item.code, item_table["Calmer Unbread"].code, "Overcooked Item ID out of range") + + if previous_item is not None: + self.assertEqual(item.code, previous_item + 1, + f"Overcooked Item ID noncontinguous: {item.code-oc2_base_id}") + previous_item = item.code + + self.assertEqual(item_table["Ok Emote"].code - item_table["Cooking Emote"].code, + 5, "Overcooked Emotes noncontigious") + + for item_name in item_frequencies: + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in item_frequencies") + + for item_name in item_name_to_config_name.keys(): + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in config mapping") + + for config_name in item_name_to_config_name.values(): + self.assertIn(config_name, vanilla_values.keys(), "Unexpected Overcooked Item in default config mapping") + + for config_name in vanilla_values.keys(): + self.assertIn(config_name, item_name_to_config_name.values(), + "Unexpected Overcooked Item in default config mapping") + + events = [ + ("Kevin-2", {"action": "UNLOCK_LEVEL", "payload": "38"}), + ("Curse Emote", {"action": "UNLOCK_EMOTE", "payload": "1"}), + ("Larger Tip Jar", {"action": "INC_TIP_COMBO", "payload": ""}), + ("Order Lookahead", {"action": "INC_ORDERS_ON_SCREEN", "payload": ""}), + ("Control Stick Batteries", {"action": "SET_VALUE", "payload": "DisableControlStick=False"}), + ] + for (item_name, expected_event) in events: + expected_event["message"] = f"{item_name} Acquired!" + event = item_to_unlock_event(item_name) + self.assertEqual(event, expected_event) + + self.assertFalse(is_progression("Preparing Emote")) + + for item_name in item_table: + item_to_unlock_event(item_name) + + def testOvercooked2Levels(self): + level_count = 0 + for _ in Overcooked2Level(): + level_count += 1 + self.assertEqual(level_count, 44) + + def testOvercooked2ShuffleFactory(self): + previous_runs = set() + for seed in range(0, 5): + levels = level_shuffle_factory(Random(seed), True, False) + self.assertEqual(len(levels), 44) + previous_level_id = None + for level_id in levels.keys(): + if previous_level_id is not None: + self.assertEqual(previous_level_id+1, level_id) + previous_level_id = level_id + + self.assertNotIn(levels[15], previous_runs) + previous_runs.add(levels[15]) + + levels = level_shuffle_factory(Random(123), False, True) + self.assertEqual(len(levels), 44) + + def testLevelNameRepresentation(self): + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + + for shortname in shortnames: + self.assertIn(shortname, level_logic.keys()) + + self.assertEqual(len(level_logic), len(level_id_to_shortname)) + + for level_name in level_logic.keys(): + if level_name != "*": + self.assertIn(level_name, level_id_to_shortname.values()) + + for level_name in level_id_to_shortname.values(): + if level_name != "Tutorial": + self.assertIn(level_name, level_logic.keys()) + + region_names = [level.level_name for level in Overcooked2Level()] + for location_name in oc2_location_name_to_id.keys(): + level_name = location_name.split(" ")[0] + self.assertIn(level_name, region_names) + + def testLogic(self): + for level_name in level_logic.keys(): + logic = level_logic[level_name] + self.assertEqual(len(logic), 3, "Levels must provide logic for 1, 2, and 3 stars") + + for l in logic: + self.assertEqual(len(l), 2) + (exclusive, additive) = l + + for req in exclusive: + self.assertEqual(type(req), str) + self.assertIn(req, item_table.keys()) + + if len(additive) != 0: + self.assertGreater(len(additive), 1) + total_weight = 0.0 + for req in additive: + self.assertEqual(len(req), 2) + (item_name, weight) = req + self.assertEqual(type(item_name), str) + self.assertEqual(type(weight), float) + total_weight += weight + self.assertIn(item_name, item_table.keys()) + + self.assertGreaterEqual(total_weight, 0.99, "Additive requirements must add to 1.0 or greater to have any effect") + + def testItemLocationMapping(self): + number_of_items = 0 + for item_name in item_frequencies: + freq = item_frequencies[item_name] + self.assertGreaterEqual(freq, 0) + number_of_items += freq + + for item_name in item_table: + if item_name not in item_frequencies.keys(): + number_of_items += 1 + + self.assertLessEqual(number_of_items, len(oc2_location_name_to_id), "Too many items (before fillers placed)") diff --git a/test/overcooked2/__init__.py b/test/overcooked2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worlds/overcooked2/Items.py b/worlds/overcooked2/Items.py new file mode 100644 index 00000000..8cbe0711 --- /dev/null +++ b/worlds/overcooked2/Items.py @@ -0,0 +1,152 @@ +from BaseClasses import Item +from typing import NamedTuple, Dict + + +class ItemData(NamedTuple): + code: int + + +class Overcooked2Item(Item): + game: str = "Overcooked! 2" + + +oc2_base_id = 213700 + +item_table: Dict[str, ItemData] = { + "Wood" : ItemData(oc2_base_id + 1 ), + "Coal Bucket" : ItemData(oc2_base_id + 2 ), + "Spare Plate" : ItemData(oc2_base_id + 3 ), + "Fire Extinguisher" : ItemData(oc2_base_id + 4 ), + "Bellows" : ItemData(oc2_base_id + 5 ), + "Clean Dishes" : ItemData(oc2_base_id + 6 ), + "Larger Tip Jar" : ItemData(oc2_base_id + 7 ), + "Progressive Dash" : ItemData(oc2_base_id + 8 ), + "Progressive Throw/Catch" : ItemData(oc2_base_id + 9 ), + "Coin Purse" : ItemData(oc2_base_id + 10), + "Control Stick Batteries" : ItemData(oc2_base_id + 11), + "Wok Wheels" : ItemData(oc2_base_id + 12), + "Dish Scrubber" : ItemData(oc2_base_id + 13), + "Burn Leniency" : ItemData(oc2_base_id + 14), + "Sharp Knife" : ItemData(oc2_base_id + 15), + "Order Lookahead" : ItemData(oc2_base_id + 16), + "Lightweight Backpack" : ItemData(oc2_base_id + 17), + "Faster Respawn Time" : ItemData(oc2_base_id + 18), + "Faster Condiment/Drink Switch" : ItemData(oc2_base_id + 19), + "Guest Patience" : ItemData(oc2_base_id + 20), + "Kevin-1" : ItemData(oc2_base_id + 21), + "Kevin-2" : ItemData(oc2_base_id + 22), + "Kevin-3" : ItemData(oc2_base_id + 23), + "Kevin-4" : ItemData(oc2_base_id + 24), + "Kevin-5" : ItemData(oc2_base_id + 25), + "Kevin-6" : ItemData(oc2_base_id + 26), + "Kevin-7" : ItemData(oc2_base_id + 27), + "Kevin-8" : ItemData(oc2_base_id + 28), + "Cooking Emote" : ItemData(oc2_base_id + 29), + "Curse Emote" : ItemData(oc2_base_id + 30), + "Serving Emote" : ItemData(oc2_base_id + 31), + "Preparing Emote" : ItemData(oc2_base_id + 32), + "Washing Up Emote" : ItemData(oc2_base_id + 33), + "Ok Emote" : ItemData(oc2_base_id + 34), + "Ramp Button" : ItemData(oc2_base_id + 35), + "Bonus Star" : ItemData(oc2_base_id + 36), + "Calmer Unbread" : ItemData(oc2_base_id + 37), +} + +item_frequencies = { + "Progressive Throw/Catch": 2, + "Larger Tip Jar": 2, + "Order Lookahead": 2, + "Progressive Dash": 2, + "Bonus Star": 0, # Filler Item + # default: 1 +} + +item_name_to_config_name = { + "Wood" : "DisableWood" , + "Coal Bucket" : "DisableCoal" , + "Spare Plate" : "DisableOnePlate" , + "Fire Extinguisher" : "DisableFireExtinguisher" , + "Bellows" : "DisableBellows" , + "Clean Dishes" : "PlatesStartDirty" , + "Control Stick Batteries" : "DisableControlStick" , + "Wok Wheels" : "DisableWokDrag" , + "Dish Scrubber" : "WashTimeMultiplier" , + "Burn Leniency" : "BurnSpeedMultiplier" , + "Sharp Knife" : "ChoppingTimeScale" , + "Lightweight Backpack" : "BackpackMovementScale" , + "Faster Respawn Time" : "RespawnTime" , + "Faster Condiment/Drink Switch": "CarnivalDispenserRefactoryTime", + "Guest Patience" : "CustomOrderLifetime" , + "Ramp Button" : "DisableRampButton" , + "Calmer Unbread" : "AggressiveHorde" , + "Coin Purse" : "DisableEarnHordeMoney" , +} + +vanilla_values = { + "DisableWood": False, + "DisableCoal": False, + "DisableOnePlate": False, + "DisableFireExtinguisher": False, + "DisableBellows": False, + "PlatesStartDirty": False, + "DisableControlStick": False, + "DisableWokDrag": False, + "DisableRampButton": False, + "WashTimeMultiplier": 1.0, + "BurnSpeedMultiplier": 1.0, + "ChoppingTimeScale": 1.0, + "BackpackMovementScale": 1.0, + "RespawnTime": 5.0, + "CarnivalDispenserRefactoryTime": 0.0, + "CustomOrderLifetime": 100.0, + "AggressiveHorde": False, + "DisableEarnHordeMoney": False, +} + +item_id_to_name: Dict[int, str] = { + data.code: item_name for item_name, data in item_table.items() if data.code +} + +item_name_to_id: Dict[str, int] = { + item_name: data.code for item_name, data in item_table.items() if data.code +} + + +def is_progression(item_name: str) -> bool: + return not item_name.endswith("Emote") + + +def item_to_unlock_event(item_name: str) -> Dict[str, str]: + message = f"{item_name} Acquired!" + action = "" + payload = "" + if item_name.startswith("Kevin"): + kevin_num = int(item_name.split("-")[-1]) + action = "UNLOCK_LEVEL" + payload = str(kevin_num + 36) + elif "Emote" in item_name: + action = "UNLOCK_EMOTE" + payload = str(item_table[item_name].code - item_table["Cooking Emote"].code) + elif item_name == "Larger Tip Jar": + action = "INC_TIP_COMBO" + elif item_name == "Order Lookahead": + action = "INC_ORDERS_ON_SCREEN" + elif item_name == "Bonus Star": + action = "INC_STAR_COUNT" + payload = "1" + elif item_name == "Progressive Dash": + action = "INC_DASH" + elif item_name == "Progressive Throw/Catch": + action = "INC_THROW" + else: + config_name = item_name_to_config_name[item_name] + vanilla_value = vanilla_values[config_name] + + action = "SET_VALUE" + payload = f"{config_name}={vanilla_value}" + + return { + "message": message, + "action": action, + "payload": payload, + } diff --git a/worlds/overcooked2/Locations.py b/worlds/overcooked2/Locations.py new file mode 100644 index 00000000..1b73b74e --- /dev/null +++ b/worlds/overcooked2/Locations.py @@ -0,0 +1,15 @@ +from BaseClasses import Location +from .Overcooked2Levels import Overcooked2Level + + +class Overcooked2Location(Location): + game: str = "Overcooked! 2" + + +oc2_location_name_to_id = dict() +oc2_location_id_to_name = dict() +for level in Overcooked2Level(): + if level.level_id == 36: + continue # level 6-6 does not have an item location + oc2_location_name_to_id[level.location_name_item] = level.level_id + oc2_location_id_to_name[level.level_id] = level.location_name_item diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py new file mode 100644 index 00000000..6fb1a50a --- /dev/null +++ b/worlds/overcooked2/Logic.py @@ -0,0 +1,3899 @@ +from BaseClasses import CollectionState +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +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: + if not state.has("Ramp Button", player): + return False # need the item to use ramps + + for req in ramp_logic[level_name]: + if not state.has(req + " Level Complete", player): + return False # This level needs another to be beaten first + + # Kevin Levels Need to have the corresponding items + if level_name.startswith("K"): + return state.has(level_name, player) + + # Must have enough stars to purchase level + star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player) + if star_count < required_star_count: + return False + + # If this isn't the first level in a world, it needs the previous level to be unlocked first + if previous_level_completed_event_name is not None: + if not state.has(previous_level_completed_event_name, player): + return False + + # If we made it this far we have all requirements + return True + + +def has_requirements_for_level_star( + state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: + assert 0 <= stars <= 3 + + # First ensure that previous stars are obtainable + if stars > 1: + if not has_requirements_for_level_star(state, level, stars-1, player): + return False + + # Second, ensure that global requirements are met + if not meets_requirements(state, "*", stars, player): + return False + + # Finally, return success only if this level's requirements are met + return meets_requirements(state, level.shortname, stars, player) + + +def meets_requirements(state: CollectionState, name: str, stars: int, player: int): + # Get requirements for level + (exclusive_reqs, additive_reqs) = level_logic[name][stars-1] + + # print(f"{name} ({stars}-Stars): {exclusive_reqs}|{additive_reqs}") + + # Check if we meet exclusive requirements + if len(exclusive_reqs) > 0 and not state.has_all(exclusive_reqs, player): + return False + + # Check if we meet additive requirements + if len(additive_reqs) == 0: + return True + + total: float = 0.0 + for (item_name, weight) in additive_reqs: + for _ in range(0, state.item_count(item_name, player)): + total += weight + if total >= 0.99: # be nice to rounding errors :) + return True + + return False + + +def is_item_progression(item_name, level_mapping, include_kevin): + if item_name.endswith("Emote"): + return False + + if "Kevin" in item_name or item_name in ["Ramp Button"]: + return True # always progression + + def item_in_logic(shortname, _item_name): + for star in range(0, 3): + (exclusive, additive) = level_logic[shortname][star] + + if _item_name in exclusive: + return True + + for req in additive: + if req[0] == _item_name: + if req[1] > 0.3: # this bit smells of a deal with the devil, but it seems to be for the better + return True + break + + return False + + if item_in_logic("*", item_name): + return True + + for level in Overcooked2Level(): + if not include_kevin and level.level_id > 36: + break + + if level_mapping is None: + unmapped_level = Overcooked2GenericLevel(level.level_id) + else: + unmapped_level = level_mapping[level.level_id] + + if item_in_logic(unmapped_level.shortname, item_name): + return True + + return False + + +def is_useful(item_name): + return item_name in [ + "Faster Respawn Time", + "Fire Extinguisher", + "Clean Dishes", + "Larger Tip Jar", + "Dish Scrubber", + "Burn Leniency", + "Sharp Knife", + "Order Lookahead", + "Guest Patience", + "Bonus Star", + ] + + +def level_shuffle_factory( + rng: Random, + shuffle_prep_levels: bool, + shuffle_horde_levels: bool, +) -> Dict[int, Overcooked2GenericLevel]: # return + # Create a list of all valid levels for selection + # (excludes tutorial, throne, kevin and sometimes horde levels) + pool = list() + for dlc in Overcooked2Dlc: + for level_id in range(dlc.start_level_id(), dlc.end_level_id()): + if level_id in dlc.excluded_levels(): + continue + + if not shuffle_horde_levels and level_id in dlc.horde_levels(): + continue + + if not shuffle_prep_levels and level_id in dlc.prep_levels(): + continue + + pool.append( + Overcooked2GenericLevel(level_id, dlc) + ) + + # Sort the pool to eliminate risk + pool.sort(key=lambda x: int(x.dlc)*1000 + x.level_id) + + result: Dict[int, Overcooked2GenericLevel] = dict() + story = Overcooked2Dlc.STORY + + while len(result) == 0 or not meets_minimum_sphere_one_requirements(result): + result.clear() + + # Shuffle the pool, using the provided RNG + rng.shuffle(pool) + + # Return the first 44 levels and assign those to each level + for level_id in range(story.start_level_id(), story.end_level_id()): + if level_id not in story.excluded_levels(): + result[level_id] = pool[level_id-1] + else: + result[level_id] = Overcooked2GenericLevel(level_id) # This is just 6-6 right now + + return result + + +def meets_minimum_sphere_one_requirements( + levels: Dict[int, Overcooked2GenericLevel], +) -> bool: + + # 1-1, 2-1, and 4-1 are garunteed to be accessible on + # the overworld without requiring a ramp or additional stars + sphere_one = [1, 7, 19] + + # 1-2, 2-2, 3-1 and 5-1 are almost always the next thing unlocked + sphere_twoish = [2, 8, 13, 25] + + # Peek the logic for sphere one and see how many are possible + # with no items + sphere_one_count = 0 + for level_id in sphere_one: + if (is_completable_no_items(levels[level_id])): + sphere_one_count += 1 + + sphere_twoish_count = 0 + for level_id in sphere_twoish: + if (is_completable_no_items(levels[level_id])): + sphere_twoish_count += 1 + + return sphere_one_count >= 2 and \ + sphere_twoish_count >= 2 and \ + sphere_one_count + sphere_twoish_count >= 6 + + +def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: + one_star_logic = level_logic[level.shortname][0] + (exclusive, additive) = one_star_logic + + # print(f"\n{level.shortname}: {exclusive} / {additive}") + + return len(exclusive) == 0 and len(additive) == 0 + + +# 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, a ramp is required to access and the button requires all levels in the +# list to be compelted before it can be pressed +# +ramp_logic = { + "1-5": [], + "2-2": [], + "3-1": [], + "5-2": [], + "6-1": [], + "6-2": ["5-1"], # 5-1 spawns blue button, blue button gets you to red button + "Kevin-1": [], + "Kevin-7": ["5-1"], # 5-1 spawns blue button, + # press blue button, + # climb blue ramp, + # jump the gap, + # climb wood ramps + "Kevin-8": ["5-1", "6-2"], # Same as above, but 6-2 spawns the ramp to K8 +} + +horde_logic = { # Additive + ("Coin Purse", 0.7), + ("Calmer Unbread", 0.35), + ("Progressive Dash", 0.2), + ("Progressive Throw/Catch", 0.15), + ("Sharp Knife", 0.15), + ("Dish Scrubber", 0.125), + ("Burn Leniency", 0.1), + ("Spare Plate", 0.075), + ("Clean Dishes", 0.025), +} + +# Level 1 - dict keyed by friendly level names +# Level 2 - tuple with 3 elements, one for each star requirement +# Level 3 - tuple with 2 elements, one for exclusive requirements and one for additive requirements +# Level 4 (exclusive) - set of item name strings of items which MUST be in the inventory to allow logical completion +# Level 4 (additive) - list of tuples containing item name and item weight where the sum of which are in the player's inventory +# must be 1.0+ to allow logical completion +# +# Each Star's logical requirements imply any previous requirements +# +level_logic = { + # "Tutorial": [], + "*": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.4), + ("Progressive Dash", 0.35), + ("Sharp Knife", 0.3), + ("Dish Scrubber", 0.25), + ("Larger Tip Jar", 0.2), + ("Spare Plate", 0.2), + ("Burn Leniency", 0.15), + ("Order Lookahead", 0.15), + ("Clean Dishes", 0.1), + ("Guest Patience", 0.1), + }, + ), + ( # 3-star + [ # Exclusive + "Progressive Dash", + "Spare Plate", + "Larger Tip Jar", + "Progressive Throw/Catch", + ], + { # Additive + }, + ) + ), + "Story 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-1": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 1.0), + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + [ # Additive + + ] + ) + ), + "Story 2-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 0.5), + ("Sharp Knife", 0.5), + ("Larger Tip Jar", 0.25), + ("Dish Scrubber", 0.25), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-5": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ) + ), + "Story 6-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-6": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + "Progressive Dash", + "Spare Plate", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-8": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf K-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-1": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-2": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-3": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-4": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-3": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Lightweight Backpack", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-4": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + } + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.5), + ("Progressive Dash", 0.5), + ("Coal Bucket", 0.5), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-1": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-3": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-5": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-6": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-7": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-8": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + "Faster Condiment/Drink Switch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + }, + { # Additive + + }, + ) + ), + "Moon 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), +} diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py new file mode 100644 index 00000000..78e0fd6e --- /dev/null +++ b/worlds/overcooked2/Options.py @@ -0,0 +1,110 @@ +from typing import TypedDict +from Options import DefaultOnToggle, Range, Choice + + +class OC2OnToggle(DefaultOnToggle): + @property + def result(self) -> bool: + return bool(self.value) + + +class AlwaysServeOldestOrder(OC2OnToggle): + """Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster.""" + display_name = "Always Serve Oldest Order" + + +class AlwaysPreserveCookingProgress(OC2OnToggle): + """Modifies the game to behave more like AYCE, where adding an item to an in-progress container doesn't reset the entire progress bar.""" + display_name = "Preserve Cooking/Mixing Progress" + + +class DisplayLeaderboardScores(OC2OnToggle): + """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" + + +class ShuffleLevelOrder(OC2OnToggle): + """Shuffles the order of kitchens on the overworld map. Also draws from DLC maps.""" + display_name = "Shuffle Level Order" + + +class IncludeHordeLevels(OC2OnToggle): + """Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" + display_name = "Include Horde Levels" + + +class KevinLevels(OC2OnToggle): + """Includes the 8 Kevin level locations on the map as unlockables. Turn off to make games shorter.""" + display_name = "Kevin Level Checks" + + +class FixBugs(OC2OnToggle): + """Fixes Bugs Present in the base game: + - Double Serving Exploit + - Sink Bug + - Control Stick Cancel/Throw Bug + - Can't Throw Near Empty Burner Bug""" + display_name = "Fix Bugs" + + +class ShorterLevelDuration(OC2OnToggle): + """Modifies level duration to be about 1/3rd shorter than in the original game, thus bringing the item discovery pace in line with other popular Archipelago games. + + Points required to earn stars are scaled accordingly. ("Boss Levels" which change scenery mid-game are not affected.)""" + display_name = "Shorter Level Duration" + + +class PrepLevels(Choice): + """Choose How "Prep Levels" are handled (levels where the timer does not start until the first order is served): + + - Original: Prep Levels may appear + + - Excluded: Prep Levels are excluded from the pool during level shuffling + + - All You Can Eat: Prep Levels may appear, but the timer automatically starts. The star score requirements are also adjusted to use the All You Can Eat World Record (if it exists)""" + auto_display_name = True + display_name = "Prep Level Behavior" + option_original = 0 + option_excluded = 1 + option_all_you_can_eat = 2 + default = 1 + + +class StarsToWin(Range): + """Number of stars required to unlock 6-6. + + Level purchase requirements between 1-1 and 6-6 will be spread between these two numbers. Using too high of a number may result in more frequent generation failures, especially when horde levels are enabled.""" + display_name = "Stars to Win" + range_start = 0 + range_end = 100 + default = 66 + + +class StarThresholdScale(Range): + """How difficult should the third star for each level be on a scale of 1-100%, where 100% is the current world record score and 45% is the average vanilla 4-star score.""" + display_name = "Star Difficulty %" + range_start = 1 + range_end = 100 + default = 45 + + +overcooked_options = { + # randomization options + "shuffle_level_order": ShuffleLevelOrder, + "include_horde_levels": IncludeHordeLevels, + "prep_levels": PrepLevels, + "kevin_levels": KevinLevels, + + # quality of life options + "fix_bugs": FixBugs, + "shorter_level_duration": ShorterLevelDuration, + "always_preserve_cooking_progress": AlwaysPreserveCookingProgress, + "always_serve_oldest_order": AlwaysServeOldestOrder, + "display_leaderboard_scores": DisplayLeaderboardScores, + + # difficulty settings + "stars_to_win": StarsToWin, + "star_threshold_scale": StarThresholdScale, +} + +OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()}) diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py new file mode 100644 index 00000000..aac9ea0c --- /dev/null +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -0,0 +1,349 @@ +from enum import Enum +from typing import List + + +class Overcooked2Dlc(Enum): + STORY = "Story" + SURF_N_TURF = "Surf 'n' Turf" + CAMPFIRE_COOK_OFF = "Campfire Cook Off" + NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde" + CARNIVAL_OF_CHAOS = "Carnival of Chaos" + SEASONAL = "Seasonal" + # CHRISTMAS = "Christmas" + # CHINESE_NEW_YEAR = "Chinese New Year" + # WINTER_WONDERLAND = "Winter Wonderland" + # MOON_HARVEST = "Moon Harvest" + # SPRING_FRESTIVAL = "Spring Festival" + # SUNS_OUT_BUNS_OUT = "Sun's Out Buns Out" + + def __int__(self) -> int: + if self == Overcooked2Dlc.STORY: + return 0 + if self == Overcooked2Dlc.SURF_N_TURF: + return 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return 2 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return 3 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return 4 + if self == Overcooked2Dlc.SEASONAL: + return 5 + assert False + + # inclusive + def start_level_id(self) -> int: + if self == Overcooked2Dlc.STORY: + return 1 + return 0 + + # exclusive + def end_level_id(self) -> int: + id = None + if self == Overcooked2Dlc.STORY: + id = 6*6 + 8 # world_count*level_count + kevin count + if self == Overcooked2Dlc.SURF_N_TURF: + id = 3*4 + 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + id = 3*4 + 3 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + id = 3*3 + 3 + 8 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + id = 3*4 + 3 + if self == Overcooked2Dlc.SEASONAL: + id = 31 + + return self.start_level_id() + id + + # Tutorial + Horde Levels + Endgame + def excluded_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [0, 36] + + return [] + + def horde_levels(self) -> List[int]: + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [12, 13, 14, 15, 16, 17, 18, 19] + if self == Overcooked2Dlc.SEASONAL: + return [13, 15] + + return [] + + def prep_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [1, 2, 5, 10, 12, 13, 28, 31] + if self == Overcooked2Dlc.SURF_N_TURF: + return [0, 4] + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return [0, 2, 4, 9] + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [0, 1, 4] + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return [0, 1, 3, 4, 5] + if self == Overcooked2Dlc.SEASONAL: + # moon 1-1 is a prep level for 1P only, but we can't make that assumption here + return [0, 1, 5, 6, 12, 14, 16, 17, 18, 22, 23, 24, 27, 29] + + return [] + + +class Overcooked2GameWorld(Enum): + ONE = 1 + TWO = 2 + THREE = 3 + FOUR = 4 + FIVE = 5 + SIX = 6 + KEVIN = 7 + + @property + def as_str(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return str(int(self.value)) + + @property + def sublevel_count(self) -> int: + if self == Overcooked2GameWorld.KEVIN: + return 8 + + return 6 + + @property + def base_id(self) -> int: + if self == Overcooked2GameWorld.ONE: + return 1 + + prev = Overcooked2GameWorld(self.value - 1) + return prev.base_id + prev.sublevel_count + + @property + def name(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return "World " + self.as_str + + +class Overcooked2GenericLevel(): + dlc: Overcooked2Dlc + level_id: int + + def __init__(self, level_id: int, dlc: Overcooked2Dlc = Overcooked2Dlc("Story")): + self.dlc = dlc + self.level_id = level_id + + def __str__(self) -> str: + return f"{self.dlc.value}|{self.level_id}" + + def __repr__(self) -> str: + return f"{self}" + + @property + def shortname(self) -> str: + return level_id_to_shortname[(self.dlc, self.level_id)] + + @property + def is_horde(self) -> bool: + return self.level_id in self.dlc.horde_levels() + + +class Overcooked2Level: + """ + Abstraction for a playable levels in Overcooked 2. By default constructor + it can be used as an iterator for all locations in the Story map. + """ + world: Overcooked2GameWorld + sublevel: int + + def __init__(self): + self.world = Overcooked2GameWorld.ONE + self.sublevel = 0 + + def __iter__(self): + return self + + def __next__(self): + self.sublevel += 1 + if self.sublevel > self.world.sublevel_count: + if self.world == Overcooked2GameWorld.KEVIN: + raise StopIteration + self.world = Overcooked2GameWorld(self.world.value + 1) + self.sublevel = 1 + + return self + + @property + def level_id(self) -> int: + return self.world.base_id + (self.sublevel - 1) + + @property + def level_name(self) -> str: + return self.world.as_str + "-" + str(self.sublevel) + + @property + def location_name_item(self) -> str: + return self.level_name + " Completed" + + @property + def location_name_level_complete(self) -> str: + return self.level_name + " Level Completed" + + @property + def event_name_level_complete(self) -> str: + return self.level_name + " Level Complete" + + def location_name_star_event(self, stars: int) -> str: + return "%s (%d-Star)" % (self.level_name, stars) + + @property + def as_generic_level(self) -> Overcooked2GenericLevel: + return Overcooked2GenericLevel(self.level_id) + + +# Note that there are valid levels beyond what is listed here, but they are all +# Onion King Dialogs +level_id_to_shortname = { + (Overcooked2Dlc.STORY , 0 ): "Tutorial" , + (Overcooked2Dlc.STORY , 1 ): "Story 1-1" , + (Overcooked2Dlc.STORY , 2 ): "Story 1-2" , + (Overcooked2Dlc.STORY , 3 ): "Story 1-3" , + (Overcooked2Dlc.STORY , 4 ): "Story 1-4" , + (Overcooked2Dlc.STORY , 5 ): "Story 1-5" , + (Overcooked2Dlc.STORY , 6 ): "Story 1-6" , + (Overcooked2Dlc.STORY , 7 ): "Story 2-1" , + (Overcooked2Dlc.STORY , 8 ): "Story 2-2" , + (Overcooked2Dlc.STORY , 9 ): "Story 2-3" , + (Overcooked2Dlc.STORY , 10 ): "Story 2-4" , + (Overcooked2Dlc.STORY , 11 ): "Story 2-5" , + (Overcooked2Dlc.STORY , 12 ): "Story 2-6" , + (Overcooked2Dlc.STORY , 13 ): "Story 3-1" , + (Overcooked2Dlc.STORY , 14 ): "Story 3-2" , + (Overcooked2Dlc.STORY , 15 ): "Story 3-3" , + (Overcooked2Dlc.STORY , 16 ): "Story 3-4" , + (Overcooked2Dlc.STORY , 17 ): "Story 3-5" , + (Overcooked2Dlc.STORY , 18 ): "Story 3-6" , + (Overcooked2Dlc.STORY , 19 ): "Story 4-1" , + (Overcooked2Dlc.STORY , 20 ): "Story 4-2" , + (Overcooked2Dlc.STORY , 21 ): "Story 4-3" , + (Overcooked2Dlc.STORY , 22 ): "Story 4-4" , + (Overcooked2Dlc.STORY , 23 ): "Story 4-5" , + (Overcooked2Dlc.STORY , 24 ): "Story 4-6" , + (Overcooked2Dlc.STORY , 25 ): "Story 5-1" , + (Overcooked2Dlc.STORY , 26 ): "Story 5-2" , + (Overcooked2Dlc.STORY , 27 ): "Story 5-3" , + (Overcooked2Dlc.STORY , 28 ): "Story 5-4" , + (Overcooked2Dlc.STORY , 29 ): "Story 5-5" , + (Overcooked2Dlc.STORY , 30 ): "Story 5-6" , + (Overcooked2Dlc.STORY , 31 ): "Story 6-1" , + (Overcooked2Dlc.STORY , 32 ): "Story 6-2" , + (Overcooked2Dlc.STORY , 33 ): "Story 6-3" , + (Overcooked2Dlc.STORY , 34 ): "Story 6-4" , + (Overcooked2Dlc.STORY , 35 ): "Story 6-5" , + (Overcooked2Dlc.STORY , 36 ): "Story 6-6" , + (Overcooked2Dlc.STORY , 37 ): "Story K-1" , + (Overcooked2Dlc.STORY , 38 ): "Story K-2" , + (Overcooked2Dlc.STORY , 39 ): "Story K-3" , + (Overcooked2Dlc.STORY , 40 ): "Story K-4" , + (Overcooked2Dlc.STORY , 41 ): "Story K-5" , + (Overcooked2Dlc.STORY , 42 ): "Story K-6" , + (Overcooked2Dlc.STORY , 43 ): "Story K-7" , + (Overcooked2Dlc.STORY , 44 ): "Story K-8" , + (Overcooked2Dlc.SURF_N_TURF , 0 ): "Surf 1-1" , + (Overcooked2Dlc.SURF_N_TURF , 1 ): "Surf 1-2" , + (Overcooked2Dlc.SURF_N_TURF , 2 ): "Surf 1-3" , + (Overcooked2Dlc.SURF_N_TURF , 3 ): "Surf 1-4" , + (Overcooked2Dlc.SURF_N_TURF , 4 ): "Surf 2-1" , + (Overcooked2Dlc.SURF_N_TURF , 5 ): "Surf 2-2" , + (Overcooked2Dlc.SURF_N_TURF , 6 ): "Surf 2-3" , + (Overcooked2Dlc.SURF_N_TURF , 7 ): "Surf 2-4" , + (Overcooked2Dlc.SURF_N_TURF , 8 ): "Surf 3-1" , + (Overcooked2Dlc.SURF_N_TURF , 9 ): "Surf 3-2" , + (Overcooked2Dlc.SURF_N_TURF , 10 ): "Surf 3-3" , + (Overcooked2Dlc.SURF_N_TURF , 11 ): "Surf 3-4" , + (Overcooked2Dlc.SURF_N_TURF , 12 ): "Surf K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 0 ): "Campfire 1-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 1 ): "Campfire 1-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 2 ): "Campfire 1-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 3 ): "Campfire 1-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 4 ): "Campfire 2-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 5 ): "Campfire 2-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 6 ): "Campfire 2-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 7 ): "Campfire 2-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 8 ): "Campfire 3-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 9 ): "Campfire 3-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 10 ): "Campfire 3-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 11 ): "Campfire 3-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 12 ): "Campfire K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 13 ): "Campfire K-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 14 ): "Campfire K-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 0 ): "Carnival 1-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 1 ): "Carnival 1-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 2 ): "Carnival 1-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 3 ): "Carnival 1-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 4 ): "Carnival 2-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 5 ): "Carnival 2-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 6 ): "Carnival 2-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 7 ): "Carnival 2-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 8 ): "Carnival 3-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 9 ): "Carnival 3-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 10 ): "Carnival 3-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 11 ): "Carnival 3-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 12 ): "Carnival K-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 13 ): "Carnival K-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 14 ): "Carnival K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 0 ): "Horde 1-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 1 ): "Horde 1-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 2 ): "Horde 1-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 3 ): "Horde 2-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 4 ): "Horde 2-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 5 ): "Horde 2-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 6 ): "Horde 3-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 7 ): "Horde 3-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 8 ): "Horde 3-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 9 ): "Horde K-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 10 ): "Horde K-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 11 ): "Horde K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 12 ): "Horde H-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 13 ): "Horde H-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 14 ): "Horde H-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 15 ): "Horde H-4" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 16 ): "Horde H-5" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 17 ): "Horde H-6" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 18 ): "Horde H-7" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 19 ): "Horde H-8" , + (Overcooked2Dlc.SEASONAL , 0 ): "Christmas 1-1" , + (Overcooked2Dlc.SEASONAL , 1 ): "Christmas 1-2" , + (Overcooked2Dlc.SEASONAL , 2 ): "Christmas 1-3" , + (Overcooked2Dlc.SEASONAL , 3 ): "Christmas 1-4" , + (Overcooked2Dlc.SEASONAL , 4 ): "Christmas 1-5" , + (Overcooked2Dlc.SEASONAL , 5 ): "Chinese 1-1" , + (Overcooked2Dlc.SEASONAL , 6 ): "Chinese 1-2" , + (Overcooked2Dlc.SEASONAL , 7 ): "Chinese 1-3" , + (Overcooked2Dlc.SEASONAL , 8 ): "Chinese 1-4" , + (Overcooked2Dlc.SEASONAL , 9 ): "Chinese 1-5" , + (Overcooked2Dlc.SEASONAL , 10 ): "Chinese 1-6" , + (Overcooked2Dlc.SEASONAL , 11 ): "Chinese 1-7" , + (Overcooked2Dlc.SEASONAL , 12 ): "Winter 1-1" , + (Overcooked2Dlc.SEASONAL , 13 ): "Winter H-2" , + (Overcooked2Dlc.SEASONAL , 14 ): "Winter 1-3" , + (Overcooked2Dlc.SEASONAL , 15 ): "Winter H-4" , + (Overcooked2Dlc.SEASONAL , 16 ): "Winter 1-5" , + (Overcooked2Dlc.SEASONAL , 17 ): "Spring 1-1" , + (Overcooked2Dlc.SEASONAL , 18 ): "Spring 1-2" , + (Overcooked2Dlc.SEASONAL , 19 ): "Spring 1-3" , + (Overcooked2Dlc.SEASONAL , 20 ): "Spring 1-4" , + (Overcooked2Dlc.SEASONAL , 21 ): "Spring 1-5" , + (Overcooked2Dlc.SEASONAL , 22 ): "SOBO 1-1" , + (Overcooked2Dlc.SEASONAL , 23 ): "SOBO 1-2" , + (Overcooked2Dlc.SEASONAL , 24 ): "SOBO 1-3" , + (Overcooked2Dlc.SEASONAL , 25 ): "SOBO 1-4" , + (Overcooked2Dlc.SEASONAL , 26 ): "SOBO 1-5" , + (Overcooked2Dlc.SEASONAL , 27 ): "Moon 1-1" , + (Overcooked2Dlc.SEASONAL , 28 ): "Moon 1-2" , + (Overcooked2Dlc.SEASONAL , 29 ): "Moon 1-3" , + (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , + (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py new file mode 100644 index 00000000..c47b755f --- /dev/null +++ b/worlds/overcooked2/__init__.py @@ -0,0 +1,510 @@ +from enum import Enum +from typing import Callable, Dict, Any, List, Optional + +from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, RegionType, Tutorial +from worlds.AutoWorld import World, WebWorld + +from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel +from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name +from .Options import overcooked_options, OC2Options, OC2OnToggle +from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies +from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful + + +class Overcooked2Web(WebWorld): + theme = "partyTime" + + bug_report_page = "https://github.com/toasterparty/oc2-modding/issues" + setup_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Overcooked! 2 randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["toasterparty"] + ) + + tutorials = [setup_en] + + +class PrepLevelMode(Enum): + original = 0 + excluded = 1 + ayce = 2 + + +class Overcooked2World(World): + """ + Overcooked! 2 is a franticly paced arcade cooking game where + players race against the clock to complete orders for points. Bring + peace to the Onion Kingdom once again by recovering lost items and abilities, + earning stars to unlock levels, and defeating the unbread horde. Levels are + randomized to increase gameplay variety. Play with up to 4 friends. + """ + + # Autoworld API + + game = "Overcooked! 2" + web = Overcooked2Web() + required_client_version = (0, 3, 4) + option_definitions = overcooked_options + topology_present: bool = False + remote_items: bool = True + remote_start_inventory: bool = False + data_version = 2 + + item_name_to_id = item_name_to_id + item_id_to_name = item_id_to_name + + location_id_to_name = oc2_location_id_to_name + location_name_to_id = oc2_location_name_to_id + + options: Dict[str, Any] + itempool: List[Overcooked2Item] + + + # Helper Functions + + def is_level_horde(self, level_id: int) -> bool: + return self.options["IncludeHordeLevels"] and \ + (self.level_mapping is not None) and \ + level_id in self.level_mapping.keys() and \ + self.level_mapping[level_id].is_horde + + def create_item(self, item: str, classification: ItemClassification = ItemClassification.progression) -> Overcooked2Item: + return Overcooked2Item(item, classification, self.item_name_to_id[item], self.player) + + def create_event(self, event: str, classification: ItemClassification) -> Overcooked2Item: + return Overcooked2Item(event, classification, None, self.player) + + def place_event(self, location_name: str, item_name: str, + classification: ItemClassification = ItemClassification.progression_skip_balancing): + location: Location = self.world.get_location(location_name, self.player) + location.place_locked_item(self.create_event(item_name, classification)) + + def add_region(self, region_name: str): + region = Region( + region_name, + RegionType.Generic, + region_name, + self.player, + self.world, + ) + self.world.regions.append(region) + + def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): + sourceRegion = self.world.get_region(source, self.player) + targetRegion = self.world.get_region(target, self.player) + + connection = Entrance(self.player, '', sourceRegion) + if rule: + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) + + def add_level_location( + self, + region_name: str, + location_name: str, + level_id: int, + stars: int, + is_event: bool = False, + ) -> None: + + if is_event: + location_id = None + else: + location_id = level_id + + region = self.world.get_region(region_name, self.player) + location = Overcooked2Location( + self.player, + location_name, + location_id, + region, + ) + + location.event = is_event + + # if level_id is none, then it's the 6-6 edge case + if level_id is None: + level_id = 36 + if self.level_mapping is not None and level_id in self.level_mapping: + level = self.level_mapping[level_id] + else: + level = Overcooked2GenericLevel(level_id) + + completion_condition: Callable[[CollectionState], bool] = \ + lambda state, level=level, stars=stars: \ + has_requirements_for_level_star(state, level, stars, self.player) + location.access_rule = completion_condition + + region.locations.append( + location + ) + + def get_options(self) -> Dict[str, Any]: + return OC2Options({option.__name__: getattr(self.world, name)[self.player].result + if issubclass(option, OC2OnToggle) else getattr(self.world, name)[self.player].value + for name, option in overcooked_options.items()}) + + # Helper Data + + level_unlock_counts: Dict[int, int] # level_id, stars to purchase + level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level + + # Autoworld Hooks + + def generate_early(self): + self.options = self.get_options() + + # 0.0 to 1.0 where 1.0 is World Record + self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0 + + # Generate level unlock requirements such that the levels get harder to unlock + # the further the game has progressed, and levels progress radially rather than linearly + self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"]) + + # Assign new kitchens to each spot on the overworld using pure random chance and nothing else + if self.options["ShuffleLevelOrder"]: + self.level_mapping = \ + level_shuffle_factory( + self.world.random, + self.options["PrepLevels"] != PrepLevelMode.excluded.value, + self.options["IncludeHordeLevels"], + ) + else: + self.level_mapping = None + + def create_regions(self) -> None: + # Menu -> Overworld + self.add_region("Menu") + self.add_region("Overworld") + self.connect_regions("Menu", "Overworld") + + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + # Create Region (e.g. "1-1") + self.add_region(level.level_name) + + # Add Location to house progression item (1-star) + if level.level_id == 36: + # 6-6 doesn't have progression, but it does have victory condition which is placed later + self.add_level_location( + level.level_name, + level.location_name_item, + None, + 1, + ) + else: + # Location to house progression item + self.add_level_location( + level.level_name, + level.location_name_item, + level.level_id, + 1, + ) + + # Location to house level completed event + self.add_level_location( + level.level_name, + level.location_name_level_complete, + level.level_id, + 1, + is_event=True, + ) + + # Add Locations to house star aquisition events, except for horde levels + if not self.is_level_horde(level.level_id): + for n in [1, 2, 3]: + self.add_level_location( + level.level_name, + level.location_name_star_event(n), + level.level_id, + n, + is_event=True, + ) + + # Overworld -> Level + required_star_count: int = self.level_unlock_counts[level.level_id] + if level.level_id % 6 != 1 and level.level_id <= 36: + previous_level_completed_event_name: str = Overcooked2GenericLevel( + level.level_id - 1).shortname.split(" ")[1] + " Level Complete" + else: + previous_level_completed_event_name = None + + 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) + self.connect_regions("Overworld", level.level_name, level_access_rule) + + # Level --> Overworld + self.connect_regions(level.level_name, "Overworld") + + completion_condition: Callable[[CollectionState], bool] = lambda state: \ + state.has("Victory", self.player) + self.world.completion_condition[self.player] = completion_condition + + def create_items(self): + self.itempool = [] + + # Make Items + # useful = list() + # filler = list() + # progression = list() + for item_name in item_table: + if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]: + # skip items which are irrelevant to the seed + continue + + if not self.options["KevinLevels"] and item_name.startswith("Kevin"): + continue + + if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]): + # print(f"{item_name} is progression") + # progression.append(item_name) + classification = ItemClassification.progression + else: + # print(f"{item_name} is filler") + if (is_useful(item_name)): + # useful.append(item_name) + classification = ItemClassification.useful + else: + # filler.append(item_name) + classification = ItemClassification.filler + + if item_name in item_frequencies: + freq = item_frequencies[item_name] + + while freq > 0: + self.itempool.append(self.create_item(item_name, classification)) + classification = ItemClassification.useful # only the first progressive item can be progression + freq -= 1 + else: + self.itempool.append(self.create_item(item_name, classification)) + + # print(f"progression: {progression}") + # print(f"useful: {useful}") + # print(f"filler: {filler}") + + # Fill any free space with filler + pool_count = len(oc2_location_name_to_id) + if not self.options["KevinLevels"]: + pool_count -= 8 + + while len(self.itempool) < pool_count: + self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful)) + + self.world.itempool += self.itempool + + def set_rules(self): + pass + + def generate_basic(self) -> None: + # Add Events (Star Acquisition) + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + if level.level_id != 36: + self.place_event(level.location_name_level_complete, level.event_name_level_complete) + + if self.is_level_horde(level.level_id): + continue # horde levels don't have star rewards + + for n in [1, 2, 3]: + self.place_event(level.location_name_star_event(n), "Star") + + # Add Victory Condition + self.place_event("6-6 Completed", "Victory") + + # Items get distributed to locations + + def fill_json_data(self) -> Dict[str, Any]: + mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}" + + # Serialize Level Order + story_level_order = dict() + + if self.options["ShuffleLevelOrder"]: + for level_id in self.level_mapping: + level: Overcooked2GenericLevel = self.level_mapping[level_id] + story_level_order[str(level_id)] = { + "DLC": level.dlc.value, + "LevelID": level.level_id, + } + + custom_level_order = dict() + custom_level_order["Story"] = story_level_order + + # Serialize Unlock Requirements + level_purchase_requirements = dict() + for level_id in self.level_unlock_counts: + level_purchase_requirements[str(level_id)] = self.level_unlock_counts[level_id] + + # Override Vanilla Unlock Chain Behavior + # (all worlds accessible from the start and progressible in any order) + level_unlock_requirements = dict() + level_force_reveal = [ + 1, # 1-1 + 7, # 2-1 + 13, # 3-1 + 19, # 4-1 + 25, # 5-1 + 31, # 6-1 + ] + for level_id in range(1, 37): + if (level_id not in level_force_reveal): + level_unlock_requirements[str(level_id)] = level_id - 1 + + # Set Kevin Unlock Requirements + if self.options["KevinLevels"]: + def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: + location = self.world.find_item(f"Kevin-{level_id-36}", self.player) + if location.player != self.player: + return None # This kevin level will be unlocked by the server at runtime + level_id = oc2_location_name_to_id[location.name] + return level_id + + for level_id in range(37, 45): + keyholder_level_id = kevin_level_to_keyholder_level_id(level_id) + if keyholder_level_id is not None: + level_unlock_requirements[str(level_id)] = keyholder_level_id + + # Place Items at Level Completion Screens (local only) + on_level_completed: Dict[str, list[Dict[str, str]]] = dict() + regions = self.world.get_regions(self.player) + for region in regions: + for location in region.locations: + if location.item is None: + continue + if location.item.code is None: + continue # it's an event + if location.item.player != self.player: + continue # not for us + level_id = str(oc2_location_name_to_id[location.name]) + on_level_completed[level_id] = [item_to_unlock_event(location.item.name)] + + # Put it all together + star_threshold_scale = self.options["StarThresholdScale"] / 100 + + base_data = { + # Changes Inherent to rando + "DisableAllMods": False, + "UnlockAllChefs": True, + "UnlockAllDLC": True, + "DisplayFPS": True, + "SkipTutorial": True, + "SkipAllOnionKing": True, + "SkipTutorialPopups": True, + "RevealAllLevels": False, + "PurchaseAllLevels": False, + "CheatsEnabled": False, + "ImpossibleTutorial": True, + "ForbidDLC": True, + "ForceSingleSaveSlot": True, + "DisableNGP": True, + "LevelForceReveal": level_force_reveal, + "SaveFolderName": mod_name, + "CustomOrderTimeoutPenalty": 10, + "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], + + # Game Modifications + "LevelPurchaseRequirements": level_purchase_requirements, + "Custom66TimerScale": max(0.4, (1.0 - star_threshold_scale)), + + "CustomLevelOrder": custom_level_order, + + # Items (Starting Inventory) + "CustomOrderLifetime": 70.0, # 100 is original + "DisableWood": True, + "DisableCoal": True, + "DisableOnePlate": True, + "DisableFireExtinguisher": True, + "DisableBellows": True, + "PlatesStartDirty": True, + "MaxTipCombo": 2, + "DisableDash": True, + "WeakDash": True, + "DisableThrow": True, + "DisableCatch": True, + "DisableControlStick": True, + "DisableWokDrag": True, + "DisableRampButton": True, + "WashTimeMultiplier": 1.4, + "BurnSpeedMultiplier": 1.43, + "MaxOrdersOnScreenOffset": -2, + "ChoppingTimeScale": 1.4, + "BackpackMovementScale": 0.75, + "RespawnTime": 10.0, + "CarnivalDispenserRefactoryTime": 4.0, + "LevelUnlockRequirements": level_unlock_requirements, + "LockedEmotes": [0, 1, 2, 3, 4, 5], + "StarOffset": 0, + "AggressiveHorde": True, + "DisableEarnHordeMoney": True, + + # Item Unlocking + "OnLevelCompleted": on_level_completed, + } + + # Set remaining data in the options dict + bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] + for bug in bugs: + self.options[bug] = self.options["FixBugs"] + self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] + self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value + self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 + self.options["LeaderboardScoreScale"] = { + "FourStars": 1.0, + "ThreeStars": star_threshold_scale, + "TwoStars": star_threshold_scale * 0.75, + "OneStar": star_threshold_scale * 0.35, + } + + base_data.update(self.options) + return base_data + + def fill_slot_data(self) -> Dict[str, Any]: + return self.fill_json_data() + + +def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]: + level_unlock_counts = dict() + level = 1 + sublevel = 1 + for n in range(1, 37): + progress: float = float(n)/36.0 + progress *= progress # x^2 curve + + star_count = int(progress*float(stars_to_win)) + min = (n-1)*3 + if (star_count > min): + star_count = min + + level_id = (level-1)*6 + sublevel + + # print("%d-%d (%d) = %d" % (level, sublevel, level_id, star_count)) + + level_unlock_counts[level_id] = star_count + + level += 1 + if level > 6: + level = 1 + sublevel += 1 + + # force sphere 1 to 0 stars to help keep our promises to the item fill algo + level_unlock_counts[1] = 0 # 1-1 + level_unlock_counts[7] = 0 # 2-1 + level_unlock_counts[19] = 0 # 4-1 + + # Force 5-1 into sphere 1 to help things out + level_unlock_counts[25] = 1 # 5-1 + + for n in range(37, 46): + level_unlock_counts[n] = 0 + + return level_unlock_counts diff --git a/worlds/overcooked2/docs/en_Overcooked! 2.md b/worlds/overcooked2/docs/en_Overcooked! 2.md new file mode 100644 index 00000000..d6de25f3 --- /dev/null +++ b/worlds/overcooked2/docs/en_Overcooked! 2.md @@ -0,0 +1,86 @@ +# Overcooked! 2 + +## Quick Links +- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## How Does Randomizer Work in the Kitchen? + +The *Overcooked! 2* Randomizer completely transforms the game into a metroidvania with items and item locations. Many of the Chefs' inherent abilities have been temporarily removed such that your scoring potential is limited at the start of the game. The more your inventory grows, the easier it will be to earn 2 and 3 Stars on each level. + +The game takes place entirely in the "Story" campaign on a fresh save file. The ultimate goal is to reach and complete level 6-6. In order to do this you must regain enough of your abilities to complete all levels in World 6 and obtain enough stars to purchase 6-6*. + +Randomizer can be played alone (one player switches between controlling two chefs) or up to 4 local/online friends. Player count can be changed at any time during the Archipelago game. + +**Note: 6-6 is excluded from "Shuffle Level Order", so it will always be the standard final boss stage.* + +## Items + +The first time a level is completed, a random item is given to the chef(s). If playing in a MultiWorld, completing a level may instead give another Archipelago user their item. The item found is displayed as text at the top of the results screen. + +Once all items have been obtained, the game will play like the original experience. + +The following items were invented for Randomizer: + +### Player Abilities +- Dash/Dash Cooldown +- Throw/Catch +- Sharp Knife +- Dish Scrubber +- Control Stick Batteries +- Lightweight Backpack +- Faster Respawn Time +- Emote (x6) + +### Objects +- Spare Plate +- Clean Dishes +- Wood +- Coal Bucket +- Bellows +- Fire Extinguisher + +### Kitchen/Environment +- Larger Tip Jar +- Guest Patience +- Burn Leniency +- Faster Condiment & Drink Switch +- Wok Wheels +- Coin Purse +- Calmer Unbread + +### Overworld +- Unlock Kevin Level (x8) +- Ramp Button +- Bonus Star (Filler Item*) + +**Note: Bonus star count varies with settings* + +## Other Game Modifications + +In addition to shuffling items, the following changes are applied to the game: + +### Quality of Life +- Tutorial is skipped +- Non-linear level order +- "Auto-Complete" feature to finish a level early when a target score is obtained +- Bugfixes for issues present in the base game (including "Sink Bug" and "Double Serving") +- All chef avatars automatically unlocked +- Optionally, level time can be reduced to make progression faster paced + +### Randomization Options + +- *Shuffle Level Order* + - Replaces each level on the overworld with a random level + - DLC levels can show up on the Story Overworld + - Optionally exclude "Horde" Levels + - Optionally exclude "Prep" Levels + +### Difficulty Adjustments +- Stars required to unlock levels have been rebalanced +- Points required to earn stars have been rebalanced + - Based off of the current World Record on the game's [Leaderboard](https://overcooked.greeny.dev) + - 1-Star/2-Star scores are much closer to the 3-Star Score +- Significantly reduced the time allotted to beat the final level +- Reduced penalty for expired order diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md new file mode 100644 index 00000000..d724f02f --- /dev/null +++ b/worlds/overcooked2/docs/setup_en.md @@ -0,0 +1,84 @@ +# Overcooked! 2 Randomizer Setup Guide + +## Quick Links +- [Main Page](../../../../games/Overcooked!%202/info/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## Required Software + +- Windows 10+ +- [Overcooked! 2](https://store.steampowered.com/bundle/13608/Overcooked_2___Gourmet_Edition/) for PC + - **Steam: Recommended** + - Steam (Beta Branch): Supported + - Epic Games: Supported + - GOG: Not officially supported - Adventurous users may choose to experiment at their own risk + - Windows Store (aka GamePass): Not Supported + - Xbox/PS/Switch: Not Supported +- [OC2-Modding Client](https://github.com/toasterparty/oc2-modding/releases) (instructions below) + +## Overview + +*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way. + +When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied. + +From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen. + +## Overcooked! 2 Modding Guide + +### Install + +1. Download and extract the contents of the latest [OC2-Modding Release](https://github.com/toasterparty/oc2-modding/releases) anywhere on your PC + +2. Double-Click **oc2-modding-install.bat** follow the instructions. + +Once *OC2-Modding* is installed, you have successfully installed everything you need to play/participate in Archipelago MultiWorld games. + +### Disable + +To temporarily turn off *OC2-Modding* and return to the original game, open **...\Overcooked! 2\BepInEx\config\OC2Modding.cfg** in a text editor like notepad and edit the following: + +`DisableAllMods = true` + +To re-enable, simply change the word **true** back to a **false**. + +### Uninstall + +To completely remove *OC2-Modding*, navigate to your game's installation folder and run **oc2-modding-uninstall.bat**. + +## Generate a MultiWorld Game + +1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste + +2. Export your yaml file and use it to generate a new randomized game +- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)) + +## Joining a MultiWorld Game + +1. Launch the game + +2. When attempting to enter the main menu from the title screen, the game will freeze and prompt you to sign in: + +![Sign-In Screen](https://i.imgur.com/goMy7o2.png) + +3. Sign-in with server address, username and password of the corresponding room you would like to join. +- Otherwise, if you just want to play the vanilla game without any modifications, you may press "Continue without Archipelago" button. + +4. Upon successful connection to the Archipelago service, you will be granted access to the main menu. The game will act as though you are playing for the first time. ***DO NOT FEAR*** — your original save data has not been overwritten; the Overcooked Randomizer just uses a temporary directory for it's save game data. + +## Playing Co-Op + +- To play local multiplayer (or Parsec/"Steam Play Together"), simply add the additional player to your game session as you would in the base game + +- To play online multiplayer, the guest *must* also have the same version of OC2-Modding installed. In order for the game to work, the guest must sign in using the same information the host used to connect to the Archipelago session. Once both host and client are both connected, they may join one another in-game and proceed as normal. It does not matter who hosts the game, and the game's hosts may be changed at any point. You may notice some things are different when playing this way: + + - Guests will still receive Archipelago messages about sent/received items the same as the host + + - When the host loads the campaign, any connected guests are forced to select "Don't Save" when prompted to pick which save slot to use. This is because randomizer uses the Archipelago service as a pseudo "cloud save", so progress will always be synchronized between all participants of that randomized *Overcooked! 2* instance. + +## Auto-Complete + +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.