249 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| Author: Louis M
 | |
| Date: Fri, 15 Mar 2024 18:41:40 +0000
 | |
| Description: Main module for Aquaria game multiworld randomizer
 | |
| """
 | |
| 
 | |
| from typing import List, Dict, ClassVar, Any
 | |
| from worlds.AutoWorld import World, WebWorld
 | |
| from BaseClasses import Tutorial, MultiWorld, ItemClassification
 | |
| from .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames
 | |
| from .Locations import location_table, AquariaLocationNames
 | |
| from .Options import (AquariaOptions, IngredientRandomizer, TurtleRandomizer, EarlyBindSong, EarlyEnergyForm,
 | |
|                       UnconfineHomeWater, Objective)
 | |
| from .Regions import AquariaRegions
 | |
| 
 | |
| 
 | |
| class AquariaWeb(WebWorld):
 | |
|     """
 | |
|     Class used to generate the Aquaria Game Web pages (setup, tutorial, etc.)
 | |
|     """
 | |
|     theme = "ocean"
 | |
| 
 | |
|     bug_report_page = "https://github.com/tioui/Aquaria_Randomizer/issues"
 | |
| 
 | |
|     setup = Tutorial(
 | |
|         "Multiworld Setup Guide",
 | |
|         "A guide to setting up Aquaria for MultiWorld.",
 | |
|         "English",
 | |
|         "setup_en.md",
 | |
|         "setup/en",
 | |
|         ["Tioui"]
 | |
|     )
 | |
| 
 | |
|     setup_fr = Tutorial(
 | |
|         "Guide de configuration Multimonde",
 | |
|         "Un guide pour configurer Aquaria MultiWorld",
 | |
|         "Français",
 | |
|         "setup_fr.md",
 | |
|         "setup/fr",
 | |
|         ["Tioui"]
 | |
|     )
 | |
| 
 | |
|     tutorials = [setup, setup_fr]
 | |
|     game_info_languages = ["en", "fr"]
 | |
| 
 | |
| 
 | |
| class AquariaWorld(World):
 | |
|     """
 | |
|     Aquaria is a side-scrolling action-adventure game. It follows Naija, an
 | |
|     aquatic humanoid woman, as she explores the underwater world of Aquaria.
 | |
|     Along her journey, she learns about the history of the world she inhabits
 | |
|     as well as her own past. The gameplay focuses on a combination of swimming,
 | |
|     singing, and combat, through which Naija can interact with the world. Her
 | |
|     songs can move items, affect plants and animals, and change her physical
 | |
|     appearance into other forms that have different abilities, like firing
 | |
|     projectiles at hostile creatures, or passing through barriers inaccessible
 | |
|     to her in her natural form.
 | |
|     From: https://en.wikipedia.org/wiki/Aquaria_(video_game)
 | |
|     """
 | |
| 
 | |
|     game: str = "Aquaria"
 | |
|     "The name of the game"
 | |
| 
 | |
|     topology_present = True
 | |
|     "show path to required location checks in spoiler"
 | |
| 
 | |
|     web: WebWorld = AquariaWeb()
 | |
|     "The web page generation informations"
 | |
| 
 | |
|     item_name_to_id: ClassVar[Dict[str, int]] = \
 | |
|         {name: data.id for name, data in item_table.items()}
 | |
|     "The name and associated ID of each item of the world"
 | |
| 
 | |
|     item_name_groups = {
 | |
|         "Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
 | |
|                    ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
 | |
|                    ItemNames.BABY_BLASTER},
 | |
|         "Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
 | |
|     }
 | |
|     """Grouping item make it easier to find them"""
 | |
| 
 | |
|     location_name_to_id = location_table
 | |
|     "The name and associated ID of each location of the world"
 | |
| 
 | |
|     base_id = 698000
 | |
|     "The starting ID of the items and locations of the world"
 | |
| 
 | |
|     ingredients_substitution: List[int]
 | |
|     "Used to randomize ingredient drop"
 | |
| 
 | |
|     options_dataclass = AquariaOptions
 | |
|     "Used to manage world options"
 | |
| 
 | |
|     options: AquariaOptions
 | |
|     "Every options of the world"
 | |
| 
 | |
|     regions: AquariaRegions | None
 | |
|     "Used to manage Regions"
 | |
| 
 | |
|     exclude: List[str]
 | |
| 
 | |
|     def __init__(self, multiworld: MultiWorld, player: int):
 | |
|         """Initialisation of the Aquaria World"""
 | |
|         super(AquariaWorld, self).__init__(multiworld, player)
 | |
|         self.regions = None
 | |
|         self.ingredients_substitution = []
 | |
|         self.exclude = []
 | |
| 
 | |
|     def generate_early(self) -> None:
 | |
|         """
 | |
|         Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
 | |
|         results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
 | |
|         """
 | |
|         self.regions = AquariaRegions(self.multiworld, self.player)
 | |
| 
 | |
|     def create_regions(self) -> None:
 | |
|         """
 | |
|         Create every Region in `regions`
 | |
|         """
 | |
|         self.regions.add_regions_to_world()
 | |
|         self.regions.connect_regions()
 | |
|         self.regions.add_event_locations()
 | |
| 
 | |
|     def create_item(self, name: str) -> AquariaItem:
 | |
|         """
 | |
|         Create an AquariaItem using 'name' as item name.
 | |
|         """
 | |
|         result: AquariaItem
 | |
|         data = item_table[name]
 | |
|         classification: ItemClassification = ItemClassification.useful
 | |
|         if data.type == ItemType.JUNK:
 | |
|             classification = ItemClassification.filler
 | |
|         elif data.type == ItemType.PROGRESSION:
 | |
|             classification = ItemClassification.progression
 | |
|         result = AquariaItem(name, classification, data.id, self.player)
 | |
| 
 | |
|         return result
 | |
| 
 | |
|     def __pre_fill_item(self, item_name: str, location_name: str, precollected,
 | |
|                         itemClassification: ItemClassification = ItemClassification.useful) -> None:
 | |
|         """Pre-assign an item to a location"""
 | |
|         if item_name not in precollected:
 | |
|             self.exclude.append(item_name)
 | |
|             data = item_table[item_name]
 | |
|             item = AquariaItem(item_name, itemClassification, data.id, self.player)
 | |
|             self.multiworld.get_location(location_name, self.player).place_locked_item(item)
 | |
| 
 | |
|     def get_filler_item_name(self):
 | |
|         """Getting a random ingredient item as filler"""
 | |
|         ingredients = []
 | |
|         for name, data in item_table.items():
 | |
|             if data.group == ItemGroup.INGREDIENT:
 | |
|                 ingredients.append(name)
 | |
|         filler_item_name = self.random.choice(ingredients)
 | |
|         return filler_item_name
 | |
| 
 | |
|     def create_items(self) -> None:
 | |
|         """Create every item in the world"""
 | |
|         precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
 | |
|         if self.options.turtle_randomizer.value != TurtleRandomizer.option_none:
 | |
|             if self.options.turtle_randomizer.value == TurtleRandomizer.option_all_except_final:
 | |
|                 self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
 | |
|                                      precollected)
 | |
|         else:
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_LEFT,
 | |
|                                  AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE, precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT,
 | |
|                                  AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE, precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_OPEN_WATERS,
 | |
|                                  AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_KELP_FOREST,
 | |
|                                  AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_HOME_WATERS, AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|             # The last two are inverted because in the original game, they are special turtle that communicate directly
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_SIMON_SAYS, AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
 | |
|                                  precollected, ItemClassification.progression)
 | |
|             self.__pre_fill_item(ItemNames.TRANSTURTLE_ARNASSI_RUINS, AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
 | |
|                                  precollected)
 | |
|         for name, data in item_table.items():
 | |
|             if name not in self.exclude:
 | |
|                 for i in range(data.count):
 | |
|                     item = self.create_item(name)
 | |
|                     self.multiworld.itempool.append(item)
 | |
| 
 | |
|     def set_rules(self) -> None:
 | |
|         """
 | |
|         Launched when the Multiworld generator is ready to generate rules
 | |
|         """
 | |
|         if self.options.early_energy_form == EarlyEnergyForm.option_early:
 | |
|             self.multiworld.early_items[self.player][ItemNames.ENERGY_FORM] = 1
 | |
|         elif self.options.early_energy_form == EarlyEnergyForm.option_early_and_local:
 | |
|             self.multiworld.local_early_items[self.player][ItemNames.ENERGY_FORM] = 1
 | |
|         if self.options.early_bind_song == EarlyBindSong.option_early:
 | |
|             self.multiworld.early_items[self.player][ItemNames.BIND_SONG] = 1
 | |
|         elif self.options.early_bind_song == EarlyBindSong.option_early_and_local:
 | |
|             self.multiworld.local_early_items[self.player][ItemNames.BIND_SONG] = 1
 | |
|         self.regions.adjusting_rules(self.options)
 | |
|         self.multiworld.completion_condition[self.player] = lambda \
 | |
|                 state: state.has(ItemNames.VICTORY, self.player)
 | |
| 
 | |
|     def generate_basic(self) -> None:
 | |
|         """
 | |
|         Player-specific randomization that does not affect logic.
 | |
|         Used to fill then `ingredients_substitution` list
 | |
|         """
 | |
|         simple_ingredients_substitution = [i for i in range(27)]
 | |
|         if self.options.ingredient_randomizer.value > IngredientRandomizer.option_off:
 | |
|             if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
 | |
|                 simple_ingredients_substitution.pop(-1)
 | |
|                 simple_ingredients_substitution.pop(-1)
 | |
|                 simple_ingredients_substitution.pop(-1)
 | |
|             self.random.shuffle(simple_ingredients_substitution)
 | |
|             if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
 | |
|                 simple_ingredients_substitution.extend([24, 25, 26])
 | |
|         dishes_substitution = [i for i in range(27, 76)]
 | |
|         if self.options.dish_randomizer:
 | |
|             self.random.shuffle(dishes_substitution)
 | |
|         self.ingredients_substitution.clear()
 | |
|         self.ingredients_substitution.extend(simple_ingredients_substitution)
 | |
|         self.ingredients_substitution.extend(dishes_substitution)
 | |
| 
 | |
|     def fill_slot_data(self) -> Dict[str, Any]:
 | |
|         return {"ingredientReplacement": self.ingredients_substitution,
 | |
|                 "aquarian_translate": bool(self.options.aquarian_translation.value),
 | |
|                 "blind_goal": bool(self.options.blind_goal.value),
 | |
|                 "secret_needed":
 | |
|                     self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator,
 | |
|                 "minibosses_to_kill": self.options.mini_bosses_to_beat.value,
 | |
|                 "bigbosses_to_kill": self.options.big_bosses_to_beat.value,
 | |
|                 "skip_first_vision": bool(self.options.skip_first_vision.value),
 | |
|                 "unconfine_home_water_energy_door":
 | |
|                     self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door
 | |
|                     or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
 | |
|                 "unconfine_home_water_transturtle":
 | |
|                     self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_transturtle
 | |
|                     or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
 | |
|                 "bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb),
 | |
|                 "no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations),
 | |
|                 "light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places),
 | |
|                 "turtle_randomizer": self.options.turtle_randomizer.value
 | |
|                 }
 | 
