Skip to content

Commit

Permalink
[OC2] Enabled DLC Option (ArchipelagoMW#1688)
Browse files Browse the repository at this point in the history
- New OC2 option `DLCOptionSet`, which is a list of DLCs whose levels should or shouldn't be used for entrance randomizer (and mention in documentation). By default, DLC owners now need to enable DLCs in weighted settings.
- Throw user-friendly exceptions when contradictory settings are enabled
- Slightly relax generation requirements for sphere 1/2 level permutations
- Write entrance randomizer info in spoiler log
- Skip adding "Dark Green Ramp" to item pool if Kevin Levels are disabled
  • Loading branch information
toasterparty authored Apr 11, 2023
1 parent 3c3954f commit c711d80
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 71 deletions.
14 changes: 13 additions & 1 deletion worlds/overcooked2/Items.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from BaseClasses import Item
from typing import NamedTuple, Dict

from .Overcooked2Levels import Overcooked2Dlc

class ItemData(NamedTuple):
code: int
Expand Down Expand Up @@ -77,6 +77,18 @@ class Overcooked2Item(Item):
"Ok Emote": 0,
}

dlc_exclusives = {
"Wood" : {Overcooked2Dlc.CAMPFIRE_COOK_OFF},
"Coal Bucket" : {Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE},
"Bellows" : {Overcooked2Dlc.SURF_N_TURF},
"Control Stick Batteries" : {Overcooked2Dlc.STORY, Overcooked2Dlc.SURF_N_TURF, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE, Overcooked2Dlc.CARNIVAL_OF_CHAOS, Overcooked2Dlc.SEASONAL},
"Wok Wheels" : {Overcooked2Dlc.SEASONAL},
"Lightweight Backpack" : {Overcooked2Dlc.CAMPFIRE_COOK_OFF},
"Faster Condiment/Drink Switch" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.CARNIVAL_OF_CHAOS},
"Calmer Unbread" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE},
"Coin Purse" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE},
}

item_name_to_config_name = {
"Wood" : "DisableWood" ,
"Coal Bucket" : "DisableCoal" ,
Expand Down
61 changes: 49 additions & 12 deletions worlds/overcooked2/Logic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from BaseClasses import CollectionState
from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level
from typing import Dict
from typing import Dict, Set
from random import Random

def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str,
Expand Down Expand Up @@ -132,11 +132,18 @@ def level_shuffle_factory(
rng: Random,
shuffle_prep_levels: bool,
shuffle_horde_levels: bool,
kevin_levels: bool,
enabled_dlc: Set[Overcooked2Dlc],
player_name: str,
) -> Dict[int, Overcooked2GenericLevel]: # return <story_level_id, level>

# Create a list of all valid levels for selection
# (excludes tutorial, throne and sometimes horde/prep levels)
pool = list()
for dlc in Overcooked2Dlc:
if dlc not in enabled_dlc:
continue

for level_id in range(dlc.start_level_id, dlc.end_level_id):
if level_id in dlc.excluded_levels():
continue
Expand All @@ -151,25 +158,55 @@ def level_shuffle_factory(
Overcooked2GenericLevel(level_id, dlc)
)

if kevin_levels:
level_count = 43
else:
level_count = 35

if len(pool) < level_count:
if shuffle_prep_levels:
prep_text = ""
else:
prep_text = " NON-PREP"

raise Exception(f"Invalid OC2 settings({player_name}): OC2 needs at least {level_count}{prep_text} levels in the level pool (currently has {len(pool)})")

# Sort the pool to eliminate risk
pool.sort(key=lambda x: int(x.dlc)*1000 + x.level_id)

result: Dict[int, Overcooked2GenericLevel] = dict()
story = Overcooked2Dlc.STORY

attempts = 0

while len(result) == 0 or not meets_minimum_sphere_one_requirements(result):
if attempts >= 15:
raise Exception("Failed to create valid Overcooked2 level shuffle permutation in a reasonable amount of attempts")

result.clear()

# Shuffle the pool, using the provided RNG
rng.shuffle(pool)

# Return the first 44 levels and assign those to each level
for level_id in range(story.start_level_id, story.end_level_id):
if level_id not in story.excluded_levels():
result[level_id] = pool[level_id-1]
elif level_id == 36:
# Level 6-6 is exempt from shuffling
result[level_id] = Overcooked2GenericLevel(level_id)
# Handle level assignment

level_id = 0
placed = 0
for level in pool:
level_id += 1
while level_id in story.excluded_levels():
level_id += 1

result[level_id] = level
placed += 1

if placed >= level_count:
break

# Level 6-6 is exempt from shuffling
result[36] = Overcooked2GenericLevel(36)

attempts += 1

return result

Expand All @@ -178,12 +215,12 @@ def meets_minimum_sphere_one_requirements(
levels: Dict[int, Overcooked2GenericLevel],
) -> bool:

# 1-1, 2-1, and 4-1 are garunteed to be accessible on
# 1-1, 2-1, and 4-1 are guaranteed to be accessible on
# the overworld without requiring a ramp or additional stars
sphere_one = [1, 7, 19]

# 1-2, 2-2, 3-1 and 5-1 are almost always the next thing unlocked
sphere_twoish = [2, 8, 13, 25]
# 1-2, 2-2, 3-1, 5-1 and 6-1 are almost always the next thing unlocked
sphere_twoish = [2, 8, 13, 25, 31]

# Peek the logic for sphere one and see how many are possible
# with no items
Expand All @@ -199,7 +236,7 @@ def meets_minimum_sphere_one_requirements(

return sphere_one_count >= 2 and \
sphere_twoish_count >= 2 and \
sphere_one_count + sphere_twoish_count >= 6
sphere_one_count + sphere_twoish_count >= 5


def is_completable_no_items(level: Overcooked2GenericLevel) -> bool:
Expand Down
12 changes: 10 additions & 2 deletions worlds/overcooked2/Options.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from enum import IntEnum
from typing import TypedDict
from Options import Toggle, DefaultOnToggle, Range, Choice

from Options import DefaultOnToggle, Toggle, Range, Choice, OptionSet
from .Overcooked2Levels import Overcooked2Dlc

class LocationBalancingMode(IntEnum):
disabled = 0
Expand Down Expand Up @@ -87,6 +87,13 @@ class ShuffleLevelOrder(OC2OnToggle):
display_name = "Shuffle Level Order"


class DLCOptionSet(OptionSet):
"""Which DLCs should be included when 'Shuffle Level Order' is enabled?'"""
display_name = "Enabled DLC"
default = {"Story", "Seasonal"}
valid_keys = [dlc.value for dlc in Overcooked2Dlc]


class IncludeHordeLevels(OC2OnToggle):
"""Includes "Horde Defense" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds
two horde-specific items into the item pool."""
Expand Down Expand Up @@ -170,6 +177,7 @@ class StarThresholdScale(Range):

# randomization options
"shuffle_level_order": ShuffleLevelOrder,
"include_dlcs": DLCOptionSet,
"include_horde_levels": IncludeHordeLevels,
"prep_levels": PrepLevels,
"kevin_levels": KevinLevels,
Expand Down
27 changes: 1 addition & 26 deletions worlds/overcooked2/Overcooked2Levels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

class Overcooked2Dlc(Enum):
STORY = "Story"
SEASONAL = "Seasonal"
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"
Expand Down Expand Up @@ -87,31 +87,6 @@ def prep_levels(self) -> List[int]:

return []

def exclusive_items(self) -> List[str]:
"""Returns list of items exclusive to this DLC"""
if self == Overcooked2Dlc.SURF_N_TURF:
return ["Bellows"]
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
return ["Wood"]
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
return ["Coal Bucket"]
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
return ["Faster Condiment/Drink Switch"]
if self == Overcooked2Dlc.SEASONAL:
return ["Wok Wheels"]

return []

ITEMS_TO_EXCLUDE_IF_NO_DLC = [
"Wood",
"Coal Bucket",
"Bellows",
"Coin Purse",
"Wok Wheels",
"Lightweight Backpack",
"Faster Condiment/Drink Switch",
"Calmer Unbread",
]

class Overcooked2GameWorld(IntEnum):
ONE = 1
Expand Down
50 changes: 40 additions & 10 deletions worlds/overcooked2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from enum import IntEnum
from typing import Callable, Dict, Any, List, Optional
from typing import Any, List, Dict, Set, Callable, Optional, TextIO

from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, Tutorial, LocationProgressType
from worlds.AutoWorld import World, WebWorld

from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel, ITEMS_TO_EXCLUDE_IF_NO_DLC
from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies, dlc_exclusives
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful


Expand Down Expand Up @@ -222,17 +222,23 @@ def get_priority_locations(self) -> List[int]:

# Helper Data

player_name: str
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level
enabled_dlc: Set[Overcooked2Dlc]

# Autoworld Hooks

def generate_early(self):
self.player_name = self.multiworld.player_name[self.player]
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

# Parse DLCOptionSet back into enums
self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options["DLCOptionSet"]}

# 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"])
Expand All @@ -244,9 +250,16 @@ def generate_early(self):
self.multiworld.random,
self.options["PrepLevels"] != PrepLevelMode.excluded,
self.options["IncludeHordeLevels"],
self.options["KevinLevels"],
self.enabled_dlc,
self.player_name,
)
else:
self.level_mapping = None
if Overcooked2Dlc.STORY not in self.enabled_dlc:
raise Exception(f"Invalid OC2 settings({self.player_name}) Need either Level Shuffle disabled or 'Story' DLC enabled")

self.enabled_dlc = {Overcooked2Dlc.STORY}

def set_location_priority(self) -> None:
priority_locations = self.get_priority_locations()
Expand Down Expand Up @@ -351,17 +364,23 @@ def create_items(self):
# not used
continue

if not self.options["ShuffleLevelOrder"] and item_name in ITEMS_TO_EXCLUDE_IF_NO_DLC:
# skip DLC items if no DLC
continue
if item_name in dlc_exclusives:
if not any(x in dlc_exclusives[item_name] for x in self.enabled_dlc):
# Item is always useless with these settings
continue

if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
# skip horde-specific items if no horde levels
continue

if not self.options["KevinLevels"] and item_name.startswith("Kevin"):
# skip kevin items if no kevin levels
continue
if not self.options["KevinLevels"]:
if item_name.startswith("Kevin"):
# skip kevin items if no kevin levels
continue

if item_name == "Dark Green Ramp":
# skip dark green ramp if there's no Kevin-1 to reveal it
continue

if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
# progression.append(item_name)
Expand Down Expand Up @@ -425,7 +444,7 @@ def generate_basic(self) -> None:
# Items get distributed to locations

def fill_json_data(self) -> Dict[str, Any]:
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}"
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}"

# Serialize Level Order
story_level_order = dict()
Expand Down Expand Up @@ -578,6 +597,17 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
def fill_slot_data(self) -> Dict[str, Any]:
return self.fill_json_data()

def write_spoiler(self, spoiler_handle: TextIO) -> None:
if not self.options["ShuffleLevelOrder"]:
return

world: Overcooked2World = self.multiworld.worlds[self.player]
spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n")
for overworld_id in world.level_mapping:
overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1]
kitchen_name = world.level_mapping[overworld_id].shortname
spoiler_handle.write(f'{overworld_name} | {kitchen_name}\n')


def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]:
level_unlock_counts = dict()
Expand Down
5 changes: 4 additions & 1 deletion worlds/overcooked2/docs/setup_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder

1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste

*By default, these settings will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Settings](../../../../weighted-settings) page*

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

*For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)*

## Joining a MultiWorld Game

Expand Down
Loading

0 comments on commit c711d80

Please sign in to comment.