diff --git a/README.md b/README.md index 36b7a07fb4b3..d60f1b96651f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Currently, the following games are supported: * Faxanadu * Saving Princess * Castlevania: Circle of the Moon +* Inscryption For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 8b39f96068af..d58207806743 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -81,6 +81,9 @@ # Hylics 2 /worlds/hylics2/ @TRPG0 +# Inscryption +/worlds/inscryption/ @DrBibop @Glowbuzz + # Kirby's Dream Land 3 /worlds/kdl3/ @Silvris diff --git a/worlds/inscryption/Items.py b/worlds/inscryption/Items.py new file mode 100644 index 000000000000..7600830ac9e2 --- /dev/null +++ b/worlds/inscryption/Items.py @@ -0,0 +1,158 @@ +from BaseClasses import ItemClassification +from typing import TypedDict, List + +from BaseClasses import Item + + +base_id = 147000 + + +class InscryptionItem(Item): + name: str = "Inscryption" + + +class ItemDict(TypedDict): + name: str + count: int + classification: ItemClassification + + +act1_items: List[ItemDict] = [ + {'name': "Stinkbug Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Stunted Wolf Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Wardrobe Key", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Skink Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Ant Cards", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Caged Wolf Card", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Squirrel Totem Head", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Dagger", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Film Roll", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Ring", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Magnificus Eye", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Oil Painting's Clover Plant", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Extra Candle", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Bee Figurine", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Greater Smoke", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Angler Hook", + 'count': 1, + 'classification': ItemClassification.useful} +] + + +act2_items: List[ItemDict] = [ + {'name': "Camera Replica", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Pile Of Meat", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Epitaph Piece", + 'count': 9, + 'classification': ItemClassification.progression}, + {'name': "Epitaph Pieces", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Monocle", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Bone Lord Femur", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Bone Lord Horn", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Bone Lord Holo Key", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mycologists Holo Key", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Ancient Obol", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Great Kraken Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Drowned Soul Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Salmon Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Dock's Clover Plant", + 'count': 1, + 'classification': ItemClassification.useful} +] + + +act3_items: List[ItemDict] = [ + {'name': "Extra Battery", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Nano Armor Generator", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Mrs. Bomb's Remote", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Inspectometer Battery", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Gems Module", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Lonely Wizbot Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Fishbot Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Ourobot Card", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Holo Pelt", + 'count': 5, + 'classification': ItemClassification.progression}, + {'name': "Quill", + 'count': 1, + 'classification': ItemClassification.progression}, +] + +filler_items: List[ItemDict] = [ + {'name': "Currency", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Card Pack", + 'count': 1, + 'classification': ItemClassification.filler} +] diff --git a/worlds/inscryption/Locations.py b/worlds/inscryption/Locations.py new file mode 100644 index 000000000000..aa124c23e06b --- /dev/null +++ b/worlds/inscryption/Locations.py @@ -0,0 +1,127 @@ +from typing import Dict, List + +from BaseClasses import Location + +base_id = 147000 + + +class InscryptionLocation(Location): + game: str = "Inscryption" + + +act1_locations = [ + "Act 1 - Boss Prospector", + "Act 1 - Boss Angler", + "Act 1 - Boss Trapper", + "Act 1 - Boss Leshy", + "Act 1 - Safe", + "Act 1 - Clock Main Compartment", + "Act 1 - Clock Upper Compartment", + "Act 1 - Dagger", + "Act 1 - Wardrobe Drawer 1", + "Act 1 - Wardrobe Drawer 2", + "Act 1 - Wardrobe Drawer 3", + "Act 1 - Wardrobe Drawer 4", + "Act 1 - Magnificus Eye", + "Act 1 - Painting 1", + "Act 1 - Painting 2", + "Act 1 - Painting 3", + "Act 1 - Greater Smoke" +] + +act2_locations = [ + "Act 2 - Boss Leshy", + "Act 2 - Boss Magnificus", + "Act 2 - Boss Grimora", + "Act 2 - Boss P03", + "Act 2 - Battle Prospector", + "Act 2 - Battle Angler", + "Act 2 - Battle Trapper", + "Act 2 - Battle Sawyer", + "Act 2 - Battle Royal", + "Act 2 - Battle Kaycee", + "Act 2 - Battle Goobert", + "Act 2 - Battle Pike Mage", + "Act 2 - Battle Lonely Wizard", + "Act 2 - Battle Inspector", + "Act 2 - Battle Melter", + "Act 2 - Battle Dredger", + "Act 2 - Dock Chest", + "Act 2 - Forest Cabin Chest", + "Act 2 - Forest Meadow Chest", + "Act 2 - Cabin Wardrobe Drawer", + "Act 2 - Cabin Safe", + "Act 2 - Crypt Casket 1", + "Act 2 - Crypt Casket 2", + "Act 2 - Crypt Well", + "Act 2 - Tower Chest 1", + "Act 2 - Tower Chest 2", + "Act 2 - Tower Chest 3", + "Act 2 - Tentacle", + "Act 2 - Factory Trash Can", + "Act 2 - Factory Drawer 1", + "Act 2 - Factory Drawer 2", + "Act 2 - Factory Chest 1", + "Act 2 - Factory Chest 2", + "Act 2 - Factory Chest 3", + "Act 2 - Factory Chest 4", + "Act 2 - Ancient Obol", + "Act 2 - Bone Lord Femur", + "Act 2 - Bone Lord Horn", + "Act 2 - Bone Lord Holo Key", + "Act 2 - Mycologists Holo Key", + "Act 2 - Camera Replica", + "Act 2 - Clover", + "Act 2 - Monocle", + "Act 2 - Epitaph Piece 1", + "Act 2 - Epitaph Piece 2", + "Act 2 - Epitaph Piece 3", + "Act 2 - Epitaph Piece 4", + "Act 2 - Epitaph Piece 5", + "Act 2 - Epitaph Piece 6", + "Act 2 - Epitaph Piece 7", + "Act 2 - Epitaph Piece 8", + "Act 2 - Epitaph Piece 9" +] + +act3_locations = [ + "Act 3 - Boss Photographer", + "Act 3 - Boss Archivist", + "Act 3 - Boss Unfinished", + "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", + "Act 3 - Bone Lord Room", + "Act 3 - Shop Holo Pelt", + "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", + "Act 3 - Crypt Holo Pelt", + "Act 3 - Tower Holo Pelt", + "Act 3 - Trader 1", + "Act 3 - Trader 2", + "Act 3 - Trader 3", + "Act 3 - Trader 4", + "Act 3 - Trader 5", + "Act 3 - Drawer 1", + "Act 3 - Drawer 2", + "Act 3 - Clock", + "Act 3 - Extra Battery", + "Act 3 - Nano Armor Generator", + "Act 3 - Chest", + "Act 3 - Goobert's Painting", + "Act 3 - Luke's File Entry 1", + "Act 3 - Luke's File Entry 2", + "Act 3 - Luke's File Entry 3", + "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", + "Act 3 - Gems Drone", + "Act 3 - The Great Transcendence", + "Act 3 - Well" +] + +regions_to_locations: Dict[str, List[str]] = { + "Menu": [], + "Act 1": act1_locations, + "Act 2": act2_locations, + "Act 3": act3_locations, + "Epilogue": [] +} diff --git a/worlds/inscryption/Options.py b/worlds/inscryption/Options.py new file mode 100644 index 000000000000..01e9dfb964a4 --- /dev/null +++ b/worlds/inscryption/Options.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass + +from Options import Toggle, Choice, DeathLinkMixin, StartInventoryPool, PerGameCommonOptions, DefaultOnToggle + + +class Act1DeathLinkBehaviour(Choice): + """If DeathLink is enabled, determines what counts as a death in act 1. This affects deaths sent and received. + + - Sacrificed: Send a death when sacrificed by Leshy. Receiving a death will extinguish all candles. + + - Candle Extinguished: Send a death when a candle is extinguished. Receiving a death will extinguish a candle.""" + display_name = "Act 1 Death Link Behaviour" + option_sacrificed = 0 + option_candle_extinguished = 1 + default = 0 + + +class Goal(Choice): + """Defines the goal to accomplish in order to complete the randomizer. + + - Full Story In Order: Complete each act in order. You can return to previously completed acts. + + - Full Story Any Order: Complete each act in any order. All acts are available from the start. + + - First Act: Complete Act 1 by finding the New Game button. Great for a smaller scale randomizer.""" + display_name = "Goal" + option_full_story_in_order = 0 + option_full_story_any_order = 1 + option_first_act = 2 + default = 0 + + +class RandomizeCodes(Toggle): + """Randomize codes and passwords in the game (clocks, safes, etc.)""" + display_name = "Randomize Codes" + + +class RandomizeDeck(Choice): + """Randomize cards in your deck into new cards. + Disable: Disable the feature. + + - Every Encounter Within Same Type: Randomize cards within the same type every encounter (keep rarity/scrybe type). + + - Every Encounter Any Type: Randomize cards into any possible card every encounter. + + - Starting Only: Only randomize cards given at the beginning of runs and acts.""" + display_name = "Randomize Deck" + option_disable = 0 + option_every_encounter_within_same_type = 1 + option_every_encounter_any_type = 2 + option_starting_only = 3 + default = 0 + + +class RandomizeSigils(Choice): + """Randomize sigils printed on the cards into new sigils every encounter. + + - Disable: Disable the feature. + + - Randomize Addons: Only randomize sigils added from sacrifices or other means. + + - Randomize All: Randomize all sigils.""" + display_name = "Randomize Abilities" + option_disable = 0 + option_randomize_addons = 1 + option_randomize_all = 2 + default = 0 + + +class OptionalDeathCard(Choice): + """Add a moment after death in act 1 where you can decide to create a death card or not. + + - Disable: Disable the feature. + + - Always On: The choice is always offered after losing all candles. + + - DeathLink Only: The choice is only offered after receiving a DeathLink event.""" + display_name = "Optional Death Card" + option_disable = 0 + option_always_on = 1 + option_deathlink_only = 2 + default = 2 + + +class SkipTutorial(DefaultOnToggle): + """Skips the first few tutorial runs of act 1. Bones are available from the start.""" + display_name = "Skip Tutorial" + + +class SkipEpilogue(Toggle): + """Completes the goal as soon as the required acts are completed without the need of completing the epilogue.""" + display_name = "Skip Epilogue" + + +class EpitaphPiecesRandomization(Choice): + """Determines how epitaph pieces in act 2 are randomized. This can affect your chances of getting stuck. + + - All Pieces: Randomizes all nine pieces as their own item. + + - In Groups: Randomizes pieces in groups of three. + + - As One Item: Group all nine pieces as a single item.""" + display_name = "Epitaph Pieces Randomization" + option_all_pieces = 0 + option_in_groups = 1 + option_as_one_item = 2 + default = 0 + + +class PaintingChecksBalancing(Choice): + """Generation options for the second and third painting checks in act 1. + + - None: Adds no progression logic to these painting checks. They will all count as sphere 1 (early game checks). + + - Balanced: Adds rules to these painting checks. Early game items are less likely to appear into these paintings. + + - Force Filler: For when you dislike doing these last two paintings. Their checks will only contain filler items.""" + display_name = "Painting Checks Balancing" + option_none = 0 + option_balanced = 1 + option_force_filler = 2 + default = 1 + + +@dataclass +class InscryptionOptions(DeathLinkMixin, PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + act1_death_link_behaviour: Act1DeathLinkBehaviour + goal: Goal + randomize_codes: RandomizeCodes + randomize_deck: RandomizeDeck + randomize_sigils: RandomizeSigils + optional_death_card: OptionalDeathCard + skip_tutorial: SkipTutorial + skip_epilogue: SkipEpilogue + epitaph_pieces_randomization: EpitaphPiecesRandomization + painting_checks_balancing: PaintingChecksBalancing diff --git a/worlds/inscryption/Regions.py b/worlds/inscryption/Regions.py new file mode 100644 index 000000000000..357261da7579 --- /dev/null +++ b/worlds/inscryption/Regions.py @@ -0,0 +1,14 @@ +from typing import Dict, List + +inscryption_regions_all: Dict[str, List[str]] = { + "Menu": ["Act 1", "Act 2", "Act 3", "Epilogue"], + "Act 1": [], + "Act 2": [], + "Act 3": [], + "Epilogue": [] +} + +inscryption_regions_act_1: Dict[str, List[str]] = { + "Menu": ["Act 1"], + "Act 1": [] +} diff --git a/worlds/inscryption/Rules.py b/worlds/inscryption/Rules.py new file mode 100644 index 000000000000..d9791ce94c7a --- /dev/null +++ b/worlds/inscryption/Rules.py @@ -0,0 +1,181 @@ +from typing import Dict, Callable, TYPE_CHECKING +from BaseClasses import CollectionState, LocationProgressType +from .Options import Goal, PaintingChecksBalancing + +if TYPE_CHECKING: + from . import InscryptionWorld +else: + InscryptionWorld = object + + +# Based on The Messenger's implementation +class InscryptionRules: + player: int + world: InscryptionWorld + location_rules: Dict[str, Callable[[CollectionState], bool]] + region_rules: Dict[str, Callable[[CollectionState], bool]] + + def __init__(self, world: InscryptionWorld) -> None: + self.player = world.player + self.world = world + self.location_rules = { + "Act 1 - Wardrobe Drawer 1": self.has_wardrobe_key, + "Act 1 - Wardrobe Drawer 2": self.has_wardrobe_key, + "Act 1 - Wardrobe Drawer 3": self.has_wardrobe_key, + "Act 1 - Wardrobe Drawer 4": self.has_wardrobe_key, + "Act 1 - Dagger": self.has_caged_wolf, + "Act 1 - Magnificus Eye": self.has_dagger, + "Act 1 - Clock Main Compartment": self.has_magnificus_eye, + "Act 2 - Battle Prospector": self.has_camera_and_meat, + "Act 2 - Battle Angler": self.has_camera_and_meat, + "Act 2 - Battle Trapper": self.has_camera_and_meat, + "Act 2 - Battle Pike Mage": self.has_tower_requirements, + "Act 2 - Battle Goobert": self.has_tower_requirements, + "Act 2 - Battle Lonely Wizard": self.has_tower_requirements, + "Act 2 - Battle Inspector": self.has_act2_bridge_requirements, + "Act 2 - Battle Melter": self.has_act2_bridge_requirements, + "Act 2 - Battle Dredger": self.has_act2_bridge_requirements, + "Act 2 - Forest Meadow Chest": self.has_camera_and_meat, + "Act 2 - Tower Chest 1": self.has_act2_bridge_requirements, + "Act 2 - Tower Chest 2": self.has_tower_requirements, + "Act 2 - Tower Chest 3": self.has_tower_requirements, + "Act 2 - Tentacle": self.has_tower_requirements, + "Act 2 - Factory Trash Can": self.has_act2_bridge_requirements, + "Act 2 - Factory Drawer 1": self.has_act2_bridge_requirements, + "Act 2 - Factory Drawer 2": self.has_act2_bridge_requirements, + "Act 2 - Factory Chest 1": self.has_act2_bridge_requirements, + "Act 2 - Factory Chest 2": self.has_act2_bridge_requirements, + "Act 2 - Factory Chest 3": self.has_act2_bridge_requirements, + "Act 2 - Factory Chest 4": self.has_act2_bridge_requirements, + "Act 2 - Monocle": self.has_act2_bridge_requirements, + "Act 2 - Boss Grimora": self.has_all_epitaph_pieces, + "Act 2 - Boss Leshy": self.has_camera_and_meat, + "Act 2 - Boss Magnificus": self.has_tower_requirements, + "Act 2 - Boss P03": self.has_act2_bridge_requirements, + "Act 2 - Bone Lord Femur": self.has_obol, + "Act 2 - Bone Lord Horn": self.has_obol, + "Act 2 - Bone Lord Holo Key": self.has_obol, + "Act 2 - Mycologists Holo Key": self.has_tower_requirements, # Could need money + "Act 2 - Ancient Obol": self.has_tower_requirements, # Need money for the pieces? Use the tower mannequin. + "Act 3 - Boss Photographer": self.has_inspectometer_battery, + "Act 3 - Boss Archivist": self.has_battery_and_quill, + "Act 3 - Boss Unfinished": self.has_gems_and_battery, + "Act 3 - Boss G0lly": self.has_gems_and_battery, + "Act 3 - Extra Battery": self.has_inspectometer_battery, # Hard to miss but soft lock still possible. + "Act 3 - Nano Armor Generator": self.has_gems_and_battery, # Costs money, so can need multiple battles. + "Act 3 - Shop Holo Pelt": self.has_gems_and_battery, # Costs money, so can need multiple battles. + "Act 3 - Middle Holo Pelt": self.has_inspectometer_battery, # Can be reached without but possible soft lock + "Act 3 - Forest Holo Pelt": self.has_inspectometer_battery, + "Act 3 - Crypt Holo Pelt": self.has_inspectometer_battery, + "Act 3 - Tower Holo Pelt": self.has_gems_and_battery, + "Act 3 - Trader 1": self.has_pelts(1), + "Act 3 - Trader 2": self.has_pelts(2), + "Act 3 - Trader 3": self.has_pelts(3), + "Act 3 - Trader 4": self.has_pelts(4), + "Act 3 - Trader 5": self.has_pelts(5), + "Act 3 - Goobert's Painting": self.has_gems_and_battery, + "Act 3 - The Great Transcendence": self.has_transcendence_requirements, + "Act 3 - Boss Mycologists": self.has_mycologists_boss_requirements, + "Act 3 - Bone Lord Room": self.has_bone_lord_room_requirements, + "Act 3 - Luke's File Entry 1": self.has_battery_and_quill, + "Act 3 - Luke's File Entry 2": self.has_battery_and_quill, + "Act 3 - Luke's File Entry 3": self.has_battery_and_quill, + "Act 3 - Luke's File Entry 4": self.has_transcendence_requirements, + "Act 3 - Well": self.has_inspectometer_battery, + "Act 3 - Gems Drone": self.has_inspectometer_battery, + "Act 3 - Clock": self.has_gems_and_battery, # Can be brute-forced, but the solution needs those items. + } + self.region_rules = { + "Act 2": self.has_act2_requirements, + "Act 3": self.has_act3_requirements, + "Epilogue": self.has_epilogue_requirements + } + + def has_wardrobe_key(self, state: CollectionState) -> bool: + return state.has("Wardrobe Key", self.player) + + def has_caged_wolf(self, state: CollectionState) -> bool: + return state.has("Caged Wolf Card", self.player) + + def has_dagger(self, state: CollectionState) -> bool: + return state.has("Dagger", self.player) + + def has_magnificus_eye(self, state: CollectionState) -> bool: + return state.has("Magnificus Eye", self.player) + + def has_useful_act1_items(self, state: CollectionState) -> bool: + return state.has_all(("Oil Painting's Clover Plant", "Squirrel Totem Head"), self.player) + + def has_all_epitaph_pieces(self, state: CollectionState) -> bool: + return state.has(self.world.required_epitaph_pieces_name, self.player, self.world.required_epitaph_pieces_count) + + def has_camera_and_meat(self, state: CollectionState) -> bool: + return state.has_all(("Camera Replica", "Pile Of Meat"), self.player) + + def has_monocle(self, state: CollectionState) -> bool: + return state.has("Monocle", self.player) + + def has_obol(self, state: CollectionState) -> bool: + return state.has("Ancient Obol", self.player) + + def has_epitaphs_and_forest_items(self, state: CollectionState) -> bool: + return self.has_camera_and_meat(state) and self.has_all_epitaph_pieces(state) + + def has_act2_bridge_requirements(self, state: CollectionState) -> bool: + return self.has_camera_and_meat(state) or self.has_all_epitaph_pieces(state) + + def has_tower_requirements(self, state: CollectionState) -> bool: + return self.has_monocle(state) and self.has_act2_bridge_requirements(state) + + def has_inspectometer_battery(self, state: CollectionState) -> bool: + return state.has("Inspectometer Battery", self.player) + + def has_gems_and_battery(self, state: CollectionState) -> bool: + return state.has("Gems Module", self.player) and self.has_inspectometer_battery(state) + + def has_pelts(self, count: int) -> Callable[[CollectionState], bool]: + return lambda state: state.has("Holo Pelt", self.player, count) and self.has_gems_and_battery(state) + + def has_mycologists_boss_requirements(self, state: CollectionState) -> bool: + return state.has("Mycologists Holo Key", self.player) and self.has_transcendence_requirements(state) + + def has_bone_lord_room_requirements(self, state: CollectionState) -> bool: + return state.has("Bone Lord Holo Key", self.player) and self.has_inspectometer_battery(state) + + def has_battery_and_quill(self, state: CollectionState) -> bool: + return state.has("Quill", self.player) and self.has_inspectometer_battery(state) + + def has_transcendence_requirements(self, state: CollectionState) -> bool: + return state.has("Quill", self.player) and self.has_gems_and_battery(state) + + def has_act2_requirements(self, state: CollectionState) -> bool: + return state.has("Film Roll", self.player) + + def has_act3_requirements(self, state: CollectionState) -> bool: + return self.has_act2_requirements(state) and self.has_all_epitaph_pieces(state) and \ + self.has_camera_and_meat(state) and self.has_monocle(state) + + def has_epilogue_requirements(self, state: CollectionState) -> bool: + return self.has_act3_requirements(state) and self.has_transcendence_requirements(state) + + def set_all_rules(self) -> None: + multiworld = self.world.multiworld + if self.world.options.goal != Goal.option_first_act: + multiworld.completion_condition[self.player] = self.has_epilogue_requirements + else: + multiworld.completion_condition[self.player] = self.has_act2_requirements + for region in multiworld.get_regions(self.player): + if self.world.options.goal == Goal.option_full_story_in_order: + if region.name in self.region_rules: + for entrance in region.entrances: + entrance.access_rule = self.region_rules[region.name] + for loc in region.locations: + if loc.name in self.location_rules: + loc.access_rule = self.location_rules[loc.name] + + if self.world.options.painting_checks_balancing == PaintingChecksBalancing.option_balanced: + self.world.get_location("Act 1 - Painting 2").access_rule = self.has_useful_act1_items + self.world.get_location("Act 1 - Painting 3").access_rule = self.has_useful_act1_items + elif self.world.options.painting_checks_balancing == PaintingChecksBalancing.option_force_filler: + self.world.get_location("Act 1 - Painting 2").progress_type = LocationProgressType.EXCLUDED + self.world.get_location("Act 1 - Painting 3").progress_type = LocationProgressType.EXCLUDED diff --git a/worlds/inscryption/__init__.py b/worlds/inscryption/__init__.py new file mode 100644 index 000000000000..d84912e1ca0b --- /dev/null +++ b/worlds/inscryption/__init__.py @@ -0,0 +1,144 @@ +from .Options import InscryptionOptions, Goal, EpitaphPiecesRandomization, PaintingChecksBalancing +from .Items import act1_items, act2_items, act3_items, filler_items, base_id, InscryptionItem, ItemDict +from .Locations import act1_locations, act2_locations, act3_locations, regions_to_locations +from .Regions import inscryption_regions_all, inscryption_regions_act_1 +from typing import Dict, Any +from . import Rules +from BaseClasses import Region, Item, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld + + +class InscrypWeb(WebWorld): + theme = "dirt" + + guide_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Inscryption Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["DrBibop"] + ) + + guide_fr = Tutorial( + "Multiworld Setup Guide", + "Un guide pour configurer Inscryption Archipelago Multiworld", + "Français", + "setup_fr.md", + "setup/fr", + ["Glowbuzz"] + ) + + tutorials = [guide_en, guide_fr] + + bug_report_page = "https://github.com/DrBibop/Archipelago_Inscryption/issues" + + +class InscryptionWorld(World): + """ + Inscryption is an inky black card-based odyssey that blends the deckbuilding roguelike, + escape-room style puzzles, and psychological horror into a blood-laced smoothie. + Darker still are the secrets inscrybed upon the cards... + """ + game = "Inscryption" + web = InscrypWeb() + options_dataclass = InscryptionOptions + options: InscryptionOptions + all_items = act1_items + act2_items + act3_items + filler_items + item_name_to_id = {item["name"]: i + base_id for i, item in enumerate(all_items)} + all_locations = act1_locations + act2_locations + act3_locations + location_name_to_id = {location: i + base_id for i, location in enumerate(all_locations)} + required_epitaph_pieces_count = 9 + required_epitaph_pieces_name = "Epitaph Piece" + + def generate_early(self) -> None: + self.all_items = [item.copy() for item in self.all_items] + + if self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_all_pieces: + self.required_epitaph_pieces_name = "Epitaph Piece" + self.required_epitaph_pieces_count = 9 + elif self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_in_groups: + self.required_epitaph_pieces_name = "Epitaph Pieces" + self.required_epitaph_pieces_count = 3 + else: + self.required_epitaph_pieces_name = "Epitaph Pieces" + self.required_epitaph_pieces_count = 1 + + if self.options.painting_checks_balancing == PaintingChecksBalancing.option_balanced: + self.all_items[6]["classification"] = ItemClassification.progression + self.all_items[11]["classification"] = ItemClassification.progression + + if self.options.painting_checks_balancing == PaintingChecksBalancing.option_force_filler \ + and self.options.goal == Goal.option_first_act: + self.all_items[3]["classification"] = ItemClassification.filler + + if self.options.epitaph_pieces_randomization != EpitaphPiecesRandomization.option_all_pieces: + self.all_items[len(act1_items) + 3]["count"] = self.required_epitaph_pieces_count + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_items)["name"] + + def create_item(self, name: str) -> Item: + item_id = self.item_name_to_id[name] + item_data = self.all_items[item_id - base_id] + return InscryptionItem(name, item_data["classification"], item_id, self.player) + + def create_items(self) -> None: + nb_items_added = 0 + useful_items = self.all_items.copy() + + if self.options.goal != Goal.option_first_act: + useful_items = [item for item in useful_items + if not any(filler_item["name"] == item["name"] for filler_item in filler_items)] + if self.options.epitaph_pieces_randomization == EpitaphPiecesRandomization.option_all_pieces: + useful_items.pop(len(act1_items) + 3) + else: + useful_items.pop(len(act1_items) + 2) + else: + useful_items = [item for item in useful_items + if any(act1_item["name"] == item["name"] for act1_item in act1_items)] + + for item in useful_items: + for _ in range(item["count"]): + new_item = self.create_item(item["name"]) + self.multiworld.itempool.append(new_item) + nb_items_added += 1 + + filler_count = len(self.all_locations if self.options.goal != Goal.option_first_act else act1_locations) + filler_count -= nb_items_added + + for i in range(filler_count): + index = i % len(filler_items) + filler_item = filler_items[index] + new_item = self.create_item(filler_item["name"]) + self.multiworld.itempool.append(new_item) + + def create_regions(self) -> None: + used_regions = inscryption_regions_all if self.options.goal != Goal.option_first_act \ + else inscryption_regions_act_1 + for region_name in used_regions.keys(): + self.multiworld.regions.append(Region(region_name, self.player, self.multiworld)) + + for region_name, region_connections in used_regions.items(): + region = self.get_region(region_name) + region.add_exits(region_connections) + region.add_locations({ + location: self.location_name_to_id[location] for location in regions_to_locations[region_name] + }) + + def set_rules(self) -> None: + Rules.InscryptionRules(self).set_all_rules() + + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict( + "death_link", + "act1_death_link_behaviour", + "goal", + "randomize_codes", + "randomize_deck", + "randomize_sigils", + "optional_death_card", + "skip_tutorial", + "skip_epilogue", + "epitaph_pieces_randomization" + ) diff --git a/worlds/inscryption/docs/en_Inscryption.md b/worlds/inscryption/docs/en_Inscryption.md new file mode 100644 index 000000000000..da6d7c8dcb0b --- /dev/null +++ b/worlds/inscryption/docs/en_Inscryption.md @@ -0,0 +1,22 @@ +# Inscryption + +## Where is the options page? +You can configure your player options with the Inscryption options page. [Click here](../player-options) to start configuring them to your liking. + +## What does randomization do to this game? +Due to the nature of the randomizer, you are allowed to return to a previous act you've previously completed if there are location checks you've missed. The "New Game" option is replaced with a "Chapter Select" option and is enabled after you beat act 1. If you prefer, you can also make all acts available from the start by changing the goal option. All items that you can find lying around, in containers, or from puzzles are randomized and replaced with location checks. Boss fights from all acts and battles from act 2 also count as location checks. + +## What is the goal of Inscryption when randomized? +By default, the goal is considered reached once you open the OLD_DATA file. This means playing through all three acts in order and the epilogue. You can change the goal option to instead complete all acts in any order or simply complete act 1. + +## Which items can be in another player's world? +All key items necessary for progression such as the film roll, the dagger, Grimora's epitaphs, etc. Unique cards that aren't randomly found in the base game (e.g. talking cards) are also included. For filler items, you can receive currency which will be added to every act's bank or card packs that you can open at any time when inspecting your deck. + +## What does another world's item look like in Inscryption? +Items from other worlds usually take the appearance of a normal card from the current act you're playing. The card's name contains the item that will be sent when picked up and its portrait is the Archipelago logo (a ring of six circles). Picking up these cards does not add them to your deck. + +## When the player receives an item, what happens? +The item is instantly granted to you. A yellow message appears in the Archipelago logs at the top-right of your screen. An audio cue is also played. If the item received is a holdable item (wardrobe key, inspectometer battery, gems module), the item will be placed where you would usually collect it in a vanilla playthrough (safe, inspectometer, drone). + +## How many items can I find or receive in my world? +By default, if all three acts are played, there are **100** randomized locations in your world and **100** of your items shuffled in the multiworld. There are **17** locations in act 1 (this will be the total amount if you decide to only play act 1), **52** locations in act 2, and **31** locations in act 3. diff --git a/worlds/inscryption/docs/setup_en.md b/worlds/inscryption/docs/setup_en.md new file mode 100644 index 000000000000..a57e266c4849 --- /dev/null +++ b/worlds/inscryption/docs/setup_en.md @@ -0,0 +1,65 @@ +# Inscryption Randomizer Setup Guide + +## Required Software + +- [Inscryption](https://store.steampowered.com/app/1092790/Inscryption/) +- For easy setup (recommended): + - [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) OR [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) +- For manual setup: + - [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/) + - [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/) + +## Installation +Before starting the installation process, here's what you should know: +- Only install the mods mentioned in this guide if you want a guaranteed smooth experience! Other mods were NOT tested with ArchipelagoMod and could cause unwanted issues. +- The ArchipelagoMod uses its own save file system when playing, but for safety measures, back up your save file by going to your Inscryption installation directory and copy the `SaveFile.gwsave` file to another folder. +- It is strongly recommended to use a mod manager if you want a quicker and easier installation process, but if you don't like installing extra software and are comfortable moving files around, you can refer to the manual setup guide instead. + +### Easy setup (mod manager) +1. Download [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) using the "Manual Download" button, then install it using the executable in the downloaded zip package (You can also use [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) which works the same, but it requires [Overwolf](https://www.overwolf.com/)) +2. Open the mod manager and select Inscryption in the game selection screen. +3. Select the default profile or create a new one. +4. Open the `Online` tab on the left, then search for `ArchipelagoMod`. +5. Expand ArchipelagoMod and click the `Download` button to install the latest version and all its dependencies. +6. Click `Start Modded` to open the game with the mods (a console should appear if everything was done correctly). + +### Manual setup +1. Download the following mods using the `Manual Download` button: + - [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/) + - [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/) +2. Open your Inscryption installation directory. On Steam, you can find it easily by right-clicking the game and clicking `Manage` > `Browse local files`. +3. Open the BepInEx pack zip file, then open the `BepInExPack_Inscryption` folder. +4. Drag all folders and files located inside the `BepInExPack_Inscryption` folder and drop them in your Inscryption directory. +5. Open the `BepInEx` folder in your Inscryption directory. +6. Open the ArchipelagoMod zip file. +7. Drag and drop the `plugins` folder in the `BepInEx` folder to fuse with the existing `plugins` folder. +8. Open the game normally to play with mods (if BepInEx was installed correctly, a console should appear). + +## Joining a new MultiWorld Game +1. After opening the game, you should see a new menu for browsing and creating save files. +2. Click on the `New Game` button, then write a unique name for your save file. +3. On the next screen, enter the information needed to connect to the MultiWorld server, then press the `Connect` button. +4. If successful, the status on the top-right will change to "Connected". If not, a red error message will appear. +5. After connecting to the server and receiving items, the game menu will appear. + +## Continuing a MultiWorld Game +1. After opening the game, you should see a list of your save files and a button to add a new one. +2. Find the save file you want to use, then click its `Play` button. +3. On the next screen, the input fields will be filled with the information you've written previously. You can adjust some fields if needed, then press the `Connect` button. +4. If successful, the status on the top-right will change to "connected". If not, a red error message will appear. +5. After connecting to the server and receiving items, the game menu will appear. + +## Troubleshooting +### The game opens normally without the new menu. +If the new menu mentioned previously doesn't appear, it can be one of two issues: + - If there was no console appearing when opening the game, this means the mods didn't load correctly. Here's what you can try: + - If you are using the mod manager, make sure to open it and press `Start Modded`. Opening the game normally from Steam won't load any mods. + - Check if the mod manager correctly found the game path. In the mod manager, click `Settings` then go to the `Locations` tab. Make sure the path listed under `Change Inscryption directory` is correct. You can verify the real path if you right-click the game on steam and click `Manage` > `Browse local files`. If the path is wrong, click that setting and change the path. + - If you installed the mods manually, this usually means BepInEx was not correctly installed. Make sure to read the installation guide carefully. + - If there is still no console when opening the game modded, try asking in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) for help. + - If there is a console, this means the mods loaded but the ArchipelagoMod wasn't found or had errors while loading. + - Look in the console and make sure you can find a message about ArchipelagoMod being loaded. + - If you see any red text, there was an error. Report the issue in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) or create an issue in our [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues). + +### I'm getting a different issue. +You can ask for help in the [Archipelago Discord Server](https://discord.gg/8Z65BR2) or, if you think you've found a bug with the mod, create an issue in our [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues). \ No newline at end of file diff --git a/worlds/inscryption/docs/setup_fr.md b/worlds/inscryption/docs/setup_fr.md new file mode 100644 index 000000000000..21d0617cbac4 --- /dev/null +++ b/worlds/inscryption/docs/setup_fr.md @@ -0,0 +1,67 @@ +# Guide d'Installation de Inscryption Randomizer + +## Logiciel Exigé + +- [Inscryption](https://store.steampowered.com/app/1092790/Inscryption/) +- Pour une installation facile (recommandé): + - [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) OU [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) +- Pour une installation manuelle: + - [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/) + - [MonoMod Loader for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/MonoMod_Loader_Inscryption/) + - [Inscryption API](https://inscryption.thunderstore.io/package/API_dev/API/) + - [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/) + +## Installation +Avant de commencer le processus d'installation, voici ce que vous deviez savoir: +- Installez uniquement les mods mentionnés dans ce guide si vous souhaitez une expérience stable! Les autres mods n'ont PAS été testés avec ArchipelagoMod et peuvent provoquer des problèmes. +- ArchipelagoMod utilise son propre système de sauvegarde lorsque vous jouez, mais pour des raisons de sécurité, sauvegardez votre fichier de sauvegarde en accédant à votre répertoire d'installation Inscryption et copiez le fichier `SaveFile.gwsave` dans un autre dossier. +- Il est fortement recommandé d'utiliser un mod manager si vous souhaitez avoir un processus d'installation plus rapide et plus facile, mais si vous n'aimez pas installer de logiciels supplémentaires et que vous êtes à l'aise pour déplacer des fichiers, vous pouvez vous référer au guide de configuration manuelle. + +### Installation facile (mod manager) +1. Téléchargez [r2modman](https://inscryption.thunderstore.io/package/ebkr/r2modman/) à l'aide du bouton `Manual Download`, puis installez-le à l'aide de l'exécutable contenu dans le zip téléchargé (vous pouvez également utiliser [Thunderstore Mod Manager](https://www.overwolf.com/app/Thunderstore-Thunderstore_Mod_Manager) qui fonctionne de la même manière, mais cela nécessite [Overwolf](https://www.overwolf.com/)) +2. Ouvrez le mod manager et sélectionnez Inscryption dans l'écran de sélection de jeu. +3. Sélectionnez le profil par défaut ou créez-en un nouveau. +4. Ouvrez l'onglet `Online` à gauche, puis recherchez `ArchipelagoMod`. +5. Développez ArchipelagoMod et cliquez sur le bouton `Download` pour installer la dernière version disponible et toutes ses dépendances. +6. Cliquez sur `Start Modded` pour ouvrir le jeu avec les mods (une console devrait apparaître si tout a été fait correctement). + +### Installation manuelle +1. Téléchargez les mods suivants en utilisant le bouton `Manual Download`: + - [BepInEx pack for Inscryption](https://inscryption.thunderstore.io/package/BepInEx/BepInExPack_Inscryption/) + - [ArchipelagoMod](https://inscryption.thunderstore.io/package/Ballin_Inc/ArchipelagoMod/) +2. Ouvrez votre dossier d'installation d'Inscryption. Sur Steam, vous pouvez le trouver facilement en faisant un clic droit sur le jeu et en cliquant sur `Gérer` > `Parcourir les fichiers locaux`. +3. Ouvrez le fichier zip du pack BepInEx, puis ouvrez le dossier `BepInExPack_Inscryption`. +4. Prenez tous les dossiers et fichiers situés dans le dossier `BepInExPack_Inscryption` et déposez-les dans votre dossier Inscryption. +5. Ouvrez le dossier `BepInEx` dans votre dossier Inscryption. +6. Ouvrez le fichier zip d'ArchipelagoMod. +7. Prenez et déposez le dossier `plugins` dans le dossier `BepInEx` pour fusionner avec le dossier `plugins` existant. +8. Ouvrez le jeu normalement pour jouer avec les mods (si BepInEx a été correctement installé, une console devrait apparaitre). + +## Rejoindre un nouveau MultiWorld +1. Après avoir ouvert le jeu, vous devriez voir un nouveau menu pour parcourir et créer des fichiers de sauvegarde. +2. Cliquez sur le bouton `New Game`, puis écrivez un nom unique pour votre fichier de sauvegarde. +3. Sur l'écran suivant, saisissez les informations nécessaires pour vous connecter au serveur MultiWorld, puis appuyez sur le bouton `Connect`. +4. En cas de succès, l'état de connexion en haut à droite changera pour "Connected". Sinon, un message d'erreur rouge apparaîtra. +5. Après s'être connecté au server et avoir reçu les items, le menu du jeu apparaîtra. + +## Poursuivre une session MultiWorld +1. Après avoir ouvert le jeu, vous devriez voir une liste de vos fichiers de sauvegarde et un bouton pour en ajouter un nouveau. +2. Choisissez le fichier de sauvegarde que vous souhaitez utiliser, puis cliquez sur son bouton `Play`. +3. Sur l'écran suivant, les champs de texte seront remplis avec les informations que vous avez écrites précédemment. Vous pouvez ajuster certains champs si nécessaire, puis appuyer sur le bouton `Connect`. +4. En cas de succès, l'état de connexion en haut à droite changera pour "Connected". Sinon, un message d'erreur rouge apparaîtra. +5. Après s'être connecté au server et avoir reçu les items, le menu du jeu apparaîtra. + +## Dépannage +### Le jeu ouvre normalement sans nouveau menu. +Si le nouveau menu mentionné précédemment n'apparaît pas, c'est peut-être l'un des deux problèmes suivants: + - Si aucune console n'apparait à l'ouverture du jeu, cela signifie que les mods ne se sont pas chargés correctement. Voici ce que vous pouvez essayer: + - Si vous utilisez le mod manager, assurez-vous de l'ouvrir et d'appuyer sur `Start Modded`. Ouvrir le jeu normalement depuis Steam ne chargera aucun mod. + - Vérifiez si le mod manager a correctement trouvé le répertoire du jeu. Dans le mod manager, cliquez sur `Settings` puis allez dans l'onglet `Locations`. Assurez-vous que le répertoire sous `Change Inscryption directory` est correct. Vous pouvez vérifier le répertoire correct si vous faites un clic droit sur le jeu Inscription sur Steam et cliquez sur `Gérer` > `Parcourir les fichiers locaux`. Si le répertoire est erroné, cliquez sur ce paramètre et modifiez le répertoire. + - Si vous avez installé les mods manuellement, cela signifie généralement que BepInEx n'a pas été correctement installé. Assurez-vous de lire attentivement le guide d'installation. + - S'il n'y a toujours pas de console lors de l'ouverture du jeu modifié, essayez de demander de l'aide sur [Archipelago Discord Server](https://discord.gg/8Z65BR2). + - S'il y a une console, cela signifie que les mods ont été chargés, mais que ArchipelagoMod n'a pas été trouvé ou a eu des erreurs lors du chargement. + - Regardez dans la console et assurez-vous que vous trouvez un message concernant le chargement d'ArchipelagoMod. + - Si vous voyez du texte rouge, il y a eu une erreur. Signalez le problème dans [Archipelago Discord Server](https://discord.gg/8Z65BR2) ou dans notre [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues). + +### J'ai un autre problème. +Vous pouvez demander de l'aide sur [le serveur Discord d'Archipelago](https://discord.gg/8Z65BR2) ou, si vous pensez avoir trouvé un bug avec le mod, signalez-le dans notre [GitHub](https://github.com/DrBibop/Archipelago_Inscryption/issues). \ No newline at end of file diff --git a/worlds/inscryption/test/TestAccess.py b/worlds/inscryption/test/TestAccess.py new file mode 100644 index 000000000000..eeafc933bbbc --- /dev/null +++ b/worlds/inscryption/test/TestAccess.py @@ -0,0 +1,221 @@ +from . import InscryptionTestBase + + +class AccessTestGeneral(InscryptionTestBase): + + def test_dagger(self) -> None: + self.assertAccessDependency(["Act 1 - Magnificus Eye"], [["Dagger"]]) + + def test_caged_wolf(self) -> None: + self.assertAccessDependency(["Act 1 - Dagger"], [["Caged Wolf Card"]]) + + def test_magnificus_eye(self) -> None: + self.assertAccessDependency(["Act 1 - Clock Main Compartment"], [["Magnificus Eye"]]) + + def test_wardrobe_key(self) -> None: + self.assertAccessDependency( + ["Act 1 - Wardrobe Drawer 1", "Act 1 - Wardrobe Drawer 2", + "Act 1 - Wardrobe Drawer 3", "Act 1 - Wardrobe Drawer 4"], + [["Wardrobe Key"]] + ) + + def test_ancient_obol(self) -> None: + self.assertAccessDependency( + ["Act 2 - Bone Lord Femur", "Act 2 - Bone Lord Horn", "Act 2 - Bone Lord Holo Key"], + [["Ancient Obol"]] + ) + + def test_holo_pelt(self) -> None: + self.assertAccessDependency( + ["Act 3 - Trader 1", "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5"], + [["Holo Pelt"]] + ) + + def test_inspectometer_battery(self) -> None: + self.assertAccessDependency( + ["Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Trader 1", "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", + "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", "Act 3 - Forest Holo Pelt", "Act 3 - Clock", + "Act 3 - Crypt Holo Pelt", "Act 3 - Gems Drone", "Act 3 - Nano Armor Generator", "Act 3 - Extra Battery", + "Act 3 - Tower Holo Pelt", "Act 3 - The Great Transcendence", "Act 3 - Boss Mycologists", + "Act 3 - Bone Lord Room", "Act 3 - Well", "Act 3 - Luke's File Entry 1", "Act 3 - Luke's File Entry 2", + "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", "Act 3 - Goobert's Painting"], + [["Inspectometer Battery"]] + ) + + def test_gem_drone(self) -> None: + self.assertAccessDependency( + ["Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", "Act 3 - Trader 1", "Act 3 - Trader 2", + "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Shop Holo Pelt", "Act 3 - Clock", + "Act 3 - Tower Holo Pelt", "Act 3 - The Great Transcendence", "Act 3 - Luke's File Entry 4", + "Act 3 - Boss Mycologists", "Act 3 - Nano Armor Generator", "Act 3 - Goobert's Painting"], + [["Gems Module"]] + ) + + def test_mycologists_holo_key(self) -> None: + self.assertAccessDependency( + ["Act 3 - Boss Mycologists"], + [["Mycologists Holo Key"]] + ) + + def test_bone_lord_holo_key(self) -> None: + self.assertAccessDependency( + ["Act 3 - Bone Lord Room"], + [["Bone Lord Holo Key"]] + ) + + def test_quill(self) -> None: + self.assertAccessDependency( + ["Act 3 - Boss Archivist", "Act 3 - Luke's File Entry 1", "Act 3 - Luke's File Entry 2", + "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", "Act 3 - The Great Transcendence", + "Act 3 - Boss Mycologists"], + [["Quill"]] + ) + + +class AccessTestOrdered(InscryptionTestBase): + options = { + "goal": 0, + } + + def test_film_roll(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", "Act 2 - Battle Sawyer", + "Act 2 - Battle Royal", "Act 2 - Battle Kaycee", "Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert", + "Act 2 - Battle Lonely Wizard", "Act 2 - Battle Inspector", "Act 2 - Battle Melter", + "Act 2 - Battle Dredger", "Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", + "Act 2 - Forest Meadow Chest", "Act 2 - Forest Cabin Chest", "Act 2 - Cabin Wardrobe Drawer", + "Act 2 - Cabin Safe", "Act 2 - Crypt Casket 1", "Act 2 - Crypt Casket 2", "Act 2 - Crypt Well", + "Act 2 - Camera Replica", "Act 2 - Clover", "Act 2 - Epitaph Piece 1", "Act 2 - Epitaph Piece 2", + "Act 2 - Epitaph Piece 3", "Act 2 - Epitaph Piece 4", "Act 2 - Epitaph Piece 5", "Act 2 - Epitaph Piece 6", + "Act 2 - Epitaph Piece 7", "Act 2 - Epitaph Piece 8", "Act 2 - Epitaph Piece 9", "Act 2 - Dock Chest", + "Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1", + "Act 2 - Ancient Obol", "Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2", + "Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy", + "Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key", + "Act 2 - Bone Lord Femur", "Act 2 - Bone Lord Horn", "Act 2 - Bone Lord Holo Key", + "Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1", + "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1", + "Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator", + "Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone", + "Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"], + [["Film Roll"]] + ) + + def test_epitaphs_and_forest_items(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", + "Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert", "Act 2 - Battle Lonely Wizard", + "Act 2 - Battle Inspector", "Act 2 - Battle Melter", "Act 2 - Battle Dredger", + "Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", "Act 2 - Forest Meadow Chest", + "Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1", "Act 2 - Ancient Obol", + "Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2", + "Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy", + "Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key", + "Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1", + "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1", + "Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator", + "Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone", + "Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"], + [["Epitaph Piece", "Camera Replica", "Pile Of Meat"]] + ) + + def test_epitaphs(self) -> None: + self.assertAccessDependency( + ["Act 2 - Boss Grimora", + "Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1", + "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1", + "Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator", + "Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone", + "Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"], + [["Epitaph Piece"]] + ) + + def test_forest_items(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", + "Act 2 - Boss Leshy", "Act 2 - Forest Meadow Chest", + "Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1", + "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1", + "Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator", + "Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone", + "Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"], + [["Camera Replica", "Pile Of Meat"]] + ) + + def test_monocle(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Goobert", "Act 2 - Battle Pike Mage", "Act 2 - Battle Lonely Wizard", + "Act 2 - Boss Magnificus", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", + "Act 2 - Tentacle", "Act 2 - Ancient Obol", "Act 2 - Mycologists Holo Key", + "Act 3 - Boss Photographer", "Act 3 - Boss Archivist", "Act 3 - Boss Unfinished", "Act 3 - Boss G0lly", + "Act 3 - Boss Mycologists", "Act 3 - Bone Lord Room", "Act 3 - Shop Holo Pelt", "Act 3 - Middle Holo Pelt", + "Act 3 - Forest Holo Pelt", "Act 3 - Crypt Holo Pelt", "Act 3 - Tower Holo Pelt", "Act 3 - Trader 1", + "Act 3 - Trader 2", "Act 3 - Trader 3", "Act 3 - Trader 4", "Act 3 - Trader 5", "Act 3 - Drawer 1", + "Act 3 - Drawer 2", "Act 3 - Clock", "Act 3 - Extra Battery", "Act 3 - Nano Armor Generator", + "Act 3 - Chest", "Act 3 - Goobert's Painting", "Act 3 - Luke's File Entry 1", "Act 3 - Gems Drone", + "Act 3 - Luke's File Entry 2", "Act 3 - Luke's File Entry 3", "Act 3 - Luke's File Entry 4", + "Act 3 - Inspectometer Battery", "Act 3 - Gems Drone", "Act 3 - The Great Transcendence", "Act 3 - Well"], + [["Monocle"]] + ) + + +class AccessTestUnordered(InscryptionTestBase): + options = { + "goal": 1, + } + + def test_epitaphs_and_forest_items(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", + "Act 2 - Battle Pike Mage", "Act 2 - Battle Goobert", "Act 2 - Battle Lonely Wizard", + "Act 2 - Battle Inspector", "Act 2 - Battle Melter", "Act 2 - Battle Dredger", + "Act 2 - Tower Chest 1", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", "Act 2 - Forest Meadow Chest", + "Act 2 - Tentacle", "Act 2 - Factory Trash Can", "Act 2 - Factory Drawer 1", "Act 2 - Ancient Obol", + "Act 2 - Factory Drawer 2", "Act 2 - Factory Chest 1", "Act 2 - Factory Chest 2", + "Act 2 - Factory Chest 3", "Act 2 - Factory Chest 4", "Act 2 - Monocle", "Act 2 - Boss Leshy", + "Act 2 - Boss Grimora", "Act 2 - Boss Magnificus", "Act 2 - Boss P03", "Act 2 - Mycologists Holo Key"], + [["Epitaph Piece", "Camera Replica", "Pile Of Meat"]] + ) + + def test_epitaphs(self) -> None: + self.assertAccessDependency( + ["Act 2 - Boss Grimora"], + [["Epitaph Piece"]] + ) + + def test_forest_items(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Prospector", "Act 2 - Battle Angler", "Act 2 - Battle Trapper", + "Act 2 - Boss Leshy", "Act 2 - Forest Meadow Chest"], + [["Camera Replica", "Pile Of Meat"]] + ) + + def test_monocle(self) -> None: + self.assertAccessDependency( + ["Act 2 - Battle Goobert", "Act 2 - Battle Pike Mage", "Act 2 - Battle Lonely Wizard", + "Act 2 - Boss Magnificus", "Act 2 - Tower Chest 2", "Act 2 - Tower Chest 3", + "Act 2 - Tentacle", "Act 2 - Ancient Obol", "Act 2 - Mycologists Holo Key"], + [["Monocle"]] + ) + +class AccessTestBalancedPaintings(InscryptionTestBase): + options = { + "painting_checks_balancing": 1, + } + + def test_paintings(self) -> None: + self.assertAccessDependency(["Act 1 - Painting 2", "Act 1 - Painting 3"], + [["Oil Painting's Clover Plant", "Squirrel Totem Head"]]) diff --git a/worlds/inscryption/test/TestGoal.py b/worlds/inscryption/test/TestGoal.py new file mode 100644 index 000000000000..975af66e45a6 --- /dev/null +++ b/worlds/inscryption/test/TestGoal.py @@ -0,0 +1,108 @@ +from . import InscryptionTestBase + + +class GoalTestOrdered(InscryptionTestBase): + options = { + "goal": 0, + } + + def test_beatable(self) -> None: + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.collect(item) + for i in range(9): + item = self.get_item_by_name("Epitaph Piece") + self.collect(item) + self.assertBeatable(True) + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.remove(item) + self.assertBeatable(False) + self.collect(item) + item = self.get_item_by_name("Epitaph Piece") + self.remove(item) + self.assertBeatable(False) + self.collect(item) + + +class GoalTestUnordered(InscryptionTestBase): + options = { + "goal": 1, + } + + def test_beatable(self) -> None: + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.collect(item) + for i in range(9): + item = self.get_item_by_name("Epitaph Piece") + self.collect(item) + self.assertBeatable(True) + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.remove(item) + self.assertBeatable(False) + self.collect(item) + item = self.get_item_by_name("Epitaph Piece") + self.remove(item) + self.assertBeatable(False) + self.collect(item) + + +class GoalTestAct1(InscryptionTestBase): + options = { + "goal": 2, + } + + def test_beatable(self) -> None: + self.assertBeatable(False) + film_roll = self.get_item_by_name("Film Roll") + self.collect(film_roll) + self.assertBeatable(True) + + +class GoalTestGroupedEpitaphs(InscryptionTestBase): + options = { + "epitaph_pieces_randomization": 1, + } + + def test_beatable(self) -> None: + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.collect(item) + for i in range(3): + item = self.get_item_by_name("Epitaph Pieces") + self.collect(item) + self.assertBeatable(True) + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.remove(item) + self.assertBeatable(False) + self.collect(item) + item = self.get_item_by_name("Epitaph Pieces") + self.remove(item) + self.assertBeatable(False) + self.collect(item) + + +class GoalTestEpitaphsAsOne(InscryptionTestBase): + options = { + "epitaph_pieces_randomization": 2, + } + + def test_beatable(self) -> None: + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.collect(item) + item = self.get_item_by_name("Epitaph Pieces") + self.collect(item) + self.assertBeatable(True) + for item_name in self.required_items_all_acts: + item = self.get_item_by_name(item_name) + self.remove(item) + self.assertBeatable(False) + self.collect(item) + item = self.get_item_by_name("Epitaph Pieces") + self.remove(item) + self.assertBeatable(False) + self.collect(item) diff --git a/worlds/inscryption/test/__init__.py b/worlds/inscryption/test/__init__.py new file mode 100644 index 000000000000..31a0cd2b112e --- /dev/null +++ b/worlds/inscryption/test/__init__.py @@ -0,0 +1,7 @@ +from test.bases import WorldTestBase + + +class InscryptionTestBase(WorldTestBase): + game = "Inscryption" + required_items_all_acts = ["Film Roll", "Camera Replica", "Pile Of Meat", "Monocle", + "Inspectometer Battery", "Gems Module", "Quill"]