from typing import Any, Dict, List from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld from worlds.AutoWorld import WebWorld, World from . import Items, Locations, Regions, Rules from .Options import FaxanaduOptions from worlds.generic.Rules import set_rule DAXANADU_VERSION = "0.3.0" class FaxanaduLocation(Location): game: str = "Faxanadu" class FaxanaduItem(Item): game: str = "Faxanadu" class FaxanaduWeb(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", "A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld", "English", "setup_en.md", "setup/en", ["Daivuk"] )] theme = "dirt" class FaxanaduWorld(World): """ Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System """ options_dataclass = FaxanaduOptions options: FaxanaduOptions game = "Faxanadu" web = FaxanaduWeb() item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None} item_name_to_item = {item.name: item for item in Items.items} location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None} def __init__(self, world: MultiWorld, player: int): self.filler_ratios: Dict[str, int] = {} super().__init__(world, player) def create_regions(self): Regions.create_regions(self) # Add locations into regions for region in self.multiworld.get_regions(self.player): for loc in [location for location in Locations.locations if location.region == region.name]: location = FaxanaduLocation(self.player, loc.name, loc.id, region) # In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops if loc.type == Locations.LocationType.shop: location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison") region.locations.append(location) def set_rules(self): Rules.set_rules(self) self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player) def create_item(self, name: str) -> FaxanaduItem: item: Items.ItemDef = self.item_name_to_item[name] return FaxanaduItem(name, item.classification, item.id, self.player) # Returns how many red potions were prefilled into shops def prefill_shop_red_potions(self) -> int: red_potion_in_shop_count = 0 if self.options.keep_shop_red_potions: red_potion_item = self.item_name_to_item["Red Potion"] red_potion_shop_locations = [ loc for loc in Locations.locations if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion ] for loc in red_potion_shop_locations: location = self.get_location(loc.name) location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player)) red_potion_in_shop_count += 1 return red_potion_in_shop_count def put_wingboot_in_shop(self, shops, region_name): item = self.item_name_to_item["Wingboots"] shop = shops.pop(region_name) slot = self.random.randint(0, len(shop) - 1) loc = shop[slot] location = self.get_location(loc.name) location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player)) # Put a rule right away that we need to have to unlocked. set_rule(location, lambda state: state.has("Unlock Wingboots", self.player)) # Returns how many wingboots were prefilled into shops def prefill_shop_wingboots(self) -> int: # Collect shops shops: Dict[str, List[Locations.LocationDef]] = {} for loc in Locations.locations: if loc.type == Locations.LocationType.shop: if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion: continue # Don't override our red potions shops.setdefault(loc.region, []).append(loc) shop_count = len(shops) wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots # At least one should be in the first 4 shops. Because we require wingboots to progress past that point. must_have_regions = [region for i, region in enumerate(shops) if i < 4] self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions)) # Fill in the rest randomly in remaining shops for i in range(wingboots_count - 1): # -1 because we added one already region = self.random.choice(list(shops.keys())) self.put_wingboot_in_shop(shops, region) return wingboots_count def create_items(self) -> None: itempool: List[FaxanaduItem] = [] # Prefill red potions in shops if option is set red_potion_in_shop_count = self.prefill_shop_red_potions() # Prefill wingboots in shops wingboots_in_shop_count = self.prefill_shop_wingboots() # Create the item pool, excluding fillers. prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count for item in Items.items: # Ignore pendant if turned off if item.name == "Pendant" and not self.options.include_pendant: continue # ignore fillers for now, we will fill them later if item.classification in [ItemClassification.filler, ItemClassification.trap] and \ item.progression_count == 0: continue prefill_loc = None if item.prefill_location: prefill_loc = self.get_location(item.prefill_location) # if require dragon slayer is turned on, we need progressive shields to be progression item_classification = item.classification if self.options.require_dragon_slayer and item.name == "Progressive Shield": item_classification = ItemClassification.progression if prefill_loc: prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player)) prefilled_count += 1 else: for i in range(item.count - item.progression_count): itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player)) for i in range(item.progression_count): itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player)) # Set up filler ratios self.filler_ratios = { item.name: item.count for item in Items.items if item.classification in [ItemClassification.filler, ItemClassification.trap] } # If red potions are locked in shops, remove the count from the ratio. self.filler_ratios["Red Potion"] -= red_potion_in_shop_count # Remove poisons if not desired if not self.options.include_poisons: self.filler_ratios["Poison"] = 0 # Randomly add fillers to the pool with ratios based on og game occurrence counts. filler_count = len(Locations.locations) - len(itempool) - prefilled_count for i in range(filler_count): itempool.append(self.create_item(self.get_filler_item_name())) self.multiworld.itempool += itempool def get_filler_item_name(self) -> str: return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0] def fill_slot_data(self) -> Dict[str, Any]: slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards") slot_data["daxanadu_version"] = DAXANADU_VERSION return slot_data