Overcooked! 2: Implementation (#1046)

Overcooked! 2 is a couch co-op arcade game with a very high skill ceiling. It has a small but occult following, and the community craves a reason to keep coming back besides just grinding high scores. as such, this PR represents 3 major milestones in one:

 * The launch of OC2 Modding, a modding framework which is the first public mod for the game beyond simple RAM trainers
 * The launch of OC2 Randomizer
 * The integration of OC2 Randomizer in Archipelago
This commit is contained in:
toasterparty 2022-10-13 10:57:50 -07:00 committed by GitHub
parent 3bd4ef3f3d
commit 7f3f886e41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 5348 additions and 0 deletions

View File

@ -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

View File

@ -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)")

View File

152
worlds/overcooked2/Items.py Normal file
View File

@ -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,
}

View File

@ -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

3899
worlds/overcooked2/Logic.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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()})

View File

@ -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" ,
}

View File

@ -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

View File

@ -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

View File

@ -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.