342 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			342 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
# Look at `Rules.dsv` first to get an idea for how this works
 | 
						|
 | 
						|
from typing import Union, Tuple, List, Dict, Set
 | 
						|
from worlds.AutoWorld import WebWorld, World
 | 
						|
from BaseClasses import Region, ItemClassification, Tutorial, CollectionState
 | 
						|
from .Checks import (
 | 
						|
    TerrariaItem,
 | 
						|
    TerrariaLocation,
 | 
						|
    goals,
 | 
						|
    rules,
 | 
						|
    rule_indices,
 | 
						|
    labels,
 | 
						|
    rewards,
 | 
						|
    item_name_to_id,
 | 
						|
    location_name_to_id,
 | 
						|
    COND_ITEM,
 | 
						|
    COND_LOC,
 | 
						|
    COND_FN,
 | 
						|
    COND_GROUP,
 | 
						|
    npcs,
 | 
						|
    pickaxes,
 | 
						|
    hammers,
 | 
						|
    mech_bosses,
 | 
						|
    progression,
 | 
						|
    armor_minions,
 | 
						|
    accessory_minions,
 | 
						|
)
 | 
						|
from .Options import TerrariaOptions
 | 
						|
 | 
						|
 | 
						|
class TerrariaWeb(WebWorld):
 | 
						|
    tutorials = [
 | 
						|
        Tutorial(
 | 
						|
            "Multiworld Setup Guide",
 | 
						|
            "A guide to setting up the Terraria randomizer connected to an Archipelago Multiworld.",
 | 
						|
            "English",
 | 
						|
            "setup_en.md",
 | 
						|
            "setup/en",
 | 
						|
            ["Seldom"],
 | 
						|
        )
 | 
						|
    ]
 | 
						|
 | 
						|
 | 
						|
class TerrariaWorld(World):
 | 
						|
    """
 | 
						|
    Terraria is a 2D multiplayer sandbox game featuring mining, building, exploration, and combat.
 | 
						|
    Features 18 bosses and 4 classes.
 | 
						|
    """
 | 
						|
 | 
						|
    game = "Terraria"
 | 
						|
    web = TerrariaWeb()
 | 
						|
    options_dataclass = TerrariaOptions
 | 
						|
    options: TerrariaOptions
 | 
						|
 | 
						|
    item_name_to_id = item_name_to_id
 | 
						|
    location_name_to_id = location_name_to_id
 | 
						|
 | 
						|
    # Turn into an option when calamity is supported in the mod
 | 
						|
    calamity = False
 | 
						|
 | 
						|
    ter_items: List[str]
 | 
						|
    ter_locations: List[str]
 | 
						|
 | 
						|
    ter_goals: Dict[str, str]
 | 
						|
    goal_items: Set[str]
 | 
						|
    goal_locations: Set[str]
 | 
						|
 | 
						|
    def generate_early(self) -> None:
 | 
						|
        goal, goal_locations = goals[self.options.goal.value]
 | 
						|
        ter_goals = {}
 | 
						|
        goal_items = set()
 | 
						|
        for location in goal_locations:
 | 
						|
            _, flags, _, _ = rules[rule_indices[location]]
 | 
						|
            item = flags.get("Item") or f"Post-{location}"
 | 
						|
            ter_goals[item] = location
 | 
						|
            goal_items.add(item)
 | 
						|
 | 
						|
        achievements = self.options.achievements.value
 | 
						|
        location_count = 0
 | 
						|
        locations = []
 | 
						|
        for rule, flags, _, _ in rules[:goal]:
 | 
						|
            if (
 | 
						|
                (not self.calamity and "Calamity" in flags)
 | 
						|
                or (achievements < 1 and "Achievement" in flags)
 | 
						|
                or (achievements < 2 and "Grindy" in flags)
 | 
						|
                or (achievements < 3 and "Fishing" in flags)
 | 
						|
                or (
 | 
						|
                    rule == "Zenith" and self.options.goal.value != 11
 | 
						|
                )  # Bad hardcoding
 | 
						|
            ):
 | 
						|
                continue
 | 
						|
            if "Location" in flags or ("Achievement" in flags and achievements >= 1):
 | 
						|
                # Location
 | 
						|
                location_count += 1
 | 
						|
                locations.append(rule)
 | 
						|
            elif (
 | 
						|
                "Achievement" not in flags
 | 
						|
                and "Location" not in flags
 | 
						|
                and "Item" not in flags
 | 
						|
            ):
 | 
						|
                # Event
 | 
						|
                locations.append(rule)
 | 
						|
 | 
						|
        item_count = 0
 | 
						|
        items = []
 | 
						|
        for rule, flags, _, _ in rules[:goal]:
 | 
						|
            if not self.calamity and "Calamity" in flags:
 | 
						|
                continue
 | 
						|
            if "Item" in flags:
 | 
						|
                # Item
 | 
						|
                item_count += 1
 | 
						|
                if rule not in goal_locations:
 | 
						|
                    items.append(rule)
 | 
						|
            elif (
 | 
						|
                "Achievement" not in flags
 | 
						|
                and "Location" not in flags
 | 
						|
                and "Item" not in flags
 | 
						|
            ):
 | 
						|
                # Event
 | 
						|
                items.append(rule)
 | 
						|
 | 
						|
        extra_checks = self.options.fill_extra_checks_with.value
 | 
						|
        ordered_rewards = [
 | 
						|
            reward
 | 
						|
            for reward in labels["ordered"]
 | 
						|
            if self.calamity or "Calamity" not in rewards[reward]
 | 
						|
        ]
 | 
						|
        while extra_checks == 1 and item_count < location_count and ordered_rewards:
 | 
						|
            items.append(ordered_rewards.pop(0))
 | 
						|
            item_count += 1
 | 
						|
 | 
						|
        random_rewards = [
 | 
						|
            reward
 | 
						|
            for reward in labels["random"]
 | 
						|
            if self.calamity or "Calamity" not in rewards[reward]
 | 
						|
        ]
 | 
						|
        self.multiworld.random.shuffle(random_rewards)
 | 
						|
        while extra_checks == 1 and item_count < location_count and random_rewards:
 | 
						|
            items.append(random_rewards.pop(0))
 | 
						|
            item_count += 1
 | 
						|
 | 
						|
        while item_count < location_count:
 | 
						|
            items.append("Reward: Coins")
 | 
						|
            item_count += 1
 | 
						|
 | 
						|
        self.ter_items = items
 | 
						|
        self.ter_locations = locations
 | 
						|
 | 
						|
        self.ter_goals = ter_goals
 | 
						|
        self.goal_items = goal_items
 | 
						|
        self.goal_locations = goal_locations
 | 
						|
 | 
						|
    def create_regions(self) -> None:
 | 
						|
        menu = Region("Menu", self.player, self.multiworld)
 | 
						|
 | 
						|
        for location in self.ter_locations:
 | 
						|
            menu.locations.append(
 | 
						|
                TerrariaLocation(
 | 
						|
                    self.player, location, location_name_to_id.get(location), menu
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
        self.multiworld.regions.append(menu)
 | 
						|
 | 
						|
    def create_item(self, item: str) -> TerrariaItem:
 | 
						|
        if item in progression:
 | 
						|
            classification = ItemClassification.progression
 | 
						|
        else:
 | 
						|
            classification = ItemClassification.filler
 | 
						|
 | 
						|
        return TerrariaItem(item, classification, item_name_to_id[item], self.player)
 | 
						|
 | 
						|
    def create_items(self) -> None:
 | 
						|
        for item in self.ter_items:
 | 
						|
            if (rule_index := rule_indices.get(item)) is not None:
 | 
						|
                _, flags, _, _ = rules[rule_index]
 | 
						|
                if "Item" in flags:
 | 
						|
                    name = flags.get("Item") or f"Post-{item}"
 | 
						|
                else:
 | 
						|
                    continue
 | 
						|
            else:
 | 
						|
                name = item
 | 
						|
 | 
						|
            self.multiworld.itempool.append(self.create_item(name))
 | 
						|
 | 
						|
        locked_items = {}
 | 
						|
 | 
						|
        for location in self.ter_locations:
 | 
						|
            _, flags, _, _ = rules[rule_indices[location]]
 | 
						|
            if "Location" not in flags and "Achievement" not in flags:
 | 
						|
                if location in progression:
 | 
						|
                    classification = ItemClassification.progression
 | 
						|
                else:
 | 
						|
                    classification = ItemClassification.useful
 | 
						|
 | 
						|
                locked_items[location] = TerrariaItem(
 | 
						|
                    location, classification, None, self.player
 | 
						|
                )
 | 
						|
 | 
						|
        for item, location in self.ter_goals.items():
 | 
						|
            locked_items[location] = self.create_item(item)
 | 
						|
        for location, item in locked_items.items():
 | 
						|
            self.multiworld.get_location(location, self.player).place_locked_item(item)
 | 
						|
 | 
						|
    def check_condition(
 | 
						|
        self,
 | 
						|
        state,
 | 
						|
        sign: bool,
 | 
						|
        ty: int,
 | 
						|
        condition: Union[str, Tuple[Union[bool, None], list]],
 | 
						|
        arg: Union[str, int, None],
 | 
						|
    ) -> bool:
 | 
						|
        if ty == COND_ITEM:
 | 
						|
            _, flags, _, _ = rules[rule_indices[condition]]
 | 
						|
            if "Item" in flags:
 | 
						|
                name = flags.get("Item") or f"Post-{condition}"
 | 
						|
            else:
 | 
						|
                name = condition
 | 
						|
 | 
						|
            return sign == state.has(name, self.player)
 | 
						|
        elif ty == COND_LOC:
 | 
						|
            _, _, operator, conditions = rules[rule_indices[condition]]
 | 
						|
            return sign == self.check_conditions(state, operator, conditions)
 | 
						|
        elif ty == COND_FN:
 | 
						|
            if condition == "npc":
 | 
						|
                if type(arg) is not int:
 | 
						|
                    raise Exception("@npc requires an integer argument")
 | 
						|
 | 
						|
                npc_count = 0
 | 
						|
                for npc in npcs:
 | 
						|
                    if state.has(npc, self.player):
 | 
						|
                        npc_count += 1
 | 
						|
                        if npc_count >= arg:
 | 
						|
                            return sign
 | 
						|
 | 
						|
                return not sign
 | 
						|
            elif condition == "calamity":
 | 
						|
                return sign == self.calamity
 | 
						|
            elif condition == "grindy":
 | 
						|
                return sign == (self.options.achievements.value >= 2)
 | 
						|
            elif condition == "pickaxe":
 | 
						|
                if type(arg) is not int:
 | 
						|
                    raise Exception("@pickaxe requires an integer argument")
 | 
						|
 | 
						|
                for pickaxe, power in pickaxes.items():
 | 
						|
                    if power >= arg and state.has(pickaxe, self.player):
 | 
						|
                        return sign
 | 
						|
 | 
						|
                return not sign
 | 
						|
            elif condition == "hammer":
 | 
						|
                if type(arg) is not int:
 | 
						|
                    raise Exception("@hammer requires an integer argument")
 | 
						|
 | 
						|
                for hammer, power in hammers.items():
 | 
						|
                    if power >= arg and state.has(hammer, self.player):
 | 
						|
                        return sign
 | 
						|
 | 
						|
                return not sign
 | 
						|
            elif condition == "mech_boss":
 | 
						|
                if type(arg) is not int:
 | 
						|
                    raise Exception("@mech_boss requires an integer argument")
 | 
						|
 | 
						|
                boss_count = 0
 | 
						|
                for boss in mech_bosses:
 | 
						|
                    if state.has(boss, self.player):
 | 
						|
                        boss_count += 1
 | 
						|
                        if boss_count >= arg:
 | 
						|
                            return sign
 | 
						|
 | 
						|
                return not sign
 | 
						|
            elif condition == "minions":
 | 
						|
                if type(arg) is not int:
 | 
						|
                    raise Exception("@minions requires an integer argument")
 | 
						|
 | 
						|
                minion_count = 1
 | 
						|
                for armor, minions in armor_minions.items():
 | 
						|
                    if state.has(armor, self.player) and minions + 1 > minion_count:
 | 
						|
                        minion_count = minions + 1
 | 
						|
                        if minion_count >= arg:
 | 
						|
                            return sign
 | 
						|
 | 
						|
                for accessory, minions in accessory_minions.items():
 | 
						|
                    if state.has(accessory, self.player):
 | 
						|
                        minion_count += minions
 | 
						|
                        if minion_count >= arg:
 | 
						|
                            return sign
 | 
						|
 | 
						|
                return not sign
 | 
						|
            else:
 | 
						|
                raise Exception(f"Unknown function {condition}")
 | 
						|
        elif ty == COND_GROUP:
 | 
						|
            operator, conditions = condition
 | 
						|
            return sign == self.check_conditions(state, operator, conditions)
 | 
						|
 | 
						|
    def check_conditions(
 | 
						|
        self,
 | 
						|
        state,
 | 
						|
        operator: Union[bool, None],
 | 
						|
        conditions: List[
 | 
						|
            Tuple[
 | 
						|
                bool,
 | 
						|
                int,
 | 
						|
                Union[str, Tuple[Union[bool, None], list]],
 | 
						|
                Union[str, int, None],
 | 
						|
            ]
 | 
						|
        ],
 | 
						|
    ) -> bool:
 | 
						|
        if operator is None:
 | 
						|
            if len(conditions) == 0:
 | 
						|
                return True
 | 
						|
            if len(conditions) > 1:
 | 
						|
                raise Exception("Found multiple conditions without an operator")
 | 
						|
            return self.check_condition(state, *conditions[0])
 | 
						|
        elif operator:
 | 
						|
            return any(
 | 
						|
                self.check_condition(state, *condition) for condition in conditions
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            return all(
 | 
						|
                self.check_condition(state, *condition) for condition in conditions
 | 
						|
            )
 | 
						|
 | 
						|
    def set_rules(self) -> None:
 | 
						|
        for location in self.ter_locations:
 | 
						|
 | 
						|
            def check(state: CollectionState, location=location):
 | 
						|
                _, _, operator, conditions = rules[rule_indices[location]]
 | 
						|
                return self.check_conditions(state, operator, conditions)
 | 
						|
 | 
						|
            self.multiworld.get_location(location, self.player).access_rule = check
 | 
						|
 | 
						|
        self.multiworld.completion_condition[self.player] = lambda state: state.has_all(
 | 
						|
            self.goal_items, self.player
 | 
						|
        )
 | 
						|
 | 
						|
    def fill_slot_data(self) -> Dict[str, object]:
 | 
						|
        return {
 | 
						|
            "goal": list(self.goal_locations),
 | 
						|
            "achievements": self.options.achievements.value,
 | 
						|
            "deathlink": bool(self.options.death_link),
 | 
						|
        }
 |