| 
									
										
										
										
											2024-11-29 16:45:36 -05:00
										 |  |  | 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): | 
					
						
							| 
									
										
										
										
											2025-01-13 18:35:01 -05:00
										 |  |  |         self.filler_ratios: Dict[str, int] = { | 
					
						
							|  |  |  |             item.name: item.count | 
					
						
							|  |  |  |             for item in Items.items | 
					
						
							|  |  |  |             if item.classification in [ItemClassification.filler, ItemClassification.trap] | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         # Remove poison by default to respect itemlinking | 
					
						
							|  |  |  |         self.filler_ratios["Poison"] = 0 | 
					
						
							| 
									
										
										
										
											2024-11-29 16:45:36 -05:00
										 |  |  |         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)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-13 18:35:01 -05:00
										 |  |  |         # Adjust filler ratios | 
					
						
							| 
									
										
										
										
											2024-11-29 16:45:36 -05:00
										 |  |  |         # If red potions are locked in shops, remove the count from the ratio. | 
					
						
							|  |  |  |         self.filler_ratios["Red Potion"] -= red_potion_in_shop_count | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-13 18:35:01 -05:00
										 |  |  |         # Add poisons if desired | 
					
						
							|  |  |  |         if self.options.include_poisons: | 
					
						
							|  |  |  |             self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count | 
					
						
							| 
									
										
										
										
											2024-11-29 16:45:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # 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 |