From 3e6b94570600253a800522d165e568db464422b4 Mon Sep 17 00:00:00 2001 From: Ehseezed <97066152+Ehseezed@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:06:23 -0500 Subject: [PATCH] Remade branches because I'm stupid --- worlds/snailiad/__init__.py | 283 +++++++++++++++++++ worlds/snailiad/docs/en_Snailiad+.md | 11 + worlds/snailiad/docs/setup_en.md | 1 + worlds/snailiad/items.py | 75 +++++ worlds/snailiad/locations.py | 90 ++++++ worlds/snailiad/options.py | 104 +++++++ worlds/snailiad/regions.py | 11 + worlds/snailiad/rules.py | 393 +++++++++++++++++++++++++++ 8 files changed, 968 insertions(+) create mode 100644 worlds/snailiad/__init__.py create mode 100644 worlds/snailiad/docs/en_Snailiad+.md create mode 100644 worlds/snailiad/docs/setup_en.md create mode 100644 worlds/snailiad/items.py create mode 100644 worlds/snailiad/locations.py create mode 100644 worlds/snailiad/options.py create mode 100644 worlds/snailiad/regions.py create mode 100644 worlds/snailiad/rules.py diff --git a/worlds/snailiad/__init__.py b/worlds/snailiad/__init__.py new file mode 100644 index 000000000000..51f4986ae614 --- /dev/null +++ b/worlds/snailiad/__init__.py @@ -0,0 +1,283 @@ +from typing import Dict, List, Any + +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification +from worlds.generic.Rules import set_rule +from .items import item_name_to_id, item_table, filler_items, item_name_group, slot_data_item_names +from .locations import location_table, location_name_group, location_name_to_id +from .rules import create_region_rules, create_location_rules, blue_door, green_door, red_door, pink_door, \ + has_devastator +from .regions import snailiad_regions +from .options import SnailiadOptions +from worlds.AutoWorld import WebWorld, World +from decimal import Decimal, ROUND_HALF_UP + + +class SnailiadWeb(WebWorld): + tutorials = [ + Tutorial( + tutorial_name= "Multiworld Setup Guide", + description= "A Guide to setting up a Snailiad+ Multiworld", + language="English", + file_name="setup_en.md", + link="setup/en", + authors=["Ehseezed, EpsilonTheDerg"] + ) + ] + theme = "ice" + game = "Snailiad+" + + +class SnailiadItem(Item): + game: str = "Snailiad" + + +class SnailiadLocation(Location): + game: str = "Snailiad" + + +class SnailiadWorld(World): + """ + Snailâ„¢ + """ # todo Description + game = "Snailiad+" + web = SnailiadWeb() + + data_version = 0 + options: SnailiadOptions + options_dataclass = SnailiadOptions + item_name_groups = item_name_group + location_name_groups = location_name_group + + item_name_to_id = item_name_to_id + location_name_to_id = location_name_to_id + + slot_data_items: List[SnailiadItem] + + def generate_early(self) -> None: + if hasattr(self.multiworld, "re_gen_passthrough"): + if "Snailiad" in self.multiworld.re_gen_passthrough: + passthrough = self.multiworld.re_gen_passthrough["Snailiad"] + self.options.Progressive_Items.value = passthrough["Progressive_Items"] + self.options.Randomization_Type.value = passthrough["Randomization_Type"] + + def create_item(self, name: str) -> SnailiadItem: + item_data = item_table[name] + return SnailiadItem(name, item_data.classification, self.item_name_to_id[name], self.player) + + def create_items(self) -> None: + character = self.options.Character_Select + progressive = self.options.Progressive_Items + helix_locks = self.options.Helix_Locks + difficulty = self.options.Difficulty_Select + traps = self.options.Trap_Fill + + nothings = 2 # SSB and DRW will never be generated + nothings += 3 # Squared Snelks, Scorching Snelks, and Hidden Hideout did not have items in the original game they are rando exclusive + + snailiad_items: List[SnailiadItem] = [] + + items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} + + if self.options.Randomization_Type.value != self.options.Randomization_Type.option_pro: + nothings -= 5 # Original Testing Room, Scorching Snelks, Squared Snelks, Hidden Hideout and Lost Loot are not locations + + if progressive: + items_to_create["Pea Shooter"] = 0 + items_to_create["Boomerang"] = 0 + items_to_create["Rainbow Wave"] = 0 + items_to_create["Progressive Weapon"] = 3 + + items_to_create["Rapid Fire"] = 0 + items_to_create["Devastator"] = 0 + items_to_create["Progressive Modifier"] = 2 + + items_to_create["Ice Snail"] = 0 + items_to_create["Gravity Shell"] = 0 + items_to_create["Full Metal Snail"] = 0 + if difficulty == difficulty.option_insane: + items_to_create["Progressive Shell"] = 2 + nothings += 1 + else: + items_to_create["Progressive Shell"] = 3 + + if difficulty == difficulty.option_insane: + items_to_create["Ice Snail"] = 0 + nothings += 1 + + if character == character.option_sluggy: + items_to_create["Shell Shield"] = 0 + nothings += 1 + + if character == character.option_upside: + items_to_create["Gravity Shell"] = 0 + items_to_create["Magnetic Foot"] = 1 + + if character == character.option_leggy: + items_to_create["Gravity Shell"] = 0 + items_to_create["Corkscrew Jump"] = 1 + + if character == character.option_blobby: + items_to_create["Gravity Shell"] = 0 + items_to_create["Angel Hop"] = 1 + + items_to_create["High Jump"] = 0 + items_to_create["Wall Grab"] = 1 + + if character == character.option_leechy: + items_to_create["Shell Shield"] = 0 + nothings += 1 + + items_to_create["Rapid Fire"] = 0 + items_to_create["Backfire"] = 1 + + # my initial plan to swap the progression status did not work so I swapped to this combo setup from TUNIC and Mssenger for readbility and functionality + if helix_locks: + if self.options.Trap_Fill: + total_pieces = 25 + else: + total_pieces = 30 + req_pieces = 25 + fragments = [self.create_item("Helix Fragment") for _ in range(total_pieces)] + for i in range(0, req_pieces): + fragments[i].classification = ItemClassification.progression_skip_balancing + items_to_create["Helix Fragment"] = 0 + + items_to_create["Nothing"] = nothings + + req_hearts = 4 + hearts = [self.create_item("Heart Container") for _ in range(11)] + for i in range(0, req_hearts): + hearts[i].classification = ItemClassification.progression + self.multiworld.itempool += hearts + + for item, quantity in items_to_create.items(): + for i in range(0, quantity): + snailiad_item: SnailiadItem = self.create_item(item) + snailiad_items.append(snailiad_item) + + self.multiworld.itempool += snailiad_items + + def create_regions(self) -> None: + for region_name in snailiad_regions: + region = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(region) + + for region_name, exits in snailiad_regions.items(): + region = self.multiworld.get_region(region_name, self.player) + region.add_exits(exits) + + for location_name, location_id in self.location_name_to_id.items(): + region = self.multiworld.get_region(location_table[location_name].region, self.player) + location = SnailiadLocation(self.player, location_name, location_id, region) + region.locations.append(location) + + if self.options.Randomization_Type.value != self.options.Randomization_Type.option_pro: + a0 = self.multiworld.get_region("Snail Town", self.player) + a0.locations.remove(self.multiworld.get_location("Original Testing Room", self.player)) + + a2 = self.multiworld.get_region("Spiralis Silere", self.player) + a2.locations.remove(self.multiworld.get_location("Squared Snelks", self.player)) + + a3 = self.multiworld.get_region("Amastrida Abyssus", self.player) + a3.locations.remove(self.multiworld.get_location("Scorching Snelks", self.player)) + a3.locations.remove(self.multiworld.get_location("Hidden Hideout", self.player)) + + a4 = self.multiworld.get_region("Lux Lirata", self.player) + a4.locations.remove(self.multiworld.get_location("Lost Loot", self.player)) + + boss_1_region = self.multiworld.get_region("Mare Carelia", self.player) + boss_1_location = SnailiadLocation(self.player, "Shell Breaker", None, boss_1_region) + + boss_1_location.place_locked_item(SnailiadItem("Boss 1", ItemClassification.progression, None, self.player)) + boss_1_region.locations.append(boss_1_location) + + boss_2_region = self.multiworld.get_region("Spiralis Silere", self.player) + boss_2_location = SnailiadLocation(self.player, "Stompy", None, boss_2_region) + boss_2_location.place_locked_item(SnailiadItem("Boss 2", ItemClassification.progression, None, self.player)) + boss_2_region.locations.append(boss_2_location) + + boss_3_region = self.multiworld.get_region("Amastrida Abyssus", self.player) + boss_3_location = SnailiadLocation(self.player, "Space Box", None, boss_3_region) + boss_3_location.place_locked_item(SnailiadItem("Boss 3", ItemClassification.progression, None, self.player)) + boss_3_region.locations.append(boss_3_location) + + boss_4_region = self.multiworld.get_region("Lux Lirata", self.player) + boss_4_location = SnailiadLocation(self.player, "Moon Snail", None, boss_4_region) + boss_4_location.place_locked_item(SnailiadItem("Boss 4", ItemClassification.progression, None, self.player)) + boss_4_region.locations.append(boss_4_location) + + helix_region = self.multiworld.get_region("Shrine of Iris", self.player) + helix_location = SnailiadLocation(self.player, "Helix Fragment Cutscene", None, helix_region) + helix_location.place_locked_item(SnailiadItem("All Helix\'s", ItemClassification.progression, None, self.player)) + helix_region.locations.append(helix_location) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Boss 4", self.player) + + def set_rules(self) -> None: + create_region_rules(self) + create_location_rules(self) + # Boss event location requirements + if self.options.Helix_Locks: + set_rule(self.multiworld.get_location("Shell Breaker", self.player), + lambda state: blue_door(state, self.player, self) and state.has("Helix Fragment", self.player, 5)) + set_rule(self.multiworld.get_location("Stompy", self.player), + lambda state: pink_door(state, self.player, self) and state.has("Helix Fragment", self.player, 10)) + set_rule(self.multiworld.get_location("Space Box", self.player), + lambda state: red_door(state, self.player, self) and state.has("Helix Fragment", self.player, 15)) + set_rule(self.multiworld.get_location("Moon Snail", self.player), + lambda state: green_door(state, self.player, self) and state.has("Helix Fragment", self.player, 25)) + else: + set_rule(self.multiworld.get_location("Shell Breaker", self.player), + lambda state: blue_door(state, self.player, self)) + set_rule(self.multiworld.get_location("Stompy", self.player), + lambda state: pink_door(state, self.player, self)) + set_rule(self.multiworld.get_location("Space Box", self.player), + lambda state: red_door(state, self.player, self)) + set_rule(self.multiworld.get_location("Moon Snail", self.player), + lambda state: green_door(state, self.player, self) and has_devastator(state, self.player)) + + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_items) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "seed": self.random.randint(0, 2147483647), + "Randomization_Type": self.options.Randomization_Type.value, + "Difficulty_Select": self.options.Difficulty_Select.value, + "Character_Select": self.options.Character_Select.value, + "Progressive_Items": self.options.Progressive_Items.value, + "Start_With_Broom": self.options.Start_With_Broom.value, + "Open_Areas": self.options.Open_Areas.value, + "Helix_Locks": self.options.Helix_Locks.value, + "Music_Shuffle": self.options.Music_Shuffle.value, + "Snails_Have_Hints": self.options.Snails_Have_Hints.value, + "Trap_Fill": self.options.Trap_Fill.value, + "Hidden_Items": self.options.Hidden_Items.value + } + + for start_item in self.options.start_inventory_from_pool: + if start_item in slot_data_item_names: + if start_item not in slot_data: + slot_data[start_item] = [] + for i in range(0, self.options.start_inventory_from_pool[start_item]): + slot_data[start_item].extend(["Your Shell(?)", self.player]) + + for plando_item in self.multiworld.plando_items[self.player]: + if plando_item["from_pool"]: + items_to_find = set() + for item_type in [key for key in ["item", "items"] if key in plando_item]: + for item in plando_item[item_type]: + items_to_find.add(item) + for item in items_to_find: + if item in slot_data_item_names: + slot_data[item] = [] + for item_location in self.multiworld.find_item_locations(item, self.player): + slot_data[item].extend([item_location.name, item_location.player]) + + return slot_data + + @staticmethod # for universal tracker + def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: + # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough + return slot_data diff --git a/worlds/snailiad/docs/en_Snailiad+.md b/worlds/snailiad/docs/en_Snailiad+.md new file mode 100644 index 000000000000..62ddb9a4b82d --- /dev/null +++ b/worlds/snailiad/docs/en_Snailiad+.md @@ -0,0 +1,11 @@ +# Snailiad+ + +## What is Snailiad+? +Snailiad+ is a remake of the original Snailiad flash game, with new content and features. +It is a metroidvania-style game where you play as a snail named Snaily, who must save the world from an alien invasion. +The game features a variety of power-ups and abilities, as well as a large interconnected world to explore. + +## What does randomization do to this game? + +In the Snailiad+ Randomizer, the locations of all the power-ups and abilities in the game are shuffled around with the +possibility of being locked behind other power-ups or abilities completely changing how you approach the world. \ No newline at end of file diff --git a/worlds/snailiad/docs/setup_en.md b/worlds/snailiad/docs/setup_en.md new file mode 100644 index 000000000000..71926e3caa77 --- /dev/null +++ b/worlds/snailiad/docs/setup_en.md @@ -0,0 +1 @@ +Just set it up 4-head \ No newline at end of file diff --git a/worlds/snailiad/items.py b/worlds/snailiad/items.py new file mode 100644 index 000000000000..ebde4653ce5e --- /dev/null +++ b/worlds/snailiad/items.py @@ -0,0 +1,75 @@ +from itertools import groupby +from typing import Dict, List, Set, NamedTuple +from BaseClasses import ItemClassification + + +class SnailiadItemData(NamedTuple): + classification: ItemClassification + quantity_in_item_pool: int + item_id_offset: int + in_game_id: int + item_group: str = "" + + +item_base_id = 609342400 + +item_table: Dict[str, SnailiadItemData] = { + "Pea Shooter": SnailiadItemData(ItemClassification.progression, 1, 0, 0, "Weapon"), # todo Progression or Not + "Boomerang": SnailiadItemData(ItemClassification.progression, 1, 1, 1, "Weapon"), + "Super Secret Boomerang": SnailiadItemData(ItemClassification.progression, 0, 2, 11, "Weapon"), # not generated but kept here for parity + "Rainbow Wave": SnailiadItemData(ItemClassification.progression, 1, 3, 2, "Weapon"), + "Debug Rainbow Wave": SnailiadItemData(ItemClassification.progression, 0, 4, 12, "Weapon"), # not generated but kept here for parity + "Progressive Weapon": SnailiadItemData(ItemClassification.progression, 0, 5, 100000, "Weapon"), # todo confirm with epsilon what IDs the progressibve items are + + "Rapid Fire": SnailiadItemData(ItemClassification.progression, 1, 6, 6, "Modifier"), + "Backfire": SnailiadItemData(ItemClassification.progression, 0, 7, 6, "Modifier"), + "Devastator": SnailiadItemData(ItemClassification.progression, 1, 8, 3, "Modifier"), + "Progressive Modifier": SnailiadItemData(ItemClassification.progression, 0, 9, 200000, "Modifier"), + + "Ice Snail": SnailiadItemData(ItemClassification.progression, 1, 10, 7, "Shell"), + "Gravity Shell": SnailiadItemData(ItemClassification.progression, 1, 11, 8, "Shell"), + "Magnetic Foot": SnailiadItemData(ItemClassification.progression, 0, 12, 8, "Shell"), + "Corkscrew Jump": SnailiadItemData(ItemClassification.progression, 0, 13, 8, "Shell"), + "Angel Hop": SnailiadItemData(ItemClassification.progression, 0, 14, 8, "Shell"), + "Full Metal Snail": SnailiadItemData(ItemClassification.progression, 1, 15, 9, "Shell"), + "Progressive Shell": SnailiadItemData(ItemClassification.progression, 0, 16, 300000, "Shell"), + + "Gravity Shock": SnailiadItemData(ItemClassification.progression, 1, 17, 10, "Ability"), + "High Jump": SnailiadItemData(ItemClassification.progression, 1, 18, 4, "Ability"), + "Wall Grab": SnailiadItemData(ItemClassification.progression, 0, 19, 4, "Ability"), + "Shell Shield": SnailiadItemData(ItemClassification.progression, 1, 20, 5, "Ability"), + "Shellmet": SnailiadItemData(ItemClassification.progression, 0, 21, 5, "Ability"), + + "Helix Fragment": SnailiadItemData(ItemClassification.filler, 30, 22, 24, ""), + "Nothing": SnailiadItemData(ItemClassification.filler, 0, 23, 400000, ""), # created in __init__ + "Heart Container": SnailiadItemData(ItemClassification.filler, 0, 24, 13, ""), # created in __init__ + + "Gravity Lock": SnailiadItemData(ItemClassification.trap, 0, 25, 500000, "Trap"), + "Weapon Lock": SnailiadItemData(ItemClassification.trap, 0, 26, 600000, "Trap"), + "Lullaby Trap": SnailiadItemData(ItemClassification.trap, 0, 27, 700000, "Trap"), + "Spider Trap": SnailiadItemData(ItemClassification.trap, 0, 28, 800000, "Trap"), +} + +slot_data_item_names = [] + +item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} + +filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler] + + +def get_item_group(item_name: str) -> str: + return item_table[item_name].item_group + + +item_name_group: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != "" +} + +extra_groups: Dict[str, Set[str]] = { + "Flight": {"Angel Hop", "Corkscrew Jump", "Magnetic Foot", "Gravity Shell"}, + "Jump": {"High Jump", "Wall Grab"}, + "Shield": {"Shell Shield", "Shellmet"}, + "Rapid": {"Rapid Fire", "Backfire"}, +} # if item groups are requested to be added, they can be added here + +item_name_group.update(extra_groups) diff --git a/worlds/snailiad/locations.py b/worlds/snailiad/locations.py new file mode 100644 index 000000000000..730cef02794b --- /dev/null +++ b/worlds/snailiad/locations.py @@ -0,0 +1,90 @@ +from typing import Dict, Set, NamedTuple +from itertools import groupby + + +class SnailiadLocationData(NamedTuple): + region: str + location_group: str = "region" + + +location_based_id = 609342400 # todo actual base ID + +location_table: Dict[str, SnailiadLocationData] = { + "Leggy Snail\'s Tunnel": SnailiadLocationData("Snail Town"), + "Town Overtunnel": SnailiadLocationData("Snail Town"), + "Super Secret Alcove": SnailiadLocationData("Snail Town"), + "Love Snail\'s Alcove": SnailiadLocationData("Snail Town"), + "Suspicious Tree": SnailiadLocationData("Snail Town"), + "Anger Management Room": SnailiadLocationData("Snail Town"), + "Percentage Snail\'s Hidey Hole": SnailiadLocationData("Snail Town"), + "Digging Grounds": SnailiadLocationData("Snail Town"), + "Cave Snail\'s Cave": SnailiadLocationData("Snail Town"), + "Fragment Cave": SnailiadLocationData("Snail Town"), + # Mare Carelia + "Discombobulatory Alcove": SnailiadLocationData("Mare Carelia"), + "Seabed Caves": SnailiadLocationData("Mare Carelia"), + "Fine Dining (Peashooter)": SnailiadLocationData("Mare Carelia"), + "Fine Dining (Fragment)": SnailiadLocationData("Mare Carelia"), + "The Maze Room": SnailiadLocationData("Mare Carelia"), + "Monument of Greatness": SnailiadLocationData("Mare Carelia"), + "Heart of the Sea": SnailiadLocationData("Mare Carelia"), + "Daily Helping of Calcium": SnailiadLocationData("Mare Carelia"), + "Dig, Snaily, Dig": SnailiadLocationData("Mare Carelia"), + "Skywatcher\'s Loot": SnailiadLocationData("Mare Carelia"), + "Signature Croissants (Boomerang)": SnailiadLocationData("Mare Carelia"), + "Signature Croissants (Heart)": SnailiadLocationData("Mare Carelia"), + # Spiralis Silere + "Frost Shrine": SnailiadLocationData("Spiralis Silere"), + "Sweater Required": SnailiadLocationData("Spiralis Silere"), + "A Secret to Snowbody": SnailiadLocationData("Spiralis Silere"), + "Devil\'s Alcove": SnailiadLocationData("Spiralis Silere"), + "Ice Climb": SnailiadLocationData("Spiralis Silere"), + "The Labyrinth (Fragment)": SnailiadLocationData("Spiralis Silere"), + "The Labyrinth (High Jump)": SnailiadLocationData("Spiralis Silere"), + "Sneaky, Sneaky": SnailiadLocationData("Spiralis Silere"), + "Prismatic Prize (Rainbow Wave)": SnailiadLocationData("Spiralis Silere"), + "Prismatic Prize (Heart)": SnailiadLocationData("Spiralis Silere"), + # Amastrida Abyssus + "Hall of Fire": SnailiadLocationData("Amastrida Abyssus"), + "Green Cache": SnailiadLocationData("Amastrida Abyssus"), + "Furnace": SnailiadLocationData("Amastrida Abyssus"), + "Slitherine Grove": SnailiadLocationData("Amastrida Abyssus"), + "Floaty Fortress (Top Left)": SnailiadLocationData("Amastrida Abyssus"), + "Floaty Fortress (Bottom Right)": SnailiadLocationData("Amastrida Abyssus"), + "Woah Mama": SnailiadLocationData("Amastrida Abyssus"), + "Shocked Shell": SnailiadLocationData("Amastrida Abyssus"), + "Gravity Shrine": SnailiadLocationData("Amastrida Abyssus"), + "Fast Food": SnailiadLocationData("Amastrida Abyssus"), + "The Bridge": SnailiadLocationData("Amastrida Abyssus"), + # lux lirata + "Transit 90": SnailiadLocationData("Lux Lirata"), + "Steel Shrine": SnailiadLocationData("Lux Lirata"), + "Space Balcony (Heart)": SnailiadLocationData("Lux Lirata"), + "Space Balcony (Fragment)": SnailiadLocationData("Lux Lirata"), + "The Vault": SnailiadLocationData("Lux Lirata"), + "Holy Hideaway": SnailiadLocationData("Lux Lirata"), + "Arctic Alcove": SnailiadLocationData("Lux Lirata"), + "Reinforcements": SnailiadLocationData("Lux Lirata"), + # Shrine of Iris + "Glitched Goodies": SnailiadLocationData("Shrine of Iris"), + # Knowledge locations + "Original Testing Room": SnailiadLocationData("Snail Town"), + "Squared Snelks": SnailiadLocationData("Spiralis Silere"), + "Scorching Snelks": SnailiadLocationData("Amastrida Abyssus"), + "Hidden Hideout": SnailiadLocationData("Amastrida Abyssus"), + "Lost Loot": SnailiadLocationData("Lux Lirata"), +} + +location_name_to_id: Dict[str, int] = {name: location_based_id + index for index, name in enumerate(location_table)} + + +def get_location_group(location_name: str) -> str: + loc_group = location_table[location_name].location_group + if loc_group == "region": + loc_group = location_table[location_name].region.lower() + return loc_group + + +location_name_group: Dict[str, Set[str]] = { + group: set(location_names) for group, location_names in groupby(sorted(location_table, key=get_location_group), get_location_group) if group != "" +} diff --git a/worlds/snailiad/options.py b/worlds/snailiad/options.py new file mode 100644 index 000000000000..17028741aff6 --- /dev/null +++ b/worlds/snailiad/options.py @@ -0,0 +1,104 @@ +from dataclasses import dataclass + +from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, PerGameCommonOptions + + +class RandomizationType(Choice): + """Changes the type of randomization between a Major Minor split + a full item randomizer and one that accounts for knowledge.""" + display_name = "Randomization Type" + option_split = 0 + option_full = 1 + option_pro = 2 + default = 1 + + +class DifficultySelect(Choice): + """Changes the ingame difficulty.""" + display_name = "Difficulty" + option_easy = 0 + option_normal = 1 + option_insane = 2 + default = 1 + + +class CharacterSelect(Choice): + """Chose your Adventurer""" + display_name = "Character Select" + option_snaily = 0 + option_sluggy = 1 + option_upside = 2 + option_leggy = 3 + option_blobby = 4 + option_leechy = 5 + default = 0 + + +class ProgressiveItems(DefaultOnToggle): + """an on/off switch that toggles the stacking of various items. + There are three progressive items: Weapon (pea > boom > wave), + Weapon Mod (rapid > devastator), and Shell (ice > grav > metal)""" + display_name = "Progressive Items" + + +class StartWithBroom(Toggle): + """toggles the option to start the character with a Broom weapon + in slot zero. The Broom acts like a melee version of the Peashooter, + able to dispatch enemies, open blue doors, and get upgraded by + Rapid Fire and Devastator, but is held back by its rate of fire and range""" + display_name = "Start with Broom" + + +class OpenAreas(Toggle): + """Removes the grey boss doors separating the areas of the game""" + display_name = "Open Areas" + + +class HelixLocks(Toggle): + """lets you lock boss doors until you have a certain + number of helix fragments under your belt. You need + 5 for boss 1, 10 for boss 2, 15 for boss 3, and 25 for boss 4""" + display_name = "Helix Locks" + + +class MusicShuffle(Choice): + """Enables the Music Randomizer""" + display_name = "Music Shuffle" + option_off = 0 + option_area = 1 + option_full = 2 + default = 0 + + +class SnailsHaveHints(Toggle): + """Enables the Snail Randomizer""" + display_name = "Snails Have Hints" + + +class TrapFill(Range): + """Number of traps to be placed in the world""" + display_name = "Trap Fill" + range_start = 0 + range_end = 25 + default = 0 + + +class HiddenItems(Toggle): + """Changes the item appearance to not have a visual tell for what it is""" + display_name = "Hidden Items" + + +@dataclass +class SnailiadOptions(PerGameCommonOptions): + Randomization_Type: RandomizationType + Difficulty_Select: DifficultySelect + Character_Select: CharacterSelect + Progressive_Items: ProgressiveItems + Start_With_Broom: StartWithBroom + Open_Areas: OpenAreas + Helix_Locks: HelixLocks + Music_Shuffle: MusicShuffle + Snails_Have_Hints: SnailsHaveHints + Trap_Fill: TrapFill + Hidden_Items: HiddenItems + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/snailiad/regions.py b/worlds/snailiad/regions.py new file mode 100644 index 000000000000..bc5a8465d3d3 --- /dev/null +++ b/worlds/snailiad/regions.py @@ -0,0 +1,11 @@ +from typing import Dict, Set + +snailiad_regions: Dict[str, Set[str]] = { + "Menu": {"Snail Town"}, + "Snail Town": {"Mare Carelia", "Spiralis Silere", "Amastrida Abyssus", "Lux Lirata", "Shrine of Iris"}, + "Mare Carelia": {"Spiralis Silere"}, + "Spiralis Silere": {"Amastrida Abyssus"}, + "Amastrida Abyssus": {"Lux Lirata"}, + "Lux Lirata": set(), + "Shrine of Iris": set() +} diff --git a/worlds/snailiad/rules.py b/worlds/snailiad/rules.py new file mode 100644 index 000000000000..ab2d108a2aa7 --- /dev/null +++ b/worlds/snailiad/rules.py @@ -0,0 +1,393 @@ +from typing import TYPE_CHECKING + +from worlds.generic.Rules import set_rule +from BaseClasses import CollectionState +from .options import SnailiadOptions +if TYPE_CHECKING: + from . import SnailiadWorld + + +def boss_1(options: SnailiadOptions, state: CollectionState, player: int) -> bool: + if options.Open_Areas == 1: + return True + return state.has("Boss 1", player) + + +def boss_2(options: SnailiadOptions, state: CollectionState, player: int) -> bool: + if options.Open_Areas == 1: + return True + return state.has("Boss 2", player) + + +def boss_3(options: SnailiadOptions, state: CollectionState, player: int) -> bool: + if options.Open_Areas == 1: + return True + return state.has("Boss 3", player) + + +def boss_4(options: SnailiadOptions, state: CollectionState, player: int) -> bool: + if options.Open_Areas == 1: + return True + return state.has("Boss 4", player) + + +def is_snaily(options: SnailiadOptions) -> bool: + if options.Character_Select == 0: + return True + else: + return False + + +def is_sluggy(options: SnailiadOptions) -> bool: + if options.Character_Select == 1: + return True + else: + return False + + +def is_upside(options: SnailiadOptions) -> bool: + if options.Character_Select == 2: + return True + else: + return False + + +def is_leggy(options: SnailiadOptions) -> bool: + if options.Character_Select == 3: + return True + else: + return False + + +def is_blobby(options: SnailiadOptions) -> bool: + if options.Character_Select == 4: + return True + else: + return False + + +def is_leechy(options: SnailiadOptions) -> bool: + if options.Character_Select == 5: + return True + else: + return False + + +def has_health(state: CollectionState, player: int) -> bool: + return state.has("Heart Container", player, 4) + + +def has_high_jump(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return state.has("High Jump", player) or state.has("Wall Grab", player) or can_fly(state, player, world) + + +def can_fly(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_gravity_shell(state, player, world) + + +def has_peashooter(state: CollectionState, player: int, options: SnailiadOptions) -> bool: + if options.Start_With_Broom == 1: + return True + return state.has("Pea Shooter", player) or state.has("Progressive Weapon", player, 1) or has_boomerang(state, player) + + +def has_boomerang(state: CollectionState, player: int) -> bool: + return state.has("Boomerang", player) or state.has("Progressive Weapon", player, 2) or has_rainbow_wave(state, player) + + +def has_rainbow_wave(state: CollectionState, player: int) -> bool: + return state.has("Rainbow Wave", player) or state.has("Progressive Weapon", player, 3) + + +def has_weapon(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + options = world.options + return has_peashooter(state, player, options) or has_boomerang(state, player) or has_rainbow_wave(state, player) + + +def has_devastator(state: CollectionState, player: int) -> bool: + return state.has("Devastator", player) or state.has("Progressive Modifier", player, 2) + + +def has_ice_shell(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + if world.options.Difficulty_Select == 2: + return True + else: + return state.has("Ice Snail", player) or state.has("Progressive Shell", player, 1) + + +def has_gravity_shell(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + if world.options.Difficulty_Select == 2: + return state.has("Gravity Shell", player) or state.has("Progressive Shell", player, 1) or state.has("Magnetic Foot", player) or state.has("Corkscrew Jump", player) or state.has("Angel Hop", player) + else: + return state.has("Gravity Shell", player) or state.has("Progressive Shell", player, 2) or state.has("Magnetic Foot", player) or state.has("Corkscrew Jump", player) or state.has("Angel Hop", player) + + +def has_metal_shell(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + if world.options.Difficulty_Select == 2: + return state.has("Full Metal Snail", player) or state.has("Progressive Shell", player, 2) + else: + return state.has("Full Metal Snail", player) or state.has("Progressive Shell", player, 3) + + +def can_gravity_shock(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return state.has("Gravity Shock", player) and has_gravity_shell(state, player, world) + + +def blue_door(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_weapon(state, player, world) or pink_door(state, player, world) + + +def pink_door(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_boomerang(state, player) or red_door(state, player, world) + + +def red_door(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_rainbow_wave(state, player) or green_door(state, player, world) or can_gravity_shock(state, player, world) + + +def green_door(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return (has_devastator(state, player) and has_weapon(state, player, world)) or (can_gravity_shock(state, player, world) and has_metal_shell(state, player, world)) + + +def level_1_breakables(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_boomerang(state, player) or level_2_breakables(state, player, world) + + +def level_2_breakables(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return has_rainbow_wave(state, player) or level_3_breakables(state, player, world) + + +def level_3_breakables(state: CollectionState, player: int, world: "SnailiadWorld") -> bool: + return (has_devastator(state, player) and has_weapon(state, player, world)) or (can_gravity_shock(state, player, world) and has_metal_shell(state, player, world)) + + +def has_secret_knowledge(options: SnailiadOptions) -> bool: + if options.Randomization_Type == options.Randomization_Type.option_pro: + return True + return False + + +def create_region_rules(world: "SnailiadWorld") -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + multiworld.get_entrance("Snail Town -> Mare Carelia", player).access_rule = \ + lambda state: True + + multiworld.get_entrance("Snail Town -> Spiralis Silere", player).access_rule = \ + lambda state: level_2_breakables(state, player, world) + + multiworld.get_entrance("Snail Town -> Amastrida Abyssus", player).access_rule = \ + lambda state: red_door(state, player, world) + + multiworld.get_entrance("Snail Town -> Lux Lirata", player).access_rule = \ + lambda state: boss_3(options, state, player) and has_gravity_shell(state, player, world) + + multiworld.get_entrance("Snail Town -> Shrine of Iris", player).access_rule = \ + lambda state: level_1_breakables(state, player, world) + + multiworld.get_entrance("Mare Carelia -> Spiralis Silere", player).access_rule = \ + lambda state: level_1_breakables(state, player, world) + + multiworld.get_entrance("Spiralis Silere -> Amastrida Abyssus", player).access_rule = \ + lambda state: boss_2(options, state, player) and red_door(state, player, world) + + multiworld.get_entrance("Amastrida Abyssus -> Lux Lirata", player).access_rule = \ + lambda state: boss_3(options, state, player) + + +def create_location_rules(world: "SnailiadWorld") -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + set_rule(multiworld.get_location("Leggy Snail\'s Tunnel", player), + lambda state: level_1_breakables(state, player, world)) + + set_rule(multiworld.get_location("Town Overtunnel", player), + lambda state: level_1_breakables(state, player, world) or (has_high_jump(state, player, world) and has_secret_knowledge(options)) + or (has_secret_knowledge(options) and (is_sluggy(options) or is_upside(options) or is_leggy(options) or is_leechy(options)))) + + set_rule(multiworld.get_location("Super Secret Alcove", player), + lambda state: (has_secret_knowledge(options) or boss_4(options, state, player)) and (level_1_breakables(state, player, world) or has_high_jump(state, player, world) + or is_sluggy(options) or is_upside(options) or is_leggy(options) or is_leechy(options))) + + set_rule(multiworld.get_location("Love Snail\'s Alcove", player), + lambda state: level_1_breakables(state, player, world) or has_high_jump(state, player, world) or is_sluggy(options) + or is_upside(options) or is_leggy(options) or is_leechy(options)) + + set_rule(multiworld.get_location("Suspicious Tree", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Anger Management Room", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Percentage Snail\'s Hidey Hole", player), + lambda state: level_2_breakables(state, player, world) and ((is_blobby(options) and has_high_jump(state, player, world)) or not (is_blobby(options))) + + set_rule(multiworld.get_location("Digging Grounds", player), + lambda state: True) + + set_rule(multiworld.get_location("Cave Snail\'s Cave", player), + lambda state: True) + + set_rule(multiworld.get_location("Fragment Cave", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Discombobulatory Alcove", player), + lambda state: has_secret_knowledge(options) and ((is_blobby(options) and has_high_jump(state, player, world)) or not (is_blobby(options))) + or can_fly(state, player, world) or is_upside(options) or is_leggy(options)) + + set_rule(multiworld.get_location("Seabed Caves", player), + lambda state: not (is_blobby(options)) or (is_blobby(options) and has_high_jump(state, player, world))) + + set_rule(multiworld.get_location("Fine Dining (Peashooter)", player), + lambda state: True) + + set_rule(multiworld.get_location("Fine Dining (Fragment)", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("The Maze Room", player), + lambda state: level_1_breakables(state, player, world)) + + set_rule(multiworld.get_location("Monument of Greatness", player), + lambda state: level_1_breakables(state, player, world)) + + set_rule(multiworld.get_location("Heart of the Sea", player), + lambda state: red_door(state, player, world)) + + set_rule(multiworld.get_location("Daily Helping of Calcium", player), + lambda state: pink_door(state, player, world) or (has_secret_knowledge(options) and blue_door(state, player, world))) + + set_rule(multiworld.get_location("Dig, Snaily, Dig", player), + lambda state: green_door(state, player, world)) + + set_rule(multiworld.get_location("Skywatcher\'s Loot", player), + lambda state: level_1_breakables(state, player, world)) + + set_rule(multiworld.get_location("Signature Croissants (Boomerang)", player), + lambda state: boss_1(options, state, player)) + + set_rule(multiworld.get_location("Signature Croissants (Heart)", player), + lambda state: boss_1(options, state, player) and level_1_breakables(state, player, world)) + + set_rule(multiworld.get_location("Frost Shrine", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Sweater Required", player), + lambda state: (has_ice_shell(state, player, world) or (has_health(state, player) and (can_fly(state, player, world) + or is_leggy(options)))) and level_1_breakables(state, player, world) and (is_blobby(options) and has_high_jump(state, player, world)) + or not (is_blobby(options))) + + set_rule(multiworld.get_location("A Secret to Snowbody", player), + lambda state: pink_door(state, player, world)) + + set_rule(multiworld.get_location("Devil\'s Alcove", player), + lambda state: green_door(state, player, world)) + + set_rule(multiworld.get_location("Ice Climb", player), + lambda state: has_secret_knowledge(options) or (has_high_jump(state, player, world) or can_fly(state, player, world) + or has_ice_shell(state, player, world) or is_upside(options) or is_leggy(options))) + + set_rule(multiworld.get_location("The Labyrinth (Fragment)", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("The Labyrinth (High Jump)", player), + lambda state: (has_secret_knowledge(options) and not is_upside(options)) + or level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Sneaky, Sneaky", player), + lambda state: red_door(state, player, world)) + + set_rule(multiworld.get_location("Prismatic Prize (Heart)", player), + lambda state: red_door(state, player, world)) + + set_rule(multiworld.get_location("Prismatic Prize (Rainbow Wave)", player), + lambda state: boss_2(options, state, player) or red_door(state, player, world)) + + set_rule(multiworld.get_location("Hall of Fire", player), + lambda state: (has_metal_shell(state, player, world) or has_health(state, player)) + and (pink_door(state, player, world) or red_door(state, player, world))) + + set_rule(multiworld.get_location("Green Cache", player), + lambda state: red_door(state, player, world) or level_3_breakables(state, player, world)) + + set_rule(multiworld.get_location("Furnace", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Slitherine Grove", player), + lambda state: red_door(state, player, world)) + + set_rule(multiworld.get_location("Floaty Fortress (Top Left)", player), + lambda state: pink_door(state, player, world) and (can_fly(state, player, world) or is_upside(options) or is_leggy(options))) + + set_rule(multiworld.get_location("Floaty Fortress (Bottom Right)", player), + lambda state: pink_door(state, player, world) and (can_fly(state, player, world) or is_upside(options) or is_leggy(options))) + + set_rule(multiworld.get_location("Woah Mama", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Shocked Shell", player), + lambda state: level_2_breakables(state, player, world) and (has_high_jump(state, player, world) or is_sluggy(options) + or is_upside(options) or is_leggy(options) or is_leechy(options))) + + set_rule(multiworld.get_location("Gravity Shrine", player), + lambda state: level_2_breakables(state, player, world)) + + set_rule(multiworld.get_location("Fast Food", player), + lambda state: can_fly(state, player, world) or (has_secret_knowledge(options) and red_door(state, player, world))) + + set_rule(multiworld.get_location("The Bridge", player), + lambda state: boss_3(options, state, player) or (red_door(state, player, world) and (has_high_jump(state, player, world) + or is_sluggy(options) or is_upside(options) or is_leggy(options) or is_leechy(options)))) + + set_rule(multiworld.get_location("Transit 90", player), + lambda state: level_3_breakables(state, player, world)) + + set_rule(multiworld.get_location("Steel Shrine", player), # todo how much health? + lambda state: red_door(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player))) + + set_rule(multiworld.get_location("Space Balcony (Heart)", player), + lambda state: level_3_breakables(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player))) + + set_rule(multiworld.get_location("Space Balcony (Fragment)", player), + lambda state: level_3_breakables(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player))) + + set_rule(multiworld.get_location("The Vault", player), + lambda state: red_door(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player))) + + set_rule(multiworld.get_location("Holy Hideaway", player), + lambda state: red_door(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player)) + and (can_fly(state, player, world) or is_upside(options) or is_leggy(options) or is_blobby(options))) + + set_rule(multiworld.get_location("Arctic Alcove", player), + lambda state: red_door(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player)) + and (can_fly(state, player, world) or is_upside(options) or is_leggy(options) or is_blobby(options))) + + set_rule(multiworld.get_location("Reinforcements", player), + lambda state: red_door(state, player, world) and (has_metal_shell(state, player, world) or has_health(state, player)) + and (can_fly(state, player, world) or is_upside(options) or is_leggy(options) or is_blobby(options))) + + set_rule(multiworld.get_location("Glitched Goodies", player), + lambda state: pink_door(state, player, world)) + + if options.Randomization_Type.value == options.Randomization_Type.option_pro: + set_rule(multiworld.get_location("Original Testing Room", player), + lambda state: has_secret_knowledge(options) and level_2_breakables(state, player, world) and + (has_high_jump(state, player, world) or is_upside(options) or is_leggy(options))) + + set_rule(multiworld.get_location("Squared Snelks", player), + lambda state: has_secret_knowledge(options) and + (can_fly(state, player, world) or is_upside(options) or is_leggy(options))) + + set_rule(multiworld.get_location("Scorching Snelks", player), + lambda state: has_secret_knowledge(options) and has_metal_shell(state, player, world) and ( + can_fly(state, player, world) or level_3_breakables(state, player, world) or red_door(state, player, world))) + + set_rule(multiworld.get_location("Hidden Hideout", player), + lambda state: has_secret_knowledge(options) and (can_fly(state, player, world) or level_3_breakables(state, player, world))) + + set_rule(multiworld.get_location("Lost Loot", player), + lambda state: has_secret_knowledge(options) and red_door(state, player, world) and (has_metal_shell(state, player, world) or + has_health(state, player)) and (can_fly(state, player, world) or is_upside(options) or is_leggy(options) or is_blobby(options)))