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:
parent
3bd4ef3f3d
commit
7f3f886e41
|
@ -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
|
||||
|
|
|
@ -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)")
|
|
@ -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,
|
||||
}
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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()})
|
|
@ -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" ,
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
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.
|
Loading…
Reference in New Issue