mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
This PR is mainly refactoring. Here is what changed: - Changing item names so that each words are capitalized (`Energy Form` instead of `Energy form`) - Removing duplication of string literal by using: - Constants for items and locations, - Region's name attribute for entrances, - Clarify some documentations, - Adding some region to be more representative of the game and to remove listing of locations in the rules (prioritize entrance rules over individual location rules). This is the other minor modifications that are not refactoring: - Adding an early bind song option since that can be used to exit starting area. - Changing Sun God to Lumerean God to be coherent with the other gods. - Changing Home Water to Home Waters and Open Water to Open Waters to be coherent with the game. - Removing a rules to have an attack to go in Mithalas Cathedral since you can to get some checks in it without an attack. - Adding some options to slot data to be used with Poptracker. - Fixing a little but still potentially logic breaking bug.
241 lines
11 KiB
Python
241 lines
11 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]
|
|
|
|
|
|
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
|
|
"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 = AquariaRegions(multiworld, player)
|
|
self.ingredients_substitution = []
|
|
self.exclude = []
|
|
|
|
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
|
|
}
|