631 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			631 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
from enum import IntEnum
 | 
						|
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 Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel
 | 
						|
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
 | 
						|
from .Options import OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
class Overcooked2Web(WebWorld):
 | 
						|
    theme = "partyTime"
 | 
						|
 | 
						|
    bug_report_page = "https://github.com/toasterparty/oc2-modding/issues"
 | 
						|
    setup_en = Tutorial(
 | 
						|
        "Multiworld Setup Guide",
 | 
						|
        "A guide to setting up the Overcooked! 2 randomizer on your computer.",
 | 
						|
        "English",
 | 
						|
        "setup_en.md",
 | 
						|
        "setup/en",
 | 
						|
        ["toasterparty"]
 | 
						|
    )
 | 
						|
 | 
						|
    tutorials = [setup_en]
 | 
						|
 | 
						|
 | 
						|
class PrepLevelMode(IntEnum):
 | 
						|
    original = 0
 | 
						|
    excluded = 1
 | 
						|
    ayce = 2
 | 
						|
 | 
						|
 | 
						|
class Overcooked2World(World):
 | 
						|
    """
 | 
						|
    Overcooked! 2 is a frantically 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, 8)
 | 
						|
    topology_present: bool = False
 | 
						|
 | 
						|
    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_dataclass = OC2Options
 | 
						|
    options: OC2Options
 | 
						|
    itempool: List[Overcooked2Item]
 | 
						|
 | 
						|
    # Helper Functions
 | 
						|
 | 
						|
    def is_level_horde(self, level_id: int) -> bool:
 | 
						|
        return self.options.include_horde_levels 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.multiworld.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,
 | 
						|
            self.player,
 | 
						|
            self.multiworld,
 | 
						|
        )
 | 
						|
        self.multiworld.regions.append(region)
 | 
						|
 | 
						|
    def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None):
 | 
						|
        sourceRegion = self.multiworld.get_region(source, self.player)
 | 
						|
        targetRegion = self.multiworld.get_region(target, self.player)
 | 
						|
        sourceRegion.connect(targetRegion, rule=rule)
 | 
						|
 | 
						|
    def add_level_location(
 | 
						|
        self,
 | 
						|
        region_name: str,
 | 
						|
        location_name: str,
 | 
						|
        level_id: int,
 | 
						|
        stars: int,
 | 
						|
        is_event: bool = False,
 | 
						|
        priority=False,
 | 
						|
    ) -> None:
 | 
						|
 | 
						|
        if is_event:
 | 
						|
            location_id = None
 | 
						|
        else:
 | 
						|
            location_id = level_id
 | 
						|
 | 
						|
        region = self.multiworld.get_region(region_name, self.player)
 | 
						|
        location = Overcooked2Location(
 | 
						|
            self.player,
 | 
						|
            location_name,
 | 
						|
            location_id,
 | 
						|
            region,
 | 
						|
        )
 | 
						|
 | 
						|
        if priority:
 | 
						|
            location.progress_type = LocationProgressType.PRIORITY
 | 
						|
        else:
 | 
						|
            location.progress_type = LocationProgressType.DEFAULT
 | 
						|
 | 
						|
        # 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_n_random_locations(self, n: int) -> List[int]:
 | 
						|
        """Return a list of n random non-repeating level locations"""
 | 
						|
        levels = list()
 | 
						|
 | 
						|
        if n == 0:
 | 
						|
            return levels
 | 
						|
 | 
						|
        for level in Overcooked2Level():
 | 
						|
            if level.level_id == 36:
 | 
						|
                continue
 | 
						|
            elif not self.options.kevin_levels and level.level_id > 36:
 | 
						|
                break
 | 
						|
 | 
						|
            levels.append(level.level_id)
 | 
						|
 | 
						|
        self.multiworld.random.shuffle(levels)
 | 
						|
        return levels[:n]
 | 
						|
 | 
						|
    def get_priority_locations(self) -> List[int]:
 | 
						|
        """Randomly generate list of priority locations, thus insulating this game
 | 
						|
        from the negative effects of being shuffled with games that contain large
 | 
						|
        ammounts of filler"""
 | 
						|
 | 
						|
        if self.multiworld.players == 1:
 | 
						|
            # random priority locations have no desirable effect on solo seeds
 | 
						|
            return list()
 | 
						|
 | 
						|
        balancing_mode = self.options.location_balancing
 | 
						|
 | 
						|
        if balancing_mode == LocationBalancingMode.disabled:
 | 
						|
            # Location balancing is disabled, progression density is purely determined by filler
 | 
						|
            return list()
 | 
						|
 | 
						|
        # Count how many progression items are required for this overcooked instance
 | 
						|
        game_item_count = len(self.itempool)
 | 
						|
        game_progression_count = 0
 | 
						|
        for item in self.itempool:
 | 
						|
            if item.classification == ItemClassification.progression:
 | 
						|
                game_progression_count += 1
 | 
						|
        game_progression_density = game_progression_count/game_item_count
 | 
						|
 | 
						|
        if balancing_mode == LocationBalancingMode.full:
 | 
						|
            # Location balancing will be employed in an attempt to keep the number of
 | 
						|
            # progression locations and proression items as close to equal as possible
 | 
						|
            return self.get_n_random_locations(game_progression_count)
 | 
						|
 | 
						|
        assert balancing_mode == LocationBalancingMode.compromise
 | 
						|
 | 
						|
        # Count how many progression items are shuffled between all games
 | 
						|
        total_item_count = len(self.multiworld.itempool)
 | 
						|
        total_progression_count = 0
 | 
						|
 | 
						|
        for item in self.multiworld.itempool:
 | 
						|
            if item.classification == ItemClassification.progression:
 | 
						|
                total_progression_count += 1
 | 
						|
        total_progression_density = total_progression_count/total_item_count
 | 
						|
 | 
						|
        if total_progression_density >= game_progression_density:
 | 
						|
            # This game has more filler than the average of all other games.
 | 
						|
            # It is not in need of location balancing
 | 
						|
            return list()
 | 
						|
 | 
						|
        # Calculate the midpoint between the two ratios
 | 
						|
        target_progression_ratio = (game_progression_density - total_progression_density) / 2.0 + total_progression_density
 | 
						|
        target_progression_count = int((target_progression_ratio * game_item_count) + 0.5) # I'm sorry I round like an old person
 | 
						|
 | 
						|
        # Location balancing will be employed in an attempt to find a compromise at
 | 
						|
        # the half-way point between natural probability and full artifical balancing
 | 
						|
        return self.get_n_random_locations(target_progression_count)
 | 
						|
 | 
						|
    # 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):
 | 
						|
        # 0.0 to 1.0 where 1.0 is World Record
 | 
						|
        self.star_threshold_scale = self.options.star_threshold_scale / 100.0
 | 
						|
 | 
						|
        # Parse DLCOptionSet back into enums
 | 
						|
        self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options.include_dlcs.value}
 | 
						|
 | 
						|
        # 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.stars_to_win.value)
 | 
						|
 | 
						|
        # Assign new kitchens to each spot on the overworld using pure random chance and nothing else
 | 
						|
        if self.options.shuffle_level_order:
 | 
						|
            self.level_mapping = \
 | 
						|
                level_shuffle_factory(
 | 
						|
                    self.multiworld.random,
 | 
						|
                    self.options.prep_levels != PrepLevelMode.excluded,
 | 
						|
                    self.options.include_horde_levels.result,
 | 
						|
                    self.options.kevin_levels.result,
 | 
						|
                    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()
 | 
						|
        for level in Overcooked2Level():
 | 
						|
            if level.level_id in priority_locations:
 | 
						|
                location: Location = self.multiworld.get_location(level.location_name_item, self.player)
 | 
						|
                location.progress_type = LocationProgressType.PRIORITY
 | 
						|
 | 
						|
 | 
						|
    def create_regions(self) -> None:
 | 
						|
        # Menu -> Overworld
 | 
						|
        self.add_region("Menu")
 | 
						|
        self.add_region("Overworld")
 | 
						|
        self.connect_regions("Menu", "Overworld")
 | 
						|
 | 
						|
        # Create and populate "regions" (a.k.a. levels)
 | 
						|
        for level in Overcooked2Level():
 | 
						|
            if not self.options.kevin_levels 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
 | 
						|
            if self.is_level_horde(level.level_id):
 | 
						|
                # in randomizer, horde levels grant a single star
 | 
						|
                star_counts = [1]
 | 
						|
            else:
 | 
						|
                star_counts = [1, 2, 3]
 | 
						|
 | 
						|
            for n in star_counts:
 | 
						|
                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.options.ramp_tricks.result, 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.multiworld.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 item_name in item_frequencies:
 | 
						|
                freq = item_frequencies[item_name]
 | 
						|
            else:
 | 
						|
                freq = 1
 | 
						|
            
 | 
						|
            if freq <= 0:
 | 
						|
                # not used
 | 
						|
                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.include_horde_levels and item_name in ["Calmer Unbread", "Coin Purse"]:
 | 
						|
                # skip horde-specific items if no horde levels
 | 
						|
                continue
 | 
						|
 | 
						|
            if not self.options.kevin_levels:
 | 
						|
                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.kevin_levels):
 | 
						|
                # progression.append(item_name)
 | 
						|
                classification = ItemClassification.progression
 | 
						|
            else:
 | 
						|
                if (is_useful(item_name)):
 | 
						|
                    # useful.append(item_name)
 | 
						|
                    classification = ItemClassification.useful
 | 
						|
                else:
 | 
						|
                    # filler.append(item_name)
 | 
						|
                    classification = ItemClassification.filler
 | 
						|
 | 
						|
            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
 | 
						|
 | 
						|
        # 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.kevin_levels:
 | 
						|
            pool_count -= 8
 | 
						|
 | 
						|
        while len(self.itempool) < pool_count:
 | 
						|
            self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful))
 | 
						|
 | 
						|
        self.multiworld.itempool += self.itempool
 | 
						|
 | 
						|
 | 
						|
    def place_events(self):
 | 
						|
        # Add Events (Star Acquisition)
 | 
						|
        for level in Overcooked2Level():
 | 
						|
            if not self.options.kevin_levels 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):
 | 
						|
                # in randomizer, horde levels grant a single star
 | 
						|
                star_counts = [1]
 | 
						|
            else:
 | 
						|
                star_counts = [1, 2, 3]
 | 
						|
 | 
						|
            for n in star_counts:
 | 
						|
                self.place_event(level.location_name_star_event(n), "Star")
 | 
						|
 | 
						|
        # Add Victory Condition
 | 
						|
        self.place_event("6-6 Completed", "Victory")
 | 
						|
 | 
						|
    def set_rules(self):
 | 
						|
        pass
 | 
						|
 | 
						|
    def generate_basic(self) -> None:
 | 
						|
        self.place_events()
 | 
						|
        self.set_location_priority()
 | 
						|
 | 
						|
    # 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.player_name}"
 | 
						|
 | 
						|
        # Serialize Level Order
 | 
						|
        story_level_order = dict()
 | 
						|
 | 
						|
        if self.options.shuffle_level_order:
 | 
						|
            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.kevin_levels:
 | 
						|
            def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
 | 
						|
                location = self.multiworld.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()
 | 
						|
        locations = self.multiworld.get_filled_locations(self.player)
 | 
						|
        for location in locations:
 | 
						|
            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.star_threshold_scale / 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],
 | 
						|
            "LocalDeathLink": self.options.deathlink != DeathLinkMode.disabled,
 | 
						|
            "BurnTriggersDeath": self.options.deathlink == DeathLinkMode.death_and_overcook,
 | 
						|
 | 
						|
            # Game Modifications
 | 
						|
            "LevelPurchaseRequirements": level_purchase_requirements,
 | 
						|
            "Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
 | 
						|
            "ShortHordeLevels": self.options.short_horde_levels.result,
 | 
						|
            "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, # Unused
 | 
						|
            "DisableGreenRampButton" : True,
 | 
						|
            "DisableYellowRampButton" : True,
 | 
						|
            "DisableBlueRampButton" : True,
 | 
						|
            "DisablePinkRampButton" : True,
 | 
						|
            "DisableGreyRampButton" : True,
 | 
						|
            "DisableRedRampButton" : True,
 | 
						|
            "DisablePurpleRampButton" : 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:
 | 
						|
            base_data[bug] = self.options.fix_bugs.result
 | 
						|
        base_data["PreserveCookingProgress"] = self.options.always_preserve_cooking_progress.result
 | 
						|
        base_data["TimerAlwaysStarts"] = self.options.prep_levels == PrepLevelMode.ayce
 | 
						|
        base_data["LevelTimerScale"] = 0.666 if self.options.shorter_level_duration else 1.0
 | 
						|
        base_data["LeaderboardScoreScale"] = {
 | 
						|
            "FourStars": 1.0,
 | 
						|
            "ThreeStars": star_threshold_scale,
 | 
						|
            "TwoStars": star_threshold_scale * 0.75,
 | 
						|
            "OneStar": star_threshold_scale * 0.35,
 | 
						|
        }
 | 
						|
        base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result
 | 
						|
 | 
						|
        return base_data
 | 
						|
 | 
						|
    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.shuffle_level_order:
 | 
						|
            return
 | 
						|
 | 
						|
        world: Overcooked2World = self
 | 
						|
        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()
 | 
						|
    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
 |