From cb3d40624ca63279d2b9344100510a9f7d04b53e Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 22 Feb 2023 17:11:27 -0800 Subject: [PATCH 01/14] Timespinner: Make RisingTidesOverrides consistent with normal yaml behaviour. (#1474) * Make RisingTidesOverrides consistent with normal yaml behaviour. * Each of the options can be either string directly specifying the option, or dictionary. * If dictionary, ensure that at least one of the options is greater than zero. * Made keys optional * A lot less copy/pasta. --------- Co-authored-by: Jarno Westhof --- worlds/timespinner/Options.py | 73 +++++++++++----------- worlds/timespinner/PreCalculatedWeights.py | 20 +++--- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 6f4b7ea876..0448e93dc4 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,7 +1,7 @@ from typing import Dict, Union, List from BaseClasses import MultiWorld from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList -from schema import Schema, And, Optional +from schema import Schema, And, Optional, Or class StartWithJewelryBox(Toggle): @@ -308,47 +308,44 @@ class RisingTides(Toggle): display_name = "Rising Tides" +def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]: + if with_save_point_option: + return { + Optional(location): Or( + And({ + Optional("Dry"): And(int, lambda n: n >= 0), + Optional("Flooded"): And(int, lambda n: n >= 0), + Optional("FloodedWithSavePointAvailable"): And(int, lambda n: n >= 0) + }, lambda d: any(v > 0 for v in d.values())), + "Dry", + "Flooded", + "FloodedWithSavePointAvailable") + } + else: + return { + Optional(location): Or( + And({ + Optional("Dry"): And(int, lambda n: n >= 0), + Optional("Flooded"): And(int, lambda n: n >= 0) + }, lambda d: any(v > 0 for v in d.values())), + "Dry", + "Flooded") + } + + class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" schema = Schema({ - Optional("Xarion"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("Maw"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("AncientPyramidShaft"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("Sandman"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("CastleMoat"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("CastleBasement"): { - "Dry": And(int, lambda n: n >= 0), - "FloodedWithSavePointAvailable": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("CastleCourtyard"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("LakeDesolation"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - }, - Optional("LakeSerene"): { - "Dry": And(int, lambda n: n >= 0), - "Flooded": And(int, lambda n: n >= 0) - } + **rising_tide_option("Xarion"), + **rising_tide_option("Maw"), + **rising_tide_option("AncientPyramidShaft"), + **rising_tide_option("Sandman"), + **rising_tide_option("CastleMoat"), + **rising_tide_option("CastleBasement", with_save_point_option=True), + **rising_tide_option("CastleCourtyard"), + **rising_tide_option("LakeDesolation"), + **rising_tide_option("LakeSerene") }) display_name = "Rising Tides Overrides" default = { diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 193bf84dd6..8501ef73a0 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -21,7 +21,7 @@ class PreCalculatedWeights: dry_lake_serene: bool def __init__(self, world: MultiWorld, player: int): - weights_overrrides: Dict[str, Dict[str, int]] = self.get_flood_weights_overrides(world, player) + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) self.flood_basement, self.flood_basement_high = \ self.roll_flood_setting_with_available_save(world, player, weights_overrrides, "CastleBasement") @@ -87,8 +87,8 @@ class PreCalculatedWeights: ) @staticmethod - def get_flood_weights_overrides( world: MultiWorld, player: int) -> Dict[str, int]: - weights_overrides_option: Union[int, Dict[str, Dict[str, int]]] = \ + def get_flood_weights_overrides( world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ get_option_value(world, player, "RisingTidesOverrides") if weights_overrides_option == 0: @@ -97,26 +97,32 @@ class PreCalculatedWeights: return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, weights: Dict[str, Dict[str, int]], key: str) -> bool: + def roll_flood_setting(world: MultiWorld, player: int, weights: Dict[str, Union[Dict[str, int], str]], key: str) -> bool: if not world or not is_option_enabled(world, player, "RisingTides"): return False weights = weights[key] if key in weights else { "Dry": 67, "Flooded": 33 } - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + if isinstance(weights, dict): + result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + else: + result: str = weights return result == "Flooded" @staticmethod def roll_flood_setting_with_available_save(world: MultiWorld, player: int, - weights: Dict[str, Dict[str, int]], key: str) -> Tuple[bool, bool]: + weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: if not world or not is_option_enabled(world, player, "RisingTides"): return False, False weights = weights[key] if key in weights else {"Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17} - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + if isinstance(weights, dict): + result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + else: + result: str = weights if result == "Dry": return False, False From 1d6ab130157c6a8f5b0473cdba097e75e9fc0d9d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 23 Feb 2023 20:16:10 -0600 Subject: [PATCH 02/14] ArchipIDLE: add a completion condition instead of hard coding tests around a game (#1444) --- test/general/TestImplemented.py | 2 +- worlds/archipidle/Rules.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/general/TestImplemented.py b/test/general/TestImplemented.py index 66a09981ba..22c546eff1 100644 --- a/test/general/TestImplemented.py +++ b/test/general/TestImplemented.py @@ -8,7 +8,7 @@ class TestImplemented(unittest.TestCase): def testCompletionCondition(self): """Ensure a completion condition is set that has requirements.""" for game_name, world_type in AutoWorldRegister.world_types.items(): - if not world_type.hidden and game_name not in {"ArchipIDLE", "Sudoku"}: + if not world_type.hidden and game_name not in {"Sudoku"}: with self.subTest(game_name): multiworld = setup_solo_multiworld(world_type) self.assertFalse(multiworld.completion_condition[1](multiworld.state)) diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index 94c6e099cc..ddf906c21a 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -31,3 +31,7 @@ def set_rules(world: MultiWorld, player: int): world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), lambda state: state._archipidle_location_is_accessible(player, 20) ) + + world.completion_condition[player] =\ + lambda state:\ + state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player) From 7c68e91d4ab80c79452dd9e39d006befef283e1c Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Thu, 23 Feb 2023 23:33:09 -0700 Subject: [PATCH 03/14] Blasphemous: Implement new game (#1446) Adds @BrandenEK's Blasphemous Randomizer as a new Archipelago game. --- worlds/blasphemous/Exits.py | 135 ++ worlds/blasphemous/Items.py | 754 +++++++++++ worlds/blasphemous/Locations.py | 1295 ++++++++++++++++++ worlds/blasphemous/Options.py | 257 ++++ worlds/blasphemous/Rules.py | 1455 +++++++++++++++++++++ worlds/blasphemous/Vanilla.py | 246 ++++ worlds/blasphemous/__init__.py | 413 ++++++ worlds/blasphemous/docs/en_Blasphemous.md | 64 + worlds/blasphemous/docs/setup_en.md | 21 + 9 files changed, 4640 insertions(+) create mode 100644 worlds/blasphemous/Exits.py create mode 100644 worlds/blasphemous/Items.py create mode 100644 worlds/blasphemous/Locations.py create mode 100644 worlds/blasphemous/Options.py create mode 100644 worlds/blasphemous/Rules.py create mode 100644 worlds/blasphemous/Vanilla.py create mode 100644 worlds/blasphemous/__init__.py create mode 100644 worlds/blasphemous/docs/en_Blasphemous.md create mode 100644 worlds/blasphemous/docs/setup_en.md diff --git a/worlds/blasphemous/Exits.py b/worlds/blasphemous/Exits.py new file mode 100644 index 0000000000..a4460e2aed --- /dev/null +++ b/worlds/blasphemous/Exits.py @@ -0,0 +1,135 @@ +from typing import List, Dict + + +region_exit_table: Dict[str, List[str]] = { + "menu" : ["New Game"], + + "albero" : ["To The Holy Line", + "To Desecrated Cistern", + "To Wasteland of the Buried Churches", + "To Dungeons"], + + "attots" : ["To Mother of Mothers"], + + "ar" : ["To Mother of Mothers", + "To Wall of the Holy Prohibitions", + "To Deambulatory of His Holiness"], + + "bottc" : ["To Wasteland of the Buried Churches", + "To Ferrous Tree"], + + "botss" : ["To The Holy Line", + "To Mountains of the Endless Dusk"], + + "coolotcv" : ["To Graveyard of the Peaks", + "To Wall of the Holy Prohibitions"], + + "dohh" : ["To Archcathedral Rooftops"], + + "dc" : ["To Albero", + "To Mercy Dreams", + "To Mountains of the Endless Dusk", + "To Echoes of Salt", + "To Grievance Ascends"], + + "eos" : ["To Jondo", + "To Mountains of the Endless Dusk", + "To Desecrated Cistern", + "To The Resting Place of the Sister", + "To Mourning and Havoc"], + + "ft" : ["To Bridge of the Three Cavalries", + "To Hall of the Dawning", + "To Patio of the Silent Steps"], + + "gotp" : ["To Where Olive Trees Wither", + "To Convent of Our Lady of the Charred Visage"], + + "ga" : ["To Jondo", + "To Desecrated Cistern"], + + "hotd" : ["To Ferrous Tree"], + + "jondo" : ["To Mountains of the Endless Dusk", + "To Grievance Ascends"], + + "kottw" : ["To Mother of Mothers"], + + "lotnw" : ["To Mother of Mothers", + "To The Sleeping Canvases"], + + "md" : ["To Wasteland of the Buried Churches", + "To Desecrated Cistern", + "To The Sleeping Canvases"], + + "mom" : ["To Patio of the Silent Steps", + "To Archcathedral Rooftops", + "To Knot of the Three Words", + "To Library of the Negated Words", + "To All the Tears of the Sea"], + + "moted" : ["To Brotherhood of the Silent Sorrow", + "To Jondo", + "To Desecrated Cistern"], + + "mah" : ["To Echoes of Salt", + "To Mother of Mothers"], + + "potss" : ["To Ferrous Tree", + "To Mother of Mothers", + "To Wall of the Holy Prohibitions"], + + "petrous" : ["To The Holy Line"], + + "thl" : ["To Brotherhood of the Silent Sorrow", + "To Petrous", + "To Albero"], + + "trpots" : ["To Echoes of Salt"], + + "tsc" : ["To Library of the Negated Words", + "To Mercy Dreams"], + + "wothp" : ["To Archcathedral Rooftops", + "To Convent of Our Lady of the Charred Visage"], + + "wotbc" : ["To Albero", + "To Where Olive Trees Wither", + "To Mercy Dreams"], + + "wotw" : ["To Wasteland of the Buried Churches", + "To Graveyard of the Peaks"] +} + +exit_lookup_table: Dict[str, str] = { + "New Game": "botss", + "To Albero": "albero", + "To All the Tears of the Sea": "attots", + "To Archcathedral Rooftops": "ar", + "To Bridge of the Three Cavalries": "bottc", + "To Brotherhood of the Silent Sorrow": "botss", + "To Convent of Our Lady of the Charred Visage": "coolotcv", + "To Deambulatory of His Holiness": "dohh", + "To Desecrated Cistern": "dc", + "To Echoes of Salt": "eos", + "To Ferrous Tree": "ft", + "To Graveyard of the Peaks": "gotp", + "To Grievance Ascends": "ga", + "To Hall of the Dawning": "hotd", + "To Jondo": "jondo", + "To Knot of the Three Words": "kottw", + "To Library of the Negated Words": "lotnw", + "To Mercy Dreams": "md", + "To Mother of Mothers": "mom", + "To Mountains of the Endless Dusk": "moted", + "To Mourning and Havoc": "mah", + "To Patio of the Silent Steps": "potss", + "To Petrous": "petrous", + "To The Holy Line": "thl", + "To The Resting Place of the Sister": "trpots", + "To The Sleeping Canvases": "tsc", + "To Wall of the Holy Prohibitions": "wothp", + "To Wasteland of the Buried Churches": "wotbc", + "To Where Olive Trees Wither": "wotw", + "To Dungeons": "dungeon" +} \ No newline at end of file diff --git a/worlds/blasphemous/Items.py b/worlds/blasphemous/Items.py new file mode 100644 index 0000000000..97dcefde7e --- /dev/null +++ b/worlds/blasphemous/Items.py @@ -0,0 +1,754 @@ +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, List, Set + + +class ItemDict(TypedDict): + name: str + count: int + classification: ItemClassification + +base_id = 1909000 + +item_table: List[ItemDict] = [ + # Rosary Beads + {'name': "Dove Skull", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Ember of the Holy Cremation", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Silver Grape", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Uvula of Proclamation", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Hollow Pearl", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Knot of Hair", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Painted Wood Bead", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Piece of a Golden Mask", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Moss Preserved in Glass", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Frozen Olive", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Quirce's Scorched Bead", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Wicker Knot", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Perpetva's Protection", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Thorned Symbol", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Piece of a Tombstone", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Sphere of the Sacred Smoke", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Bead of Red Wax", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Little Toe made of Limestone", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Big Toe made of Limestone", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Fourth Toe made of Limestone", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Bead of Blue Wax", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Pelican Effigy", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Drop of Coagulated Ink", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Amber Eye", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Muted Bell", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Consecrated Amethyst", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Embers of a Broken Star", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Scaly Coin", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Seashell of the Inverted Spiral", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Calcified Eye of Erudition", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Weight of True Guilt", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Reliquary of the Fervent Heart", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Reliquary of the Suffering Heart", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Reliquary of the Sorrowful Heart", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Token of Appreciation", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Cloistered Ruby", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Bead of Gold Thread", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Cloistered Sapphire", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Fire Enclosed in Enamel", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Light of the Lady of the Lamp", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Scale of Burnished Alabaster", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "The Young Mason's Wheel", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Crown of Gnawed Iron", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Crimson Heart of a Miura", + 'count': 1, + 'classification': ItemClassification.useful}, + + # Prayers + {'name': "Seguiriya to your Eyes like Stars", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Debla of the Lights", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Saeta Dolorosa", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Campanillero to the Sons of the Aurora", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Lorquiana", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Zarabanda of the Safe Haven", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Taranto to my Sister", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Solea of Excommunication", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Tiento to your Thorned Hairs", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Cante Jondo of the Three Sisters", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Verdiales of the Forsaken Hamlet", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Romance to the Crimson Mist", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Zambra to the Resplendent Crown", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Aubade of the Nameless Guardian", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Cantina of the Blue Rose", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mirabras of the Return to Port", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Tirana of the Celestial Bastion", + 'count': 1, + 'classification': ItemClassification.progression}, + + # Relics + {'name': "Blood Perpetuated in Sand", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Incorrupt Hand of the Fraternal Master", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Nail Uprooted from Dirt", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Shroud of Dreamt Sins", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Linen of Golden Thread", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Silvered Lung of Dolphos", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Three Gnarled Tongues", + 'count': 1, + 'classification': ItemClassification.progression}, + + # Mea Culpa Hearts + {'name': "Smoking Heart of Incense", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of the Virtuous Pain", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of Saltpeter Blood", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of Oils", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of Cerulean Incense", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of the Holy Purge", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Molten Heart of Boiling Blood", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of the Single Tone", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Heart of the Unnamed Minstrel", + 'count': 1, + 'classification': ItemClassification.useful}, + {'name': "Brilliant Heart of Dawn", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Apodictic Heart of Mea Culpa", + 'count': 1, + 'classification': ItemClassification.progression}, + + # Quest Items + {'name': "Cord of the True Burying", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mark of the First Refuge", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mark of the Second Refuge", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mark of the Third Refuge", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Tentudia's Carnal Remains", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Remains of Tentudia's Hair", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Tentudia's Skeletal Remains", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Melted Golden Coins", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Torn Bridal Ribbon", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Black Grieving Veil", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Egg of Deformity", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Hatched Egg of Deformity", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Bouquet of Rosemary", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Incense Garlic", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Thorn Upgrade", + 'count': 8, + 'classification': ItemClassification.progression}, + {'name': "Olive Seeds", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Holy Wound of Attrition", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Holy Wound of Contrition", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Holy Wound of Compunction", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Empty Bile Vessel", + 'count': 8, + 'classification': ItemClassification.progression}, + {'name': "Knot of Rosary Rope", + 'count': 6, + 'classification': ItemClassification.progression}, + {'name': "Golden Thimble Filled with Burning Oil", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key to the Chamber of the Eldest Brother", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Empty Golden Thimble", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Deformed Mask of Orestes", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Mirrored Mask of Dolphos", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Embossed Mask of Crescente", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Dried Clove", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Sooty Garlic", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Bouquet of Thyme", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Linen Cloth", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Severed Hand", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Dried Flowers bathed in Tears", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key of the Secular", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key of the Scribe", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key of the Inquisitor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key of the High Peaks", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Chalice of Inverted Verses", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Quicksilver", + 'count': 5, + 'classification': ItemClassification.useful}, + {'name': "Petrified Bell", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Verses Spun from Gold", + 'count': 4, + 'classification': ItemClassification.progression}, + {'name': "Severed Right Eye of the Traitor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Broken Left Eye of the Traitor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Incomplete Scapular", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Key Grown from Twisted Wood", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Holy Wound of Abnegation", + 'count': 1, + 'classification': ItemClassification.progression}, + + # Skills + {'name': "Combo Skill", + 'count': 3, + 'classification': ItemClassification.useful}, + {'name': "Charged Skill", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Ranged Skill", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Dive Skill", + 'count': 3, + 'classification': ItemClassification.progression}, + {'name': "Lunge Skill", + 'count': 3, + 'classification': ItemClassification.useful}, + + # Other + {'name': "Parietal bone of Lasser, the Inquisitor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Jaw of Ashgan, the Inquisitor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Cervical vertebra of Zicher, the Brewmaster", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Clavicle of Dalhuisen, the Schoolchild", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Sternum of Vitas, the Performer", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Ribs of Sabnock, the Guardian", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Vertebra of John, the Gambler", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Scapula of Carlos, the Executioner", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Humerus of McMittens, the Nurse", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Ulna of Koke, the Troubadour", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Radius of Helzer, the Poet", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Frontal of Martinus, the Ropemaker", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Metacarpus of Hodges, the Blacksmith", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of Arthur, the Sailor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of Miriam, the Counsellor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of Brannon, the Gravedigger", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Coxal of June, the Prostitute", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Sacrum of the Dark Warlock", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Coccyx of Daniel, the Possessed", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Femur of Karpow, the Bounty Hunter", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Kneecap of Sebastien, the Puppeteer", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Tibia of Alsahli, the Mystic", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Fibula of Rysp, the Ranger", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Temporal of Joel, the Thief", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Metatarsus of Rikusyo, the Traveller", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of Zeth, the Prisoner", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of William, the Sceptic", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Phalanx of Aralcarim, the Archivist", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Occipital of Tequila, the Metalsmith", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Maxilla of Tarradax, the Cleric", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Nasal bone of Charles, the Artist", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Hyoid bone of Senex, the Beggar", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Vertebra of Lindquist, the Forger", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Trapezium of Jeremiah, the Hangman", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Trapezoid of Yeager, the Jeweller", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Capitate of Barock, the Herald", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Hamate of Vukelich, the Copyist", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Pisiform of Hernandez, the Explorer", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Triquetral of Luca, the Tailor", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Lunate of Keiya, the Butcher", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Scaphoid of Fierce, the Leper", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Anklebone of Weston, the Pilgrim", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Calcaneum of Persian, the Bandit", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Navicular of Kahnnyhoo, the Murderer", + 'count': 1, + 'classification': ItemClassification.progression}, + {'name': "Child of Moonlight", + 'count': 38, + 'classification': ItemClassification.progression}, + {'name': "Life Upgrade", + 'count': 6, + 'classification': ItemClassification.progression}, + {'name': "Fervour Upgrade", + 'count': 6, + 'classification': ItemClassification.progression}, + {'name': "Mea Culpa Upgrade", + 'count': 7, + 'classification': ItemClassification.progression}, + {'name': "Tears of Atonement (250)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (300)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (500)", + 'count': 3, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (625)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (750)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (1000)", + 'count': 4, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (1250)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (1500)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (1750)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (2000)", + 'count': 2, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (2100)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (2500)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (2600)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (3000)", + 'count': 2, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (4300)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (5000)", + 'count': 4, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (5500)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (9000)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (10000)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (11250)", + 'count': 1, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (18000)", + 'count': 5, + 'classification': ItemClassification.filler}, + {'name': "Tears of Atonement (30000)", + 'count': 1, + 'classification': ItemClassification.filler} +] + +group_table: Dict[str, Set[str]] = { + "wounds" : ["Holy Wound of Attrition", + "Holy Wound of Contrition", + "Holy Wound of Compunction"], + + "masks" : ["Deformed Mask of Orestes", + "Mirrored Mask of Dolphos", + "Embossed Mask of Crescente"], + + "tirso" : ["Bouquet of Rosemary", + "Incense Garlic", + "Olive Seeds", + "Dried Clove", + "Sooty Garlic", + "Bouquet of Thyme"], + + "tentudia": ["Tentudia's Carnal Remains", + "Remains of Tentudia's Hair", + "Tentudia's Skeletal Remains"], + + "egg" : ["Melted Golden Coins", + "Torn Bridal Ribbon", + "Black Grieving Veil"], + + "bones" : ["Parietal bone of Lasser, the Inquisitor", + "Jaw of Ashgan, the Inquisitor", + "Cervical vertebra of Zicher, the Brewmaster", + "Clavicle of Dalhuisen, the Schoolchild", + "Sternum of Vitas, the Performer", + "Ribs of Sabnock, the Guardian", + "Vertebra of John, the Gambler", + "Scapula of Carlos, the Executioner", + "Humerus of McMittens, the Nurse", + "Ulna of Koke, the Troubadour", + "Radius of Helzer, the Poet", + "Frontal of Martinus, the Ropemaker", + "Metacarpus of Hodges, the Blacksmith", + "Phalanx of Arthur, the Sailor", + "Phalanx of Miriam, the Counsellor", + "Phalanx of Brannon, the Gravedigger", + "Coxal of June, the Prostitute", + "Sacrum of the Dark Warlock", + "Coccyx of Daniel, the Possessed", + "Femur of Karpow, the Bounty Hunter", + "Kneecap of Sebastien, the Puppeteer", + "Tibia of Alsahli, the Mystic", + "Fibula of Rysp, the Ranger", + "Temporal of Joel, the Thief", + "Metatarsus of Rikusyo, the Traveller", + "Phalanx of Zeth, the Prisoner", + "Phalanx of William, the Sceptic", + "Phalanx of Aralcarim, the Archivist", + "Occipital of Tequila, the Metalsmith", + "Maxilla of Tarradax, the Cleric", + "Nasal bone of Charles, the Artist", + "Hyoid bone of Senex, the Beggar", + "Vertebra of Lindquist, the Forger", + "Trapezium of Jeremiah, the Hangman", + "Trapezoid of Yeager, the Jeweller", + "Capitate of Barock, the Herald", + "Hamate of Vukelich, the Copyist", + "Pisiform of Hernandez, the Explorer", + "Triquetral of Luca, the Tailor", + "Lunate of Keiya, the Butcher", + "Scaphoid of Fierce, the Leper", + "Anklebone of Weston, the Pilgrim", + "Calcaneum of Persian, the Bandit", + "Navicular of Kahnnyhoo, the Murderer"], + + "power" : ["Life Upgrade", + "Fervour Upgrade", + "Empty Bile Vessel", + "Quicksilver"], + + "prayer" : ["Seguiriya to your Eyes like Stars", + "Debla of the Lights", + "Saeta Dolorosa", + "Campanillero to the Sons of the Aurora", + "Lorquiana", + "Zarabanda of the Safe Haven", + "Taranto to my Sister", + "Solea of Excommunication", + "Tiento to your Thorned Hairs", + "Cante Jondo of the Three Sisters", + "Verdiales of the Forsaken Hamlet", + "Romance to the Crimson Mist", + "Zambra to the Resplendent Crown", + "Cantina of the Blue Rose", + "Mirabras of the Return to Port"] +} + +tears_set: Set[str] = [ + "Tears of Atonement (500)", + "Tears of Atonement (625)", + "Tears of Atonement (750)", + "Tears of Atonement (1000)", + "Tears of Atonement (1250)", + "Tears of Atonement (1500)", + "Tears of Atonement (1750)", + "Tears of Atonement (2000)", + "Tears of Atonement (2100)", + "Tears of Atonement (2500)", + "Tears of Atonement (2600)", + "Tears of Atonement (3000)", + "Tears of Atonement (4300)", + "Tears of Atonement (5000)", + "Tears of Atonement (5500)", + "Tears of Atonement (9000)", + "Tears of Atonement (10000)", + "Tears of Atonement (11250)", + "Tears of Atonement (18000)", + "Tears of Atonement (30000)" +] + +reliquary_set: Set[str] = [ + "Reliquary of the Fervent Heart", + "Reliquary of the Suffering Heart", + "Reliquary of the Sorrowful Heart" +] + +skill_set: Set[str] = [ + "Combo Skill", + "Charged Skill", + "Ranged Skill", + "Dive Skill", + "Lunge Skill" +] \ No newline at end of file diff --git a/worlds/blasphemous/Locations.py b/worlds/blasphemous/Locations.py new file mode 100644 index 0000000000..88065de442 --- /dev/null +++ b/worlds/blasphemous/Locations.py @@ -0,0 +1,1295 @@ +from typing import List, Set, TypedDict + + +class LocationDict(TypedDict): + name: str + region: str + game_id: str + room: str + + +location_table: List[LocationDict] = [ + # Albero (35) + {'name': "Albero: Tirso's house, top floor", + 'region': "albero", + 'game_id': "RB01", + 'room': "D01Z02S02"}, + {'name': "Albero: Outside Ossuary", + 'region': "albero", + 'game_id': "CO43", + 'room': "D01Z02S04"}, + {'name': "Albero: Graveyard", + 'region': "albero", + 'game_id': "CO16", + 'room': "D01Z02S05"}, + {'name': "Albero: Gate of Travel room", + 'region': "albero", + 'game_id': "QI65", + 'room': "D01Z02S07"}, + {'name': "Albero: Child of Moonlight", + 'region': "albero", + 'game_id': "RESCUED_CHERUB_08", + 'room': "D01Z02S03"}, + {'name': "Albero: Bless Linen Cloth", + 'region': "albero", + 'game_id': "RE04", + 'room': "D01Z02S01"}, + {'name': "Albero: Bless Hatched Egg", + 'region': "albero", + 'game_id': "RE10", + 'room': "D01Z02S01"}, + {'name': "Albero: Bless Severed Hand", + 'region': "albero", + 'game_id': "RE02", + 'room': "D01Z02S01"}, + {'name': "Albero: First gift for Cleofas", + 'region': "albero", + 'game_id': "QI01", + 'room': "D01Z02S03"}, + {'name': "Albero: Final gift for Cleofas", + 'region': "albero", + 'game_id': "PR11", + 'room': "D01Z02S03"}, + {'name': "Albero: Tirso's 1st reward", + 'region': "albero", + 'game_id': "QI66", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's 2nd reward", + 'region': "albero", + 'game_id': "Tirso[500]", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's 3rd reward", + 'region': "albero", + 'game_id': "Tirso[1000]", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's 4th reward", + 'region': "albero", + 'game_id': "Tirso[2000]", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's 5th reward", + 'region': "albero", + 'game_id': "Tirso[5000]", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's 6th reward", + 'region': "albero", + 'game_id': "Tirso[10000]", + 'room': "D01Z02S02"}, + {'name': "Albero: Tirso's final reward", + 'region': "albero", + 'game_id': "QI56", + 'room': "D01Z02S02"}, + {'name': "Albero: Lvdovico's 1st reward", + 'region': "albero", + 'game_id': "Lvdovico[500]", + 'room': "D01Z02S03"}, + {'name': "Albero: Lvdovico's 2nd reward", + 'region': "albero", + 'game_id': "Lvdovico[1000]", + 'room': "D01Z02S03"}, + {'name': "Albero: Lvdovico's 3rd reward", + 'region': "albero", + 'game_id': "PR03", + 'room': "D01Z02S03"}, + {'name': "Ossuary: Isidora, Voice of the Dead", + 'region': "albero", + 'game_id': "QI201", + 'room': "D01BZ08S01"}, + {'name': "Albero: Mea Culpa altar", + 'region': "albero", + 'game_id': "Sword[D01Z02S06]", + 'room': "D01Z02S06"}, + {'name': "Albero: Donate 5000 Tears", + 'region': "albero", + 'game_id': "RB104", + 'room': "D01BZ04S01"}, + {'name': "Albero: Donate 50000 Tears", + 'region': "albero", + 'game_id': "RB105", + 'room': "D01BZ04S01"}, + {'name': "Ossuary: 1st reward", + 'region': "albero", + 'game_id': "Undertaker[250]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 2nd reward", + 'region': "albero", + 'game_id': "Undertaker[500]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 3rd reward", + 'region': "albero", + 'game_id': "Undertaker[750]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 4th reward", + 'region': "albero", + 'game_id': "Undertaker[1000]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 5th reward", + 'region': "albero", + 'game_id': "Undertaker[1250]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 6th reward", + 'region': "albero", + 'game_id': "Undertaker[1500]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 7th reward", + 'region': "albero", + 'game_id': "Undertaker[1750]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 8th reward", + 'region': "albero", + 'game_id': "Undertaker[2000]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 9th reward", + 'region': "albero", + 'game_id': "Undertaker[2500]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 10th reward", + 'region': "albero", + 'game_id': "Undertaker[3000]", + 'room': "D01BZ06S01"}, + {'name': "Ossuary: 11th reward", + 'region': "albero", + 'game_id': "Undertaker[5000]", + 'room': "D01BZ06S01"}, + + # All the Tears of the Sea (1) + {'name': "AtTotS: Miriam's gift", + 'region': "attots", + 'game_id': "PR201", + 'room': "D04Z04S02"}, + + # Archcathedral Rooftops (11) + {'name': "AR: First soldier fight", + 'region': "ar", + 'game_id': "QI02", + 'room': "D06Z01S03"}, + {'name': "AR: Second soldier fight", + 'region': "ar", + 'game_id': "QI03", + 'room': "D06Z01S06"}, + {'name': "AR: Third soldier fight", + 'region': "ar", + 'game_id': "QI04", + 'room': "D06Z01S21"}, + {'name': "AR: Upper west shaft ledge", + 'region': "ar", + 'game_id': "CO06", + 'room': "D06Z01S12"}, + {'name': "AR: Upper west shaft Child of Moonlight", + 'region': "ar", + 'game_id': "RESCUED_CHERUB_36", + 'room': "D06Z01S12"}, + {'name': "AR: Upper west shaft chest", + 'region': "ar", + 'game_id': "PR12", + 'room': "D06Z01S12"}, + {'name': "AR: Statue near MoM", + 'region': "ar", + 'game_id': "HE04", + 'room': "D06Z01S22"}, + {'name': "AR: Lady of the Six Sorrows", + 'region': "ar", + 'game_id': "Lady[D06Z01S24]", + 'room': "D06Z01S24"}, + {'name': "AR: Upper east shaft ledge", + 'region': "ar", + 'game_id': "CO40", + 'room': "D06Z01S15"}, + {'name': "AR: Mea Culpa altar", + 'region': "ar", + 'game_id': "Sword[D06Z01S11]", + 'room': "D06Z01S11"}, + {'name': "AR: Crisanta of the Wrapped Agony", + 'region': "ar", + 'game_id': "BS16", + 'room': "D06Z01S25"}, + + # Bridge of the Three Cavalries (3) + {'name': "BotTC: Esdras, of the Anointed Legion", + 'region': "bottc", + 'game_id': "BS12", + 'room': "D08Z01S01"}, + {'name': "BotTC: Esdras' gift", + 'region': "bottc", + 'game_id': "PR09", + 'room': "D08Z01S01"}, + {'name': "BotTC: Inside giant statue", + 'region': "bottc", + 'game_id': "HE101", + 'room': "D08Z01S02"}, + + # Brotherhood of the Silent Sorrow (11) + {'name': "BotSS: Beginning gift", + 'region': "botss", + 'game_id': "QI106", + 'room': "D17Z01S01"}, + {'name': "BotSS: Starting room Child of Moonlight", + 'region': "botss", + 'game_id': "RESCUED_CHERUB_06", + 'room': "D17Z01S01"}, + {'name': "BotSS: Starting room ledge", + 'region': "botss", + 'game_id': "RB204", + 'room': "D17Z01S01"}, + {'name': "BotSS: Chamber of the Eldest Brother", + 'region': "botss", + 'game_id': "RE01", + 'room': "D17BZ01S01[relic]"}, + {'name': "BotSS: Mea Culpa altar", + 'region': "botss", + 'game_id': "Sword[D17Z01S08]", + 'room': "D17Z01S08"}, + {'name': "BotSS: Platforming gauntlet", + 'region': "botss", + 'game_id': "CO25", + 'room': "D17Z01S04"}, + {'name': "BotSS: Blue candle", + 'region': "botss", + 'game_id': "RB25", + 'room': "D17Z01S04"}, + {'name': "BotSS: Outside church", + 'region': "botss", + 'game_id': "PR203", + 'room': "D17Z01S14"}, + {'name': "BotSS: Esdras' final gift", + 'region': "botss", + 'game_id': "QI204", + 'room': "D17Z01S15"}, + {'name': "BotSS: Crisanta's gift", + 'region': "botss", + 'game_id': "QI301", + 'room': "D17Z01S15"}, + {'name': "BotSS: Warden of the Silent Sorrow", + 'region': "botss", + 'game_id': "BS13", + 'room': "D17Z01S11"}, + + # Convent of Our Lady of the Charred Visage (13) + {'name': "CoOLotCV: Snowy window ledge", + 'region': "coolotcv", + 'game_id': "CO05", + 'room': "D02Z03S03"}, + {'name': "CoOLotCV: Center enemy lineup", + 'region': "coolotcv", + 'game_id': "CO15", + 'room': "D02Z03S07"}, + {'name': "CoOLotCV: Center miasma room", + 'region': "coolotcv", + 'game_id': "RB08", + 'room': "D02Z03S05"}, + {'name': "CoOLotCV: Lower west statue", + 'region': "coolotcv", + 'game_id': "HE03", + 'room': "D02Z03S12"}, + {'name': "CoOLotCV: Lady of the Six Sorrows", + 'region': "coolotcv", + 'game_id': "Lady[D02Z03S15]", + 'room': "D02Z03S15"}, + {'name': "CoOLotCV: Mea Culpa altar", + 'region': "coolotcv", + 'game_id': "Sword[D02Z03S13]", + 'room': "D02Z03S13"}, + {'name': "CoOLotCV: Red candle", + 'region': "coolotcv", + 'game_id': "RB18", + 'room': "D02Z03S06"}, + {'name': "CoOLotCV: Blue candle", + 'region': "coolotcv", + 'game_id': "RB24", + 'room': "D02Z03S17"}, + {'name': "CoOLotCV: Outside pathway", + 'region': "coolotcv", + 'game_id': "RB107", + 'room': "D02Z03S23"}, + {'name': "CoOLotCV: Fountain of burning oil", + 'region': "coolotcv", + 'game_id': "QI57", + 'room': "D02Z03S21"}, + {'name': "CoOLotCV: Our Lady of the Charred Visage", + 'region': "coolotcv", + 'game_id': "BS03", + 'room': "D02Z03S20"}, + {'name': "CoOLotCV: Visage of Compunction", + 'region': "coolotcv", + 'game_id': "QI40", + 'room': "D02Z03S21"}, + {'name': "CoOLotCV: Mask room", + 'region': "coolotcv", + 'game_id': "QI61", + 'room': "D02Z03S19"}, + + # Deambulatory of His Holiness (3) + {'name': "DoHH: Viridiana's gift", + 'region': "dohh", + 'game_id': "PR08", + 'room': "D07Z01S01"}, + + # Desecrated Cistern (20) + {'name': "DC: Lady of the Six Sorrows, from MD", + 'region': "dc", + 'game_id': "Lady[D01Z05S22]", + 'room': "D01Z05S22"}, + {'name': "DC: Behind sewage drips", + 'region': "dc", + 'game_id': "CO41", + 'room': "D01Z05S15"}, + {'name': "DC: Child of Moonlight, above water", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_11", + 'room': "D01Z05S14"}, + {'name': "DC: Lower east tunnel chest", + 'region': "dc", + 'game_id': "QI45", + 'room': "D01Z05S11"}, + {'name': "DC: Upper east tunnel chest", + 'region': "dc", + 'game_id': "PR16", + 'room': "D01Z05S06"}, + {'name': "DC: Upper east Child of Moonlight", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_13", + 'room': "D01Z05S06"}, + {'name': "DC: Hidden alcove near fountain", + 'region': "dc", + 'game_id': "QI67", + 'room': "D01Z05S05"}, + {'name': "DC: Shortcut to WotBC", + 'region': "dc", + 'game_id': "CO09", + 'room': "D01Z05S05"}, + {'name': "DC: Oil of the Pilgrims", + 'region': "dc", + 'game_id': "Oil[D01Z05S07]", + 'room': "D01Z05S07"}, + {'name': "DC: Child of Moonlight, miasma room", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_14", + 'room': "D01Z05S08"}, + {'name': "DC: Behind gate in miasma room", + 'region': "dc", + 'game_id': "QI12", + 'room': "D01Z05S08"}, + {'name': "DC: Child of Moonlight, behind pillar", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_12", + 'room': "D01Z05S13"}, + {'name': "DC: High ledge near elevator shaft", + 'region': "dc", + 'game_id': "CO32", + 'room': "D01Z05S17"}, + {'name': "DC: Shroud puzzle", + 'region': "dc", + 'game_id': "RB03", + 'room': "D01Z05S21"}, + {'name': "DC: Chalice room", + 'region': "dc", + 'game_id': "QI75", + 'room': "D01Z05S23"}, + {'name': "DC: Mea Culpa altar", + 'region': "dc", + 'game_id': "Sword[D01Z05S24]", + 'room': "D01Z05S24"}, + {'name': "DC: Lady of the Six Sorrows, elevator shaft", + 'region': "dc", + 'game_id': "Lady[D01Z05S26]", + 'room': "D01Z05S26"}, + {'name': "DC: Top of elevator Child of Moonlight", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_15", + 'room': "D01Z05S20"}, + {'name': "DC: Elevator shaft Child of Moonlight", + 'region': "dc", + 'game_id': "RESCUED_CHERUB_22", + 'room': "D01Z05S25"}, + {'name': "DC: Elevator shaft ledge", + 'region': "dc", + 'game_id': "CO44", + 'room': "D01Z05S25"}, + + # Echoes of Salt (2) + {'name': "EoS: Lantern jump near MotED", + 'region': "eos", + 'game_id': "RB108", + 'room': "D20Z01S02"}, + {'name': "EoS: Lantern jump near elevator", + 'region': "eos", + 'game_id': "RB202", + 'room': "D20Z01S09"}, + + # Graveyard of the Peaks (21) + {'name': "GotP: Shop cave Child of Moonlight", + 'region': "gotp", + 'game_id': "RESCUED_CHERUB_31", + 'room': "D02Z02S08"}, + {'name': "GotP: Shop cave hidden hole", + 'region': "gotp", + 'game_id': "CO42", + 'room': "D02Z02S08"}, + {'name': "GotP: Shop item 1", + 'region': "gotp", + 'game_id': "QI11", + 'room': "D02BZ01S01"}, + {'name': "GotP: Shop item 2", + 'region': "gotp", + 'game_id': "RB37", + 'room': "D02BZ01S01"}, + {'name': "GotP: Shop item 3", + 'region': "gotp", + 'game_id': "RB02", + 'room': "D02BZ01S01"}, + {'name': "GotP: Confessor Dungeon room", + 'region': "gotp", + 'game_id': "RB38", + 'room': "D02Z02S06"}, + {'name': "GotP: Elevator shaft Child of Moonlight", + 'region': "gotp", + 'game_id': "RESCUED_CHERUB_26", + 'room': "D02Z02S11"}, + {'name': "GotP: Elevator shaft ledge", + 'region': "gotp", + 'game_id': "QI53", + 'room': "D02Z02S11"}, + {'name': "GotP: Lady of the Six Sorrows", + 'region': "gotp", + 'game_id': "Lady[D02Z02S12]", + 'room': "D02Z02S12"}, + {'name': "GotP: Self sacrifice statue", + 'region': "gotp", + 'game_id': "HE11", + 'room': "D02Z02S13"}, + {'name': "GotP: Lower east shaft", + 'region': "gotp", + 'game_id': "QI46", + 'room': "D02Z02S03"}, + {'name': "GotP: Center east shaft", + 'region': "gotp", + 'game_id': "CO29", + 'room': "D02Z02S03"}, + {'name': "GotP: Upper east shaft", + 'region': "gotp", + 'game_id': "QI08", + 'room': "D02Z02S03"}, + {'name': "GotP: East cliffside", + 'region': "gotp", + 'game_id': "RB106", + 'room': "D02Z02S14"}, + {'name': "GotP: West shaft Child of Moonlight", + 'region': "gotp", + 'game_id': "RESCUED_CHERUB_25", + 'room': "D02Z02S04"}, + {'name': "GotP: Lower west shaft", + 'region': "gotp", + 'game_id': "RB32", + 'room': "D02Z02S04"}, + {'name': "GotP: Upper west shaft", + 'region': "gotp", + 'game_id': "CO01", + 'room': "D02Z02S04"}, + {'name': "GotP: Center shaft Child of Moonlight", + 'region': "gotp", + 'game_id': "RESCUED_CHERUB_24", + 'room': "D02Z02S02"}, + {'name': "GotP: Center shaft ledge", + 'region': "gotp", + 'game_id': "RB15", + 'room': "D02Z02S05"}, + {'name': "GotP: Oil of the Pilgrims", + 'region': "gotp", + 'game_id': "Oil[D02Z02S10]", + 'room': "D02Z02S10"}, + {'name': "GotP: Amanecida of the Bejeweled Arrow", + 'region': "gotp", + 'game_id': "D02Z02S14[18000]", + 'room': "D02Z02S14"}, + + # Grievance Ascends (12) + {'name': "GA: Lower west ledge", + 'region': "ga", + 'game_id': "QI44", + 'room': "D03Z03S02"}, + {'name': "GA: Miasma room treasure", + 'region': "ga", + 'game_id': "RE07", + 'room': "D03Z03S06"}, + {'name': "GA: Miasma room Child of Moonlight", + 'region': "ga", + 'game_id': "RESCUED_CHERUB_19", + 'room': "D03Z03S06"}, + {'name': "GA: Miasma room floor", + 'region': "ga", + 'game_id': "CO12", + 'room': "D03Z03S06"}, + {'name': "GA: Oil of the Pilgrims", + 'region': "ga", + 'game_id': "Oil[D03Z03S13]", + 'room': "D03Z03S13"}, + {'name': "GA: End of blood bridge", + 'region': "ga", + 'game_id': "QI10", + 'room': "D03Z03S08"}, + {'name': "GA: Blood bridge Child of Moonlight", + 'region': "ga", + 'game_id': "RESCUED_CHERUB_21", + 'room': "D03Z03S08"}, + {'name': "GA: Lower east Child of Moonlight", + 'region': "ga", + 'game_id': "RESCUED_CHERUB_20", + 'room': "D03Z03S09"}, + {'name': "GA: Altasgracias' gift", + 'region': "ga", + 'game_id': "QI13", + 'room': "D03Z03S10"}, + {'name': "GA: Empty giant egg", + 'region': "ga", + 'game_id': "RB06", + 'room': "D03Z03S10"}, + {'name': "GA: Tres Angustias", + 'region': "ga", + 'game_id': "BS04", + 'room': "D03Z03S15"}, + {'name': "GA: Visage of Contrition", + 'region': "ga", + 'game_id': "QI39", + 'room': "D03Z03S16"}, + + # Hall of the Dawning (2) + {'name': "HotD: Mirror room", + 'region': "hotd", + 'game_id': "QI105", + 'room': "D08Z02S01"}, + {'name': "HotD: Laudes, the First of the Amanecidas", + 'region': "hotd", + 'game_id': "LaudesBossTrigger[30000]", + 'room': "D08Z02S03"}, + + # Jondo (13) + {'name': "Jondo: Upper east ledge", + 'region': "jondo", + 'game_id': "CO08", + 'room': "D03Z03S01"}, + {'name': "Jondo: Upper east chest", + 'region': "jondo", + 'game_id': "PR10", + 'room': "D03Z03S01"}, + {'name': "Jondo: Lower east under chargers", + 'region': "jondo", + 'game_id': "CO33", + 'room': "D03Z03S04"}, + {'name': "Jondo: Lower east bell trap", + 'region': "jondo", + 'game_id': "QI19", + 'room': "D03Z03S06"}, + {'name': "Jondo: Upper east Child of Moonlight", + 'region': "jondo", + 'game_id': "RESCUED_CHERUB_18", + 'room': "D03Z03S05"}, + {'name': "Jondo: Spike tunnel Child of Moonlight", + 'region': "jondo", + 'game_id': "RESCUED_CHERUB_37", + 'room': "D03Z03S11"}, + {'name': "Jondo: Spike tunnel statue", + 'region': "jondo", + 'game_id': "HE06", + 'room': "D03Z03S11"}, + {'name': "Jondo: Spike tunnel cave", + 'region': "jondo", + 'game_id': "QI103", + 'room': "D03Z03S15"}, + {'name': "Jondo: Lower west lift alcove", + 'region': "jondo", + 'game_id': "CO07", + 'room': "D03Z03S07"}, + {'name': "Jondo: Lower west bell alcove", + 'region': "jondo", + 'game_id': "QI41", + 'room': "D03Z03S08"}, + {'name': "Jondo: Upper west bell puzzle", + 'region': "jondo", + 'game_id': "QI52", + 'room': "D03Z03S12"}, + {'name': "Jondo: Upper west tree root", + 'region': "jondo", + 'game_id': "RB28", + 'room': "D03Z03S13"}, + {'name': "Jondo: Upper west Child of Moonlight", + 'region': "jondo", + 'game_id': "RESCUED_CHERUB_17", + 'room': "D03Z03S10"}, + + # Knot of the Three Words (1) + {'name': "KotTW: Gift from the Traitor", + 'region': "kottw", + 'game_id': "HE201", + 'room': "D04Z03S02"}, + + # Library of the Negated Words (18) + {'name': "LotNW: Platform room Child of Moonlight", + 'region': "lotnw", + 'game_id': "RESCUED_CHERUB_01", + 'room': "D05Z01S04"}, + {'name': "LotNW: Platform room ledge", + 'region': "lotnw", + 'game_id': "CO18", + 'room': "D05Z01S04"}, + {'name': "LotNW: Root ceiling platform", + 'region': "lotnw", + 'game_id': "CO22", + 'room': "D05Z01S05"}, + {'name': "LotNW: Hidden floor", + 'region': "lotnw", + 'game_id': "QI50", + 'room': "D05Z01S05"}, + {'name': "LotNW: Miasma hallway chest", + 'region': "lotnw", + 'game_id': "RB31", + 'room': "D05Z01S06"}, + {'name': "LotNW: Lady of the Six Sorrows", + 'region': "lotnw", + 'game_id': "Lady[D05Z01S14]", + 'room': "D05Z01S14"}, + {'name': "LotNW: Bone puzzle", + 'region': "lotnw", + 'game_id': "PR15", + 'room': "D05Z01S18"}, + {'name': "LotNW: Lowest west upper ledge", + 'region': "lotnw", + 'game_id': "CO28", + 'room': "D05Z01S11"}, + {'name': "LotNW: Platform puzzle chest", + 'region': "lotnw", + 'game_id': "PR07", + 'room': "D05Z01S10"}, + {'name': "LotNW: Lowest west center ledge", + 'region': "lotnw", + 'game_id': "RB30", + 'room': "D05Z01S11"}, + {'name': "LotNW: Lowest west Child of Moonlight", + 'region': "lotnw", + 'game_id': "RESCUED_CHERUB_02", + 'room': "D05Z01S11"}, + {'name': "LotNW: Oil of the Pilgrims", + 'region': "lotnw", + 'game_id': "Oil[D05Z01S19]", + 'room': "D05Z01S19"}, + {'name': "LotNW: Elevator Child of Moonlight", + 'region': "lotnw", + 'game_id': "RESCUED_CHERUB_32", + 'room': "D05Z01S21"}, + {'name': "LotNW: Mask room", + 'region': "lotnw", + 'game_id': "QI62", + 'room': "D05Z01S15"}, + {'name': "LotNW: Mea Culpa altar", + 'region': "lotnw", + 'game_id': "Sword[D05Z01S13]", + 'room': "D05Z01S13"}, + {'name': "LotNW: Red candle", + 'region': "lotnw", + 'game_id': "RB19", + 'room': "D05Z01S02"}, + {'name': "LotNW: Silence for Diosdado", + 'region': "lotnw", + 'game_id': "RB203", + 'room': "D05Z01S11"}, # ? + {'name': "LotNW: Twisted wood hidden wall", + 'region': "lotnw", + 'game_id': "RB301", + 'room': "D05BZ01S01"}, + + # Mercy Dreams (15) + {'name': "MD: First area hidden wall", + 'region': "md", + 'game_id': "CO30", + 'room': "D01Z04S05"}, + {'name': "MD: Second area trapped chest", + 'region': "md", + 'game_id': "PR01", + 'room': "D01Z04S07"}, + {'name': "MD: Second area ledge", + 'region': "md", + 'game_id': "CO03", + 'room': "D01Z04S06"}, + {'name': "MD: Second area Child of Moonlight", + 'region': "md", + 'game_id': "RESCUED_CHERUB_09", + 'room': "D01Z04S06"}, + {'name': "MD: Red candle", + 'region': "md", + 'game_id': "RB17", + 'room': "D01Z04S08"}, + {'name': "MD: Shop item 1", + 'region': "md", + 'game_id': "QI58", + 'room': "D01BZ02S01"}, + {'name': "MD: Shop item 2", + 'region': "md", + 'game_id': "RB05", + 'room': "D01BZ02S01"}, + {'name': "MD: Shop item 3", + 'region': "md", + 'game_id': "RB09", + 'room': "D01BZ02S01"}, + {'name': "MD: Third area hidden room", + 'region': "md", + 'game_id': "QI48", + 'room': "D01Z04S11"}, + {'name': "MD: Sliding challenge", + 'region': "md", + 'game_id': "CO38", + 'room': "D01Z04S14"}, + {'name': "MD: Ten Piedad", + 'region': "md", + 'game_id': "BS01", + 'room': "D01Z04S18"}, + {'name': "MD: Visage of Attrition", + 'region': "md", + 'game_id': "QI38", + 'room': "D01Z04S19"}, + {'name': "MD: Blue candle", + 'region': "md", + 'game_id': "RB26", + 'room': "D01Z04S16"}, + {'name': "MD: Cave Child of Moonlight", + 'region': "md", + 'game_id': "RESCUED_CHERUB_33", + 'room': "D01Z04S16"}, + {'name': "MD: Behind gate to TSC", + 'region': "md", + 'game_id': "CO21", + 'room': "D01Z04S13"}, + + # Mother of Mothers (14) + {'name': "MoM: Oil of the Pilgrims", + 'region': "mom", + 'game_id': "Oil[D04Z02S14]", + 'room': "D04Z02S14"}, + {'name': "MoM: Upper east ledge", + 'region': "mom", + 'game_id': "RB33", + 'room': "D04Z02S07"}, + {'name': "MoM: East chandelier platform", + 'region': "mom", + 'game_id': "CO35", + 'room': "D04Z02S"}, + {'name': "MoM: Lower west Child of Moonlight", + 'region': "mom", + 'game_id': "RESCUED_CHERUB_30", + 'room': ""}, + {'name': "MoM: Upper west floor", + 'region': "mom", + 'game_id': "CO17", + 'room': "D04Z02S02"}, + {'name': "MoM: Redento's treasure", + 'region': "mom", + 'game_id': "RE03", + 'room': "D04BZ02S01"}, + {'name': "MoM: Final meeting with Redento", + 'region': "mom", + 'game_id': "QI54", + 'room': "D04BZ02S01"}, + {'name': "MoM: Giant chandelier statue", + 'region': "mom", + 'game_id': "HE01", + 'room': "D04Z02S16"}, + {'name': "MoM: Outside Cleofas' room", + 'region': "mom", + 'game_id': "CO34", + 'room': "D04Z02S06"}, + {'name': "MoM: Upper center floor", + 'region': "mom", + 'game_id': "CO20", + 'room': "D04Z02S11"}, + {'name': "MoM: Upper center Child of Moonlight", + 'region': "mom", + 'game_id': "RESCUED_CHERUB_29", + 'room': ""}, + {'name': "MoM: Mea Culpa altar", + 'region': "mom", + 'game_id': "Sword[D04Z02S12]", + 'room': "D04Z02S12"}, + {'name': "MoM: Melquiades, The Exhumed Archbishop", + 'region': "mom", + 'game_id': "BS05", + 'room': "D04Z02S22"}, + {'name': "MoM: Mask room", + 'region': "mom", + 'game_id': "QI60", + 'room': "D04Z02S15"}, + + # Mountains of the Endless Dusk (8) + {'name': "MotED: Under entrance to DC", + 'region': "moted", + 'game_id': "CO13", + 'room': "D03Z01S01"}, + {'name': "MotED: Perpetva", + 'region': "moted", + 'game_id': "RB13", + 'room': "D03Z01S06"}, + {'name': "MotED: Child of Moonlight, above chasm", + 'region': "moted", + 'game_id': "RESCUED_CHERUB_16", + 'room': "D03Z01S03"}, + {'name': "MotED: Platform above chasm", + 'region': "moted", + 'game_id': "QI47", + 'room': "D03Z01S03"}, + {'name': "MotED: 1st meeting with Redento", + 'region': "moted", + 'game_id': "RB22", + 'room': "D03Z01S03"}, + {'name': "MotED: Blood platform alcove", + 'region': "moted", + 'game_id': "QI63", + 'room': "D03Z01S04"}, + {'name': "MotED: Egg hatching", + 'region': "moted", + 'game_id': "QI14", + 'room': "D03Z01S06"}, + {'name': "MotED: Amanecida of the Golden Blades", + 'region': "moted", + 'game_id': "D03Z01S03[18000]", + 'room': "D03Z01S03"}, + + # Mourning and Havoc (4) + {'name': "MaH: West chest", + 'region': "mah", + 'game_id': "PR202", + 'room': "D20Z02S11"}, + {'name': "MaH: Upper east chest", + 'region': "mah", + 'game_id': "RB201", + 'room': "D20Z02S02"}, + {'name': "MaH: Sierpes' eye", + 'region': "mah", + 'game_id': "QI202", + 'room': "D20Z02S08"}, + {'name': "MaH: Sierpes", + 'region': "mah", + 'game_id': "BossTrigger[5000]", + 'room': "D20Z02S08"}, + + # Patio of the Silent Steps (9) + {'name': "PotSS: First area Child of Moonlight", + 'region': "potss", + 'game_id': "RESCUED_CHERUB_35", + 'room': "D04Z01S01"}, + {'name': "PotSS: First area ledge", + 'region': "potss", + 'game_id': "CO23", + 'room': "D04Z01S01"}, + {'name': "PotSS: Second area ledge", + 'region': "potss", + 'game_id': "RB14", + 'room': "D04Z01S02"}, + {'name': "PotSS: Third area Child of Moonlight", + 'region': "potss", + 'game_id': "RESCUED_CHERUB_28", + 'room': "D04Z01S03"}, + {'name': "PotSS: Third area lower ledge", + 'region': "potss", + 'game_id': "QI37", + 'room': "D04Z01S03"}, + {'name': "PotSS: Third area upper ledge", + 'region': "potss", + 'game_id': "CO39", + 'room': "D04Z01S03"}, + {'name': "PotSS: Climb to WotHP", + 'region': "potss", + 'game_id': "QI102", + 'room': "D04Z01S05"}, + {'name': "PotSS: 4th meeting with Redento", + 'region': "potss", + 'game_id': "RB21", + 'room': "D04Z01S01"}, + {'name': "PotSS: Amanecida of the Chiselled Steel", + 'region': "potss", + 'game_id': "D04Z01S04[18000]", + 'room': "D04Z01S04"}, + + # Petrous (1) + {'name': "Petrous: Temple entrance", + 'region': "petrous", + 'game_id': "QI101", + 'room': "D01Z06S01"}, + + # The Resting Place of the Sister (1) + {'name': "TRPotS: Perpetva's shrine", + 'region': "trpots", + 'game_id': "QI203", + 'room': "D20Z03S01"}, + + # The Sleeping Canvases (10) + {'name': "TSC: Painting ladder ledge", + 'region': "tsc", + 'game_id': "QI64", + 'room': "D05Z02S02"}, + {'name': "TSC: Candle wax puzzle", + 'region': "tsc", + 'game_id': "HE07", + 'room': "D05Z02S08"}, + {'name': "TSC: Shop item 1", + 'region': "tsc", + 'game_id': "RB12", + 'room': "D05BZ02S01"}, + {'name': "TSC: Shop item 2", + 'region': "tsc", + 'game_id': "QI49", + 'room': "D05BZ02S01"}, + {'name': "TSC: Shop item 3", + 'region': "tsc", + 'game_id': "QI71", + 'room': "D05BZ02S01"}, + {'name': "TSC: Swinging blade tunnel", + 'region': "tsc", + 'game_id': "QI104", + 'room': "D05Z02S15"}, + {'name': "TSC: Exposito, Scion of Abjuration", + 'region': "tsc", + 'game_id': "BS06", + 'room': "D05Z02S14"}, + {'name': "TSC: Under elevator shaft", + 'region': "tsc", + 'game_id': "CO31", + 'room': "D05Z02S11"}, + {'name': "TSC: Jocinero's 1st reward", + 'region': "tsc", + 'game_id': "RE05", + 'room': "D05Z02S10"}, # ? + {'name': "TSC: Jocinero's final reward", + 'region': "tsc", + 'game_id': "PR05", + 'room': "D05Z02S10"}, # ? + + # The Holy Line (6) + {'name': "THL: Deogracias' gift", + 'region': "thl", + 'game_id': "QI31", + 'room': "D01Z01S07"}, + {'name': "THL: Hanging skeleton", + 'region': "thl", + 'game_id': "PR14", + 'room': "D01Z01S02"}, + {'name': "THL: Across blood platforms", + 'region': "thl", + 'game_id': "RB07", + 'room': "D01Z01S02"}, + {'name': "THL: Child of Moonlight", + 'region': "thl", + 'game_id': "RESCUED_CHERUB_07", + 'room': "D01Z01S03"}, + {'name': "THL: Underground ledge", + 'region': "thl", + 'game_id': "CO04", + 'room': "D01Z01S03"}, + {'name': "THL: Underground chest", + 'region': "thl", + 'game_id': "QI55", + 'room': "D01Z01S03"}, + + # Wall of the Holy Prohibitions (19) + {'name': "WotHP: Upper east room, lift puzzle", + 'region': "wothp", + 'game_id': "RB11", + 'room': "D09Z01S02"}, + {'name': "WotHP: Upper east room, center cell ledge", + 'region': "wothp", + 'game_id': "CO10", + 'room': "D09BZ01S01[Cell22]"}, + {'name': "WotHP: Upper east room, center cell floor", + 'region': "wothp", + 'game_id': "QI69", + 'room': "D09BZ01S01[Cell22]"}, + {'name': "WotHP: Upper east room, top bronze cell", + 'region': "wothp", + 'game_id': "RESCUED_CHERUB_03", + 'room': "D09BZ01S01[Cell1]"}, + {'name': "WotHP: Upper east room, top silver cell", + 'region': "wothp", + 'game_id': "CO24", + 'room': "D09BZ01S01[Cell6]"}, + {'name': "WotHP: Upper east room, center gold cell", + 'region': "wothp", + 'game_id': "QI51", + 'room': "D09Z01S02"}, + {'name': "WotHP: Upper west room, center gold cell", + 'region': "wothp", + 'game_id': "CO26", + 'room': "D09BZ01S01[Cell16]"}, + {'name': "WotHP: Lower west room, bottom gold cell", + 'region': "wothp", + 'game_id': "CO02", + 'room': "D09BZ01S01[Cell21]"}, + {'name': "WotHP: Upper west room, top silver cell", + 'region': "wothp", + 'game_id': "RESCUED_CHERUB_34", + 'room': "D09BZ01S01[Cell17~18]"}, # ? + {'name': "WotHP: Lower west room, top ledge", + 'region': "wothp", + 'game_id': "RB16", + 'room': "D09BZ01S01[Cell24]"}, + {'name': "WotHP: Lower east room, hidden ledge", + 'region': "wothp", + 'game_id': "CO27", + 'room': "D09Z01S10"}, + {'name': "WotHP: Lower east room, bottom silver cell", + 'region': "wothp", + 'game_id': "RESCUED_CHERUB_04", + 'room': "D09BZ01S01[Cell11]"}, + {'name': "WotHP: Lower east room, top bronze cell", + 'region': "wothp", + 'game_id': "QI70", + 'room': "D09Z01S10"}, + {'name': "WotHP: Lower east room, top silver cell", + 'region': "wothp", + 'game_id': "CO37", + 'room': "D09BZ01S01[Cell10]"}, + {'name': "WotHP: Outside Child of Moonlight", + 'region': "wothp", + 'game_id': "RESCUED_CHERUB_05", + 'room': "D09Z01S06"}, + {'name': "WotHP: Oil of the Pilgrims", + 'region': "wothp", + 'game_id': "Oil[D09Z01S12]", + 'room': "D09Z01S12"}, + {'name': "WotHP: Quirce, Returned By The Flames", + 'region': "wothp", + 'game_id': "BS14", + 'room': "D09Z01S03"}, + {'name': "WotHP: Collapsing floor ledge", + 'region': "wothp", + 'game_id': "QI72", + 'room': "D09Z01S08"}, + {'name': "WotHP: Amanecida of the Molten Thorn", + 'region': "wothp", + 'game_id': "D09Z01S01[18000]", + 'room': "D09Z01S01"}, + + # Wasteland of the Buried Churches (8) + {'name': "WotBC: Lower log path", + 'region': "wotbc", + 'game_id': "RB04", + 'room': "D01Z03S01"}, + {'name': "WotBC: Hidden alcove", + 'region': "wotbc", + 'game_id': "CO14", + 'room': "D01Z03S02"}, + {'name': "WotBC: Outside ledge", + 'region': "wotbc", + 'game_id': "CO36", + 'room': "D01Z03S03"}, + {'name': "WotBC: Outside Child of Moonlight", + 'region': "wotbc", + 'game_id': "RESCUED_CHERUB_10", + 'room': "D01Z03S03"}, + {'name': "WotBC: Under broken bridge", + 'region': "wotbc", + 'game_id': "QI06", + 'room': "D01Z03S05"}, + {'name': "WotBC: Cliffside statue", + 'region': "wotbc", + 'game_id': "HE02", + 'room': "D01Z03S07"}, + {'name': "WotBC: Cliffside Child of Moonlight", + 'region': "wotbc", + 'game_id': "RESCUED_CHERUB_38", + 'room': "D01Z03S07"}, + {'name': "WotBC: 3rd meeting with Redento", + 'region': "wotbc", + 'game_id': "RB20", + 'room': "D01Z03S01"}, # ? + + # Where Olive Trees Wither (11) + {'name': "WOTW: Below Prie Dieu", + 'region': "wotw", + 'game_id': "CO11", + 'room': "D02Z01S01"}, + {'name': "WOTW: Entrance to tomb", + 'region': "wotw", + 'game_id': "QI20", + 'room': "D02Z01S04"}, + {'name': "WOTW: Gift for the tomb", + 'region': "wotw", + 'game_id': "QI68", + 'room': "D02Z01S"}, + {'name': "WOTW: Underground tomb", + 'region': "wotw", + 'game_id': "PR04", + 'room': "D02Z01S08"}, + {'name': "WOTW: Underground Child of Moonlight", + 'region': "wotw", + 'game_id': "RESCUED_CHERUB_27", + 'room': "D02Z01S06"}, + {'name': "WOTW: Underground ledge", + 'region': "wotw", + 'game_id': "CO19", + 'room': "D02Z01S06"}, + {'name': "WOTW: Upper east Child of Moonlight", + 'region': "wotw", + 'game_id': "RESCUED_CHERUB_23", + 'room': "D02Z01S09"}, + {'name': "WOTW: Upper east statue", + 'region': "wotw", + 'game_id': "HE05", + 'room': "D02Z01S09"}, + {'name': "WOTW: Death run", + 'region': "wotw", + 'game_id': "QI07", + 'room': "D02Z01S05"}, + {'name': "WOTW: Gemino's gift", + 'region': "wotw", + 'game_id': "QI59", + 'room': "D02Z01S01"}, + {'name': "WOTW: Gemino's reward", + 'region': "wotw", + 'game_id': "RB10", + 'room': "D02Z01S01"}, + + # Various (20) + {'name': "Confessor Dungeon 1 extra", + 'region': "dungeon", + 'game_id': "Arena_NailManager[1000]", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 1 main", + 'region': "dungeon", + 'game_id': "QI32", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 2 extra", + 'region': "dungeon", + 'game_id': "HE10", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 2 main", + 'region': "dungeon", + 'game_id': "QI33", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 3 extra", + 'region': "dungeon", + 'game_id': "Arena_NailManager[3000]", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 3 main", + 'region': "dungeon", + 'game_id': "QI34", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 4 extra", + 'region': "dungeon", + 'game_id': "RB34", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 4 main", + 'region': "dungeon", + 'game_id': "QI35", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 5 extra", + 'region': "dungeon", + 'game_id': "Arena_NailManager[5000]", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 5 main", + 'region': "dungeon", + 'game_id': "QI79", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 6 extra", + 'region': "dungeon", + 'game_id': "RB35", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 6 main", + 'region': "dungeon", + 'game_id': "QI80", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 7 extra", + 'region': "dungeon", + 'game_id': "RB36", + 'room': "dungeon"}, + {'name': "Confessor Dungeon 7 main", + 'region': "dungeon", + 'game_id': "QI81", + 'room': "dungeon"}, + {'name': "Defeat 1 Amanecida", + 'region': "dungeon", + 'game_id': "QI107", + 'room': "dungeon"}, + {'name': "Defeat 2 Amanecidas", + 'region': "dungeon", + 'game_id': "QI108", + 'room': "dungeon"}, + {'name': "Defeat 3 Amanecidas", + 'region': "dungeon", + 'game_id': "QI109", + 'room': "dungeon"}, + {'name': "Defeat 4 Amanecidas", + 'region': "dungeon", + 'game_id': "QI110", + 'room': "dungeon"}, + {'name': "Defeat all Amanecidas", + 'region': "dungeon", + 'game_id': "PR101", + 'room': "dungeon"}, + {'name': "Skill 1, Tier 1", + 'region': "dungeon", + 'game_id': "COMBO_1", + 'room': "dungeon"}, + {'name': "Skill 1, Tier 2", + 'region': "dungeon", + 'game_id': "COMBO_2", + 'room': "dungeon"}, + {'name': "Skill 1, Tier 3", + 'region': "dungeon", + 'game_id': "COMBO_3", + 'room': "dungeon"}, + {'name': "Skill 2, Tier 1", + 'region': "dungeon", + 'game_id': "CHARGED_1", + 'room': "dungeon"}, + {'name': "Skill 2, Tier 2", + 'region': "dungeon", + 'game_id': "CHARGED_2", + 'room': "dungeon"}, + {'name': "Skill 2, Tier 3", + 'region': "dungeon", + 'game_id': "CHARGED_3", + 'room': "dungeon"}, + {'name': "Skill 3, Tier 1", + 'region': "dungeon", + 'game_id': "RANGED_1", + 'room': "dungeon"}, + {'name': "Skill 3, Tier 2", + 'region': "dungeon", + 'game_id': "RANGED_2", + 'room': "dungeon"}, + {'name': "Skill 3, Tier 3", + 'region': "dungeon", + 'game_id': "RANGED_3", + 'room': "dungeon"}, + {'name': "Skill 4, Tier 1", + 'region': "dungeon", + 'game_id': "VERTICAL_1", + 'room': "dungeon"}, + {'name': "Skill 4, Tier 2", + 'region': "dungeon", + 'game_id': "VERTICAL_2", + 'room': "dungeon"}, + {'name': "Skill 4, Tier 3", + 'region': "dungeon", + 'game_id': "VERTICAL_3", + 'room': "dungeon"}, + {'name': "Skill 5, Tier 1", + 'region': "dungeon", + 'game_id': "LUNGE_1", + 'room': "dungeon"}, + {'name': "Skill 5, Tier 2", + 'region': "dungeon", + 'game_id': "LUNGE_2", + 'room': "dungeon"}, + {'name': "Skill 5, Tier 3", + 'region': "dungeon", + 'game_id': "LUNGE_3", + 'room': "dungeon"}, +] + +shop_set: Set[str] = [ + "GotP: Shop item 1", + "GotP: Shop item 2", + "GotP: Shop item 3", + "MD: Shop item 1", + "MD: Shop item 2", + "MD: Shop item 3", + "TSC: Shop item 1", + "TSC: Shop item 2", + "TSC: Shop item 3" +] \ No newline at end of file diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py new file mode 100644 index 0000000000..be43d8b7c8 --- /dev/null +++ b/worlds/blasphemous/Options.py @@ -0,0 +1,257 @@ +from Options import Choice, Toggle, DefaultOnToggle, DeathLink + + +class PrieDieuWarp(DefaultOnToggle): + """Automatically unlocks the ability to warp between Prie Dieu shrines.""" + display_name = "Unlock Fast Travel" + + +class SkipCutscenes(DefaultOnToggle): + """Automatically skips most cutscenes.""" + display_name = "Auto Skip Cutscenes" + + +class CorpseHints(DefaultOnToggle): + """Changes the 34 corpses in game to give various hints about item locations.""" + display_name = "Corpse Hints" + + +class Difficulty(Choice): + """Adjusts the logic required to defeat bosses. + Impossible: Removes all logic requirements for bosses. Good luck.""" + display_name = "Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_impossible = 3 + default = 1 + + +class Penitence(Toggle): + """Allows one of the three Penitences to be chosen at the beginning of the game.""" + display_name = "Penitence" + + +class ExpertLogic(Toggle): + """Expands the logic used by the randomizer to allow for some difficult and/or lesser known tricks.""" + display_name = "Expert Logic" + + +class Ending(Choice): + """Choose which ending is required to complete the game.""" + display_name = "Ending" + option_any_ending = 0 + option_ending_b = 1 + option_ending_c = 2 + default = 0 + + +class ThornShuffle(Choice): + """Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.""" + display_name = "Shuffle Thorn" + option_anywhere = 0 + option_local_only = 1 + option_vanilla = 2 + default = 0 + + +class ReliquaryShuffle(DefaultOnToggle): + """Adds the True Torment exclusive Reliquary rosary beads into the item pool.""" + display_name = "Shuffle Penitence Rewards" + + +class CherubShuffle(DefaultOnToggle): + """Shuffles Children of Moonlight into the item pool.""" + display_name = "Shuffle Children of Moonlight" + + +class LifeShuffle(DefaultOnToggle): + """Shuffles life upgrades from the Lady of the Six Sorrows into the item pool.""" + display_name = "Shuffle Life Upgrades" + + +class FervourShuffle(DefaultOnToggle): + """Shuffles fervour upgrades from the Oil of the Pilgrims into the item pool.""" + display_name = "Shuffle Fervour Upgrades" + + +class SwordShuffle(DefaultOnToggle): + """Shuffles Mea Culpa upgrades from the Mea Culpa Altars into the item pool.""" + display_name = "Shuffle Mea Culpa Upgrades" + + +class BlessingShuffle(DefaultOnToggle): + """Shuffles blessings from the Lake of Silent Pilgrims into the item pool.""" + display_name = "Shuffle Blessings" + + +class DungeonShuffle(DefaultOnToggle): + """Shuffles rewards from completing Confessor Dungeons into the item pool.""" + display_name = "Shuffle Dungeon Rewards" + + +class TirsoShuffle(DefaultOnToggle): + """Shuffles rewards from delivering herbs to Tirso into the item pool.""" + display_name = "Shuffle Tirso's Rewards" + + +class MiriamShuffle(DefaultOnToggle): + """Shuffles the prayer given by Miriam into the item pool.""" + display_name = "Shuffle Miriram's Reward" + + +class RedentoShuffle(DefaultOnToggle): + """Shuffles rewards from assisting Redento into the item pool.""" + display_name = "Shuffle Redento's Rewards" + + +class JocineroShuffle(DefaultOnToggle): + """Shuffles rewards from rescuing 20 and 38 Children of Moonlight into the item pool.""" + display_name = "Shuffle Jocinero's Rewards" + + +class AltasgraciasShuffle(DefaultOnToggle): + """Shuffles the reward given by Altasgracias and the item left behind by them into the item pool.""" + display_name = "Shuffle Altasgracias' Rewards" + + +class TentudiaShuffle(DefaultOnToggle): + """Shuffles the rewards from delivering Tentudia's remains to Lvdovico into the item pool.""" + display_name = "Shuffle Lvdovico's Rewards" + + +class GeminoShuffle(DefaultOnToggle): + """Shuffles the rewards from Gemino's quest and the hidden tomb into the item pool.""" + display_name = "Shuffle Gemino's Rewards" + + +class GuiltShuffle(DefaultOnToggle): + """Shuffles the Weight of True Guilt into the item pool.""" + display_name = "Shuffle Immaculate Bead" + + +class OssuaryShuffle(DefaultOnToggle): + """Shuffles the rewards from delivering bones to the Ossuary into the item pool.""" + display_name = "Shuffle Ossuary Rewards" + + +class BossShuffle(DefaultOnToggle): + """Shuffles the Tears of Atonement from defeating bosses into the item pool.""" + display_name = "Shuffle Boss Tears" + + +class WoundShuffle(DefaultOnToggle): + """Shuffles the Holy Wounds required to pass the Bridge of the Three Cavalries into the item pool.""" + display_name = "Shuffle Holy Wounds" + + +class MaskShuffle(DefaultOnToggle): + """Shuffles the masks required to use the elevator in Archcathedral Rooftops into the item pool.""" + display_name = "Shuffle Masks" + + +class EyeShuffle(DefaultOnToggle): + """Shuffles the Eyes of the Traitor from defeating Isidora and Sierpes into the item pool.""" + display_name = "Shuffle Traitor's Eyes" + + +class HerbShuffle(DefaultOnToggle): + """Shuffles the herbs required for Tirso's quest into the item pool.""" + display_name = "Shuffle Herbs" + + +class ChurchShuffle(DefaultOnToggle): + """Shuffles the rewards from donating 5,000 and 50,000 Tears of Atonement to the Church in Albero into the item pool.""" + display_name = "Shuffle Donation Rewards" + + +class ShopShuffle(DefaultOnToggle): + """Shuffles the items sold in Candelaria's shops into the item pool.""" + display_name = "Shuffle Shop Items" + + +class CandleShuffle(DefaultOnToggle): + """Shuffles the Beads of Wax and their upgrades into the item pool.""" + display_name = "Shuffle Candles" + + +class StartWheel(Toggle): + """Changes the beginning gift to The Young Mason's Wheel.""" + display_name = "Start with Wheel" + + +class SkillRando(Toggle): + """Randomizes the abilities from the skill tree into the item pool.""" + display_name = "Skill Randomizer" + + +class EnemyRando(Choice): + """Randomizes the enemies that appear in each room. + Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game. + Randomized: Every enemy is completely random, and can appear any number of times. + Some enemies will never be randomized.""" + display_name = "Enemy Randomizer" + option_disabled = 0 + option_shuffled = 1 + option_randomized = 2 + default = 0 + + +class EnemyGroups(DefaultOnToggle): + """Randomized enemies will chosen from sets of specific groups. + (Weak, normal, large, flying) + Has no effect if Enemy Randomizer is disabled.""" + display_name = "Enemy Groups" + + +class EnemyScaling(DefaultOnToggle): + """Randomized enemies will have their stats increased or decreased depending on the area they appear in. + Has no effect if Enemy Randomizer is disabled.""" + display_name = "Enemy Scaling" + + +class BlasphemousDeathLink(DeathLink): + """When you die, everyone dies. The reverse is also true. + Note that Guilt Fragments will not appear when killed by Death Link.""" + + +blasphemous_options = { + "prie_dieu_warp": PrieDieuWarp, + "skip_cutscenes": SkipCutscenes, + "corpse_hints": CorpseHints, + "difficulty": Difficulty, + "penitence": Penitence, + "expert_logic": ExpertLogic, + "ending": Ending, + "thorn_shuffle" : ThornShuffle, + "reliquary_shuffle": ReliquaryShuffle, + "cherub_shuffle" : CherubShuffle, + "life_shuffle" : LifeShuffle, + "fervour_shuffle" : FervourShuffle, + "sword_shuffle" : SwordShuffle, + "blessing_shuffle" : BlessingShuffle, + "dungeon_shuffle" : DungeonShuffle, + "tirso_shuffle" : TirsoShuffle, + "miriam_shuffle" : MiriamShuffle, + "redento_shuffle" : RedentoShuffle, + "jocinero_shuffle" : JocineroShuffle, + "altasgracias_shuffle" : AltasgraciasShuffle, + "tentudia_shuffle" : TentudiaShuffle, + "gemino_shuffle" : GeminoShuffle, + "guilt_shuffle" : GuiltShuffle, + "ossuary_shuffle" : OssuaryShuffle, + "boss_shuffle" : BossShuffle, + "wound_shuffle" : WoundShuffle, + "mask_shuffle" : MaskShuffle, + "eye_shuffle": EyeShuffle, + "herb_shuffle" : HerbShuffle, + "church_shuffle" : ChurchShuffle, + "shop_shuffle" : ShopShuffle, + "candle_shuffle" : CandleShuffle, + "start_wheel": StartWheel, + "skill_randomizer": SkillRando, + "enemy_randomizer": EnemyRando, + "enemy_groups": EnemyGroups, + "enemy_scaling": EnemyScaling, + "death_link": BlasphemousDeathLink +} \ No newline at end of file diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py new file mode 100644 index 0000000000..6bf4a6858d --- /dev/null +++ b/worlds/blasphemous/Rules.py @@ -0,0 +1,1455 @@ +from worlds.generic.Rules import set_rule, add_rule +from ..AutoWorld import LogicMixin + + +class BlasphemousLogic(LogicMixin): + def _blasphemous_blood_relic(self, player): + return self.has("Blood Perpetuated in Sand", player) + + def _blasphemous_water_relic(self, player): + return self.has("Nail Uprooted from Dirt", player) + + def _blasphemous_corpse_relic(self, player): + return self.has("Shroud of Dreamt Sins", player) + + def _blasphemous_fall_relic(self, player): + return self.has("Linen of Golden Thread", player) + + def _blasphemous_miasma_relic(self, player): + return self.has("Silvered Lung of Dolphos", player) + + def _blasphemous_root_relic(self, player): + return self.has("Three Gnarled Tongues", player) + + def _blasphemous_open_holes(self, player): + return self.has_any({"Dive Skill", "Charged Skill"}, player) or \ + self.has_group("prayer", player, 1) or \ + (self.has_any({"Tirana of the Celestial Bastion", "Aubade of the Nameless Guardian"}, player) and \ + self.has("Fervour Upgrade", player, 2)) + + def _blasphemous_bell(self, player): + return self.has("Petrified Bell", player) + + def _blasphemous_bead(self, player): + return self.has("Weight of True Guilt", player) + + def _blasphemous_cloth(self, player): + return self.has("Linen Cloth", player) + + def _blasphemous_pre_egg(self, player): + return self.has("Egg of Deformity", player) + + def _blasphemous_egg(self, player): + return self.has("Hatched Egg of Deformity", player) + + def _blasphemous_hand(self, player): + return self.has("Severed Hand", player) + + def _blasphemous_chalice(self, player): + return self.has("Chalice of Inverted Verses", player) + + def _blasphemous_thimble(self, player): + return self.has("Empty Golden Thimble", player) + + def _blasphemous_full_thimble(self, player): + return self.has("Golden Thimble Filled with Burning Oil", player) + + def _blasphemous_flowers(self, player): + return self.has("Dried Flowers bathed in Tears", player) + + def _blasphemous_redento(self, player): + return self.has_all({"Little Toe made of Limestone", "Big Toe made of Limestone", \ + "Fourth Toe made of Limestone"}, player) and \ + self.has("Knot of Rosary Rope", player) + + def _blasphemous_cord(self, player): + return self.has("Cord of the True Burying", player) + + def _blasphemous_marks(self, player): + return self.has_all({"Mark of the First Refuge", "Mark of the Second Refuge", \ + "Mark of the Third Refuge"}, player) + + def _blasphemous_red_wax(self, player): + return self.has("Bead of Red Wax", player) + + def _blasphemous_blue_wax(self, player): + return self.has("Bead of Blue Wax", player) + + def _blasphemous_both_wax(self, player): + return self.has("Bead of Red Wax", player, 3) and \ + self.has("Bead of Blue Wax", player, 3) + + def _blasphemous_elder_key(self, player): + return self.has("Key to the Chamber of the Eldest Brother", player) + + def _blasphemous_bronze_key(self, player): + return self.has("Key of the Secular", player) + + def _blasphemous_silver_key(self, player): + return self.has("Key of the Scribe", player) + + def _blasphemous_gold_key(self, player): + return self.has("Key of the Inquisitor", player) + + def _blasphemous_high_key(self, player): + return self.has("Key of the High Peaks", player) + + def _blasphemous_wood_key(self, player): + return self.has("Key Grown from Twisted Wood", player) + + def _blasphemous_scapular(self, player): + return self.has("Incomplete Scapular", player) + + def _blasphemous_heart_c(self, player): + return self.has("Apodictic Heart of Mea Culpa", player) + + def _blasphemous_eyes(self, player): + return self.has("Severed Right Eye of the Traitor", player) and \ + self.has("Broken Left Eye of the Traitor", player) + + def _blasphemous_debla(self, player): + return self.has("Debla of the Lights", player) + + def _blasphemous_taranto(self, player): + return self.has("Taranto to my Sister", player) + + def _blasphemous_tirana(self, player): + return self.has("Tirana of the Celestial Bastion", player) + + def _blasphemous_aubade(self, player): + return self.has("Aubade of the Nameless Guardian", player) + + def _blasphemous_cherub_6(self, player): + return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Verdiales of the Forsaken Hamlet", \ + "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + + def _blasphemous_cherub_13(self, player): + return self.has_any({"Ranged Skill", "Debla of the Lights", "Taranto to my Sister", \ + "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ + "Cloistered Ruby"}, player) + + def _blasphemous_cherub_20(self, player): + return self.has_any({"Debla of the Lights", "Lorqiana", "Zarabanda of the Safe Haven", "Taranto to my Sister", \ + "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ + "Cloistered Ruby"}, player) + + def _blasphemous_cherub_21(self, player): + return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ + "Verdiales of the Forsaken Hamlet", "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + + def _blasphemous_cherub_22_23_31_32(self, player): + return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cloistered Ruby"}, player) + + def _blasphemous_cherub_24_33(self, player): + return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ + "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + + def _blasphemous_cherub_25(self, player): + return self.has_any({"Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ + "Cante Jondo of the Three Sisters", "Verdiales of the Forsaken Hamlet", "Aubade of the Nameless Guardian", \ + "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + + def _blasphemous_cherub_27(self, player): + return self.has_any({"Ranged Skill", "Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ + "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", \ + "Cloistered Ruby"}, player) + + def _blasphemous_cherub_38(self, player): + return self.has_any({"Ranged Skill", "Lorquiana", "Cante Jondo of the Three Sisters", \ + "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ + (self.has("The Young Mason's Wheel", player) and \ + self.has("Brilliant Heart of Dawn", player)) + + def _blasphemous_wheel(self, player): + return self.has("The Young Mason's Wheel", player) + + def _blasphemous_dawn_heart(self, player): + return self.has("Brilliant Heart of Dawn", player) + + def _blasphemous_tirso_1(self, player): + return self.has_group("tirso", player, 1) + + def _blasphemous_tirso_2(self, player): + return self.has_group("tirso", player, 2) + + def _blasphemous_tirso_3(self, player): + return self.has_group("tirso", player, 3) + + def _blasphemous_tirso_4(self, player): + return self.has_group("tirso", player, 4) + + def _blasphemous_tirso_5(self, player): + return self.has_group("tirso", player, 5) + + def _blasphemous_tirso_6(self, player): + return self.has_group("tirso", player, 6) + + def _blasphemous_tentudia_1(self, player): + return self.has_group("tentudia", player, 1) + + def _blasphemous_tentudia_2(self, player): + return self.has_group("tentudia", player, 2) + + def _blasphemous_tentudia_3(self, player): + return self.has_group("tentudia", player, 3) + + def _blasphemous_altasgracias_3(self, player): + return self.has_group("egg", player, 3) + + def _blasphemous_cherubs_20(self, player): + return self.has("Child of Moonlight", player, 20) + + def _blasphemous_cherubs_all(self, player): + return self.has("Child of Moonlight", player, 38) + + def _blasphemous_bones_4(self, player): + return self.has_group("bones", player, 4) + + def _blasphemous_bones_8(self, player): + return self.has_group("bones", player, 8) + + def _blasphemous_bones_12(self, player): + return self.has_group("bones", player, 12) + + def _blasphemous_bones_16(self, player): + return self.has_group("bones", player, 16) + + def _blasphemous_bones_20(self, player): + return self.has_group("bones", player, 20) + + def _blasphemous_bones_24(self, player): + return self.has_group("bones", player, 24) + + def _blasphemous_bones_28(self, player): + return self.has_group("bones", player, 28) + + def _blasphemous_bones_30(self, player): + return self.has_group("bones", player, 30) + + def _blasphemous_bones_32(self, player): + return self.has_group("bones", player, 32) + + def _blasphemous_bones_36(self, player): + return self.has_group("bones", player, 36) + + def _blasphemous_bones_40(self, player): + return self.has_group("bones", player, 40) + + def _blasphemous_bones_44(self, player): + return self.has_group("bones", player, 44) + + def _blasphemous_sword_1(self, player): + return self.has("Mea Culpa Upgrade", player) + + def _blasphemous_sword_2(self, player): + return self.has("Mea Culpa Upgrade", player, 2) + + def _blasphemous_sword_3(self, player): + return self.has("Mea Culpa Upgrade", player, 3) + + def _blasphemous_sword_4(self, player): + return self.has("Mea Culpa Upgrade", player, 4) + + def _blasphemous_sword_5(self, player): + return self.has("Mea Culpa Upgrade", player, 5) + + def _blasphemous_sword_6(self, player): + return self.has("Mea Culpa Upgrade", player, 6) + + def _blasphemous_sword_7(self, player): + return self.has("Mea Culpa Upgrade", player, 7) + + def _blasphemous_ranged(self, player): + return self.has("Ranged Skill", player) + + def _blasphemous_bridge_access(self, player): + return self.has_group("wounds", player, 3) + + def _blasphemous_ex_bridge_access(self, player): + return self.has_group("wounds", player, 3) or \ + (self.has("Brilliant Heart of Dawn", player) and \ + self.has("Ranged Skill", player) and \ + self.has("Blood Perpetuated in Sand", player)) or \ + (self.has("Blood Perpetuated in Sand", player) and \ + self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2)) + + def _blasphemous_1_mask(self, player): + return self.has_group("masks", player, 1) + + def _blasphemous_2_masks(self, player): + return self.has_group("masks", player, 2) + + def _blasphemous_3_masks(self, player): + return self.has_group("masks", player, 3) + + def _blasphemous_laudes_gate(self, player): + return self.has_all({"Petrified Bell", "Blood Perpetuated in Sand", "Three Gnarled Tongues", "Key of the Secular", "Key of the Scribe", "Verses Spun from Gold"}, player) + + # Ten Piedad, Tres Angustias, Our Lady of the Charred Visage + def _blasphemous_wound_boss_easy(self, player): + return self.has("Mea Culpa Upgrade", player, 2) and \ + self.has_group("power", player, 3) + + def _blasphemous_wound_boss_normal(self, player): + return self.has("Mea Culpa Upgrade", player, 1) + + def _blasphemous_wound_boss_hard(self, player): + return True + + # Esdras + def _blasphemous_esdras_boss_easy(self, player): + return self.has("Mea Culpa Upgrade", player, 3) and \ + self.has_group("power", player, 5) + + def _blasphemous_esdras_boss_normal(self, player): + return self.has("Mea Culpa Upgrade", player, 2) and \ + self.has_group("power", player, 2) + + def _blasphemous_esdras_boss_hard(self, player): + return self.has("Mea Culpa Upgrade", player, 1) and \ + self.has_group("power", player, 1) + + # Melquiades, Exposito, Quirce + def _blasphemous_mask_boss_easy(self, player): + return self.has("Mea Culpa Upgrade", player, 4) and \ + self.has_group("power", player, 8) + + def _blasphemous_mask_boss_normal(self, player): + return self.has("Mea Culpa Upgrade", player, 3) and \ + self.has_group("power", player, 4) + + def _blasphemous_mask_boss_hard(self, player): + return self.has("Mea Culpa Upgrade", player, 2) and \ + self.has_group("power", player, 2) + + # Crisanta, Isidora, Sierpes, Amanecidas, Laudes + def _blasphemous_endgame_boss_easy(self, player): + return self.has("Mea Culpa Upgrade", player, 6) and \ + self.has_group("power", player, 16) + + def _blasphemous_endgame_boss_normal(self, player): + return self.has("Mea Culpa Upgrade", player, 5) and \ + self.has_group("power", player, 8) + + def _blasphemous_endgame_boss_hard(self, player): + return self.has("Mea Culpa Upgrade", player, 4) and \ + self.has_group("power", player, 5) + + +def rules(blasphemousworld): + world = blasphemousworld.multiworld + player = blasphemousworld.player + + # entrances + for i in world.get_region("Deambulatory of His Holiness", player).entrances: + set_rule(i, lambda state: state._blasphemous_3_masks(player)) + for i in world.get_region("Ferrous Tree", player).entrances: + set_rule(i, lambda state: state._blasphemous_bridge_access(player)) + for i in world.get_region("Mother of Mothers", player).entrances: + set_rule(i, lambda state: state._blasphemous_bridge_access(player)) + for i in world.get_region("Mourning and Havoc", player).entrances: + set_rule(i, lambda state: state._blasphemous_blood_relic(player) or \ + state.can_reach(world.get_region("Mother of Mothers", player), player)) + for i in world.get_region("Patio of the Silent Steps", player).entrances: + set_rule(i, lambda state: state._blasphemous_bridge_access(player)) + for i in world.get_region("The Resting Place of the Sister", player).entrances: + set_rule(i, lambda state: state._blasphemous_blood_relic(player)) + for i in world.get_region("The Sleeping Canvases", player).entrances: + set_rule(i, lambda state: state._blasphemous_bridge_access(player)) + for i in world.get_region("Wall of the Holy Prohibitions", player).entrances: + set_rule(i, lambda state: state._blasphemous_1_mask(player) and \ + state._blasphemous_bridge_access(player)) + + # Albero + set_rule(world.get_location("Albero: Bless Linen Cloth", player), + lambda state: state._blasphemous_cloth(player)) + set_rule(world.get_location("Albero: Bless Hatched Egg", player), + lambda state: state._blasphemous_egg(player)) + set_rule(world.get_location("Albero: Bless Severed Hand", player), + lambda state: state._blasphemous_hand(player)) + set_rule(world.get_location("Albero: First gift for Cleofas", player), + lambda state: state.can_reach(world.get_region("Mother of Mothers", player))) + set_rule(world.get_location("Albero: Final gift for Cleofas", player), + lambda state: state.can_reach(world.get_region("Mother of Mothers", player)) and \ + state._blasphemous_marks(player) and \ + state._blasphemous_cord(player)) + set_rule(world.get_location("Albero: Tirso's 1st reward", player), + lambda state: state._blasphemous_tirso_1(player)) + set_rule(world.get_location("Albero: Tirso's 2nd reward", player), + lambda state: state._blasphemous_tirso_2(player)) + set_rule(world.get_location("Albero: Tirso's 3rd reward", player), + lambda state: state._blasphemous_tirso_3(player)) + set_rule(world.get_location("Albero: Tirso's 4th reward", player), + lambda state: state._blasphemous_tirso_4(player)) + set_rule(world.get_location("Albero: Tirso's 5th reward", player), + lambda state: state._blasphemous_tirso_5(player)) + set_rule(world.get_location("Albero: Tirso's 6th reward", player), + lambda state: state._blasphemous_tirso_6(player)) + set_rule(world.get_location("Albero: Tirso's final reward", player), + lambda state: state._blasphemous_tirso_6(player) and \ + state.can_reach(world.get_region("Wall of the Holy Prohibitions", player)) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("Albero: Lvdovico's 1st reward", player), + lambda state: state._blasphemous_tentudia_1(player)) + set_rule(world.get_location("Albero: Lvdovico's 2nd reward", player), + lambda state: state._blasphemous_tentudia_2(player)) + set_rule(world.get_location("Albero: Lvdovico's 3rd reward", player), + lambda state: state._blasphemous_tentudia_3(player)) + set_rule(world.get_location("Ossuary: Isidora, Voice of the Dead", player), + lambda state: state._blasphemous_bones_30(player)) + set_rule(world.get_location("Ossuary: 1st reward", player), + lambda state: state._blasphemous_bones_4(player)) + set_rule(world.get_location("Ossuary: 2nd reward", player), + lambda state: state._blasphemous_bones_8(player)) + set_rule(world.get_location("Ossuary: 3rd reward", player), + lambda state: state._blasphemous_bones_12(player)) + set_rule(world.get_location("Ossuary: 4th reward", player), + lambda state: state._blasphemous_bones_16(player)) + set_rule(world.get_location("Ossuary: 5th reward", player), + lambda state: state._blasphemous_bones_20(player)) + set_rule(world.get_location("Ossuary: 6th reward", player), + lambda state: state._blasphemous_bones_24(player)) + set_rule(world.get_location("Ossuary: 7th reward", player), + lambda state: state._blasphemous_bones_28(player)) + set_rule(world.get_location("Ossuary: 8th reward", player), + lambda state: state._blasphemous_bones_32(player)) + set_rule(world.get_location("Ossuary: 9th reward", player), + lambda state: state._blasphemous_bones_36(player)) + set_rule(world.get_location("Ossuary: 10th reward", player), + lambda state: state._blasphemous_bones_40(player)) + set_rule(world.get_location("Ossuary: 11th reward", player), + lambda state: state._blasphemous_bones_44(player)) + + # All the Tears of the Sea + set_rule(world.get_location("AtTotS: Miriam's gift", player), + lambda state: state._blasphemous_2_masks(player) and \ + state._blasphemous_fall_relic(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_miasma_relic(player)) + + # Archcathedral Rooftops + set_rule(world.get_location("AR: Second soldier fight", player), + lambda state: state._blasphemous_1_mask(player)) + set_rule(world.get_location("AR: Third soldier fight", player), + lambda state: state._blasphemous_2_masks(player)) + set_rule(world.get_location("AR: Upper west shaft Child of Moonlight", player), + lambda state: state._blasphemous_1_mask(player)) + set_rule(world.get_location("AR: Upper west shaft chest", player), + lambda state: state._blasphemous_2_masks(player) and \ + state._blasphemous_fall_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("AR: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("AR: Upper east shaft ledge", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_1_mask(player)) + set_rule(world.get_location("AR: Mea Culpa altar", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_2_masks(player)) + set_rule(world.get_location("AR: Crisanta of the Wrapped Agony", player), + lambda state: state._blasphemous_3_masks(player)) + + # Bridge of the Three Cavalries + set_rule(world.get_location("BotTC: Esdras, of the Anointed Legion", player), + lambda state: state._blasphemous_bridge_access(player)) + set_rule(world.get_location("BotTC: Esdras' gift", player), + lambda state: state._blasphemous_bridge_access(player)) + set_rule(world.get_location("BotTC: Inside giant statue", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_laudes_gate(player) and \ + state._blasphemous_1_mask(player)) + + # Brotherhood of the Silent Sorrow + set_rule(world.get_location("BotSS: Starting room Child of Moonlight", player), + lambda state: (state._blasphemous_blood_relic(player) and \ + (state._blasphemous_root_relic(player)) or \ + (state._blasphemous_fall_relic(player))) or \ + (state._blasphemous_blood_relic(player) and \ + state._blasphemous_cherub_6(player)) or \ + (state._blasphemous_debla(player) or \ + state._blasphemous_taranto(player))) + set_rule(world.get_location("BotSS: Starting room ledge", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_fall_relic(player)) + set_rule(world.get_location("BotSS: Chamber of the Eldest Brother", player), + lambda state: state._blasphemous_elder_key(player)) + set_rule(world.get_location("BotSS: Blue candle", player), + lambda state: state._blasphemous_blue_wax(player)) + set_rule(world.get_location("BotSS: Outside church", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("BotSS: Esdras' final gift", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_scapular(player) and \ + state._blasphemous_bridge_access(player)) + set_rule(world.get_location("BotSS: Crisanta's gift", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_scapular(player) and \ + state._blasphemous_heart_c(player) and \ + state._blasphemous_3_masks(player) and \ + state._blasphemous_bridge_access(player)) + + # Convent of our Lady of the Charred Visage + set_rule(world.get_location("CoOLotCV: Lower west statue", player), + lambda state: state._blasphemous_miasma_relic(player)) + set_rule(world.get_location("CoOLotCV: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_high_key(player)) + set_rule(world.get_location("CoOLotCV: Red candle", player), + lambda state: state._blasphemous_red_wax(player)) + set_rule(world.get_location("CoOLotCV: Fountain of burning oil", player), + lambda state: state._blasphemous_thimble(player)) + set_rule(world.get_location("CoOLotCV: Mask room", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_high_key(player)) + + # Desecrated Cistern + set_rule(world.get_location("DC: Upper east tunnel chest", player), + lambda state: state._blasphemous_water_relic(player) or \ + state._blasphemous_fall_relic(player)) + set_rule(world.get_location("DC: Upper east Child of Moonlight", player), + lambda state: state._blasphemous_water_relic(player) or \ + state._blasphemous_fall_relic(player) or \ + state._blasphemous_cherub_13(player)) + set_rule(world.get_location("DC: Hidden alcove near fountain", player), + lambda state: state._blasphemous_water_relic(player)) + set_rule(world.get_location("DC: Shroud puzzle", player), + lambda state: state._blasphemous_corpse_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player)) + set_rule(world.get_location("DC: Chalice room", player), + lambda state: (state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + state._blasphemous_root_relic(player)) or \ + (state._blasphemous_fall_relic(player) and \ + state._blasphemous_root_relic(player))) + set_rule(world.get_location("DC: Mea Culpa altar", player), + lambda state: state._blasphemous_chalice(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("DC: Child of Moonlight, behind pillar", player), + lambda state: state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player)) + set_rule(world.get_location("DC: High ledge near elevator shaft", player), + lambda state: state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player)) + set_rule(world.get_location("DC: Elevator shaft Child of Moonlight", player), + lambda state: state._blasphemous_fall_relic(player) or \ + (state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + state._blasphemous_cherub_22_23_31_32(player))) + set_rule(world.get_location("DC: Elevator shaft ledge", player), + lambda state: state._blasphemous_fall_relic(player)) + + # Graveyard of the Peaks + set_rule(world.get_location("GotP: Shop cave Child of Moonlight", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_fall_relic(player) or \ + state._blasphemous_cherub_22_23_31_32(player)) + # to do: or dive + set_rule(world.get_location("GotP: Shop cave hidden hole", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_open_holes(player)) + set_rule(world.get_location("GotP: Upper east shaft", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("GotP: East cliffside", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("GotP: West shaft Child of Moonlight", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_cherub_25(player)) + set_rule(world.get_location("GotP: Center shaft Child of Moonlight", player), + lambda state: state._blasphemous_fall_relic(player) or \ + state._blasphemous_cherub_24_33(player)) + # to do: requires dive + set_rule(world.get_location("GotP: Amanecida of the Bejeweled Arrow", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_open_holes(player)) + + # Grievance Ascends + set_rule(world.get_location("GA: Lower west ledge", player), + lambda state: state._blasphemous_miasma_relic(player)) + set_rule(world.get_location("GA: Miasma room floor", player), + lambda state: state._blasphemous_miasma_relic(player)) + set_rule(world.get_location("GA: Oil of the Pilgrims", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("GA: End of blood bridge", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("GA: Blood bridge Child of Moonlight", player), + lambda state: state._blasphemous_blood_relic(player) and \ + ((state._blasphemous_aubade(player) and \ + state._blasphemous_ranged(player)) or \ + state._blasphemous_cherub_21(player))) + set_rule(world.get_location("GA: Lower east Child of Moonlight", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_cherub_20(player)) + set_rule(world.get_location("GA: Altasgracias' gift", player), + lambda state: state._blasphemous_altasgracias_3(player)) + set_rule(world.get_location("GA: Empty giant egg", player), + lambda state: state._blasphemous_altasgracias_3(player) and \ + state._blasphemous_egg(player)) + + # Hall of the Dawning + set_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_laudes_gate(player)) + + # Jondo + set_rule(world.get_location("Jondo: Upper east chest", player), + lambda state: state._blasphemous_fall_relic(player) or \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("Jondo: Upper west tree root", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player)) + + # Knot of the Three Words + set_rule(world.get_location("KotTW: Gift from the Traitor", player), + lambda state: state._blasphemous_wood_key(player) and \ + state._blasphemous_eyes(player)) + + # Library of the Negated Words + set_rule(world.get_location("LotNW: Root ceiling platform", player), + lambda state: state._blasphemous_root_relic(player)) + # to do: requires dive (sometimes opens with other skills?) + set_rule(world.get_location("LotNW: Hidden floor", player), + lambda state: state._blasphemous_open_holes(player)) + set_rule(world.get_location("LotNW: Miasma hallway chest", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_miasma_relic(player)) + set_rule(world.get_location("LotNW: Platform puzzle chest", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("LotNW: Red candle", player), + lambda state: state._blasphemous_red_wax(player)) + set_rule(world.get_location("LotNW: Twisted wood hidden wall", player), + lambda state: state._blasphemous_wood_key(player)) + + # Mercy Dreams + set_rule(world.get_location("MD: Blue candle", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_blue_wax(player)) + set_rule(world.get_location("MD: Cave Child of Moonlight", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_cherub_24_33(player)) + set_rule(world.get_location("MD: Behind gate to TSC", player), + lambda state: state._blasphemous_bridge_access(player)) + + # Mother of Mothers + set_rule(world.get_location("MoM: East chandelier platform", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_dawn_heart(player)) + set_rule(world.get_location("MoM: Redento's treasure", player), + lambda state: state._blasphemous_redento(player)) + set_rule(world.get_location("MoM: Final meeting with Redento", player), + lambda state: state._blasphemous_redento(player)) + set_rule(world.get_location("MoM: Giant chandelier statue", player), + lambda state: state._blasphemous_blood_relic(player)) + + # Mountains of the Endless Dusk + set_rule(world.get_location("MotED: Platform above chasm", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("MotED: Blood platform alcove", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("MotED: Egg hatching", player), + lambda state: state._blasphemous_pre_egg(player)) + # to do: requires dive + set_rule(world.get_location("MotED: Amanecida of the Golden Blades", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player)) + + # Mourning and Havoc + set_rule(world.get_location("MaH: Upper east chest", player), + lambda state: state._blasphemous_bridge_access(player) and \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("MaH: Sierpes' eye", player), + lambda state: state._blasphemous_bridge_access(player) and \ + (state._blasphemous_root_relic(player)) or \ + state._blasphemous_water_relic(player) or \ + state._blasphemous_dawn_heart(player)) + set_rule(world.get_location("MaH: Sierpes", player), + lambda state: state._blasphemous_bridge_access(player) and \ + (state._blasphemous_root_relic(player)) or \ + state._blasphemous_water_relic(player) or \ + state._blasphemous_dawn_heart(player)) + + # Patio of the Silent Steps + set_rule(world.get_location("PotSS: Second area ledge", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("PotSS: Third area upper ledge", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player)) + set_rule(world.get_location("PotSS: Climb to WotHP", player), + lambda state: (state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player)) or \ + (state.can_reach(world.get_region("Wall of the Holy Prohibitions", player)) and \ + state._blasphemous_bronze_key(player))) + # to do: requires dive + set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player)) + + # Petrous + # to do: requires dive + set_rule(world.get_location("Petrous: Temple entrance", player), + lambda state: state._blasphemous_open_holes(player)) + + # The Sleeping Canvases + set_rule(world.get_location("TSC: Candle wax puzzle", player), + lambda state: state._blasphemous_both_wax(player)) + set_rule(world.get_location("TSC: Under elevator shaft", player), + lambda state: state._blasphemous_fall_relic(player)) + set_rule(world.get_location("TSC: Jocinero's 1st reward", player), + lambda state: state._blasphemous_cherubs_20(player)) + set_rule(world.get_location("TSC: Jocinero's final reward", player), + lambda state: state._blasphemous_cherubs_all(player)) + + # The Holy Line + set_rule(world.get_location("THL: Across blood platforms", player), + lambda state: state._blasphemous_blood_relic(player)) + set_rule(world.get_location("THL: Underground chest", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_water_relic(player)) + + # Wall of the Holy Prohibitions + set_rule(world.get_location("WotHP: Upper east room, top bronze cell", player), + lambda state: state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Upper east room, top silver cell", player), + lambda state: state._blasphemous_silver_key(player)) + set_rule(world.get_location("WotHP: Upper east room, center gold cell", player), + lambda state: state._blasphemous_gold_key(player)) + set_rule(world.get_location("WotHP: Upper west room, center gold cell", player), + lambda state: state._blasphemous_gold_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower west room, bottom gold cell", player), + lambda state: state._blasphemous_gold_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Upper west room, top silver cell", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower west room, top ledge", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower east room, hidden ledge", player), + lambda state: state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower east room, bottom silver cell", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower east room, top bronze cell", player), + lambda state: state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Lower east room, top silver cell", player), + lambda state: state._blasphemous_silver_key(player)) + set_rule(world.get_location("WotHP: Outside Child of Moonlight", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Oil of the Pilgrims", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Quirce, Returned By The Flames", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Collapsing floor ledge", player), + lambda state: state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + set_rule(world.get_location("WotHP: Amanecida of the Molten Thorn", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player)) + + # Wasteland of the Buried Churches + set_rule(world.get_location("WotBC: Under broken bridge", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_dawn_heart(player)) + set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player), + lambda state: state._blasphemous_cherub_38(player)) + + # Where Olive Trees Wither + set_rule(world.get_location("WOTW: Gift for the tomb", player), + lambda state: state._blasphemous_full_thimble(player)) + set_rule(world.get_location("WOTW: Underground tomb", player), + lambda state: state._blasphemous_flowers(player) and \ + (state._blasphemous_full_thimble(player) or \ + state._blasphemous_fall_relic(player))) + set_rule(world.get_location("WOTW: Underground Child of Moonlight", player), + lambda state: (state._blasphemous_full_thimble(player) or \ + state._blasphemous_fall_relic(player)) and \ + state._blasphemous_cherub_27(player)) + set_rule(world.get_location("WOTW: Underground ledge", player), + lambda state: (state._blasphemous_full_thimble(player) or \ + state._blasphemous_fall_relic(player)) and \ + state._blasphemous_blood_relic(player)) + set_rule(world.get_location("WOTW: Upper east Child of Moonlight", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_cherub_22_23_31_32(player)) + set_rule(world.get_location("WOTW: Upper east statue", player), + lambda state: state._blasphemous_root_relic(player)) + set_rule(world.get_location("WOTW: Gemino's reward", player), + lambda state: state._blasphemous_full_thimble(player)) + + # Various + set_rule(world.get_location("Confessor Dungeon 1 extra", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 1 main", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 2 extra", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 2 main", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 3 extra", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 3 main", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 4 extra", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 4 main", player), + lambda state: state._blasphemous_bead(player)) + set_rule(world.get_location("Confessor Dungeon 5 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Confessor Dungeon 5 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Confessor Dungeon 6 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player) and \ + (state._blasphemous_1_mask(player) or \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player))) + set_rule(world.get_location("Confessor Dungeon 6 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player) and \ + (state._blasphemous_1_mask(player) or \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player))) + set_rule(world.get_location("Confessor Dungeon 7 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_blood_relic(player)) + set_rule(world.get_location("Confessor Dungeon 7 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_blood_relic(player)) + # to do: requires dive + set_rule(world.get_location("Defeat 1 Amanecida", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player)) + set_rule(world.get_location("Defeat 2 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_blood_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_bridge_access(player))) + set_rule(world.get_location("Defeat 3 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_blood_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + (state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)))) + set_rule(world.get_location("Defeat 4 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)) + set_rule(world.get_location("Defeat all Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)) + + # expert logic + if world.expert_logic[player]: + # entrances + for i in world.get_region("Ferrous Tree", player).entrances: + set_rule(i, lambda state: state._blasphemous_ex_bridge_access(player)) + for i in world.get_region("Mother of Mothers", player).entrances: + set_rule(i, lambda state: state._blasphemous_ex_bridge_access(player)) + for i in world.get_region("Patio of the Silent Steps", player).entrances: + set_rule(i, lambda state: state._blasphemous_ex_bridge_access(player)) + for i in world.get_region("The Sleeping Canvases", player).entrances: + set_rule(i, lambda state: state._blasphemous_ex_bridge_access(player)) + for i in world.get_region("Wall of the Holy Prohibitions", player).entrances: + set_rule(i, lambda state: state._blasphemous_1_mask(player) and \ + state._blasphemous_ex_bridge_access(player)) + + # locations + set_rule(world.get_location("AR: Upper west shaft chest", player), + lambda state: state._blasphemous_2_masks(player) and \ + state._blasphemous_fall_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("BotTC: Esdras, of the Anointed Legion", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("BotTC: Esdras' gift", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("BotTC: Inside giant statue", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_laudes_gate(player) and \ + state._blasphemous_1_mask(player)) + set_rule(world.get_location("BotSS: Esdras' final gift", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_scapular(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("BotSS: Crisanta's gift", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_scapular(player) and \ + state._blasphemous_heart_c(player) and \ + state._blasphemous_3_masks(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("CoOLotCV: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_high_key(player)) + set_rule(world.get_location("CoOLotCV: Mask room", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_high_key(player)) + set_rule(world.get_location("DC: Chalice room", player), + lambda state: (state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player)))) or \ + (state._blasphemous_fall_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_ranged(player)))) + set_rule(world.get_location("DC: Mea Culpa altar", player), + lambda state: state._blasphemous_chalice(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + (state._blasphemous_fall_relic(player) and \ + (state._blasphemous_ranged(player) or \ + state._blasphemous_root_relic(player))) or \ + (state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))))) + set_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_laudes_gate(player)) + set_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), + lambda state: state._blasphemous_blood_relic(player) and \ + (state._blasphemous_cherub_22_23_31_32(player) and \ + state._blasphemous_dawn_heart(player) and \ + state._blasphemous_ranged(player)) or \ + state._blasphemous_root_relic(player)) + set_rule(world.get_location("MD: Cave Child of Moonlight", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_cherub_24_33(player)) + set_rule(world.get_location("MD: Behind gate to TSC", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("MoM: East chandelier platform", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("MaH: Upper east chest", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + (state._blasphemous_root_relic(player)) or \ + (state._blasphemous_dawn_heart(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("MaH: Sierpes' eye", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + (state._blasphemous_root_relic(player)) or \ + state._blasphemous_dawn_heart(player) or \ + state._blasphemous_water_relic(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("MaH: Sierpes", player), + lambda state: state._blasphemous_ex_bridge_access(player) and \ + (state._blasphemous_root_relic(player)) or \ + state._blasphemous_dawn_heart(player) or \ + state._blasphemous_water_relic(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("PotSS: Third area upper ledge", player), + lambda state: state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("WotBC: Under broken bridge", player), + lambda state: state._blasphemous_blood_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))) + set_rule(world.get_location("Confessor Dungeon 5 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Confessor Dungeon 5 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Confessor Dungeon 6 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + (state._blasphemous_1_mask(player) or \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player))) + set_rule(world.get_location("Confessor Dungeon 6 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + (state._blasphemous_1_mask(player) or \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_bronze_key(player))) + set_rule(world.get_location("Confessor Dungeon 7 extra", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_blood_relic(player)) + set_rule(world.get_location("Confessor Dungeon 7 main", player), + lambda state: state._blasphemous_bead(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player) and \ + state._blasphemous_blood_relic(player)) + set_rule(world.get_location("Defeat 2 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_blood_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_ex_bridge_access(player))) + set_rule(world.get_location("Defeat 3 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_blood_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + (state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)))) + set_rule(world.get_location("Defeat 4 Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)) + set_rule(world.get_location("Defeat all Amanecidas", player), + lambda state: state._blasphemous_bell(player) and \ + state._blasphemous_open_holes(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_blood_relic(player) and \ + state._blasphemous_root_relic(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_silver_key(player)) + + # skill rando + if world.skill_randomizer[player] and not world.expert_logic[player]: + set_rule(world.get_location("Skill 1, Tier 3", player), + lambda state: state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Skill 5, Tier 3", player), + lambda state: state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Skill 3, Tier 2", player), + lambda state: state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Skill 2, Tier 3", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_2_masks(player) and \ + state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Skill 4, Tier 3", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_2_masks(player) and \ + state._blasphemous_bridge_access(player)) + set_rule(world.get_location("Skill 3, Tier 3", player), + lambda state: state._blasphemous_chalice(player) and \ + state._blasphemous_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + state._blasphemous_root_relic(player)) + elif world.skill_randomizer[player] and world.expert_logic[player]: + set_rule(world.get_location("Skill 1, Tier 3", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Skill 5, Tier 3", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Skill 3, Tier 2", player), + lambda state: state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Skill 2, Tier 3", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_2_masks(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Skill 4, Tier 3", player), + lambda state: state._blasphemous_blood_relic(player) and \ + state._blasphemous_miasma_relic(player) and \ + state._blasphemous_2_masks(player) and \ + state._blasphemous_ex_bridge_access(player)) + set_rule(world.get_location("Skill 3, Tier 3", player), + lambda state: state._blasphemous_chalice(player) and \ + state._blasphemous_ex_bridge_access(player) and \ + state._blasphemous_1_mask(player) and \ + state._blasphemous_bronze_key(player) and \ + (state._blasphemous_fall_relic(player) and \ + (state._blasphemous_ranged(player) or \ + state._blasphemous_root_relic(player))) or \ + (state._blasphemous_miasma_relic(player) and \ + state._blasphemous_water_relic(player) and \ + (state._blasphemous_root_relic(player) or \ + state._blasphemous_dawn_heart(player) or \ + (state._blasphemous_wheel(player) and \ + state._blasphemous_ranged(player))))) + + # difficulty (easy) + if world.difficulty[player].value == 0: + for i in world.get_region("Desecrated Cistern", player).entrances: + add_rule(i, lambda state: state._blasphemous_wound_boss_easy(player)) + for i in world.get_region("Ferrous Tree", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_easy(player)) + for i in world.get_region("Patio of the Silent Steps", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_easy(player)) + for i in world.get_region("The Sleeping Canvases", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_easy(player)) + for i in world.get_region("Deambulatory of His Holiness", player).entrances: + add_rule(i, lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Albero: Donate 5000 Tears", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Albero: Donate 50000 Tears", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Albero: Tirso's final reward", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("Ossuary: Isidora, Voice of the Dead", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("AR: Crisanta of the Wrapped Agony", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("BotTC: Esdras, of the Anointed Legion", player), + lambda state: state._blasphemous_esdras_boss_easy(player)) + add_rule(world.get_location("BotTC: Esdras' gift", player), + lambda state: state._blasphemous_esdras_boss_easy(player)) + add_rule(world.get_location("BotTC: Inside giant statue", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("BotSS: Crisanta's gift", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("CoOLotCV: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("CoOLotCV: Mask room", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("GotP: Amanecida of the Bejeweled Arrow", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("LotNW: Mask room", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("LotNW: Mea Culpa altar", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("LotNW: Red candle", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("MD: Blue candle", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("MD: Cave Child of Moonlight", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("MD: Behind gate to TSC", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("MoM: Melquiades, The Exhumed Archbishop", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("MoM: Mask room", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("MotED: Amanecida of the Golden Blades", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("MaH: Sierpes' eye", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("MaH: Sierpes", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("TSC: Under elevator shaft", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("TSC: Exposito, Scion of Abjuration", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("WotHP: Quirce, Returned By The Flames", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("WotHP: Collapsing floor ledge", player), + lambda state: state._blasphemous_mask_boss_easy(player)) + add_rule(world.get_location("WotHP: Amanecida of the Molten Thorn", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 4 extra", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 4 main", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 5 extra", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 5 main", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 6 extra", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 6 main", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 7 extra", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Confessor Dungeon 7 main", player), + lambda state: state._blasphemous_wound_boss_easy(player)) + add_rule(world.get_location("Defeat 1 Amanecida", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Defeat 2 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Defeat 3 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Defeat 4 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + add_rule(world.get_location("Defeat all Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_easy(player)) + + # difficulty (normal) + elif world.difficulty[player].value == 1: + for i in world.get_region("Desecrated Cistern", player).entrances: + add_rule(i, lambda state: state._blasphemous_wound_boss_normal(player)) + for i in world.get_region("Ferrous Tree", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_normal(player)) + for i in world.get_region("Patio of the Silent Steps", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_normal(player)) + for i in world.get_region("The Sleeping Canvases", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_normal(player)) + for i in world.get_region("Deambulatory of His Holiness", player).entrances: + add_rule(i, lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Albero: Donate 5000 Tears", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Albero: Donate 50000 Tears", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Albero: Tirso's final reward", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("Ossuary: Isidora, Voice of the Dead", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("AR: Crisanta of the Wrapped Agony", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("BotTC: Esdras, of the Anointed Legion", player), + lambda state: state._blasphemous_esdras_boss_normal(player)) + add_rule(world.get_location("BotTC: Esdras' gift", player), + lambda state: state._blasphemous_esdras_boss_normal(player)) + add_rule(world.get_location("BotTC: Inside giant statue", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("BotSS: Crisanta's gift", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("CoOLotCV: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("CoOLotCV: Mask room", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("GotP: Amanecida of the Bejeweled Arrow", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("LotNW: Mask room", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("LotNW: Mea Culpa altar", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("LotNW: Red candle", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("MD: Blue candle", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("MD: Cave Child of Moonlight", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("MD: Behind gate to TSC", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("MoM: Melquiades, The Exhumed Archbishop", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("MoM: Mask room", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("MotED: Amanecida of the Golden Blades", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("MaH: Sierpes' eye", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("MaH: Sierpes", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("TSC: Under elevator shaft", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("TSC: Exposito, Scion of Abjuration", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("WotHP: Quirce, Returned By The Flames", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("WotHP: Collapsing floor ledge", player), + lambda state: state._blasphemous_mask_boss_normal(player)) + add_rule(world.get_location("WotHP: Amanecida of the Molten Thorn", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 4 extra", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 4 main", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 5 extra", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 5 main", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 6 extra", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 6 main", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 7 extra", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Confessor Dungeon 7 main", player), + lambda state: state._blasphemous_wound_boss_normal(player)) + add_rule(world.get_location("Defeat 1 Amanecida", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Defeat 2 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Defeat 3 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Defeat 4 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + add_rule(world.get_location("Defeat all Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_normal(player)) + + # difficulty (hard) + elif world.difficulty[player].value == 2: + for i in world.get_region("Desecrated Cistern", player).entrances: + add_rule(i, lambda state: state._blasphemous_wound_boss_hard(player)) + for i in world.get_region("Ferrous Tree", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_hard(player)) + for i in world.get_region("Patio of the Silent Steps", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_hard(player)) + for i in world.get_region("The Sleeping Canvases", player).entrances: + add_rule(i, lambda state: state._blasphemous_esdras_boss_hard(player)) + for i in world.get_region("Deambulatory of His Holiness", player).entrances: + add_rule(i, lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Albero: Donate 5000 Tears", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Albero: Donate 50000 Tears", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Albero: Tirso's final reward", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("Ossuary: Isidora, Voice of the Dead", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("AR: Crisanta of the Wrapped Agony", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("BotTC: Esdras, of the Anointed Legion", player), + lambda state: state._blasphemous_esdras_boss_hard(player)) + add_rule(world.get_location("BotTC: Esdras' gift", player), + lambda state: state._blasphemous_esdras_boss_hard(player)) + add_rule(world.get_location("BotTC: Inside giant statue", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("BotSS: Crisanta's gift", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("CoOLotCV: Lady of the Six Sorrows", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("CoOLotCV: Mask room", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("GotP: Amanecida of the Bejeweled Arrow", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("LotNW: Mask room", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("LotNW: Mea Culpa altar", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("LotNW: Red candle", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("MD: Blue candle", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("MD: Cave Child of Moonlight", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("MD: Behind gate to TSC", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("MoM: Melquiades, The Exhumed Archbishop", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("MoM: Mask room", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("MotED: Amanecida of the Golden Blades", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("MaH: Sierpes' eye", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("MaH: Sierpes", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("TSC: Under elevator shaft", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("TSC: Exposito, Scion of Abjuration", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("WotHP: Quirce, Returned By The Flames", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("WotHP: Collapsing floor ledge", player), + lambda state: state._blasphemous_mask_boss_hard(player)) + add_rule(world.get_location("WotHP: Amanecida of the Molten Thorn", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 4 extra", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 4 main", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 5 extra", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 5 main", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 6 extra", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 6 main", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 7 extra", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Confessor Dungeon 7 main", player), + lambda state: state._blasphemous_wound_boss_hard(player)) + add_rule(world.get_location("Defeat 1 Amanecida", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Defeat 2 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Defeat 3 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Defeat 4 Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) + add_rule(world.get_location("Defeat all Amanecidas", player), + lambda state: state._blasphemous_endgame_boss_hard(player)) \ No newline at end of file diff --git a/worlds/blasphemous/Vanilla.py b/worlds/blasphemous/Vanilla.py new file mode 100644 index 0000000000..82ec7c5f4e --- /dev/null +++ b/worlds/blasphemous/Vanilla.py @@ -0,0 +1,246 @@ +from typing import Set, Dict + +unrandomized_dict: Dict[str, str] = { + "CoOLotCV: Fountain of burning oil": "Golden Thimble Filled with Burning Oil", + "MotED: Egg hatching": "Hatched Egg of Deformity", + "BotSS: Crisanta's gift": "Holy Wound of Abnegation", + "DC: Chalice room": "Chalice of Inverted Verses" +} + +cherub_set: Set[str] = [ + "Albero: Child of Moonlight", + "AR: Upper west shaft Child of Moonlight", + "BotSS: Starting room Child of Moonlight", + "DC: Child of Moonlight, above water", + "DC: Upper east Child of Moonlight", + "DC: Child of Moonlight, miasma room", + "DC: Child of Moonlight, behind pillar", + "DC: Top of elevator Child of Moonlight", + "DC: Elevator shaft Child of Moonlight", + "GotP: Shop cave Child of Moonlight", + "GotP: Elevator shaft Child of Moonlight", + "GotP: West shaft Child of Moonlight", + "GotP: Center shaft Child of Moonlight", + "GA: Miasma room Child of Moonlight", + "GA: Blood bridge Child of Moonlight", + "GA: Lower east Child of Moonlight", + "Jondo: Upper east Child of Moonlight", + "Jondo: Spike tunnel Child of Moonlight", + "Jondo: Upper west Child of Moonlight", + "LotNW: Platform room Child of Moonlight", + "LotNW: Lowest west Child of Moonlight", + "LotNW: Elevator Child of Moonlight", + "MD: Second area Child of Moonlight", + "MD: Cave Child of Moonlight", + "MoM: Lower west Child of Moonlight", + "MoM: Upper center Child of Moonlight", + "MotED: Child of Moonlight, above chasm", + "PotSS: First area Child of Moonlight", + "PotSS: Third area Child of Moonlight", + "THL: Child of Moonlight", + "WotHP: Upper east room, top bronze cell", + "WotHP: Upper west room, top silver cell", + "WotHP: Lower east room, bottom silver cell", + "WotHP: Outside Child of Moonlight", + "WotBC: Outside Child of Moonlight", + "WotBC: Cliffside Child of Moonlight", + "WOTW: Underground Child of Moonlight", + "WOTW: Upper east Child of Moonlight", +] + +life_set: Set[str] = [ + "AR: Lady of the Six Sorrows", + "CoOLotCV: Lady of the Six Sorrows", + "DC: Lady of the Six Sorrows, from MD", + "DC: Lady of the Six Sorrows, elevator shaft", + "GotP: Lady of the Six Sorrows", + "LotNW: Lady of the Six Sorrows" +] + +fervour_set: Set[str] = [ + "DC: Oil of the Pilgrims", + "GotP: Oil of the Pilgrims", + "GA: Oil of the Pilgrims", + "LotNW: Oil of the Pilgrims", + "MoM: Oil of the Pilgrims", + "WotHP: Oil of the Pilgrims" +] + +sword_set: Set[str] = [ + "Albero: Mea Culpa altar", + "AR: Mea Culpa altar", + "BotSS: Mea Culpa altar", + "CoOLotCV: Mea Culpa altar", + "DC: Mea Culpa altar", + "LotNW: Mea Culpa altar", + "MoM: Mea Culpa altar" +] + +blessing_dict: Dict[str, str] = { + "Albero: Bless Severed Hand": "Incorrupt Hand of the Fraternal Master", + "Albero: Bless Linen Cloth": "Shroud of Dreamt Sins", + "Albero: Bless Hatched Egg": "Three Gnarled Tongues" +} + +dungeon_dict: Dict[str, str] = { + "Confessor Dungeon 1 extra": "Tears of Atonement (1000)", + "Confessor Dungeon 2 extra": "Heart of the Single Tone", + "Confessor Dungeon 3 extra": "Tears of Atonement (3000)", + "Confessor Dungeon 4 extra": "Embers of a Broken Star", + "Confessor Dungeon 5 extra": "Tears of Atonement (5000)", + "Confessor Dungeon 6 extra": "Scaly Coin", + "Confessor Dungeon 7 extra": "Seashell of the Inverted Spiral" +} + +tirso_dict: Dict[str, str] = { + "Albero: Tirso's 1st reward": "Linen Cloth", + "Albero: Tirso's 2nd reward": "Tears of Atonement (500)", + "Albero: Tirso's 3rd reward": "Tears of Atonement (1000)", + "Albero: Tirso's 4th reward": "Tears of Atonement (2000)", + "Albero: Tirso's 5th reward": "Tears of Atonement (5000)", + "Albero: Tirso's 6th reward": "Tears of Atonement (10000)", + "Albero: Tirso's final reward": "Knot of Rosary Rope" +} + +redento_dict: Dict[str, str] = { + "MoM: Redento's treasure": "Nail Uprooted from Dirt", + "MoM: Final meeting with Redento": "Knot of Rosary Rope", + "MotED: 1st meeting with Redento": "Fourth Toe made of Limestone", + "PotSS: 4th meeting with Redento": "Big Toe made of Limestone", + "WotBC: 3rd meeting with Redento": "Little Toe made of Limestone" +} + +jocinero_dict: Dict[str, str] = { + "TSC: Jocinero's 1st reward": "Linen of Golden Thread", + "TSC: Jocinero's final reward": "Campanillero to the Sons of the Aurora" +} + +altasgracias_dict: Dict[str, str] = { + "GA: Altasgracias' gift": "Egg of Deformity", + "GA: Empty giant egg": "Knot of Hair" +} + +tentudia_dict: Dict[str, str] = { + "Albero: Lvdovico's 1st reward": "Tears of Atonement (500)", + "Albero: Lvdovico's 2nd reward": "Tears of Atonement (1000)", + "Albero: Lvdovico's 3rd reward": "Debla of the Lights" +} + +gemino_dict: Dict[str, str] = { + "WOTW: Gift for the tomb": "Dried Flowers bathed in Tears", + "WOTW: Underground tomb": "Saeta Dolorosa", + "WOTW: Gemino's gift": "Empty Golden Thimble", + "WOTW: Gemino's reward": "Frozen Olive" +} + +ossuary_dict: Dict[str, str] = { + "Ossuary: 1st reward": "Tears of Atonement (250)", + "Ossuary: 2nd reward": "Tears of Atonement (500)", + "Ossuary: 3rd reward": "Tears of Atonement (750)", + "Ossuary: 4th reward": "Tears of Atonement (1000)", + "Ossuary: 5th reward": "Tears of Atonement (1250)", + "Ossuary: 6th reward": "Tears of Atonement (1500)", + "Ossuary: 7th reward": "Tears of Atonement (1750)", + "Ossuary: 8th reward": "Tears of Atonement (2000)", + "Ossuary: 9th reward": "Tears of Atonement (2500)", + "Ossuary: 10th reward": "Tears of Atonement (3000)", + "Ossuary: 11th reward": "Tears of Atonement (5000)", +} + +boss_dict: Dict[str, str] = { + "BotTC: Esdras, of the Anointed Legion": "Tears of Atonement (4300)", + "BotSS: Warden of the Silent Sorrow": "Tears of Atonement (300)", + "CoOLotCV: Our Lady of the Charred Visage": "Tears of Atonement (2600)", + "HotD: Laudes, the First of the Amanecidas": "Tears of Atonement (30000)", + "GotP: Amanecida of the Bejeweled Arrow": "Tears of Atonement (18000)", + "GA: Tres Angustias": "Tears of Atonement (2100)", + "MD: Ten Piedad": "Tears of Atonement (625)", + "MoM: Melquiades, The Exhumed Archbishop": "Tears of Atonement (5500)", + "MotED: Amanecida of the Golden Blades": "Tears of Atonement (18000)", + "MaH: Sierpes": "Tears of Atonement (5000)", + "PotSS: Amanecida of the Chiselled Steel": "Tears of Atonement (18000)", + "TSC: Exposito, Scion of Abjuration": "Tears of Atonement (9000)", + "WotHP: Quirce, Returned By The Flames": "Tears of Atonement (11250)", + "WotHP: Amanecida of the Molten Thorn": "Tears of Atonement (18000)" +} + +wound_dict: Dict[str, str] = { + "CoOLotCV: Visage of Compunction": "Holy Wound of Compunction", + "GA: Visage of Contrition": "Holy Wound of Contrition", + "MD: Visage of Attrition": "Holy Wound of Attrition" +} + +mask_dict: Dict[str, str] = { + "CoOLotCV: Mask room": "Mirrored Mask of Dolphos", + "LotNW: Mask room": "Embossed Mask of Crescente", + "MoM: Mask room": "Deformed Mask of Orestes" +} + +eye_dict: Dict[str, str] = { + "Ossuary: Isidora, Voice of the Dead": "Severed Right Eye of the Traitor", + "MaH: Sierpes' eye": "Broken Left Eye of the Traitor" +} + +herb_dict: Dict[str, str] = { + "Albero: Gate of Travel room": "Bouquet of Thyme", + "Jondo: Lower east bell trap": "Bouquet of Rosemary", + "MotED: Blood platform alcove": "Dried Clove", + "PotSS: Third area lower ledge": "Olive Seeds", + "TSC: Painting ladder ledge": "Sooty Garlic", + "WOTW: Entrance to tomb": "Incense Garlic" +} + +church_dict: Dict[str, str] = { + "Albero: Donate 5000 Tears": "Token of Appreciation", + "Albero: Donate 50000 Tears": "Cloistered Ruby" +} + +shop_dict: Dict[str, str] = { + "GotP: Shop item 1": "Torn Bridal Ribbon", + "GotP: Shop item 2": "Calcified Eye of Erudition", + "GotP: Shop item 3": "Ember of the Holy Cremation", + "MD: Shop item 1": "Key to the Chamber of the Eldest Brother", + "MD: Shop item 2": "Hollow Pearl", + "MD: Shop item 3": "Moss Preserved in Glass", + "TSC: Shop item 1": "Wicker Knot", + "TSC: Shop item 2": "Empty Bile Vessel", + "TSC: Shop item 3": "Key of the Inquisitor" +} + +thorn_set: Set[str] = { + "THL: Deogracias' gift", + "Confessor Dungeon 1 main", + "Confessor Dungeon 2 main", + "Confessor Dungeon 3 main", + "Confessor Dungeon 4 main", + "Confessor Dungeon 5 main", + "Confessor Dungeon 6 main", + "Confessor Dungeon 7 main", +} + +candle_dict: Dict[str, str] = { + "CoOLotCV: Red candle": "Bead of Red Wax", + "LotNW: Red candle": "Bead of Red Wax", + "MD: Red candle": "Bead of Red Wax", + "BotSS: Blue candle": "Bead of Blue Wax", + "CoOLotCV: Blue candle": "Bead of Blue Wax", + "MD: Blue candle": "Bead of Blue Wax" +} + +skill_dict: Dict[str, str] = { + "Skill 1, Tier 1": "Combo Skill", + "Skill 1, Tier 2": "Combo Skill", + "Skill 1, Tier 3": "Combo Skill", + "Skill 2, Tier 1": "Charged Skill", + "Skill 2, Tier 2": "Charged Skill", + "Skill 2, Tier 3": "Charged Skill", + "Skill 3, Tier 1": "Ranged Skill", + "Skill 3, Tier 2": "Ranged Skill", + "Skill 3, Tier 3": "Ranged Skill", + "Skill 4, Tier 1": "Dive Skill", + "Skill 4, Tier 2": "Dive Skill", + "Skill 4, Tier 3": "Dive Skill", + "Skill 5, Tier 1": "Lunge Skill", + "Skill 5, Tier 2": "Lunge Skill", + "Skill 5, Tier 3": "Lunge Skill", +} \ No newline at end of file diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py new file mode 100644 index 0000000000..70aea1ef76 --- /dev/null +++ b/worlds/blasphemous/__init__.py @@ -0,0 +1,413 @@ +from typing import Dict, Set, Any +from collections import Counter +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification +from ..AutoWorld import World, WebWorld +from .Items import base_id, item_table, group_table, tears_set, reliquary_set, skill_set +from .Locations import location_table, shop_set +from .Exits import region_exit_table, exit_lookup_table +from .Rules import rules +from worlds.generic.Rules import set_rule +from .Options import blasphemous_options +from . import Vanilla + + +class BlasphemousWeb(WebWorld): + theme = "stone" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Blasphemous randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["TRPG"] + )] + + +class BlasphemousWorld(World): + """ + Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped + in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break + your eternal damnation! + """ + + game: str = "Blasphemous" + web = BlasphemousWeb() + data_version: 1 + + item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} + location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} + location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table} + + item_name_groups = group_table + option_definitions = blasphemous_options + + + def set_rules(self): + rules(self) + + + def create_item(self, name: str) -> "BlasphemousItem": + item_id: int = self.item_name_to_id[name] + id = item_id - base_id + + return BlasphemousItem(name, item_table[id]["classification"], item_id, player=self.player) + + + def create_event(self, event: str): + return BlasphemousItem(event, ItemClassification.progression_skip_balancing, None, self.player) + + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(tears_set) + + + def generate_basic(self): + placed_items = [] + + placed_items.extend(Vanilla.unrandomized_dict.values()) + + if not self.multiworld.reliquary_shuffle[self.player]: + placed_items.extend(reliquary_set) + elif self.multiworld.reliquary_shuffle[self.player]: + placed_items.append("Tears of Atonement (250)") + placed_items.append("Tears of Atonement (300)") + placed_items.append("Tears of Atonement (500)") + + if not self.multiworld.cherub_shuffle[self.player]: + for i in range(38): + placed_items.append("Child of Moonlight") + + if not self.multiworld.life_shuffle[self.player]: + for i in range(6): + placed_items.append("Life Upgrade") + + if not self.multiworld.fervour_shuffle[self.player]: + for i in range(6): + placed_items.append("Fervour Upgrade") + + if not self.multiworld.sword_shuffle[self.player]: + for i in range(7): + placed_items.append("Mea Culpa Upgrade") + + if not self.multiworld.blessing_shuffle[self.player]: + placed_items.extend(Vanilla.blessing_dict.values()) + + if not self.multiworld.dungeon_shuffle[self.player]: + placed_items.extend(Vanilla.dungeon_dict.values()) + + if not self.multiworld.tirso_shuffle[self.player]: + placed_items.extend(Vanilla.tirso_dict.values()) + + if not self.multiworld.miriam_shuffle[self.player]: + placed_items.append("Cantina of the Blue Rose") + + if not self.multiworld.redento_shuffle[self.player]: + placed_items.extend(Vanilla.redento_dict.values()) + + if not self.multiworld.jocinero_shuffle[self.player]: + placed_items.extend(Vanilla.jocinero_dict.values()) + + if not self.multiworld.altasgracias_shuffle[self.player]: + placed_items.extend(Vanilla.altasgracias_dict.values()) + + if not self.multiworld.tentudia_shuffle[self.player]: + placed_items.extend(Vanilla.tentudia_dict.values()) + + if not self.multiworld.gemino_shuffle[self.player]: + placed_items.extend(Vanilla.gemino_dict.values()) + + if not self.multiworld.guilt_shuffle[self.player]: + placed_items.append("Weight of True Guilt") + + if not self.multiworld.ossuary_shuffle[self.player]: + placed_items.extend(Vanilla.ossuary_dict.values()) + + if not self.multiworld.boss_shuffle[self.player]: + placed_items.extend(Vanilla.boss_dict.values()) + + if not self.multiworld.wound_shuffle[self.player]: + placed_items.extend(Vanilla.wound_dict.values()) + + if not self.multiworld.mask_shuffle[self.player]: + placed_items.extend(Vanilla.mask_dict.values()) + + if not self.multiworld.eye_shuffle[self.player]: + placed_items.extend(Vanilla.eye_dict.values()) + + if not self.multiworld.herb_shuffle[self.player]: + placed_items.extend(Vanilla.herb_dict.values()) + + if not self.multiworld.church_shuffle[self.player]: + placed_items.extend(Vanilla.church_dict.values()) + + if not self.multiworld.shop_shuffle[self.player]: + placed_items.extend(Vanilla.shop_dict.values()) + + if self.multiworld.thorn_shuffle[self.player] == 2: + for i in range(8): + placed_items.append("Thorn Upgrade") + + if not self.multiworld.candle_shuffle[self.player]: + placed_items.extend(Vanilla.candle_dict.values()) + + if self.multiworld.start_wheel[self.player]: + placed_items.append("The Young Mason's Wheel") + + if not self.multiworld.skill_randomizer[self.player]: + placed_items.extend(Vanilla.skill_dict.values()) + + counter = Counter(placed_items) + + pool = [] + + for item in item_table: + count = item["count"] - counter[item["name"]] + + if count <= 0: + continue + else: + for i in range(count): + pool.append(self.create_item(item["name"])) + + self.multiworld.itempool += pool + + + def pre_fill(self): + self.place_items_from_dict(Vanilla.unrandomized_dict) + + if not self.multiworld.cherub_shuffle[self.player]: + self.place_items_from_set(Vanilla.cherub_set, "Child of Moonlight") + + if not self.multiworld.life_shuffle[self.player]: + self.place_items_from_set(Vanilla.life_set, "Life Upgrade") + + if not self.multiworld.fervour_shuffle[self.player]: + self.place_items_from_set(Vanilla.fervour_set, "Fervour Upgrade") + + if not self.multiworld.sword_shuffle[self.player]: + self.place_items_from_set(Vanilla.sword_set, "Mea Culpa Upgrade") + + if not self.multiworld.blessing_shuffle[self.player]: + self.place_items_from_dict(Vanilla.blessing_dict) + + if not self.multiworld.dungeon_shuffle[self.player]: + self.place_items_from_dict(Vanilla.dungeon_dict) + + if not self.multiworld.tirso_shuffle[self.player]: + self.place_items_from_dict(Vanilla.tirso_dict) + + if not self.multiworld.miriam_shuffle[self.player]: + self.multiworld.get_location("AtTotS: Miriam's gift", self.player)\ + .place_locked_item(self.create_item("Cantina of the Blue Rose")) + + if not self.multiworld.redento_shuffle[self.player]: + self.place_items_from_dict(Vanilla.redento_dict) + + if not self.multiworld.jocinero_shuffle[self.player]: + self.place_items_from_dict(Vanilla.jocinero_dict) + + if not self.multiworld.altasgracias_shuffle[self.player]: + self.place_items_from_dict(Vanilla.altasgracias_dict) + + if not self.multiworld.tentudia_shuffle[self.player]: + self.place_items_from_dict(Vanilla.tentudia_dict) + + if not self.multiworld.gemino_shuffle[self.player]: + self.place_items_from_dict(Vanilla.gemino_dict) + + if not self.multiworld.guilt_shuffle[self.player]: + self.multiworld.get_location("GotP: Confessor Dungeon room", self.player)\ + .place_locked_item(self.create_item("Weight of True Guilt")) + + if not self.multiworld.ossuary_shuffle[self.player]: + self.place_items_from_dict(Vanilla.ossuary_dict) + + if not self.multiworld.boss_shuffle[self.player]: + self.place_items_from_dict(Vanilla.boss_dict) + + if not self.multiworld.wound_shuffle[self.player]: + self.place_items_from_dict(Vanilla.wound_dict) + + if not self.multiworld.mask_shuffle[self.player]: + self.place_items_from_dict(Vanilla.mask_dict) + + if not self.multiworld.eye_shuffle[self.player]: + self.place_items_from_dict(Vanilla.eye_dict) + + if not self.multiworld.herb_shuffle[self.player]: + self.place_items_from_dict(Vanilla.herb_dict) + + if not self.multiworld.church_shuffle[self.player]: + self.place_items_from_dict(Vanilla.church_dict) + + if not self.multiworld.shop_shuffle[self.player]: + self.place_items_from_dict(Vanilla.shop_dict) + + if self.multiworld.thorn_shuffle[self.player] == 2: + self.place_items_from_set(Vanilla.thorn_set, "Thorn Upgrade") + + if not self.multiworld.candle_shuffle[self.player]: + self.place_items_from_dict(Vanilla.candle_dict) + + if self.multiworld.start_wheel[self.player]: + self.multiworld.get_location("BotSS: Beginning gift", self.player)\ + .place_locked_item(self.create_item("The Young Mason's Wheel")) + + if not self.multiworld.skill_randomizer[self.player]: + self.place_items_from_dict(Vanilla.skill_dict) + + if self.multiworld.thorn_shuffle[self.player] == 1: + self.multiworld.local_items[self.player].value.add("Thorn Upgrade") + + + def place_items_from_set(self, location_set: Set[str], name: str): + for loc in location_set: + self.multiworld.get_location(loc, self.player)\ + .place_locked_item(self.create_item(name)) + + + def place_items_from_dict(self, option_dict: Dict[str, str]): + for loc, item in option_dict.items(): + self.multiworld.get_location(loc, self.player)\ + .place_locked_item(self.create_item(item)) + + + def create_regions(self) -> None: + + player = self.player + world = self.multiworld + + region_table: Dict[str, Region] = { + "menu" : Region("Menu", player, world), + "albero" : Region("Albero", player, world), + "attots" : Region("All the Tears of the Sea", player, world), + "ar" : Region("Archcathedral Rooftops", player, world), + "bottc" : Region("Bridge of the Three Cavalries", player, world), + "botss" : Region("Brotherhood of the Silent Sorrow", player, world), + "coolotcv": Region("Convent of Our Lady of the Charred Visage", player, world), + "dohh" : Region("Deambulatory of His Holiness", player, world), + "dc" : Region("Desecrated Cistern", player, world), + "eos" : Region("Echoes of Salt", player, world), + "ft" : Region("Ferrous Tree", player, world), + "gotp" : Region("Graveyard of the Peaks", player, world), + "ga" : Region("Grievance Ascends", player, world), + "hotd" : Region("Hall of the Dawning", player, world), + "jondo" : Region("Jondo", player, world), + "kottw" : Region("Knot of the Three Words", player, world), + "lotnw" : Region("Library of the Negated Words", player, world), + "md" : Region("Mercy Dreams", player, world), + "mom" : Region("Mother of Mothers", player, world), + "moted" : Region("Mountains of the Endless Dusk", player, world), + "mah" : Region("Mourning and Havoc", player, world), + "potss" : Region("Patio of the Silent Steps", player, world), + "petrous" : Region("Petrous", player, world), + "thl" : Region("The Holy Line", player, world), + "trpots" : Region("The Resting Place of the Sister", player, world), + "tsc" : Region("The Sleeping Canvases", player, world), + "wothp" : Region("Wall of the Holy Prohibitions", player, world), + "wotbc" : Region("Wasteland of the Buried Churches", player, world), + "wotw" : Region("Where Olive Trees Wither", player, world), + "dungeon" : Region("Dungeons", player, world) + } + + for rname, reg in region_table.items(): + world.regions.append(reg) + + for ename, exits in region_exit_table.items(): + if ename == rname: + for i in exits: + ent = Entrance(player, i, reg) + reg.exits.append(ent) + + for e, r in exit_lookup_table.items(): + if i == e: + ent.connect(region_table[r]) + + for loc in location_table: + id = base_id + location_table.index(loc) + region_table[loc["region"]].locations\ + .append(BlasphemousLocation(self.player, loc["name"], id, region_table[loc["region"]])) + + victory = Location(self.player, "His Holiness Escribar", None, self.multiworld.get_region("Deambulatory of His Holiness", self.player)) + victory.place_locked_item(self.create_event("Victory")) + self.multiworld.get_region("Deambulatory of His Holiness", self.player).locations.append(victory) + + if self.multiworld.ending[self.player].value == 1: + set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8)) + elif self.multiworld.ending[self.player].value == 2: + set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and \ + state.has("Holy Wound of Abnegation", player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = {} + locations = [] + + for loc in self.multiworld.get_filled_locations(self.player): + if loc.name == "His Holiness Escribar": + continue + else: + data = { + "id": self.location_name_to_game_id[loc.name], + "ap_id": loc.address, + "name": loc.item.name, + "player_name": self.multiworld.player_name[loc.item.player] + } + + if loc.name in shop_set: + data["type"] = loc.item.classification.name + + locations.append(data) + + config = { + "versionCreated": "AP", + "general": { + "teleportationAlwaysUnlocked": bool(self.multiworld.prie_dieu_warp[self.player].value), + "skipCutscenes": bool(self.multiworld.skip_cutscenes[self.player].value), + "enablePenitence": bool(self.multiworld.penitence[self.player].value), + "hardMode": False, + "customSeed": 0, + "allowHints": bool(self.multiworld.corpse_hints[self.player].value) + }, + "items": { + "type": 1, + "lungDamage": False, + "disableNPCDeath": True, + "startWithWheel": bool(self.multiworld.start_wheel[self.player].value), + "shuffleReliquaries": bool(self.multiworld.reliquary_shuffle[self.player].value) + }, + "enemies": { + "type": self.multiworld.enemy_randomizer[self.player].value, + "maintainClass": bool(self.multiworld.enemy_groups[self.player].value), + "areaScaling": bool(self.multiworld.enemy_scaling[self.player].value) + }, + "prayers": { + "type": 0, + "removeMirabis": False + }, + "doors": { + "type": 0 + }, + "debug": { + "type": 0 + } + } + + slot_data = { + "locations": locations, + "cfg": config, + "ending": self.multiworld.ending[self.player].value, + "death_link": bool(self.multiworld.death_link[self.player].value) + } + + return slot_data + + +class BlasphemousItem(Item): + game: str = "Blasphemous" + + +class BlasphemousLocation(Location): + game: str = "Blasphemous" \ No newline at end of file diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md new file mode 100644 index 0000000000..15223213ac --- /dev/null +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -0,0 +1,64 @@ +# Blasphemous + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +All items that appear on the ground are randomized, and there are options to randomize more of the game, such as stat upgrades, enemies, skills, and more. + +In addition, there are other changes to the game that make it better optimized for a randomizer: + +- Some items and enemies are never randomized. +- Teleportation between Prie Dieus can be unlocked from the beginning. +- New save files are created in True Torment mode (NG+), but enemies and bosses will use their regular attack & defense values. A penitence can be chosen if the option is enabled. +- Save files can not be ascended - the randomizer is meant to be completed in a single playthrough. +- The Ossuary will give a reward for every four bones collected. +- Side quests have been modified so that the items received from them cannot be missed. +- The Apodictic Heart of Mea Culpa can be unequipped. +- Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt. +- If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them. + +## What has been changed about the side quests? + +Tirso: +- Tirso's helpers will never die. Herbs can be given to him at any time. + +Gemino: +- Gemino will never freeze. The thimble can be given to him at any time. + +Viridiana: +- Viridiana will never die. The player can ask for her assistance at all 5 boss fights, and she will still appear at the rooftops. + +Redento: +- No changes. + +Cleofas: +- The choice to end Socorro's suffering has been removed. +- Cleofas will not jump off the rooftops, even after talking to him without the Cord of the True Burying. + +Crisanta / Ending C: +- The Incomplete Scapular will not skip the fight with Esdras. Instead, it is required to open the door to the church in Brotherhood of the Silent Sorrow. +- Perpetva's item from The Resting Place of the Sister is always accessible, even after defeating Esdras. +- Crisanta's gift in Brotherhood of the Silent Sorrow will always be the Holy Wound of Abnegation. +- When fighting Crisanta, it is no longer required to have the Apodictic Heart of Mea Culpa equipped to continue with Ending C, it just needs to be in the player's inventory. + +## Which items and enemies are never randomized? + +Items: +- Golden Thimble Filled with Burning Oil - from the fountain of burning oil in Convent of Our Lady of the Charred Visage +- Hatched Egg of Deformity - from the tree in Mountains of the Endless Dusk +- Chalice of Inverted Verses - from the statue in Desecrated Cistern +- Holy Wound of Abnegation - given by Crisanta in Brotherhood of the Silent Sorrow + +Enemies: +- The Charging Knell in Mountains of the Endless Dusk +- The bell ringer in lower east Jondo +- The first Phalaris, Lionheart, and Sleepless Tomb in their respective areas (Chalice of Inverted Verses quest) + +In addition, any enemies that appear in some kind of arena (such as the Confessor Dungeons, or the bridges in Archcathedral Rooftops or Grievance Ascends) will not be randomized to prevent softlocking. + +## What does another world's item look like in Blasphemous? + +Items retain their original appearance. You won't know if an item is for another player until you collect it. The only exception to this is the shops, where items that belong to other players are represented by the Archipelago logo. \ No newline at end of file diff --git a/worlds/blasphemous/docs/setup_en.md b/worlds/blasphemous/docs/setup_en.md new file mode 100644 index 0000000000..35b8670f6d --- /dev/null +++ b/worlds/blasphemous/docs/setup_en.md @@ -0,0 +1,21 @@ +# Blasphemous Multiworld Setup Guide + +## Required Software + +- Blasphemous from: [Steam](https://store.steampowered.com/app/774361/Blasphemous/) +- Blasphemous Modding API from: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API) +- Blasphemous Randomizer from: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer) +- Blasphemous Multiworld from: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld) + +## Instructions (Windows) + +1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page. + +2. After the Modding API has been installed, download the [Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and [Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both into the `Modding` folder. + +3. Start Blasphemous. To verfy that the mods are working, look for a version number for both the Randomizer and Multiworld on the title screen. + +## Connecting + +To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use the command `multiworld connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used. +**Make sure to connect to the server before attempting to start a new save file.** \ No newline at end of file From 5966aa5327b68b64c4ed6d0578a249d1a8d22d80 Mon Sep 17 00:00:00 2001 From: FlySniper Date: Fri, 24 Feb 2023 01:35:09 -0500 Subject: [PATCH 04/14] Wargroove: Implement New Game (#1401) This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod --- Launcher.py | 2 + Utils.py | 3 + WargrooveClient.py | 443 ++++++++++++++++++ host.yaml | 6 + worlds/wargroove/Items.py | 104 ++++ worlds/wargroove/Locations.py | 41 ++ worlds/wargroove/Options.py | 38 ++ worlds/wargroove/Regions.py | 169 +++++++ worlds/wargroove/Rules.py | 161 +++++++ worlds/wargroove/Wargroove.kv | 28 ++ worlds/wargroove/__init__.py | 139 ++++++ .../data/mods/ArchipelagoMod/maps.dat | Bin 0 -> 113803 bytes .../data/mods/ArchipelagoMod/mod.dat | Bin 0 -> 591 bytes .../data/mods/ArchipelagoMod/modAssets.dat | Bin 0 -> 53372 bytes ...paign-c40a6e5b0cdf86ddac03b276691c483d.cmp | Bin 0 -> 113792 bytes ...n-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak | Bin 0 -> 113792 bytes worlds/wargroove/docs/en_Wargroove.md | 34 ++ worlds/wargroove/docs/wargroove_en.md | 83 ++++ 18 files changed, 1251 insertions(+) create mode 100644 WargrooveClient.py create mode 100644 worlds/wargroove/Items.py create mode 100644 worlds/wargroove/Locations.py create mode 100644 worlds/wargroove/Options.py create mode 100644 worlds/wargroove/Regions.py create mode 100644 worlds/wargroove/Rules.py create mode 100644 worlds/wargroove/Wargroove.kv create mode 100644 worlds/wargroove/__init__.py create mode 100644 worlds/wargroove/data/mods/ArchipelagoMod/maps.dat create mode 100644 worlds/wargroove/data/mods/ArchipelagoMod/mod.dat create mode 100644 worlds/wargroove/data/mods/ArchipelagoMod/modAssets.dat create mode 100644 worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp create mode 100644 worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak create mode 100644 worlds/wargroove/docs/en_Wargroove.md create mode 100644 worlds/wargroove/docs/wargroove_en.md diff --git a/Launcher.py b/Launcher.py index 7d5b2f7316..d5ade1f184 100644 --- a/Launcher.py +++ b/Launcher.py @@ -151,6 +151,8 @@ components: Iterable[Component] = ( Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 Component('Starcraft 2 Client', 'Starcraft2Client'), + # Wargroove + Component('Wargroove Client', 'WargrooveClient'), # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), diff --git a/Utils.py b/Utils.py index 010cc3e5d3..098d5f01e7 100644 --- a/Utils.py +++ b/Utils.py @@ -310,6 +310,9 @@ def get_default_options() -> OptionsType: "lufia2ac_options": { "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", }, + "wargroove_options": { + "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" + } } return options diff --git a/WargrooveClient.py b/WargrooveClient.py new file mode 100644 index 0000000000..fec20cc861 --- /dev/null +++ b/WargrooveClient.py @@ -0,0 +1,443 @@ +from __future__ import annotations +import os +import sys +import asyncio +import random +import shutil +from typing import Tuple, List, Iterable, Dict + +from worlds.wargroove import WargrooveWorld +from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData + +import ModuleUpdate +ModuleUpdate.update() + +import Utils +import json +import logging + +if __name__ == "__main__": + Utils.init_logging("WargrooveClient", exception_logger="Client") + +from NetUtils import NetworkItem, ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ + CommonContext, server_loop + +wg_logger = logging.getLogger("WG") + + +class WargrooveClientCommandProcessor(ClientCommandProcessor): + def _cmd_resync(self): + """Manually trigger a resync.""" + self.output(f"Syncing items.") + self.ctx.syncing = True + + def _cmd_commander(self, *commander_name: Iterable[str]): + """Set the current commander to the given commander.""" + if commander_name: + self.ctx.set_commander(' '.join(commander_name)) + else: + if self.ctx.can_choose_commander: + commanders = self.ctx.get_commanders() + wg_logger.info('Unlocked commanders: ' + + ', '.join((commander.name for commander, unlocked in commanders if unlocked))) + wg_logger.info('Locked commanders: ' + + ', '.join((commander.name for commander, unlocked in commanders if not unlocked))) + else: + wg_logger.error('Cannot set commanders in this game mode.') + + +class WargrooveContext(CommonContext): + command_processor: int = WargrooveClientCommandProcessor + game = "Wargroove" + items_handling = 0b111 # full remote + current_commander: CommanderData = faction_table["Starter"][0] + can_choose_commander: bool = False + commander_defense_boost_multiplier: int = 0 + income_boost_multiplier: int = 0 + starting_groove_multiplier: float + faction_item_ids = { + 'Starter': 0, + 'Cherrystone': 52025, + 'Felheim': 52026, + 'Floran': 52027, + 'Heavensong': 52028, + 'Requiem': 52029, + 'Outlaw': 52030 + } + buff_item_ids = { + 'Income Boost': 52023, + 'Commander Defense Boost': 52024, + } + + def __init__(self, server_address, password): + super(WargrooveContext, self).__init__(server_address, password) + self.send_index: int = 0 + self.syncing = False + self.awaiting_bridge = False + # self.game_communication_path: files go in this path to pass data between us and the actual game + if "appdata" in os.environ: + options = Utils.get_options() + root_directory = options["wargroove_options"]["root_directory"].replace("/", "\\") + data_directory = "lib\\worlds\\wargroove\\data\\" + dev_data_directory = "worlds\\wargroove\\data\\" + appdata_wargroove = os.path.expandvars("%APPDATA%\\Chucklefish\\Wargroove\\") + if not os.path.isfile(root_directory + "\\win64_bin\\wargroove64.exe"): + print_error_and_close("WargrooveClient couldn't find wargroove64.exe. " + "Unable to infer required game_communication_path") + self.game_communication_path = root_directory + "\\AP" + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + + if not os.path.isdir(appdata_wargroove): + print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!" + "Boot Wargroove and then close it to attempt to fix this error") + if not os.path.isdir(data_directory): + data_directory = dev_data_directory + if not os.path.isdir(data_directory): + print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!") + shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True) + else: + print_error_and_close("WargrooveClient couldn't detect system type. " + "Unable to infer required game_communication_path") + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(WargrooveContext, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + await super(WargrooveContext, self).connection_closed() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root + "/" + file) + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + await super(WargrooveContext, self).shutdown() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root+"/"+file) + + def on_package(self, cmd: str, args: dict): + if cmd in {"Connected"}: + filename = f"AP_settings.json" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + slot_data = args["slot_data"] + json.dump(args["slot_data"], f) + self.can_choose_commander = slot_data["can_choose_commander"] + print('can choose commander:', self.can_choose_commander) + self.starting_groove_multiplier = slot_data["starting_groove_multiplier"] + self.income_boost_multiplier = slot_data["income_boost"] + self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"] + f.close() + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.close() + self.update_commander_data() + self.ui.update_tracker() + + random.seed(self.seed_name + str(self.slot)) + # Our indexes start at 1 and we have 24 levels + for i in range(1, 25): + filename = f"seed{i}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.write(str(random.randint(0, 4294967295))) + f.close() + + if cmd in {"RoomInfo"}: + self.seed_name = args["seed_name"] + + if cmd in {"ReceivedItems"}: + received_ids = [item.item for item in self.items_received] + for network_item in self.items_received: + filename = f"AP_{str(network_item.item)}.item" + path = os.path.join(self.game_communication_path, filename) + + # Newly-obtained items + if not os.path.isfile(path): + open(path, 'w').close() + # Announcing commander unlocks + item_name = self.item_names[network_item.item] + if item_name in faction_table.keys(): + for commander in faction_table[item_name]: + logger.info(f"{commander.name} has been unlocked!") + + with open(path, 'w') as f: + item_count = received_ids.count(network_item.item) + if self.buff_item_ids["Income Boost"] == network_item.item: + f.write(f"{item_count * self.income_boost_multiplier}") + elif self.buff_item_ids["Commander Defense Boost"] == network_item.item: + f.write(f"{item_count * self.commander_defense_boost_multiplier}") + else: + f.write(f"{item_count}") + f.close() + + print_filename = f"AP_{str(network_item.item)}.item.print" + print_path = os.path.join(self.game_communication_path, print_filename) + if not os.path.isfile(print_path): + open(print_path, 'w').close() + with open(print_path, 'w') as f: + f.write("Received " + + self.item_names[network_item.item] + + " from " + + self.player_names[network_item.player]) + f.close() + self.update_commander_data() + self.ui.update_tracker() + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.close() + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager, HoverBehavior, ServerToolTip + from kivy.uix.tabbedpanel import TabbedPanelItem + from kivy.lang import Builder + from kivy.uix.button import Button + from kivy.uix.togglebutton import ToggleButton + from kivy.uix.boxlayout import BoxLayout + from kivy.uix.gridlayout import GridLayout + from kivy.uix.image import AsyncImage, Image + from kivy.uix.stacklayout import StackLayout + from kivy.uix.label import Label + from kivy.properties import ColorProperty + from kivy.uix.image import Image + import pkgutil + + class TrackerLayout(BoxLayout): + pass + + class CommanderSelect(BoxLayout): + pass + + class CommanderButton(ToggleButton): + pass + + class FactionBox(BoxLayout): + pass + + class CommanderGroup(BoxLayout): + pass + + class ItemTracker(BoxLayout): + pass + + class ItemLabel(Label): + pass + + class WargrooveManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("WG", "WG Console"), + ] + base_title = "Archipelago Wargroove Client" + ctx: WargrooveContext + unit_tracker: ItemTracker + trigger_tracker: BoxLayout + boost_tracker: BoxLayout + commander_buttons: Dict[int, List[CommanderButton]] + tracker_items = { + "Swordsman": ItemData(None, "Unit", False), + "Dog": ItemData(None, "Unit", False), + **item_table + } + + def build(self): + container = super().build() + panel = TabbedPanelItem(text="Wargroove") + panel.content = self.build_tracker() + self.tabs.add_widget(panel) + return container + + def build_tracker(self) -> TrackerLayout: + try: + tracker = TrackerLayout(orientation="horizontal") + commander_select = CommanderSelect(orientation="vertical") + self.commander_buttons = {} + + for faction, commanders in faction_table.items(): + faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70) + commander_group = CommanderGroup() + commander_buttons = [] + for commander in commanders: + commander_button = CommanderButton(text=commander.name, group="commanders") + if faction == "Starter": + commander_button.disabled = False + commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text)) + commander_buttons.append(commander_button) + commander_group.add_widget(commander_button) + self.commander_buttons[faction] = commander_buttons + faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10)) + faction_box.add_widget(commander_group) + commander_select.add_widget(faction_box) + item_tracker = ItemTracker(padding=[0,20]) + self.unit_tracker = BoxLayout(orientation="vertical") + other_tracker = BoxLayout(orientation="vertical") + self.trigger_tracker = BoxLayout(orientation="vertical") + self.boost_tracker = BoxLayout(orientation="vertical") + other_tracker.add_widget(self.trigger_tracker) + other_tracker.add_widget(self.boost_tracker) + item_tracker.add_widget(self.unit_tracker) + item_tracker.add_widget(other_tracker) + tracker.add_widget(commander_select) + tracker.add_widget(item_tracker) + self.update_tracker() + return tracker + except Exception as e: + print(e) + + def update_tracker(self): + received_ids = [item.item for item in self.ctx.items_received] + for faction, item_id in self.ctx.faction_item_ids.items(): + for commander_button in self.commander_buttons[faction]: + commander_button.disabled = not (faction == "Starter" or item_id in received_ids) + self.unit_tracker.clear_widgets() + self.trigger_tracker.clear_widgets() + for name, item in self.tracker_items.items(): + if item.type in ("Unit", "Trigger"): + status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1) + label = ItemLabel(text=name, color=status_color) + if item.type == "Unit": + self.unit_tracker.add_widget(label) + else: + self.trigger_tracker.add_widget(label) + self.boost_tracker.clear_widgets() + extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier + extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier + income_boost = ItemLabel(text="Extra Income: " + str(extra_income)) + defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense)) + self.boost_tracker.add_widget(income_boost) + self.boost_tracker.add_widget(defense_boost) + + self.ui = WargrooveManager(self) + data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode() + Builder.load_string(data) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + def update_commander_data(self): + if self.can_choose_commander: + faction_items = 0 + faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] + for network_item in self.items_received: + if self.item_names[network_item.item] in faction_item_names: + faction_items += 1 + starting_groove = (faction_items - 1) * self.starting_groove_multiplier + # Must be an integer larger than 0 + starting_groove = int(max(starting_groove, 0)) + data = { + "commander": self.current_commander.internal_name, + "starting_groove": starting_groove + } + else: + data = { + "commander": "seed", + "starting_groove": 0 + } + filename = 'commander.json' + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + json.dump(data, f) + if self.ui: + self.ui.update_tracker() + + def set_commander(self, commander_name: str) -> bool: + """Sets the current commander to the given one, if possible""" + if not self.can_choose_commander: + wg_logger.error("Cannot set commanders in this game mode.") + return + match_name = commander_name.lower() + for commander, unlocked in self.get_commanders(): + if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name: + if unlocked: + self.current_commander = commander + self.syncing = True + wg_logger.info(f"Commander set to {commander.name}.") + self.update_commander_data() + return True + else: + wg_logger.error(f"Commander {commander.name} has not been unlocked.") + return False + else: + wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.") + + def get_commanders(self) -> List[Tuple[CommanderData, bool]]: + """Gets a list of commanders with their unlocked status""" + commanders = [] + received_ids = [item.item for item in self.items_received] + for faction in faction_table.keys(): + unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids + commanders += [(commander, unlocked) for commander in faction_table[faction]] + return commanders + + +async def game_watcher(ctx: WargrooveContext): + from worlds.wargroove.Locations import location_table + while not ctx.exit_event.is_set(): + if ctx.syncing == True: + sync_msg = [{'cmd': 'Sync'}] + if ctx.locations_checked: + sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) + await ctx.send_msgs(sync_msg) + ctx.syncing = False + sending = [] + victory = False + for root, dirs, files in os.walk(ctx.game_communication_path): + for file in files: + if file.find("send") > -1: + st = file.split("send", -1)[1] + sending = sending+[(int(st))] + if file.find("victory") > -1: + victory = True + ctx.locations_checked = sending + message = [{"cmd": 'LocationChecks', "locations": sending}] + await ctx.send_msgs(message) + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + await asyncio.sleep(0.1) + + +def print_error_and_close(msg): + logger.error("Error: " + msg) + Utils.messagebox("Error", msg, error=True) + sys.exit(1) + +if __name__ == '__main__': + async def main(args): + ctx = WargrooveContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + game_watcher(ctx), name="WargrooveProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + import colorama + + parser = get_base_parser(description="Wargroove Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/host.yaml b/host.yaml index ce242fd4c9..78fff669e1 100644 --- a/host.yaml +++ b/host.yaml @@ -139,6 +139,12 @@ pokemon_rb_options: # True for operating system default program # Alternatively, a path to a program to open the .gb file with rom_start: true + +wargroove_options: + # Locate the Wargroove root directory on your system. + # This is used by the Wargroove client, so it knows where to send communication files to + root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" + zillion_options: # File name of the Zillion US rom rom_file: "Zillion (UE) [!].sms" diff --git a/worlds/wargroove/Items.py b/worlds/wargroove/Items.py new file mode 100644 index 0000000000..acb31a84b1 --- /dev/null +++ b/worlds/wargroove/Items.py @@ -0,0 +1,104 @@ +import typing + +from BaseClasses import Item, ItemClassification +from typing import Dict, List + +PROGRESSION = ItemClassification.progression +PROGRESSION_SKIP_BALANCING = ItemClassification.progression_skip_balancing +USEFUL = ItemClassification.useful +FILLER = ItemClassification.filler + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + type: str + classification: ItemClassification = PROGRESSION + + +item_table: Dict[str, ItemData] = { + # Units + 'Spearman': ItemData(52000, 'Unit'), + 'Wagon': ItemData(52001, 'Unit', USEFUL), + 'Mage': ItemData(52002, 'Unit'), + 'Archer': ItemData(52003, 'Unit'), + 'Knight': ItemData(52004, 'Unit'), + 'Ballista': ItemData(52005, 'Unit'), + 'Golem': ItemData(52006, 'Unit', USEFUL), + 'Harpy': ItemData(52007, 'Unit'), + 'Witch': ItemData(52008, 'Unit', USEFUL), + 'Dragon': ItemData(52009, 'Unit'), + 'Balloon': ItemData(52010, 'Unit', USEFUL), + 'Barge': ItemData(52011, 'Unit'), + 'Merfolk': ItemData(52012, 'Unit'), + 'Turtle': ItemData(52013, 'Unit'), + 'Harpoon Ship': ItemData(52014, 'Unit'), + 'Warship': ItemData(52015, 'Unit'), + 'Thief': ItemData(52016, 'Unit'), + 'Rifleman': ItemData(52017, 'Unit'), + + # Map Triggers + 'Eastern Bridges': ItemData(52018, 'Trigger'), + 'Southern Walls': ItemData(52019, 'Trigger'), + 'Final Bridges': ItemData(52020, 'Trigger', PROGRESSION_SKIP_BALANCING), + 'Final Walls': ItemData(52021, 'Trigger', PROGRESSION_SKIP_BALANCING), + 'Final Sickle': ItemData(52022, 'Trigger', PROGRESSION_SKIP_BALANCING), + + # Player Buffs + 'Income Boost': ItemData(52023, 'Boost', FILLER), + + 'Commander Defense Boost': ItemData(52024, 'Boost', FILLER), + + # Factions + 'Cherrystone Commanders': ItemData(52025, 'Faction', USEFUL), + 'Felheim Commanders': ItemData(52026, 'Faction', USEFUL), + 'Floran Commanders': ItemData(52027, 'Faction', USEFUL), + 'Heavensong Commanders': ItemData(52028, 'Faction', USEFUL), + 'Requiem Commanders': ItemData(52029, 'Faction', USEFUL), + 'Outlaw Commanders': ItemData(52030, 'Faction', USEFUL), + + # Event Items + 'Wargroove Victory': ItemData(None, 'Goal') + +} + + +class CommanderData(typing.NamedTuple): + name: str + internal_name: str + alt_name: str = None + + +faction_table: Dict[str, List[CommanderData]] = { + 'Starter': [ + CommanderData('Mercival', 'commander_mercival') + ], + 'Cherrystone': [ + CommanderData('Mercia', 'commander_mercia'), + CommanderData('Emeric', 'commander_emeric'), + CommanderData('Caesar', 'commander_caesar'), + ], + 'Felheim': [ + CommanderData('Valder', 'commander_valder'), + CommanderData('Ragna', 'commander_ragna'), + CommanderData('Sigrid', 'commander_sigrid') + ], + 'Floran': [ + CommanderData('Greenfinger', 'commander_greenfinger'), + CommanderData('Sedge', 'commander_sedge'), + CommanderData('Nuru', 'commander_nuru') + ], + 'Heavensong': [ + CommanderData('Tenri', 'commander_tenri'), + CommanderData('Koji', 'commander_koji'), + CommanderData('Ryota', 'commander_ryota') + ], + 'Requiem': [ + CommanderData('Elodie', 'commander_elodie'), + CommanderData('Dark Mercia', 'commander_darkmercia') + ], + 'Outlaw': [ + CommanderData('Wulfar', 'commander_wulfar'), + CommanderData('Twins', 'commander_twins', 'Errol & Orla'), + CommanderData('Vesper', 'commander_vesper') + ] +} \ No newline at end of file diff --git a/worlds/wargroove/Locations.py b/worlds/wargroove/Locations.py new file mode 100644 index 0000000000..e9fe52a188 --- /dev/null +++ b/worlds/wargroove/Locations.py @@ -0,0 +1,41 @@ +location_table = { + 'Humble Beginnings: Caesar': 53001, + 'Humble Beginnings: Chest 1': 53002, + 'Humble Beginnings: Chest 2': 53003, + 'Humble Beginnings: Victory': 53004, + 'Best Friendssss: Find Sedge': 53005, + 'Best Friendssss: Victory': 53006, + 'A Knight\'s Folly: Caesar': 53007, + 'A Knight\'s Folly: Victory': 53008, + 'Denrunaway: Chest': 53009, + 'Denrunaway: Victory': 53010, + 'Dragon Freeway: Victory': 53011, + 'Deep Thicket: Find Sedge': 53012, + 'Deep Thicket: Victory': 53013, + 'Corrupted Inlet: Victory': 53014, + 'Mage Mayhem: Caesar': 53015, + 'Mage Mayhem: Victory': 53016, + 'Endless Knight: Victory': 53017, + 'Ambushed in the Middle: Victory (Blue)': 53018, + 'Ambushed in the Middle: Victory (Green)': 53019, + 'The Churning Sea: Victory': 53020, + 'Frigid Archery: Light the Torch': 53021, + 'Frigid Archery: Victory': 53022, + 'Archery Lessons: Chest': 53023, + 'Archery Lessons: Victory': 53024, + 'Surrounded: Caesar': 53025, + 'Surrounded: Victory': 53026, + 'Darkest Knight: Victory': 53027, + 'Robbed: Victory': 53028, + 'Open Season: Caesar': 53029, + 'Open Season: Victory': 53030, + 'Doggo Mountain: Find all the Dogs': 53031, + 'Doggo Mountain: Victory': 53032, + 'Tenri\'s Fall: Victory': 53033, + 'Master of the Lake: Victory': 53034, + 'A Ballista\'s Revenge: Victory': 53035, + 'Rebel Village: Victory (Pink)': 53036, + 'Rebel Village: Victory (Red)': 53037, + 'Foolish Canal: Victory': 53038, + 'Wargroove Finale: Victory': None, +} diff --git a/worlds/wargroove/Options.py b/worlds/wargroove/Options.py new file mode 100644 index 0000000000..c8b8b37ee1 --- /dev/null +++ b/worlds/wargroove/Options.py @@ -0,0 +1,38 @@ +import typing +from Options import Choice, Option, Range + + +class IncomeBoost(Range): + """How much extra income the player gets per turn per boost received.""" + display_name = "Income Boost" + range_start = 0 + range_end = 100 + default = 25 + + +class CommanderDefenseBoost(Range): + """How much extra defense the player's commander gets per boost received.""" + display_name = "Commander Defense Boost" + range_start = 0 + range_end = 8 + default = 2 + + +class CommanderChoice(Choice): + """How the player's commander is selected for missions. + Locked Random: The player's commander is randomly predetermined for each level. + Unlockable Factions: The player starts with Mercival and can unlock playable factions. + Random Starting Faction: The player starts with a random starting faction and can unlock the rest. + When playing with unlockable factions, faction items are added to the pool. + Extra faction items after the first also reward starting Groove charge.""" + display_name = "Commander Choice" + option_locked_random = 0 + option_unlockable_factions = 1 + option_random_starting_faction = 2 + + +wargroove_options: typing.Dict[str, type(Option)] = { + "income_boost": IncomeBoost, + "commander_defense_boost": CommanderDefenseBoost, + "commander_choice": CommanderChoice +} diff --git a/worlds/wargroove/Regions.py b/worlds/wargroove/Regions.py new file mode 100644 index 0000000000..02f5ab879b --- /dev/null +++ b/worlds/wargroove/Regions.py @@ -0,0 +1,169 @@ +def create_regions(world, player: int): + from . import create_region + from .Locations import location_table + + world.regions += [ + create_region(world, player, 'Menu', None, ['Humble Beginnings']), + # Level 1 + create_region(world, player, 'Humble Beginnings', [ + 'Humble Beginnings: Caesar', + 'Humble Beginnings: Chest 1', + 'Humble Beginnings: Chest 2', + 'Humble Beginnings: Victory', + ], ['Best Friendssss', 'A Knight\'s Folly', 'Denrunaway', 'Wargroove Finale']), + + # Levels 2A-2C + create_region(world, player, 'Best Friendssss', [ + 'Best Friendssss: Find Sedge', + 'Best Friendssss: Victory' + ], ['Dragon Freeway', 'Deep Thicket', 'Corrupted Inlet']), + + create_region(world, player, 'A Knight\'s Folly', [ + 'A Knight\'s Folly: Caesar', + 'A Knight\'s Folly: Victory' + ], ['Mage Mayhem', 'Endless Knight', 'Ambushed in the Middle']), + + create_region(world, player, 'Denrunaway', [ + 'Denrunaway: Chest', + 'Denrunaway: Victory' + ], ['The Churning Sea', 'Frigid Archery', 'Archery Lessons']), + + # Levels 3AA-3AC + create_region(world, player, 'Dragon Freeway', [ + 'Dragon Freeway: Victory', + ], ['Surrounded']), + + create_region(world, player, 'Deep Thicket', [ + 'Deep Thicket: Find Sedge', + 'Deep Thicket: Victory', + ], ['Darkest Knight']), + + create_region(world, player, 'Corrupted Inlet', [ + 'Corrupted Inlet: Victory', + ], ['Robbed']), + + # Levels 3BA-3BC + create_region(world, player, 'Mage Mayhem', [ + 'Mage Mayhem: Caesar', + 'Mage Mayhem: Victory', + ], ['Open Season', 'Foolish Canal: Mage Mayhem Entrance']), + + create_region(world, player, 'Endless Knight', [ + 'Endless Knight: Victory', + ], ['Doggo Mountain', 'Foolish Canal: Endless Knight Entrance']), + + create_region(world, player, 'Ambushed in the Middle', [ + 'Ambushed in the Middle: Victory (Blue)', + 'Ambushed in the Middle: Victory (Green)', + ], ['Tenri\'s Fall']), + + # Levels 3CA-3CC + create_region(world, player, 'The Churning Sea', [ + 'The Churning Sea: Victory', + ], ['Rebel Village']), + + create_region(world, player, 'Frigid Archery', [ + 'Frigid Archery: Light the Torch', + 'Frigid Archery: Victory', + ], ['A Ballista\'s Revenge']), + + create_region(world, player, 'Archery Lessons', [ + 'Archery Lessons: Chest', + 'Archery Lessons: Victory', + ], ['Master of the Lake']), + + # Levels 4AA-4AC + create_region(world, player, 'Surrounded', [ + 'Surrounded: Caesar', + 'Surrounded: Victory', + ]), + + create_region(world, player, 'Darkest Knight', [ + 'Darkest Knight: Victory', + ]), + + create_region(world, player, 'Robbed', [ + 'Robbed: Victory', + ]), + + # Levels 4BAA-4BCA + create_region(world, player, 'Open Season', [ + 'Open Season: Caesar', + 'Open Season: Victory', + ]), + + create_region(world, player, 'Doggo Mountain', [ + 'Doggo Mountain: Find all the Dogs', + 'Doggo Mountain: Victory', + ]), + + create_region(world, player, 'Tenri\'s Fall', [ + 'Tenri\'s Fall: Victory', + ]), + + # Level 4BAB + create_region(world, player, 'Foolish Canal', [ + 'Foolish Canal: Victory', + ]), + + # Levels 4CA-4CC + create_region(world, player, 'Master of the Lake', [ + 'Master of the Lake: Victory', + ]), + + create_region(world, player, 'A Ballista\'s Revenge', [ + 'A Ballista\'s Revenge: Victory', + ]), + + create_region(world, player, 'Rebel Village', [ + 'Rebel Village: Victory (Pink)', + 'Rebel Village: Victory (Red)', + ]), + + # Final Level + create_region(world, player, 'Wargroove Finale', [ + 'Wargroove Finale: Victory' + ]), + ] + + # link up our regions with the entrances + world.get_entrance('Humble Beginnings', player).connect(world.get_region('Humble Beginnings', player)) + world.get_entrance('Best Friendssss', player).connect(world.get_region('Best Friendssss', player)) + world.get_entrance('A Knight\'s Folly', player).connect(world.get_region('A Knight\'s Folly', player)) + world.get_entrance('Denrunaway', player).connect(world.get_region('Denrunaway', player)) + world.get_entrance('Wargroove Finale', player).connect(world.get_region('Wargroove Finale', player)) + + world.get_entrance('Dragon Freeway', player).connect(world.get_region('Dragon Freeway', player)) + world.get_entrance('Deep Thicket', player).connect(world.get_region('Deep Thicket', player)) + world.get_entrance('Corrupted Inlet', player).connect(world.get_region('Corrupted Inlet', player)) + + world.get_entrance('Mage Mayhem', player).connect(world.get_region('Mage Mayhem', player)) + world.get_entrance('Endless Knight', player).connect(world.get_region('Endless Knight', player)) + world.get_entrance('Ambushed in the Middle', player).connect(world.get_region('Ambushed in the Middle', player)) + + world.get_entrance('The Churning Sea', player).connect(world.get_region('The Churning Sea', player)) + world.get_entrance('Frigid Archery', player).connect(world.get_region('Frigid Archery', player)) + world.get_entrance('Archery Lessons', player).connect(world.get_region('Archery Lessons', player)) + + world.get_entrance('Surrounded', player).connect(world.get_region('Surrounded', player)) + + world.get_entrance('Darkest Knight', player).connect(world.get_region('Darkest Knight', player)) + + world.get_entrance('Robbed', player).connect(world.get_region('Robbed', player)) + + world.get_entrance('Open Season', player).connect(world.get_region('Open Season', player)) + + world.get_entrance('Doggo Mountain', player).connect(world.get_region('Doggo Mountain', player)) + + world.get_entrance('Tenri\'s Fall', player).connect(world.get_region('Tenri\'s Fall', player)) + + world.get_entrance('Foolish Canal: Mage Mayhem Entrance', player).connect(world.get_region('Foolish Canal', player)) + world.get_entrance('Foolish Canal: Endless Knight Entrance', player).connect( + world.get_region('Foolish Canal', player) + ) + + world.get_entrance('Master of the Lake', player).connect(world.get_region('Master of the Lake', player)) + + world.get_entrance('A Ballista\'s Revenge', player).connect(world.get_region('A Ballista\'s Revenge', player)) + + world.get_entrance('Rebel Village', player).connect(world.get_region('Rebel Village', player)) diff --git a/worlds/wargroove/Rules.py b/worlds/wargroove/Rules.py new file mode 100644 index 0000000000..e163377393 --- /dev/null +++ b/worlds/wargroove/Rules.py @@ -0,0 +1,161 @@ +from typing import List + +from BaseClasses import MultiWorld, Region, Location +from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule + + +class WargrooveLogic(LogicMixin): + def _wargroove_has_item(self, player: int, item: str) -> bool: + return self.has(item, player) + + def _wargroove_has_region(self, player: int, region: str) -> bool: + return self.can_reach(region, 'Region', player) + + def _wargroove_has_item_and_region(self, player: int, item: str, region: str) -> bool: + return self.can_reach(region, 'Region', player) and self.has(item, player) + + +def set_rules(world: MultiWorld, player: int): + # Final Level + set_rule(world.get_location('Wargroove Finale: Victory', player), + lambda state: state._wargroove_has_item(player, "Final Bridges") and + state._wargroove_has_item(player, "Final Walls") and + state._wargroove_has_item(player, "Final Sickle")) + # Level 1 + set_rule(world.get_location('Humble Beginnings: Caesar', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Chest 1', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Chest 2', player), lambda state: True) + set_rule(world.get_location('Humble Beginnings: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Humble Beginnings', player), + [world.get_location('Humble Beginnings: Victory', player)]) + + # Levels 2A-2C + set_rule(world.get_location('Best Friendssss: Find Sedge', player), lambda state: True) + set_rule(world.get_location('Best Friendssss: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Best Friendssss', player), + [world.get_location('Best Friendssss: Victory', player)]) + + set_rule(world.get_location('A Knight\'s Folly: Caesar', player), lambda state: True) + set_rule(world.get_location('A Knight\'s Folly: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('A Knight\'s Folly', player), + [world.get_location('A Knight\'s Folly: Victory', player)]) + + set_rule(world.get_location('Denrunaway: Chest', player), lambda state: True) + set_rule(world.get_location('Denrunaway: Victory', player), lambda state: True) + set_region_exit_rules(world.get_region('Denrunaway', player), [world.get_location('Denrunaway: Victory', player)]) + + # Levels 3AA-3AC + set_rule(world.get_location('Dragon Freeway: Victory', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_region_exit_rules(world.get_region('Dragon Freeway', player), + [world.get_location('Dragon Freeway: Victory', player)]) + + set_rule(world.get_location('Deep Thicket: Find Sedge', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_rule(world.get_location('Deep Thicket: Victory', player), + lambda state: state._wargroove_has_item(player, 'Mage')) + set_region_exit_rules(world.get_region('Deep Thicket', player), + [world.get_location('Deep Thicket: Victory', player)]) + + set_rule(world.get_location('Corrupted Inlet: Victory', player), + lambda state: state._wargroove_has_item(player, 'Barge') or + state._wargroove_has_item(player, 'Merfolk') or + state._wargroove_has_item(player, 'Warship')) + set_region_exit_rules(world.get_region('Corrupted Inlet', player), + [world.get_location('Corrupted Inlet: Victory', player)]) + + # Levels 3BA-3BC + set_rule(world.get_location('Mage Mayhem: Caesar', player), + lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon')) + set_rule(world.get_location('Mage Mayhem: Victory', player), + lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon')) + set_region_exit_rules(world.get_region('Mage Mayhem', player), [world.get_location('Mage Mayhem: Victory', player)]) + + set_rule(world.get_location('Endless Knight: Victory', player), + lambda state: state._wargroove_has_item(player, 'Eastern Bridges') and ( + state._wargroove_has_item(player, 'Spearman') or + state._wargroove_has_item(player, 'Harpy') or + state._wargroove_has_item(player, 'Dragon'))) + set_region_exit_rules(world.get_region('Endless Knight', player), + [world.get_location('Endless Knight: Victory', player)]) + + set_rule(world.get_location('Ambushed in the Middle: Victory (Blue)', player), + lambda state: state._wargroove_has_item(player, 'Spearman')) + set_rule(world.get_location('Ambushed in the Middle: Victory (Green)', player), + lambda state: state._wargroove_has_item(player, 'Spearman')) + set_region_exit_rules(world.get_region('Ambushed in the Middle', player), + [world.get_location('Ambushed in the Middle: Victory (Blue)', player), + world.get_location('Ambushed in the Middle: Victory (Green)', player)]) + + # Levels 3CA-3CC + set_rule(world.get_location('The Churning Sea: Victory', player), + lambda state: (state._wargroove_has_item(player, 'Merfolk') or state._wargroove_has_item(player, 'Turtle')) + and state._wargroove_has_item(player, 'Harpoon Ship')) + set_region_exit_rules(world.get_region('The Churning Sea', player), + [world.get_location('The Churning Sea: Victory', player)]) + + set_rule(world.get_location('Frigid Archery: Light the Torch', player), + lambda state: state._wargroove_has_item(player, 'Archer') and + state._wargroove_has_item(player, 'Southern Walls')) + set_rule(world.get_location('Frigid Archery: Victory', player), + lambda state: state._wargroove_has_item(player, 'Archer')) + set_region_exit_rules(world.get_region('Frigid Archery', player), + [world.get_location('Frigid Archery: Victory', player)]) + + set_rule(world.get_location('Archery Lessons: Chest', player), + lambda state: state._wargroove_has_item(player, 'Knight') and + state._wargroove_has_item(player, 'Southern Walls')) + set_rule(world.get_location('Archery Lessons: Victory', player), + lambda state: state._wargroove_has_item(player, 'Knight') and + state._wargroove_has_item(player, 'Southern Walls')) + set_region_exit_rules(world.get_region('Archery Lessons', player), + [world.get_location('Archery Lessons: Victory', player)]) + + # Levels 4AA-4AC + set_rule(world.get_location('Surrounded: Caesar', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded')) + set_rule(world.get_location('Surrounded: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded')) + set_rule(world.get_location('Darkest Knight: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Darkest Knight')) + set_rule(world.get_location('Robbed: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Thief', 'Robbed') and + state._wargroove_has_item(player, 'Rifleman')) + + # Levels 4BA-4BC + set_rule(world.get_location('Open Season: Caesar', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and + state._wargroove_has_item(player, 'Knight')) + set_rule(world.get_location('Open Season: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and + state._wargroove_has_item(player, 'Knight')) + set_rule(world.get_location('Doggo Mountain: Find all the Dogs', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain')) + set_rule(world.get_location('Doggo Mountain: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain')) + set_rule(world.get_location('Tenri\'s Fall: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Tenri\'s Fall') and + state._wargroove_has_item(player, 'Thief')) + set_rule(world.get_location('Foolish Canal: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Foolish Canal') and + state._wargroove_has_item(player, 'Spearman')) + + # Levels 4CA-4CC + set_rule(world.get_location('Master of the Lake: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Warship', 'Master of the Lake')) + set_rule(world.get_location('A Ballista\'s Revenge: Victory', player), + lambda state: state._wargroove_has_item_and_region(player, 'Ballista', 'A Ballista\'s Revenge')) + set_rule(world.get_location('Rebel Village: Victory (Pink)', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village')) + set_rule(world.get_location('Rebel Village: Victory (Red)', player), + lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village')) + + +def set_region_exit_rules(region: Region, locations: List[Location], operator: str = "or"): + if operator == "or": + exit_rule = lambda state: any(location.access_rule(state) for location in locations) + else: + exit_rule = lambda state: all(location.access_rule(state) for location in locations) + for region_exit in region.exits: + region_exit.access_rule = exit_rule diff --git a/worlds/wargroove/Wargroove.kv b/worlds/wargroove/Wargroove.kv new file mode 100644 index 0000000000..9609684a42 --- /dev/null +++ b/worlds/wargroove/Wargroove.kv @@ -0,0 +1,28 @@ +: + orientation: 'vertical' + padding: [10,5,10,5] + size_hint_y: 0.14 + +: + orientation: 'horizontal' + +: + text_size: self.size + size_hint: (None, 0.8) + width: 100 + markup: True + halign: 'center' + valign: 'middle' + padding_x: 5 + outline_width: 1 + disabled: True + on_release: setattr(self, 'state', 'down') + +: + orientation: 'horizontal' + padding_y: 5 + +: + size_hint_x: None + size: self.texture_size + pos_hint: {'left': 1} \ No newline at end of file diff --git a/worlds/wargroove/__init__.py b/worlds/wargroove/__init__.py new file mode 100644 index 0000000000..ca387c4142 --- /dev/null +++ b/worlds/wargroove/__init__.py @@ -0,0 +1,139 @@ +import os +import string +import json + +from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification +from .Items import item_table, faction_table +from .Locations import location_table +from .Regions import create_regions +from .Rules import set_rules +from ..AutoWorld import World, WebWorld +from .Options import wargroove_options + + +class WargrooveWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Wargroove for Archipelago.", + "English", + "wargroove_en.md", + "wargroove/en", + ["Fly Sniper"] + )] + + +class WargrooveWorld(World): + """ + Command an army, in this retro style turn based strategy game! + """ + + option_definitions = wargroove_options + game = "Wargroove" + topology_present = True + data_version = 1 + web = WargrooveWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + def _get_slot_data(self): + return { + 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)), + 'income_boost': self.multiworld.income_boost[self.player], + 'commander_defense_boost': self.multiworld.commander_defense_boost[self.player], + 'can_choose_commander': self.multiworld.commander_choice[self.player] != 0, + 'starting_groove_multiplier': 20 # Backwards compatibility in case this ever becomes an option + } + + def generate_early(self): + # Selecting a random starting faction + if self.multiworld.commander_choice[self.player] == 2: + factions = [faction for faction in faction_table.keys() if faction != "Starter"] + starting_faction = WargrooveItem(self.multiworld.random.choice(factions) + ' Commanders', self.player) + self.multiworld.push_precollected(starting_faction) + + def generate_basic(self): + # Fill out our pool with our items from the item table + pool = [] + precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} + ignore_faction_items = self.multiworld.commander_choice[self.player] == 0 + for name, data in item_table.items(): + if data.code is not None and name not in precollected_item_names and not data.classification == ItemClassification.filler: + if name.endswith(' Commanders') and ignore_faction_items: + continue + item = WargrooveItem(name, self.player) + pool.append(item) + + # Matching number of unfilled locations with filler items + locations_remaining = len(location_table) - 1 - len(pool) + while locations_remaining > 0: + # Filling the pool equally with both types of filler items + pool.append(WargrooveItem("Commander Defense Boost", self.player)) + locations_remaining -= 1 + if locations_remaining > 0: + pool.append(WargrooveItem("Income Boost", self.player)) + locations_remaining -= 1 + + self.multiworld.itempool += pool + + # Placing victory event at final location + victory = WargrooveItem("Wargroove Victory", self.player) + self.multiworld.get_location("Wargroove Finale: Victory", self.player).place_locked_item(victory) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Wargroove Victory", self.player) + + def set_rules(self): + set_rules(self.multiworld, self.player) + + def create_item(self, name: str) -> Item: + return WargrooveItem(name, self.player) + + def create_regions(self): + create_regions(self.multiworld, self.player) + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in wargroove_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = int(option.value) + return slot_data + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(["Commander Defense Boost", "Income Boost"]) + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, player, world) + if locations: + for location in locations: + loc_id = location_table.get(location, 0) + location = WargrooveLocation(player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret + + +class WargrooveLocation(Location): + game: str = "Wargroove" + + def __init__(self, player: int, name: str, address=None, parent=None): + super(WargrooveLocation, self).__init__(player, name, address, parent) + if address is None: + self.event = True + self.locked = True + + +class WargrooveItem(Item): + game = "Wargroove" + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(WargrooveItem, self).__init__( + name, + item_data.classification, + item_data.code, + player + ) diff --git a/worlds/wargroove/data/mods/ArchipelagoMod/maps.dat b/worlds/wargroove/data/mods/ArchipelagoMod/maps.dat new file mode 100644 index 0000000000000000000000000000000000000000..3e1aeae247594b7ac7bb3506d9f566ebbb243a08 GIT binary patch literal 113803 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 KZQ$HLB_@XC0XPU0eIr63E z{vVOB1uGh-cFwf)=u{9;7ZYDT?TGsG=VBsD#l+K}J>SU88XR=FNXR>gG2p6DiYE)i z(26;|!M^^t9Rz%3PPr?Ha4nK?xb=@wLuFCHlcuVbEw}D1?kwLB9{WCO_iL9g*UOL1 zx**_i@Y3Y1ugyQFJ?q`Pnczh>5N0Q52L^az5%_OxXd+y)bWZ)e*fp4N5jl6Yp=cf?tN1F z_wRYF0av9&0h<(0h*i6CQG{FZk_Lr~I_+PqX^2n0x5wl8tXJZvR|($y52M359R{a_;+z$#XWPARj5+>U!;k9OAFx06OUDKqK3~}3^zWTMUjC@%%&#{#xa)@} z4=%JkUN!93;gPdP&;Go}+F8~e5nVogvg3-Jqk#qWt=;1?=lLeA?fA*jO|NB732QU| z;acIx-tX4KbLzk^MF(r;#|>MUciZ6ARmawz_F&yW!tsQgw>BACf9BLKEnX>j;>j0Q z_x2q)dhArC&u`vX%YJ^^>2>8_JlgL0j~Yx0J9qQ3%F8}&w>R-p-ofumf9rW}--7+? z;%BcqJpZLzE6-{upRCkuboY~W`$zU&|J{Zfl`?MKG-v*=``+>0^?1UWPfxsk`-&;I zw0ZaIlU+}r+WXVy+!x;3|J761-Pomd{*;JKC!78H{?gwM4E*J}_t@99@h zoVoF`;{IP+jeWFn_c5`(C#_q3dYEfl^^3h5rMwz_wt2Jb zzwenneVVgt&fRae@89nU+tx3ldX|2=?T_oF7qYX;t0?^4nf1efsWElG|MJ%-GaIBw zKX=Z1?!&jH58U&~+T2#<%SZJuow$C`Pg9p2n$yn}mp1vy!;6+a9P`0L)#Eoesf!1TVwyq8&<4{xOd~t$BXZ&vf%Bwn-Yhg9@IbM z-1+AZzgp~np!>;r2bP}=J5VL1--HRhhTs26>P>T|uD=kIpzNI6+FqqJ$|31Rg-mU_QksB!k(w%Z$5QfRtzPQS2s zV`}vn_08Tr-CO2wOPL~B_F2{qF5mjYGc`s}eYst!w7kje=Bd?ZpD(WZVb){Q0y~_I z=Pq71d_mp!AK0<*u4Zlb&RLf>YgqNsA03QXUcUB~B|m@t=x&R>{j5J{W^VoL{N|N^ zN*`u>ZLPP5om#)-oejQA-xYeEN^QR8*%mw7Ra3Bda>&5}O=Ih(_mNtEIU+2o$^+KE zXIhWl`1G{5%j-u?vE14!zhvzzXD19>chk)Ecl*0O9e-rp+L=$k_^&-%Ug@%=lke-# z7QM7@Z0+(+<#k3ZUD0{)<`HYkv%dLZdO=Y_%a3R6nB`x8KJn7sla`IEUDT*ZTAW#& z)^ct4H``Pzy?5Qrb+a1Rm{cp=QZjAw!VX)vE~@-k@u4r0j(po}`K@_v+8(y<9AvH8 zyw19;OS9j4@s$I!7JfZAugk+nPpCJyuh;TneWyP zZ?`*RS;Cu>8%KPdr9bZ7xV6@xvDZHm zvHZ7)`bWRpWG{Pjd->}48>`meHD>DEMRjI& zY@55b;jbf4-cb41>>W#=pc=Z#+)QZ&@@+B-kKUMYTI^6n)KXHF_S_ekRw&f+Cc z|5-Nl^1RbGm9<*dZrt>n*Ok5S)mKdhuLvypZtdlPZxoi#yE!&#`v$W?89RxjyNzWAGU!%okcbL&^h zW%t*a^XI7a&i&&im%lLR#mB!toE5k6aLx07-uCp-rw>gW+33OtkFIgu_e8haGg4k3 z{B(zUb>8libmEf}@ond>+xFD3I~%u%d9&tMdtTV`ZuZnyYc6n>e$=D+n*DuW+q3$o z+Y^#bto5w!^ue-zyZ7ID+8$BsgX}+SOS1xho~u%G{_~sHhqY;0=W^rRW0zjsv#;>p zX0=}I^Jv2tGcI*#wW447{({c8-@kpun3E4p`{0RR?MdZdycqj+Uhb}oRf`sU7(T1} zyO-CV`?7WF(qWr-)hurD*ax*5Z~O7FA^q*IPwepF-sYjyZS{~kv#mL#lNx^mftX}+-N>X!De6~@2Qyu5a~ zTt56{tE9%$+SR`8#BwRW@9@pzo*Z}At|ooDY)KzEZSdoh2Q_Zo#Pz}hO?&QprSZbp zoyQMNuyl?6BKoN*zqGM5h*&&t+t%)BF_XU!JJ9B)9+y5Tw_6)*>9@Z@&j)Wz=y!MX zZ<>@p^If)mXhPrLn)J<|`e@@GIq7S%zpnM*9jBWu$bGR_7snkhZpl~{xp7LZ^1|XO z^LM4K&gnb9&y%a-><6Cj`E^S5gH=Ddb>+_s?|x!n>Z023Jv;C8OMQCJb{E$?6sR=) zg-wmyOyBV8yN!N`d8PH7ho@9+wRlIM+vwHK_qX{^el&GJ%TLD7ed+j|PV-CO{%FE8 z&pv+V=|3AS{PVqsrk#se)84m1erSDp(!jbGYdtuz`={r>pOJR%o}VW?SeW?D`#0@P zX?pvxF1zRPiNCiA&zbo_&J*vBf1}5n8|L?2H1pV7k1eX+B>6(_XVLK! zw%po%r}T(C{_&=cmTz^~l705j_;&YvRyO>kzuF%c|Gcy9!I8Cc3*wh%b=h}nX4a9z zwa*{9`Qy&k3$2D$z3~2{FS{OW+U@;;-CnmnU)Z+UT@RmKn7RJ0t|QML``A;qRZ467{7WbI&U+ww*XXB<&fmOh{*=j` z>)&$b#DY!lOnJ%v`|EGKv1sVAe&x+?JriHE_T@Vt?D+A*8eML@f7PEwS$C%oZaMM& ztb$tWKWh9-*XzGYz3%8e-r478NB6wg*j?@AH?AN0PUHLoxoy((&Ne-MdiZreR@H2H zx~Bnb_~YeEpK?a5tiEDpSlgpxZ`gQUMs%~VAvKTPTvig^^rbhC%>QBKCr!IPUUTHO zStW~;>dmj)Zua8W^1ptoEdEGj!Y#8KAE4AJEq{Jtsh?8J#^R9+xM^C zwdY7=hm*gaTk>godGv_eUf%gd(-qdX^1;Pl+=WkHJU-+19@C>%k$B$}v3SU$ z`Er|-iEEn{ycs#ezpHuq+Fj$V+Zqphwa4_f(cXfN-{sGZ*xhz#{_`(4UX&m$bKQR+ z`MI3H-3wlCG`Hc>?cQhd`gSWDSG$w-;EA6W7T-K1uHW*$Z|->P*t{*H>SXNtWqIm> z=JhXhxM{%ix8#mW`)$bbRqwvNeQKVkZ&FJ2Ra<{3`Qz;^+h3XG7~JI1pJV%Nc>leG zfDK@V8!J11m&UQbucEl=Io4t?1(W_u)?=NrhCp35^Ze^|C zKKb(DI@8N<{&3(W%fL%JX0<<`(*JnZ{lDD$;Qn#;n=|JhN!|C>-S1sLAp3>JyUSu9 zS-*4kj{~2WT<6ZGQtPyC?i+mfptzfEUD~v*?NW~q`wfin^>NI9>iDY0d(zY9r0sq4 z+x_1}f4@BI+e)|I@yq&0KDQ5^xo~UG$EHc1yl#ia9o=~2b6XEp`to#qoeP`4c&=q^ zRO2W8eY$=2%7XYtjqm7Nx^35q_*E&LFMNEa=OfL=b$;x-mGOf=>U6tp(nFs)TSflZ z;PCH_tG)c@L*GrFu{tCDlgFEVo!laQ-SlVLZr^;dw;!Cr;WPtxxAQ;iS-WjEAKM;kLKIPII4C3vdZsqw=`K}d+6x;_3LeQ4%{d=CPwSM?O?(3U?(p_G?fRoy(X%2Dgoh;q-kdppla zf3K~j@ueFE^gLa&CPe#@^;KT}V#B6!Q+8MB(xPT2bCKd!Icj);{9H z52{$Y?*1bBsIc6PwrwAowx-eNiSIS8X_@=L+Z~T?`nl(k6BjB!`Q2xZT@P&AZ5xo# z?_!fM&$LJDyw@##T=rMLY~OUc{`%ZK%iL>ko}U#lIL|Fz|HC)KySDc4Y0)sFb;fh2 zb}VT2+LrDQPpm$y=MQ_`T_5(GI~=ik+GmaT+|%gW*S8#h@W|K2k;{KrJ9X68YwI7% z*fnO_f?mVE>@arcmyYBgZ|eTspqX#HKV|9@J000CH#q$AYg;QX_^s;qW476y-JAE@ zowuOFn+cr;?%R3((9XyAAZ$h*2Ul6?f$~0OFJ8!?!Tu-*A6k~ z?)j&c<5P>)h3) zS3-mPKB)O#^v4$$O}qc{o-Z$Cex1_dhJJJY$eFRB$M@$(zLl`xcutLx4_(*&tB5~u zYteP;5l>n3anCg$cX)lLy&YaHDE>5O?6}lc zWoqe5a|VsA{Y=ckJ+B=*{=_G)rgu$Ud}47?#M;Pqk2JfpWs9>PzcOZW)!G>?N8Fiu zpvlaCJv09MWxG@QG%8P=(s1eW!7(Eje;Z-HZ_vKgi#y-At@n>f8J{jLI??p`uFtw2 z$=Ex+`jPgZ4@kOkef#HcestRRn|eS0%DN4)ZC8(a-B-~0k0I+fY_Lu0=l&J~)%mlN z&W%eFw;gFU{m!!YVhXO`{-gWW8oxcYyWYUNHa^hq@R$jAxBRrvT~Yr-pRlMXDXYNg zlk6UcEcx8d;$qn^Ig5&2a*^!z`=nx@9B_D|yf(MPQzW?qMY*z9%9q`;*XH+l!>Ini zfWJ84PnP22qH>+6?Dos~vezfYL`NsYbxP>ep<`U9L@Jnu9IxBvl6-!fUzV&sS$0Gu zONsFk>iHriyUi`-%92;MIV6u)D)Bl|%I24B?lQ^mERqREZ;!{p>N|aYrya10y`Ey( z>vzgN*lcM~#sJADj|<3dyPQnr@d>z{c^+?((_a=TQd}N)zJyv%z(S?C#6+pB6gN5{ zQL@^K0kG6r1O#1WNK1^5P-+F-#a`LrwEJzjP8XoD&Zl~cifw*pu1l6moc;o-f50Xs zM#o0tPaOS;kEEo&X<5{Cf!|;3OK#sjAIL}Nqj(@U+U_Z8-!tFijmj+aI*a`QwwwE9*}Ssk0FB4X4k^#;DU$pJvQL&0JAy%WFQkl^ zo=mWK8$30qP%g{qAfd6uBx0WOL34Ut09f&fNij(sVq;>GIsz(SB_uFl=4i!sOzadFo75pDAt50)F(ORE zG0)|JjCYYBX$8^jhdwjlw)>qPH>BGM!37O-ydIA~CoZw0)#)f@=<%L_kO8k7da4wZ zsGwBjaRgi*NOnib>T$~?<*{)|l3Yq^1L3Da&^W*X3Lu}-X8`5g$d8nYA|(eAFOsT8 zN^#H;D&J0urC#9Um5XJYU;Myttxi75(*IV6bO*W!4R%QJq|>nxQftBr(53bgG{k}5 zMFK>??RUD6al5Vs>YbEN1!T8FMF}>OtGP2vHP5*H%hU zddG>t3)>uyoZ!L`1y~GLns9>{aGSFuZOwu45~L*rp>m1wdJ`?#VD|%F zFAP0$p{A^s1aIJUL+GI2Wvmuji;+qSWD-YDJ`|oh>-GB-;H>KkiDU~>JF251xtv6u{f8KM9`PjR9J89YrfJ~1rKymQ3pe}&`lqN9wyq@ ze}NRQgU&O)BuPD1f5}Y+7p)Y@ z<#fw7FPyYH-wWSHywRMnRA3uVI-M6FgRFTq$dZBVOol}-@x3iK-b{z^vz_?KrJWdH zBmfgOj!%ENpa@X_`2^9lu2qO7O5qc19Tf6H=>(zm7$kAKAnI~Upp?5CMu_WJp|e;b zb2i@Q3dp1daL56>tn`{|M`8!AAMoZSvg9VgMp%NvxiTn7rb75?(l$Mv(;Tu*^4kg_ z=nT@J)ZWRGufUl{A&3JulZ7o7MGh=dZW%D~mJ#tqWCZ;uw??f&Je82hEBgX2#ZYq7 zLZ)BoA4shz2+wqD3ZmEw7|YSt*mwx1PENfMq&aaBOkGCD$B{`L?N#Z|Y$sPVH^>`H zCJUt^z-y7pM3iKi$+J)dG9(<2p5s{z*ac)B7jVL#TZCb7PLLh#WtMBHEnnM_7fT?GVduway`m4jFC>463)LCto0^m$H?nlaV~2dV>2 zztzlQFPNswq)sDK=}Zc9ztrl)NtCKu9+44Egs?``2CX@fqM10_lLmTj>W=MeoV0Dv)fJ1nDshtjaExWCBDWN=Bd7xR6dn5sFJb#cm%&5Z&^^`p*+=C)agcGE9c~TJU)L= z`81Z*opOwIP@@QnCVO)na-Qt=nOE<@^Gxa!Pnq}2fi?y;O8m`)DHy;;FuHb`zzL&` zm|rgTc#Q~CfebeM$r9PR+A5!}3Pveu1eN6(0`Mdq#1}ZQA#N9h2PVtLB~aHE!~!UP;sLBrQz<1$%5%CMsW2>Fd0?I*%T|RvLJ#PtOR}?4q=XWSQzNA^{D_gD zl0+_yG$d!FRP14c4`;E>>Gi3AqLGXs#^d%yNDdFLK$SSmXo9DHh41tDqDw;xQEQ@5 z897K4D&vJz)l*@pB}-w2TbZH3YpU?n3S|{n;8lcO<~3DXa}pV-I*5cL%^gD2NE!qC zQ(ZQnFU?`3Fa+D_fDKYi9g!%A_-?0*B4eoz~|6(%WSVudjY2OnK#T9683?C+9c{C${mu&PZ*3IQ$U0$L_b5=5pd^ijnn z4jEKMdDgill6ty z1xr&9b7I~`2oX;<)k_r@qdG!t3>+qNnC3`f#Ep<3NKvsY_|giYHAD?=Dx;HXW(FdH zM2@J<<%x}RfdZ2Op@KSn*A*mDpn@a=qM*=uDnuUv5oQ`i0k|#wX(u_#=vz=(gS=>T zB7s38yeSNV1uXhUFFLB&7-^mf7ZpNu-jWYlR((5)eX!ahrQ8SvW-KVQMYASvmvZ?R zz!ws$*cu&6MdJ!2T_mE06;_N){Bo%h#Xv4BDGvCsVnp6@k;Fq1O*jf8rSUW}<`#Ph zg>%tVFvwJMLg#@>PERyu`P^1Z%Ak=WQwHT&r08hG*gn4(vA1=61pTxy5SB>E;5^Hv^OrzIv&sx`J3VBGZk~j(35;Nca!0*l!4z8bd-x#1nwLydoYn{( zmLykzrz>6&Tvk#CHAYio%2Gv!e$a;$ibdHpH^9DFL4LKNU>rQrc9+K|Tk~`oMx#uAPF%uJXj054yeep zFYJ%W2**~ejshxHZ#IJ9N;g z8G#Vv4hV<&S%r2~NdHlVL9fmF0^Hgg1(fnt3DFm>uz9r$IvGr;qi zv)KZJZ`1%HgtU|>{wpt;t*%)uNpaumIO(F_0mrxP)QU*z)#6L+jWm@Cf33MeX&`occbR&ZnH({w@AZda` zViP1Z%g&Z%$YEgx67%kS#uB6bGR(9FO9@X7mJy8pP&mz@no=$#q@7oFFNtw6aq$&} zQ5u|~Y~B?^aBebS78!I?XHGdvc{TK*#L(zR6+X%qMud@=R9_QBdx6cJFQ;PyxXh}M zfQK@yohu5Ke%9zpY!Ne0CVXLj4KodFg&V#tNOKt>uI9yw%nAQH$;_EdBPtq}XherX zQwZrxQT5u2fLAa{jP$4$#q(kch5@<4Fwg1h6Tn0t8@@#=jErEx167TtKWwrh+UZMG z<^)aRHdfeSD?+4}MYHyUWG{`_fIb^17uia!Sf`~ab5#~$1Y-s6BwO^+x*8~^nUEo9 zouQ^HrwSwcBD6omd=Wl;dM|Zgkc@HaDt!fpio*eUE6`hl_8(#eBOLt+r7~Hd;A{tt zaxB{HtWS32%NC7aYPV1f!}ZWOEGmlkpHH(Y{NZa58u|I+7$;yaY#7MoqdRq&rg zr#ZD8&K0&|NvlLjCS<9geNKwhfj%68p#4Fkg7DDCTm(YvdBv(`ELUQ4`g_q9lVJL4 zpxEdp)rL6+pT5wwsjOid`+vXAD)LRE9`(DK%IfT;RT>M>e>j+j1LN0 zUf~Gx#T6}RQCC=qAsmv&VP+~T zTIDtw&~&Q?n%VpZye;pI7~TB#_PJRZDi#6`2Id}EcX46Dvr&Jbid zhKhpH+}{=kiW(qXnb_druv%CHSlLj(UJ;us@PNsJ!VL8X`y#|7WUxerkEEOX8O6!) z{Sa4jN9nY~NU2PtaOQd7i8QwaZ-iar@J7)F|D#sYxgecKQDzdkMJ9?=KeC@7{Suu`P&xz>xn(szYR|DB#+Sso~W(9`Ccr^WvNOqq%tLWpJt zc>%7-ijq=n$M+o2A|fJlv#D>Q{(oW$EdWcVnD>m-Q3}QEpL21LLW&f(1gdMR zASz7Ll@Q8Badi4n zX;}GEIcWx1EoGu7Vy_h%;OlX0mWC#vv5T9{($EBrawLYO8gz$NN3KnGsASL5V%d%z z%547u_+i(~u%d9v5)0QBPqDGoK%5q#!=qMZB*%*pYz3y!ES?dv${n%+jCY`-bu|Pa z6>Q$nH>ql;$?hyb&=sv{n})iI8NAy>22C^uVrp?84O-rXs%f4xWLsN@=Yq zflqeJB{XTTZvz!98RtvxiD8 zv+Pif6Qz^QO0$?XlE!I+JUBebr~lYjpWi!(PQpeIKRJszad;WBkm!=i0a|vUG;Cp2 zfq)8BS+Gx6kH!R|7}e2FG*W!5xLpe!uhgnlRE&9N7HwJ1XDOT(Cf^1;*58w2hqew92~0n+>q6x6;{7lB1DBOG=UvskZ`d-P`EU!1q+6h z<7+Rz;Oqh`qgKtpL98O$!mWv_zAZYZ#j@23qAl_}A+4a0z)0;Z9gr4`4@T06H52@a zjN?q^N#HG!>~|gxT2Z(#aT5P+x8a1jGvD3IWy|OOFOmd>m4$oLB`ZnHNYIgN4G2!M z`bn#Tfu;uhKCGFG?f;@dI_2*7WqBy7W{8DNQoAZBOFj`<*a;BQU%jU`W{M+B3=NTk zO}!4Rg7mANNT5G{M z71Y%{i;+64x-l~p0zy~JK9bC8?P_VE4mw5t6IWCV-8IKX6J|nay5u00>EKevK^f{q*y)sS7 ziR!e`!U(`LI}{4UOn~72T9ZC4nMNjIA-boRYMgRWB$ywUHa7@pUOi}OqJ=nW%}S_| z0`0FKYV$hD)cdS%3<;FO*plS)+CgZo-E7Jp#p+WlI7^9azrlQqB5E12kchLtx?N9z zOM9NwLIPMeN<}*zbcB<>v6IVc8KMuBV|1b~M#L?OeTS#m#Aa_MTOp({f z5iPpz>qUOZ&?6&JqM4(j?or<`iz*wtaDH4L^}Tp+;X34Vkegr*RKu~E7AfooAd zwaE~j2}eg|RGjQq+R?)F}O&+(ZYGGGkwj)a1%gv1pk=kCY07WLcEb zf;G-*wC!ZFr?WOe&0j^C^nlHzSmY~&QFX;gfU6-9QvIs*2&r);!F#Y&2I&z|6v7#4lJ@6Iq z$xOxGmlV)ksy=0?UQ*-PD-PbUN{qN&WS}j#aX;vY}{o{>ts_i%pvnG?S+*UF)z1Hn%zS) zp;a_fsQCMeOrU}i&C&6_DX9ZR8cyso46j->WI>N6BvS@mbA2?OGKgQTp=+A8t@Tr# zt5C#9%i?GX5{8r$>Bivzr>V|Dhqx@Ao#urr226}+azbB(RI3~?Z9|8qAg z&}u!nlA{rqd<4-Un3jYAk&*tT57Ab9w2@M9A1Wd*RU|6V6d*QxcGOgXgA|BE`f27{ zk&84pq!;&{Dw`k(x{41~1cijj3YjxgYzN;1W~fCqMK$AmnOY~L_}}CpwM3sQa56;R zc@6$IgvWXohajocG-yl^WmRoAW48i8&-RMa^ktOq^e_EyGBc*mj#~3DMVf{ka%_lf zX0npCX;ZcvFQ@`deSVRD_#Ub7gSt_2o!k$iz-nvp~TrSiofRznwp@Kj%R z5G}>e5Glpkt5Iam_<=E%O(N9L0%q0|qOYzgFP8bRMHyGP0YEsLP|We<<*?b7aHCRd zwS?Lvv)f2A$>3Xg)q#2GGz%GSq68t>5F)^c{-F;;n|3Us8QRTd?E5u071GSrJB<>5 zugj?TKhxog8>+awdI5x^w7F~dh=2tV7iQ4;sSqiJB1>%^& zR*f2yNpc<`CMN`I5&&Y9@-O?M_5iz5bR%$3}$V?azr5CuZ)j5%!?5qKnkYo6h!1eO@Svn-8a(HeVcjoU-+ zP4;pR7sO=_iT=_hK*`1}TtQ?IKZ9)%x33B-1(yJ^0y7=73Pc-n_{)uABQ8qul!bmIxS@gh6HR= zv!1>b0)eZB@ii4>p#jc5GuE;=Ndb)6fG%n)R5Xq2prMe`x~`~_6bg}mFvN7F)ihSz z5OM_v#Y(8fgiDAZx71vr)V=|{7~tbV6Ln9L`c2)K#0St7?_%O7GII=@nb?VDeFFi* zU9;337C9J-ATemGBX%VzCtAA8xG!6WG2a*^+V+bZMRRgdO1DcYsIu0R;0;rD+<}&k zyM>*_*uO)!NX2QJsc@Br4OxUUo4~gCu%im4Xs@orlW)P5fw~Qm`X+;SSQ+wYUIZ8T z2b_$XS!|mX!M}Q;(A`%0I<${F6foP8B=$yeIe=ePx}=H(P1axw6HD*q5q=_MHxs6; z4Uphtlx`>xl^Y~!B!lIS0BA>JQGQXK(lQM}wS9b|r~sywrn=ftnLJ5a3du1W3~ze{lCGC$!-;#LN`v(a=A!1C!hinmayUi z6=s@d7!Oq`4bZ>A82$~MLs~Q>s6W^j-XV}Zlr&&K(CN4kPaE03l?vrYG z)D7}_!=r3nR8@rWM_hQWx)qZP%qw;+2A=|@&fprW2h9C|z*O~F|F-{8I-|8nSZZ!V zXYoamU}LW&Yf}Y=?X*|Q*3nsAY82)F?uJ@bUX(4iN~{bcqfZLC*incA?Mn#}lWVbu z6pQnUq$Q|fXbqQF=LRrv6GKj|AXh1V^FFQlX*sGM2zbmt&>#J2H}!~+**%qi!>|Hx z#h}LESp-9$2oL4+w+(3!Vg9ZKtc;)0WF0ZD(dPNK%#{xlr;jVpxe9? zAsy$@4_%+6>p!grhV);wX9!049HEH(gQsDX4hBzFToJ*)94rZF2DJX+SdfvNCP747 zqQA9qL@7I)=$guRtkMNF{xTPyK9$Sx@+e#3XVS$UsB$@T=`l+xU(xfRy2|YnJLM(h z+VkRLJLJlVafx^l)RtR2Y!q%@>nO7Fce`D&~C!tN+eS%JC`N1_*+&;!~Cj8DP}L=AQ5-&^Otvhi1_$@(tas6T1ufd7-JPeLjnphH1&Csr7_vK&Iqs6GULT= zaj=XUgF1L~*8|$%OsPu>EaPwrTp3#a3;jF+qliM24%f?mX0lIguq z`WlC?thq^Jm^5{l?KXN1h*&|b(CRBc8lVx_MwGxJ2&lx`lUe0s6DH}9Gha~T0r+>%^Wj02h~ zjd4n15^dlPxNy@VJzJ3>m zB0)_4#4O57wp_ZPOF@`j^~41j0)eM&SpwHPG(eI9a2YuKCIb#lwu7GJXz_9*LI-}4 z1HO|Tb77GZ`r*mN<4b6Q8dDV)y2r1TBI^o{p*!j`!JK`9QpjPIsBv2{I|>XH5;6cW zVVzUV8#8#CX;W+@*$y9$%o3nT{_Ha$BbVCcV%q-?)X)QT#^FIJvJk}(H7vZ5EKoL9 z4@L%}WFIhqEkIbnUf*VaiapR`X0C+gVhE)%!Z@6H=q8qQmvN6Z#6QK;dK`Xpki6ro7K=bsx6cWk|XHK3@?U{Bp_!Sg8?v?s5+LcGo*`)I4cd`?}19-F>Ts*skQDyZQH;iXACw{=S^+gu zb z`@TZT>tQgP77i0y;dO3Yhp_tUwJP_=iEg>^5GKxN0=bFdBS(fuO5qmz)1Ll}%w>P` z*k3pM>t%oa;ot_v)gr17;Q2#>Vgf`8Cm>Qd0g=K9h!jphq;M687zEgfq~dCPCr8Uj zS6V&wCFlDBxmG)2H!_^IvcRLT#zAkvXO)CP=b^_ei3v}J@(?&(O@VuB@z|ByLE#QL zJQ8bviHA7>9}F6V4RJvanVDqhKptHOnXElYCfV}vrn>~|&ZHdIMBnGeg3=9h^ zNqfl_FdWJ3s5k%|XQ!${KD;B0H%8GJn&V_o$AM<_8{vxqyr4WnEOeIswv$?+N;2DN zXbm6T+Se95M~0?~lufKeBF`a}KBj}yT# z^`tQvb3lH$6cJbGnk}-I;{qPP5Ew;ZVXhgSn5+tO0Ix`p!9@)OJ+GyL&>9KlF7wW0 z5^?}q$6)*tTw)%AoCyU^;o*9f2R&V7Ip}f@7dUJ$?Fq?(zvK}dlWK$|bm)y5B8<=! z{)rZFz*fTj4kjo>7I;3AU272N2yWMiNnAW>@gqcdIBtN6T;O5}#|9+=DOiaT#~xoU8CqLEu@Hu)uJy)GJO zv>`)asQ?}~Ai?k;UV0HXf>)pU(E&HgjVNy@$dL^e9=y9ouR{d4f9(J;g;PQ0a2lNI zdaY4QL~}#j#2HLku;~Xf2+2HjW1=mrr$AeA=?Y@(iWF{`*oa4XB*tnsD5epm!5G;v zUByP~Vj>dZnmSL)OrbXXhLO|l4`MJ`#GnhJ&>oMRg)os{=8-XMn|MqKOf-*x!ZT!g zr-!CIFyq2*ktF`9(UrkqaZjbeuQ2jvz0E{OPhlBeGN-|c!CdpO^h&{%C;_WDku#OS z97Mr_HZLY%l9qAYAcnZZgQg`}M4rQALt+5eTmDyLzrHWJcc*wT$p$g{eWlb61J0|N zawB|FxK635200NrTA>{%P(nq9Ex|JAzNkadl)e*s$Dc}1u&L9ixw5}RMgR(MH1o(d zbsG3a5b49wHXUk(CsX`KmW|Pwbv^119wt){#Mn+pZ~}-Hv(dLvJm3`*JVyBpA_h%;b0zDhbos1q_T)Ixcz@lHsC(5KnXbGQTk310YO>v@Fuds zu>I#`g9jlMiw5$uTj39d_WWWjm!>6z=|9alxF;sj5=Jyat3|A*W1KLtOrp$&2+FOt zAz~2~PJ;v{CQ2~g+96I8>J15I3u(=&q9!RM27gpzD}*_?In@GdUFykgs5XNSbQljp zn}XX?2aLkS(2Sc#7*a-cH3)FXJLtf0>2`s)xWu2u*V*AzC0w}#EKZ!y`8?l?hPk*%>xI5+2`x4k;j7 zP#z+o9*T$|%?&iLKY)lQ`O0t@pcFmP7Wr88rEge~9YT-csIF2I2#zQ}Tm!P1oTg#x z1wM6Kvbv0JUb;j%g`+>hBChs84L-%K4&Jgv4($yIxnYA-lOu@n9bM-tAP_l33SIZp zMN-}$vJ=8M%fLvbo|X+N$+tQ%QKmUBlm8?ks!HqXaswY<8XM-v z#FQM7$D%fIp@0HK6jbj+v(WYss!?5jS{SBm{Vq1c!M8N_5Bx zrn20Wa=gIIPa_3U(LitGaZMwA{#)eMn5I!&l?z%z3nlm@mC9_yh>h}C zdBl~?oXXK5473&FT7FF0d&Fa*p%6n@_!rq%;l(n4iH8|7n1Bg=_S}dxNPSv{FQ|dT znXu^WO&qcS6>1fbp)=RU68|msnlVFAD@S_>=ilXs(6cOI!T8D@*POp#B6)=&%4sEG z-*{@i%adz!Wzy{gRA3MrUQLurQ79?_lazF7I;ukqnIzp%O@*a9`4lu4M%D#&RWy~| z^|*sPv`t}L;4gVKZ8X8TO)?>9A+G9I9U<=qG>hG*6Ve>(8QT)ZhE7^TqJ~*Z3b5d& z1O_ZywRxsgfm-s0*?Frx(fWdu*B11MH)R7E|TIzlFf@5 z35qeOZoEh=bUX74NL?r~K_o&iUAXSgCXy3Ha=z2X-aBRI4@4SPz=}yzDXpVO!)^ua zhu~>RBF*8|A=ybJQac_6^;o4lcr$ z#QWPyzCxv5E)T9O3CUJOp^pq+x*_KIAw?95WzaS@dJrY31%+w@1AdpSBse>sD92m_ z*AzU>4H^hN(Y@eqx5+*m4ubO@>oOcRZy{c&v^#AknX=1+^W$b2sN%GnWaN7>RfH8U zMDZrMg`TlaljI89XBn_XCUx8aZ@?rO>g6`;iMPz-H%sR)+lP|H=gfy(n>2vcA+K2< z{Mhx1C%;1I3+y3(W*L~bG|eapxbna%16#>%>Yqd6%vMvgEr{&sk&EuM1Buq%eaTWP zXZffuiSaQp7#Ckt9lWWI9i$ds30DhTd!;y0DgN3j4RzY_B2byAlweY+JE79c>x7T- z;gc-E1~{Zl+S@22~000H5G>Fgm78 zF3JtC%a1WJ1!pFuI~@R_)|9N>DXQdLj5XtU%GCh$CTpocFq4BUG@Dw0u_{y=qHj|h z=dH$>Ndr_SAJZi%EX;wd^=FX{bs)=wG*Qz`DuQboh#F@m6rvHSKPZbh!ywpkW>OJS z!PthGRAl1fH`=X67a-|vTAZ0s=nES`o5@5EI6ty7T>pcKYbFyJp4?nQ#0cAr`FQlq zO)LXlg77wDw4t;Cc)S^pdwBBmJyJSdgMnK@ga#wjVseU1ALnws8JC0W8e22tau)eK zvW{pWsO#}&OipK!A>Z?j4gR)5O=$#PW_(VOx&uDN+de6SrX%x1s+n;-L(Y|5;D-zM zCu$WW>maAQP^ebC8Sg8qmw`7h1eRt3&0Z4B)=5&!-LCaeiP%)o!;QmhPqauV~C zT_oeBCJGyZ?!tn(QzjAgx5l9QMBS8&b{Cdlx$>|IR5cZYNL=rSM+&$U37TWaT|smn z2I*dwi6dh{_i(k7VxkjKCs?)AF3@FH>9$g5QJ@G*Ao;SG5kq;*_!%2RFRlnCi=FPz zq5}ZNiWYX@LQ`vKX9`aAgdY|{U5zmi~9yxe6`EmIj|LWqhlZwt$di1xW?PG6cwTuWB>9^T4+dN z3VhPvy#t2O7GP+-jHVB!prtZ;IgE7OLwsQSQB(1(<`vu-fRePJE81)dNWVR9@5Tp$UA;^Ptr08_0k}jLd zlx1!ibx*8@kSF+_CLJw=LIhjNittrC6l`0~x)EGPrAtVsI2HjgPBS%d9i$s3kH>-p z)?nE5uxJHG?EzMpeo~E)zF+A!CSxmLC&ygyhadSbrGqaj^P@{?lMIt!_KJa)Q6{b% zQqWP$(JDXgUczdB@KsI3UvaUM5fh!X1=k^ui#gpgjvS97(*POQJ1L%CT=RtP2x91> zVwLg9h%kl73qe=Aq6xV2O0W=g&cV{+2v585}kahy$(@&otWcI8BtDVFK?uhDq1zPAnX;xG1EFBoMKB7jeLT{Uh5)^ z2sSrzCh+&Esc0}EN^IEJsJEs;T&YBPf#UCJL^H_xPl7=q*KE)QenE9p-$Vr-#3YQ0c# z5rHdl2Lw^v3z?0O3jxJF&?+#T|L2DU`AIe#MwAdRI?d+gJ}FumpFtPqjmob%!K zT?(6&_LKAZnH-BXdvxTeb`c67Va*i>j%^lHi7G2fqL&1*_D@Uyq6}aD7eClC51}TY zUg7RMRq@fHps6P7IEsV;h>2;8l%h0)NDXOqv_)-Z&=q5bw#RuidW_TK%^@Tm zUQe;Y2V-Xldw{|krQ2b(Fv!>^!gWgCuGYhu%%!@=MiYTWh`C{Hxq=d(@oimg=ny(lT$m-157&%Z}Z+{gwkU`y2MZYwo8t=2w5VY3i3Z z_4(!17j9TI;Ps8`y06%B?z&^1*VoN@=?=;F_??4R&!2Jk)ZY%qr>viyTy^*(AG~-x zbI0uox7|GL`h+9)-&zcQqp%m_Sap%_P_kZ(HYTm zw+-7j=)PM1dHz=>Ke}h-XEXbkzjiMFjjH2%Y;4)?<&BkI-F)ZUlfV4B`l%Dm-}`vq zEV;*-4`-b1^l8k!b!N6$G2|9+!NRQ6D(hB`D(`jogKZjF>r@$0w64mG!}slf=Kc{= z-(OJo!66SXox7^u10@X(PT%*^nzeOa8CfUR{dChC>L2*J<;z7`?V4QZms4Zxm?KHw zq;2in>Ak6~R@D#t*XGoyLH(Y;Zqf69oVvblwJ%To*?80KZKu~5d)Jcdp8r?-dY8v< z{rZI=v(tW`d+UUqbPE3t*f7Pnx;;IV|uRPMc^2isf)~dC3SBuJD)>;^zUgfBC{lXW9{@Td0`uO+r z{#dcG>RUJWYt_EmvjZD$==R6FK*`f1o1J?5WY3RJ+E0{KnbhReH)C#zSt2hf>{EHx z-3d~5(v0F-UyQtN!q4w*>))->M)~l9w`LbSIct5r=hMpj%DYR8yq(un`RLVW?z&H2 zTgfpq=ArFhR;%3KGy20FXAdRq?s8yV)|P!|JT1328e6>4ck0dWMu zm^ns!*W?j`5z&3%5;szVN*7<<(EKAsJ_$B+&FC!th7%pl`(y2A%Ny|{z`MKFQ+pkmOzxO zJMO-D$sQ~2@-7cC>|QmxIxq`K0x*N(W4CS*ZNjm#^0Vddf%PCNlABxA=lOGzW_U+o z8{XOFd19Q>mZq74fJv*pU(YORt6hrjtZ)z(q;VhMgW#mx7nb#Tj}y&?QuWZG(is9Bs6MHrhmeX-%X z3lJ|}4R&5#aSd>{jdj}#e}KcuP+#QH_&-#Pq)i{TQ}1Iw*$%*$)b|_WmT0YxBCjzO zRpX(~LtCU?GbxUmr0{^$V^H)Hb8bYD3_w+zmn(f=%*2rb7%*;~dCe#|wy=9=9qk(s ze0!&_e9O(A9;o~eKbE%<_j@imSU8Yy6f637*4?tJB2=h`V#iccf?FmEI%p$z#21&} z0?$}?XH{9Vya_I+I>n0NM+ysNzYL)dxSoOvB06V(;d@h7^_=kY8PWg*6HJZ2yY47& z8@Cufxx!%KYI!wPOUKsz;k3)N7V?sA>{4-O{ZM352S;U46cH{5;E^wE`JdtAwQ}U% zb0&Uyi*VRYzUR0E4wAH0r)Ej-Q9^*x5c?bFy^wwQDJ+Vs3x{b*iS3scug>TnET9ZUX9A!h$37Ri?WlB?{a3V!S}7h=Me`K9GJp-E*!H#FO@0Nu`u_u+HKY>V0@co zEc|zvTdooTz!VH2&i(&Q_+7i)1T%?h5L8JOYptDdhP`d|J#VTf&kxbg+;|XaTcYe0 z3u5R&6dTM&)aOr>k8o9;xR%k1T!GPN)`cia2X+nH+$aI8Se<>+jIfrI7~bEgFo@(< z_+Z{kM){7`>8Bmp=LMaAc8GAvj&f0DXoC>QpPw-!oQ&D)zvyB+Y%y+cA9WRT$F}Hw zg;+6|0J(%WWYkn6(9KzN_&G-Os%*H7KYM=&osBxXE##qN?8Y4_awj{2Bvxe_sajLI zJ^@81;N}H4bn$zUas-DMs7O}nzFz{G#lS;BuFpi+x7Q{fwWr9ar(__qd7pk6=Uz2c zTfu;&P0D9ON?A$J!}`K#A(}}B`Iv~%5IZXP-4%q*!m+dBY_2>1$FQNtS2HD1b6QVm z_;ZZkS$OK+U>tr@Tgu?Ak;jUfmXD{sWWc7Wh6>ZqmBVw|Q>4=df_Ab5S_H8rxAeJcbEG^auC)TTv_^QVCBlZe#( zmbmWhHShEUP!=P8i*b+AEmGo|k1N?Zbn?U4PH-v}*?O7?A!7PC`hu{1koe1(kgKMi*3%km!Bn(SFA)o-&m zaO?vHO~euF++b5q25`{ec@NFO+ui*l{;7gp5lh?6UhViU!ZGfqdYM6^R=}GOANESF zLTCW)xUP-hfjJr|52kk8Ds5%*0a7mQaSosC0g)|8EXv~li+@fzn~01d;d@0?72Aq^ z$=2=907kWKk&wex7CP7m=!jF7a>enTW&KZkL>N@>lv2O|5lpclur3Ww{g)Eehhi+v;?$V%4zzhm$&~Vzep` zb~qDrU5b5bW>izJXELaT)R)6=S&P_;qkJfW5A$NylA#`yRinm!e;+IEI^pLpkBpF2 zzMJNvF#6vq3q~sxMmcQhA8@63Y?H{dOxf+jVvIm4-=EtnYU1rJ_jon;f6M)@WMx4Q z6Htc`7J4?KH19GN?q8E3vwo1OOIduiC75P)z$#3fTO_&j+Uysoxjs})<1oM^n708-oGtef8MY9{d6->?t#5F!~5XrP8| z7M)pQHGaj6P*m=6D|4v|$k1d)d}S4x+UQ#fg8FM71g1WtYNiU?U0<1*!+VarO%B7y z3=#0ISw55CR;rjjduX3xhh9e%PvpTTD=GfJPolV+%_6#X{pCIX4N8oag7}mld*+L@ z`Q3pYZ8(jMwf5mB+E9vF9-nqims}ewCkuPMRvsv5#_{$ZPHdxlilWGpHZD$_^p$)& ztARZ*dwJf=##ux(%}7|D88@#5dK878x?vHyEBK2D#9!T5nz@mzIhzWTVgN-AMSu8R~OiyN7wH2RT> zzI9$KvX!_-TBN~kQ=MII0xDv|x*;ew~=lLax7D=Fl6@wMJz|o0%-dw*g^uV6o;S*Z6uq3Xt0ynIjY=t`ktI^ILPN zd2V-Ljy^{)>^d74q`RWXTsm05wP`F&9YP3?%7ZXFNWZ4I)BNuc`8`ijS70J+f2_&7 z>0sQ>HQ)w*Jk*Ua(Fz3{}XVcrDJn8><-M)+Vusa zHF0ZoTxmV_lel7{`6B(DB(@+;rVB~NgLuH8iaX@ZDY&v zdaoexvmJp}SO(4rmy^;)(=hXq-GJA%_E8xk9#RCK{X`8*&5Bn4>wcdPDdBY|3asQx z@szUXH3)|#%gCA!C_jz`bHp=@_V?~rw@tIMF8z0xAxywK<+niYc!&CNP-8o^mpaFu zYlfVH1)5_vL{iL2yc2bV%9c`w{=?(~o2hOO*zR&rqPC~AI^~r)0f!A*%%?0?`FR5TLKPmc4bIgD263M|v%!0!VX`l`V}+QFA5xNxs?@m;HOp zy+tM4w2-2+Fb6B4` z2fBZo7tp2~n!EqC?DVp+UZAL6iJitk+qv%TFh5RxjPSYffp%`zD4p$lm8?0vGoUTI z^?oU*5a4{(U8r83n@gi*e_S*9+v*~*0TumRQ3`=oQuY@$ zyocJK+LA`r+pAt&R{0DfS@NW=ms8BfxdRWt8HvILW0by)- zku3B!Z5l?5sY-n;GR!tKOk7-y3p_EBX}!T=sZ)-as6FcbOxFPDK^G&-!39RyPqoJb zTF*n8#OA?D*sp6z<>$Q6)u$+B!jb-YNvu+HK{do%Mf-zcvJ&R8{5VjTzb*Wk)?#rj zdiW6}GTLBs=P16*0GOhew)13HmSQSQfIxAWBI&adtI3~?&a79v;QjY30S75^wXmAf zuS#xEizhdP=IvAJf_>t54mcLdJf$%yC_IWBaF>YHj<>dd3EAZB?qJbymCxVnYePr} z$JySIKUidxfX^GXsd!+avPmVcM=?+(QZwM~zp&^wo)-LyIwQ9tjbb67P9eH$i!a^E zL@#Y8tsb2ms@}Z%-CU#pf(fOniWvdT-^a}u;wN}!L@o3udPUyas5$|opo_iTT_!Zz zDCTk0b}m%oz$zJ%8(k20<3y?kQQS9keh-~|R)fC4l#K*@40{2d-7vKyYHKj;gk zons(e|4|)%QTsBoYQWvEMJ)))ZsOY4Ir_9a$7)W40EFm&&jYkyr6p|WPTdQSM1{3- zwIuR`-4ZoS`6BkT;V*Q?YM(f^)}8Z5J;n1+cF!A=2;6V-@RI?7@a1BVj`S}XQbtDW zT{dq6AJ4Urv3HL~4+sT9nI^&lK(SoPhV(4M^!Ic~xW=zR0QaPa13TCG{C{bG?jOc# z@sYP(iwNu9tK52gX*FMT75oj#QC@AE3I3!6QvV`aKp#Llp~_qJ0})mThDvJB!RTCg z%jRinp@sNach`B()kcxVQsW_c=^W&CZ{VkL@YuZ)TgZnlT?ULP@Cn9?%c=p${XTnW z8o&+Z@{!4U2kylvQ?aQJ41#d+=c;K)Jj{&T%Wmu|FwBhzDA$I_ ztT7lnVqP))DMSa0RsvJXFI9y8RGvg1W6lI?mSK`Q>{!TAY>!R@ntj0)0R-{t2`;53 zyhhoLgC}xlG}*lE<-VhoYi6Z}Kan&HwSg_P)gc_t570V6dF8J{yf+72(rmrE|5aL% z}bu9yJ~CsXO5oG{#CCo+p-R<7)6i1WGylZ%@RoMMS}#Cs%&!ba>zXFxTa7=EOa zxRA75$akTan*#9XxGuGk-+$*f0Q)k;p|aP@@NEzfp+3HjpN~B9J13mI?6Yv;_O(FZ zG5!%T4DMT&H)`eO;>pLjxe^vbg2y6|$bcuLQVF_aZ94yr2S}{*k2C3ECP+4&tm=bu z8$p<8-1NJ5x%V^b2qi!zgVt8gDyQx5+|319$j#YcaH}JomtQ1qMy)6FClA6D`ulbH zN+lksw%g_TvZHMXE3rK(DD!ya4X_3IrzJ$EysO34OYgBa3;BuApyJ1th8Sd9Z^t^1 z(f*9ADric_W@rkZlXTnr(43@}b8f4EM6jD_Jh` z;f;#K)9R%nIu10~E)gYTE@g`$}g}Q384cP&pM9Sn#@^Do7v~v(p0^3qDVwm*O51+ZkRO_mNJwAOWDa(kxv@2mgm%*N# z)buNUasqP*sKAjWmEX-}8dj)j;6~A2f`UpStwl;Z$l_*(kGvsed$dqh0u{3W$srH3 zdZ*LJGqqd>L7JNc7xqPxU{m$pQ~1%LwcAw@6$nfl=E{kkGyCqlxScPqLm+kx`xcuW z5aI|mS7;fEwRnM5Ra%NbV|BTmFhgl3*k%V?4YuCKc3`0?e5sd_7h@e4`Ptr2~42?28;IxkW8AP+XPU{Gr*!V`&m;I5z1un1IanuN*_!HNRwAgBrx zp)56-A76XDMeQG~*5*721W5WA5ox7RrOM>%2TS?Xxxww{5ItW|?qyy=ARg}*`SQl} zNrGcIR_b0m_1V>DK!h{wJ)z~8Hqx+1aGbcuLgaj<3(H?%aoXAM)d+M7v77?2oxWI* zSQ3lsv24i8{dm?r`<3om#-)_6vTzF)O~PFt=y-ejZbM;Q(>0iQdDBp2carMB2J(WYIMUgu(XfuQ`;P3YE}$T}OS zDnqTwYA)fI$vMTrcn_7)11b|!SX$XH%p;t;&~c6AQ*OpR8&&cW`Z^FS-lWO1r@h`? zlz&6HnlhnnYb4OW>ekzesxd8ii<4{XR8zIptjFySJw~W5zt!j5ZuO6SW3Q<5OZUjm z2QxQl>9b*6eq}?pFDEc^0Jqu@q~Km$?qhRtqk1Yy{llCLT&OWh-z6U^UzrCDZQ^WW zL#PLy@Q^`eI5}7mJ#Ul3NJx|i{wbA0e2(1veFb(?XbJ>N9%?DQ30Z-{1l&ZY1v1oo zv=w42!oovx+9mS{M*Z*QZr*+^Omb>pPi?~;bA7X?*yXzm)gT+xt8?G{)Ry>^$2sgM016sr%NQUY zP)NgD*&gPgaNOps*2Zmp^5Eq4`>f_kZ`O{c0~2c8>BLzK0wEhutYlKv;kj!_-F%aQ zm7EH{->0Hhw*z^~^c;wUxyg6ZO12*X;oS!1h_M@V_xkC~x|(pR3&d2xLnjRL9l`Vz zSg!!I7MbT8QV)apY+@qu7TdmG#EyltavzFoJmHK0pGTyv5P8`t>{6g@Ao+ZUcj32C z0}>v^Nxr=jx*3}j|2>RiE3Lm#1a1BPE(~ z*jIS5#ytX^#Zh?vb9Cpt{8MQrv{CXq-i`MSDaenF&L=O*D08t6NIdaj5RnktGnG5D ztszP>FJU{s0?gBdm>-p;`fq66l@si))nJjak;~Ile_zie$ZSHIE$#QD4afii^k(7`6+H?P>C`^NPp%0~n z!S`&x#0~|)_paRJX1cfWTr8!TW)s2DdOI|!Pam*S%vi?$6eMIn4B=v$dO7!#^Eai}+gHOspeMGUs7{v;$&&u{#| zcoSB*&@5w*U;}^KvIlg7l;%4Tf{iaMfQ^*i#dVJdI<^dhL^)o!OB zqs}K?U`zj2u6gvaUK4QcH4y1OS(h2%RdqMakZeZdMLk5W&W891hH_u-5W$?EOh+ju z3;LUk$;N-O*qdxC?>lOG>bq|6q(n5Br*J-;0d0o_j9LrI6O1FMtjrcjc{>Pmj1kf$ zeeqwQlH=Wph1;6(Pg!o-Pe0@SfY^EiXtF7I(ianojn%@WX6&e4M(8FC|6|fM(~S4E zo)RF2;HqlMS~NTH5shZ$LERv-P5r4Gu3yD>CjdO;kXOHwiExEWDb(d};|C*+kCBh_pBi)^Sq-?Oh zvziWGCTFqJ^F>}nA-arvC(h+lT<$%a(kHA3f1n7~0+NH>vhLbD2>W5974Q|7Ylbjr zGCpn5d|M(7Gz(bGg+s%#t3$axr+6?$_O7lb+dE(!od@Q~Yv7jWdbc1SSyEt@>AN`I zLsJMb!i13zsN_)B@-x1^=4;nr$X@NZ-DJNKEpMoBy+gk~q>F{x(5E5fi#xi6E4uo` z&CycJ_<>EU~@g;R}3MjZLw+NV9`X%dWU*gd}r6lH7Y&5oc2K?aftSsD{ zUka(ql7ldSMq=+-ZdF=7ic=ormJr-h(`T3p0Ws`3-lND*SxDsqbk4xNw^4)%8GA;j zA~H3i3#yf_Tu8IABIKgrA-yB?!%4DmLAAgDfa>|}wu%WL&e&916 zfdzr9S824Uw61_aTtE8$4Soh>E$>yJzN;IK!QPJIh4i}EWLwS-<_uK!$OW-4nbx7H z75Q=m8#bR5;JjO4Axh^ueGUUPlDKJpJilol;?_c=TWqDdXUU;xDijDEB&TTX00w{S z*61P_68e3Qb#0U1FyU>J;W_q{Fm#*`;dvcy@Ti6d8#ZdbCX*+uq}{v=fU@daHP#UY;w| ze|*p@sW~2b;BP=H0Z$LkaH6`H+McxV)2tPI=l_b?an3I56ooc*;$9-TY2z9$ZewVY zN~&p%CIF(E^fDk?#(B_FjcKJwOQG}OtID%CjeWXHLn-%cO z=~syV!y@L=j+Gg(@#oq1(r<PW1w9gU#qr89T@}Wzr!WedU73`aTcPy8_`ep|4iJ%0*KC^? z-cBm@{OFvXrnPX+wB4BD5`E!(tnNyv5OuPCZW21Oeg5`X8RXOQ?1gp0i){R2#Go3! z4&@8tuJur7--lYBiY1f-d<{uL(Di#eb%2(>_XF2J43j#Hri3yIx1zzfrqA=(}~KIzCWkl6Jns zbl{y=+B|_XCAvF15wlyYNCLG!0W7-?dGi@W5%5_Boo^g69vip3Or=6JP0VJPAR{p&bUC6QZ;qXK`(6h} zQ>8|Q?1N{F&}b-ksSn<8=_%+JNAvo)k(@JE$e?ATG8-xPnwAkuHOOG7 z{9g@pezI%BId-?D4gG7UsB(E71p{SP0+GdyN@;ii-V-I5L3UWyM3_xF7^0YHDV*H0ic@;$9@4b11JM&8?wX84PIi$dt`s2f+3BC}_$P9z zj_$X;V{Cg-kQ2=m5`1+77BFqX^_=mr-EoPs zV5k`bTgb%jmjDIM$sickukqROodB!-jD35Uc7XgzBId%`$T$V8 zpG{f4+T;5&p1kJl$F;#f3ZEqI&(Sk`FyY7V`a$h8v84!#U@y1(-JbSX1dvYuQKW;cGvg5PKY% zE?J_YYp)c*fF{ogM>YIX^?ajJuA>#I$*tbRJ1383UIZI}g12aH=lo{* zl2T^I2`n~Wm)A?8W90Xq&6#wUlDyHJrSwasqA@ASIT*9+ZgZc&cCXRltGl}FAP$(@ zT=HvW2F^7~fvQlpv+Q0kEz2=7XTt^|_*URTnrq0lAv(c$EDDZBZ**@)gSN3Of;Vf% z&VsCj)C|;)Dos37@@R@~n#3;BVrk$x0_E*VC^5aIk?`lnU!(Up}n+6SwdiJ=!J-A9pwJ0`g9gW zzxRnCyT>hnQDKl`qPvSH9ZlK)gqdQivv*e<2>k_CbEdaKyXjOY@s6}_Xqb)+?)xu+ zSYf9T#y1J%O0&9D_)P#Wt`Ah>(BklXSFRFB&T7i0<1kQ&j6F(0 z&k>j@e;e|tTjk_bHfVWF=MZZ|DMuJH{h4^N7>Iu8A6o?cQ_6H)HLx zXY|kAUwrG%yy#g71EWAJXQK06*M2}(W24S8f{@Zc^br<#?kO4LBJT zIQ1CNp~8Id%N9n%3sZs)j7V3yW)tq#kebNMSaxNFJQ(2ZcyyO%<|#B6VDH2E<3DFe zero$^L*B%9z3Wrq?`K+kg%7H+2AcJD!|idp`CfWQwI1@u`8q9uRw)EMMx+vs z))XVN%M>e%W@XAz9t#)fMH7^?S8TbYl_qpats49uFY0o}Eb}q7+A<3b9e&_-l-3>% zQ2Xk_%l4Q1M7VDUzoqU{aOg^e@poiads&<)psq!^$q84$>joHB#asxGILL5^ma*#l zM)O`6kkf zXs08JFV8+28XU+FF{~~^wnF>3dH=8~Q?S(bCRW2C>&=RwyA?T!ZbzkaQ(}AzJL-Wk z60YT;&DmcDu%j>HE?q5m@@d!JHdBvW@L$rN*^2wLD0to*S<_`YHHGVvtL(-*OSI(Q zmH17(WB;Rt%LBS1-7e27?wx1;Kvk3V&pAWTIdNmgjVyb>MmuLp7DE=D0oSBcuTnhF z;#!HgPK&pu&$54S7aadz&qTk@xux#^e+G$XNMnBpU&*E;kJVUps{levMW|Oe_Ek=H zdH@Gn9OX#Q*=z@{>uX{vzpr8UeHt0ol{;ey?Ih)0N%1E9X2d7~p&# z79n|Le!D0|q#*q-X zL6RBYMOwhwpPz&uacm+QbAJ(H%gJGUR+4(G#FXCys_?S@`rpiBsVG+pH*nnd31tLm zGGYLqn6Z8@qg{W1u_Txx6eeN5Xwf&ayIyCDDiZUGsw<*`%>(=k3wzk2w7UeGgt9a~ zJn3Y^Z=RuK{`Ocdvwa*^qj8~NZTrtT#-Fm;qXokM;p>*0J*GP>v*5sV>Z=MlBWY=2 zi54~~q@LsP7Hj89@e*&V#O#BkRA=LBAfJE$H8&DbzEV3eJ`^pz9! zm;^AG-f8zhXW)=Yfn|K^=f{UhHJ(+Li>f&pt%3MGim=T&_9NV@7w7{mMe{Mw+BCE; zl(5m$6GGXu^5vM|k6DHyz;PJIs+!LfhQJr~ytojV=s}SSeV1yH;+JphL8A;kEpBtFKfF!19)w45TLt9b;kPsG|TB zhhHHXS(CChLGKu!_zx)$eXEg;j?ek<_^4(j)V6i;!ix_N4>ES4tLy-Q9X3>(L%Y& z#^G-A!LH0`&xS_J)pF`1l+w(-<^C^-&}0`8gk`H!T!}p5^Q`tdJ0}ewfdW;9C`dC( zQ;^P+Uhd>A{lP}2gt}_P_BPBD4;?-#_fB&o2)HX?Cil|WIsZ5cS|JTA*G7Lmm+$)? zffA5~C$yjwl{hA_COEI)U^Z#Q?E37?$k7y#7bK*QT#j-3&M=liLusCCVf8F_dK}?l z=<4TGMX=1*sem;gad({UI~6)E?bi2lAvE*zEW5@2)6g;`4w_a3 zaxhmh6IHWFt9lhcWiy%t;D&ThDzTUFwf=`wVaSa7w7@bC(0uuqZlAHFM{HC2#zmmx zp`93SD@L;W`TFJC|m6auonaQ+$~>% z4;riY7AM2*cTjnH3T%CcR3jUeu-CZbgrqf>66wWpAOjVrLj*xOK$uqGZnYj~uigtQ>uk9_WSjWh?J?Sd_ zcV!rRU!}7*9N#msKb+0F&bd0dX#e()P|k1VdbyXntjb zX35k@yITNA9;P9Xa^`ZSNSL1Fj4jseZA{X7)CL018#%UbF(?$$y=%iHHj56SG5opG zFo6LQ^g{y){TY1h&6Pb+Y|CEz>6AMB&&!-yy)v!Me$s*9yei-Ee$qw(9IF%eDweyO zp_`~?FI|^=s&IbSo6c|+oj7`vamArv(ky+jqp@5`ez`Zjh1oG@K*W}_{0SBy<}(w+ zr(jqp*XYaAleetto`6`)g&Il1-~@vUZwIB8pWJWRJK1)id0I}n0Z&)h|;x1W3ZquGm>3L>bln8+Bf zm`HXkDp+Rf==;pM5b~}ZHO8%WFE6Tb;}BZki>xLj@RFh|*sMWoGHsP8&I*kddJk0l zN56w-TF~Y7NStpg6@E&910iA4#P7I$xEsz|i zIKb{}D!LYVJ8Qi5dPh5L9CHfjrzHg!wjFu(|KElOe&hPLo9(H0+>l~=B|*n4Y$3*f zFXJR~Z1%SV^^3+(pW2asqPGwKiWY36FnRO9#j2 z@Lw5E=XN3xcrRor6f%HEja7UkwObBvrK*`RCSM|djov0%E5E7tQ|7&ytpGc`wm$$f zQ}CCKwu~BcPa=ftOw3eQ&{c*ec~~_BZ*H?4f#kHZBFjPBKSu$XAV+c0{!ClgH*RXz zsYHyNCpleW#)hDmK*SElif~9W?z&fEEmV);V2a^HeE}X#!7O7)_Tz;L884gk4vKPT zFXxWPI&6??9Tw|q7gwv#DzH)KvN7m;PV5{D)D4Hpz{gUQQ9L8#z!5&#suNVFtxE9> zl!^9g+Eh480SIv@zz9zVy^?*8@?Zevn$|v8bh5IsL1{y!=nLee>?ereLhMwhM%S}t zE@LoE=9V`e0v@qP#xs{JIk2Nhi-cR5EZy#PTLdP9Aots+0)NFqk~avMZ8n%`Cn=vv zRgJeTr>yKbM9Lq~F{eb?4T$jBtcf%Kh5PHytJaXg2;(gGp zq-ED4PO594Dz^#aVaC7b!^VB^#^cASmVepL-PJ0&56z-NSc3e{R*eR5g1a<%`;Dr) zCg!DJ2Tl)-?y?G0BYAX`w1#dsAOshiQUAj5poWY~0bCfMafpz5N`PjTX#1lDunm5) zQ`|3Drk^S{9SCb2BTbtwVf1j|#w4dis$@sSAo)a-prnJlrW(Wc;MqC-pUB%%?aA#Y z5K$^F)fwhu4n8%OoXBRsnL0LJaDygQ>ieovaas4~D4qiZFU=!fVq&p9r2m&uVn21^ z`V(2>3>lMrPVr{g7v}$jHlCL@~Ns4fFMS(Tpx`(_^BO-{cw8)_SDR(py#Y1)Si&S$}NU zlss6LqM74X)xU2A4OJ7#YoOxS4-2hldmdCHu$w=S zFehS~*}a85<;g{^)YhdCAt2VQ>=%M9YM!Q77zs>Vk4R2l#x+IcH%jY~I5Kh%QzG8? zP-dB?kuw@E`IQF_rCJ2$&8S_a%#7kO`1W{kyHuITJ63UC^gOVV$hc_ zA3d$O*cpt(y6lf^{&4>WIHGUth4!U?>3su{S5c5BYl`Swh$dLRH^w3SA<^wg5X5GX zF7{N;6^@FcJV^^b4}~wxaM}3JgfBM4Q-xkieP-R?^p(Rm>kpDVr0%|FsJp0O5rn?4ZDWz_IAdwF>JMu>QgOq7gvcQ-#@ zNIx-22^k7k?TdlkaA9~9f(%5$iLMn3)R2Z)o1oqvtoD;Y*3Hsd6$ZgGnKT;$Qcxy& z56S5{j59M3FLR5N8JKPO!+*!o4K4seR?95o6`{@C|Kbsu4)BoJJb#9Aorcl!*HDqZ z;|i{7J6cXK#AqYM+NcbTkRS(8>vZ4E&&f7Tb5g!qhY0@lj3FKBdOT8G+0V=v92NNwS zd;2S*!ZCr~46tPHffs#DP(7!fvbJ-n7(C;ZI1tx%jU_nLI8oZm^x}!L*f}TMJY6ni zTty=jA<1zv#QZ=3c)?V7MT{Q{J#NWHZ@Rbwg9$1fnx7}H5Xn69UCBV167c1tWVc)G zfFLT3rSHdGK)aSI6% z;18y_aw2PuF(L7;K-r}n$TtF^hQ)EB5t4Na3A-^;2+9ytZ!HsS>Yk86zDuR zmtWFh=N~+8@#1i4_teRVZ@UHyD@Ua(2A!b>3Np3cLOvwj7SgXk2DcSfO!wk`9UH`# zV+S&!TLJr5R9mU-#&Vfov=<9~^wh9wjnfa8%K{ZjfYY&D#>w&o#oH~6TTIZpm8tVO z&~!i!nd^7HUJDYLhRV`s1O= z)UjJ#19gs)$e5V}sy_b=^i)cnZCZGtTaXcPpt>uav)L$I*M-)BY7pDJ8dW}$(*GXG zZ+{=p)ox^@!^)b-egNx!ijgZbptvHVlv{@52zZZluTBB_Xc$mEnGna)HBlT;w$$W? zzZJFWsx`LG>jiU+gdC`&mwP86`^dkw1PcnzNqSw&Kv|Bw;L0VCyn%kBDw^q>Bc=2$ zJ2SLhcu|gIVrg%gX}UXwEzZj9_hr8X0c>9YnS@l^2ff`-+O4FFA`WSiR-bgHwitr4 z&$H9`n)6F%-Kho28%cdBKP`0$MBlbZi|fx#r2`iRAauBPC2fCmjW6wcm9KTK4RHN- zl@X8WSoN?qv~lo9R@ytFS6sF_go0=ypbp{2Y`~qh3Qf4cwC9q;uLR{NiR*)Zu?SI> zyRHnvC8FH_lmTGQ8G+nAecKCGkw`p!7_^mS^H3IDJF!2p#Tbpb0+5I-R}o<;4$2Gp zp%42kS=LbY_42deWkBJQUWG!wtMiIC)srPNo_gB|SO#GhKjknE_A-F5*uUX^%$^c4-UhPykqXMSl&660Q-If~DQ{44Qj5%a}5%je;qpd9(Zf!~^N)d36Aue?~C(jwL7dKsq zJA~f3GxwL(B9XQh>n>v5ff+_<8fPEWXCZuqu$r|WEh$*YX0fS@kqL>)u}bHinbZl$ zv*0OX>LJ^)*#e(I#(U?J>4D-mg8#@QQi+${2{7T)m|f#x(HDc-+47!tHSJ za}9PjgFQd9E)IW8HcsF*?;;=(xj%aGl%T)&;vI@<@l7R>iRo}_1*AouOe@zPI9$yw zr53zQ%E_LkAUUXA#LcfL*}ja>=|r|_GN*1L2GPh{NcD5 z8ANep&4nHA`0?^yNS_qqkF@7+UoIB9rHIK|g%MSY!*UY48VFjxv_#*0_VI!*Hdv3c zId(#$OH$Ie7sv6|L~i5~8$efP4qO9Tv$J-#RSfiZ(9vA7sEc3TaypaRogKBDe1x4_ ze?e2Rp1$DV)V%iJRY)lR#mvR-d$v{Py zZx4z4J`?buDXEo?azRa-POr{h!id-dF+e?!)GO~Tm&X+o3(f!pe@ zu)teN{Zq$Y{K7xqyc3Ws_rB08yekunoEV^1vl>xt=I^vmb6^Y&T@zCD4+|H zi;g>&2WGD9=cG}EqcG#yGT>~~w)u7)`)1*>5)<6vN)*PNTM!V~MS)4f1%}xI_<*co z)SzJIr<8u^^(!(RvIazGH{Uw0Dw9>Hgn;keH60&5VO1>Rn*F^)JZNT;C;~a|l5PXs zJP~rS(a^)Hn?liqM~iYuC)q5njHEk=&fqK^YDbQ5%Ikm>4L_#m~6eE}_%Wwu_t z7Tr0EtD|$=3+Pf(zFsd_ko%vN=(7$JW=kbT!%dbE!Hhq%-H9>PDvJ7OlEV8-<6q?~ z;=Mu@-&iq4LSA(_r~_}4{gstn`;lml75&|g#wB|K`l13Whh8^-2CN;6Oo~etjNj+5 zTL1LEXxI1btZ8F3KH+TgkngpF#2f+%J(oc2_(JTt)U=Q>P_ z8ZEgz+fb6;y&i|Cvdt&7Xe&e4;Ov zP0_#Mc*ed1sp;>m^dX^VWt%Pk{qTAvbO+PuM2YBJ{c>RB)h-(fTq*b-xf%^0Rvn}X zjSdZ9Fn0%{N&Ay`Hmn=jAjk08);dL&*B1txFo$JCck73#xdU4h(=UePpB8Sc7L5O! zSN?i3Nv#LnozBEyv+g3Xx_w+U^&D_n!-Q50A%nG!ToV=8Z4wiT#to3Djcv6lmX=`1Ws+K010 zhdkS%ceJPn0Yy1JH5^8K*V;?TuUZOvf9%!-5oo##iECj?T(%xDI#8Mt4KinRb4KUV zNaab+W#gQcg(~J3jc5+AXn9e3yUHUtfhxSkIIYg?>rE-T4S(q3BRWBCT>VK7BUxu3=9HglSmJece z$+8XI3Nvdqqv(NDMDZdwQg4}hs#7+JxGQLX^`D!gB-|73xYeW`tRKu7uS;)Q#CvZNgU-p)Ya_YT7uwG#j`TOh3 z$WjGHPY}hZlgxoEVOV!nubRpu%G@YW1?EzfJ^Zr<&8iUJ4?Z21tpJ0J3LAl#OIr|wDXJ_CfMs0y^12=O?%8U(i|`sP&w->?xTIgYM-NR`1=nMu7f5s zru+KI;AZ}kxPUZXgg2c= z=DxqJUZITJ{gWE`C!^)pEzKoPuM$(pkG*lk$Wn>^gcNy)2+0L%KQ1zZm-_yh@gyzK z4xvZGm+6YUo8rpysbG+!*+&$TnmlDqF>UC}o>C$FwoG5%vldgMn6r9f<5@v8u+~4`2KlY3S5BiVG@}BJ($_$6&ePGtjLivU3ErNYRBj z8Eoq@Z*C1UENZ=hvQC1KjHv61)MaS18IbN346EOMjq9Ee>w}A)fN3+ofoJfe+m=a< zlv*YFog)Ps@D;A#$yaIT+qRyjR| ze542$oodITh}9}9Zory4>3L=l0}3{hCcGa@%1(9*eINW)`+oYMU$)Pa2q9jqZA^y1 z$IpSDs z1M>gNyH-9(VLv^iFi@#S*_ci(tqm?xX_{UIZgDtuKHI(=uBvX{oNvRSs0q6E_9uJ_ z>3vpwt(h=#-iW%wm%JN#ZgHx}L47L;Oh1k~8p?~bnXd)~GwC6AQ>q19x{^8l>AxN_ zRn4{vxF*|q**nlV7zZmkmeXcA6xH`ri87Vm>4obhiXO{=(^X^>bnlEP>+S;kn66ck zgnz_I5YreYka>~h1Z9(cq}uY`d#|E&16rs$?2)@|hxX-V<;()AEvobeEzAvr;osO0 zfJdfhY73xs!KgAV^MZNVCkxa#VYcC90!h_KF+Xpzd=1Fb8E5%lc=at=(T!hMu9VIN zM1`Z++88J4s zySCA0r_m)eeFp|Pukj!;5I`HcASxxA^2@*e0)J$dpx!#*J*lxr;`$&g8Gz@rPt}@I z4~ZEjJfi2i^9MtV7nipHYIF(CDXY4w*jfKr)|M)ilvYZoezVzAxWa8 zSKmS_V9SzjcLd*{lNZC^2i7U+fWHb2G$D3>s3MSgYsElCyIlR4h>+~CbVC<~N4uGt z&A7(WStq;P$(Yx@a0|T|#IbwB2r2zSz0cuRGc}wQcCxMm&Y05^ z)|r5{f~Jih^WALY(}%t1yKcG4jym&px3}K0|K=ovb7@&zrf)60zV7<*q!gA^I&<8% zq_AL;!{qjECO-O76^!!q?;5BFJRA;eUi{orOp||*=E{MIz!?2R(x|^&|30%qA=HO! zcD-f$D2@>MFdPn{2iV1vuF}uoKrV#;JzFG9�!#G@7|#0lNH`-giiQ^@uFXaD-5?H;5?vF z1D%tBjW-X;T{O^D}Z?4)WU@^Y-2xWNgPiEaN7i2iDlkj(Tw!z7<^n)zO zlpOQ>)u}bxls3$W5IPkUT=BqhZ2|66-eUf+e?MhWN?*i({N)c+adNkpJLRAFhG<}5zI;t zneR)Hm1xPplgqsE`)t9!04Q*2qQCb(MwWhtpqvPg94hnmwuq)xFWsYJ&}DJ9MP7kK z9RT@18BYbR&zYsNW#hY!Ld;D(rE=N8z8;pbd$j(y-tZqqGjj1D0aiX?5hdEpZt?Ij zU*QHkev0eDf04_n@h{Dj^4K;rS_DiqA_uLNve0oPSRh=HLKK=#E|zHNwCFlW13AO5 zzJDM*Mvv0~8ejof)3wj22!hkGU>jW%icp$fn^nz4XDzcNKKy|yHF)Qn@njWtnmzLVqe|Pnx`ka|>hxMU1immAu z&RG4`+7@U{3c9{BRV*&{Jg9oh@l~pc4T?cK8*9PMl&cpw2R7p*^ad9BMPztfiE!N( z3s~^Xs)zk#fXmdv=895O11)W=ozd;Ij^|y|mD}Q@Pq4Q5(;Ck4^dfV!F%MmRH$QYgMM_wic)TFrHr2xiJwx;01p{>5k``B+^PvH zk^^BajbL*F;yDHqPtNyl2M*D&9nCNol!~#()LYf8njdj5c(1aVEJd@CU>rc0%c4tI zhFv_gw?hA`8I4DpX`M$GQq4v=u&2SD#cFNGlxDXwg4oVFwENk>XIke_T(t}9;|Hkl8@1AZ!6 zzy#5Vr8GPJj^VBrt{?5Z#*%r8AVP(WU7C$jg;WCJ?6SL{^x+(1@r9s#9tM!T`#yfV zzTWj|&=iv_91n8Va3R!Hw09;fJtfZOdeSZhCi3iJbc(dkTK)e-xN?{DpKL8)R19fq zeZRFA%>~9n+PQZqzl`)LU$zq-2mj>sS3k>4!-fe=uX0o=8a&44ecKb?3%cz|W=;^@ z>86KlVWuGDVUbK!<(P1S=T*!SK-h2yb6R{)bdbm|;h%ha8*nWijt#Mm1=cA4wVqT7 z#GEh1O{EyaFuR$=?>UU7h@LpJ-i4&94-_n=v3iRQNt-iTA1&-6cvdg4xR~=X*WXW9 zB$;ZcL8HjW$9i7kM~Q}NvQ$ZRWf|L z$*AYz=q>%EE$=IF8ADORFx$R|a}dR^Q`LUpC7thyCDvV3hq}!E26a}(_&R;iQ)*p{ z@2^yT6~(|2s^btbWCm*hl)0GBzLJB?*`**#`|E)FV7PHsYKw@GPq9^&0sxoTmhO&o zA2>J6ui=%rTY1|JJT)J~BvM~Ay&& zV%PdXK0J!g$H*|toaTH#3Ij6eWe>zs4rVr;}ibVC}tzw3|1$!4@hv}$TBL?rwS04i;cU+>l zge8B3^4l6^iA7^1&eua9+l%G9a!3=i8(i z)rImOsb3<~NgTp*z8Pu^V3zrRaXk-+od(YC-F&BbeS1%6Hq%k6ZguBKl6SVo3;8S& zuV76{a5R2*YxQZ@!%rERWvlg03rmV|45EHZb^`-V;J|q#@ z6LfJ^!9m+s=tTzlb)8r--YJ1{D2&s?g<^WdBqW-tC^%n^pCkT_}r;H|qi27b`*0r6*LYQ)*M__IsEQBqMe+JG$~%%V(JLM2U0W`mQR zyB-@Anuz8yJ?rm(I6Xqa@}ze~H-n$;F@KXhPFeAVRC?RU>$e#ITdUb$YIn50`OViA z8}lm$rNZyBgVK4D$iJQ1Px@0705wu3dOaA6J)^i4hy7B>-`KYW{#s*Y#HRhY^8&ZW zH&@ivFc;FEmG>{$_J-fKMO$xr3U^Kh7p4hg?EXTkJrGuF(eXTs%e!Kx%l2SbMcevV zbbV{Vh+DUb#>;z_%3oSgXXJ@xw~<*;=(t(o@vH%iNM1u3EK4#QU;$a{wgkKG&6DZZ zRPi^I15W*4C^Z{%8I%sxX8CEG;!#iqx9Je^ z^nQTc{>o+T#imXp%bOpOCcjX|X4mlpd3g0vAf;^ME)uNd;iezD&5#&egvoh=}r2K1H!qA=I4(oIN3ii#JVN*_%sFZA@s!`oVUi9~9H2NM|CA zeTRlSWuN|Tm+s(x_3Oitupt0Rs7#(LZ(Cr`;4}~$X_M7Mgj*Zmq$WcaGz=xXWZgaG zUOnfWcT~4LQ!o+Vf4g2c2In`+Zt0YV#Z0pSd-|mf^BLX9NlsMQ`N74zcHve;-Yk?1 zPRR^v(Jv9Q$M+DlelBfsj6+2&gvdq-*)}6*OvqJy4>wk3h7pTnLokk7er`VR0N|Y4 zf&TP37&E&aA$jWIt2nPq-=5WD;yYWpw?ei0mOX6bZ||C~HejjHafL(&<8XIFhjY-L zq0WsFFmc289tB5k9i1YA1(kEhvGpH^9)s*pEE(d$UgG`q_hcuWJs1{2IR1Yw> z7G4G5a_-Ky<<}?C!=gv{oc^S$Lwc3r5n5VWmb!2D8U^#_=RUH?g;E0Lb12ntT(O;st#w2qV@~0q8CjeF)i*TOfBC2H=Kqg zCk+i#o`vegGbaW|!X=Q9f1W?MkDS^f3}g_(HpjUdj@jO^{R7pJP>5hx>3)K(7)iv4 z%63H*&YyWD_n{Mo*?>1gv;{QB3zHuv1dzMAs$mS%j;7|!;tc`8PDUXN@*445nd@56 z_)TJ&XxKE5-pbobfye_T8dNz(Fep&alt0?~Ec1OhA~|_Zu{*Q-oPRD&S2C77P_Q{H zZ!k_;I*Y`mnWl42;|$Sn^jxsCNaMWULCqv(u^aX?U}`5g;cdRwZG5^ONhac}K{l|~yKMom`cTw{*1A^&Jl@6C{K{-U1_U`1e)>P z^~Nru%1?oN^qmZ7t`ZkyJg^)?)JvGjc6javHWM&&X^}9tZhv*v#@;>KS;?#Lrxc9! zBExn*+A8aiViu(?0H|EMeX4pKhKpig?aTAXX(kBtJhs8xt@@lE1;Mi@FZb$}T1YPw zWl5~A7*6|wcFE_EhV8F6HDeCn_g}zVj->h~xaj5?0}>%w%XLfKCNvh!1RJ8gxr}WC zbs5u8=diMxTF%d)R5<4ZZ3ln#}5Z$zA*bPE< z^R|Q-nS|Nche27RL8lZTX@4Q-kX!GD*jt|a zE)aI!gcCW`usAJMZ=`kZPW5gLfz{Tx9%Y1RO?`Q1Rtigy4F`s^s#ae=GkASgZoq{Q}vU9tcg|{Gd(mXWm3lzIi!79LV1Bak~z6wV*2@ ziUM;1lt+%O%ToXZ9QTDSKUKz?m9jB8)u{`pDzGYXz$j&0UEsP|~ z25OQqMk*VuCliuN`@nM{u#>|WhNsg(qLyA`m-z!0k`Mpt@rQL%J73R@v(x>+psuI7 z$)Vj*-n@Dr{w3kbLBnz=3Hmjde}bzF^0x)?qn%;6Xp@s>kLnWn{kN* zIM+G|#nBIO*0*drCAz`f}QjZzQh4ZUj_&E;sH%J-mp zY*j_@92df7Y6qaC@W23;@InlLlx=n6O}+E8OvjJ2*0V#$1g@~qzHt_9;c+5QZz(FF zq5Ebw<&%UzuJj`x(TEqZa5Xs+sETT{{NnwkX?&j8U_bg?o&|X>aZb}pH4UgFs!Yn> zhg(nzvv2?n3l1f9#ZnL;x7Bdi6Xfxx18)t7<1%!dDpa(J z8QaUjs}LVEfH{;#c)(UkP~GvakPqiks5N7T^?JV^Du=H6C+?wopL;?h7N8RNzX+AE zKj`jI(1D4BE&@D7QkBzs+9mkUa_JjwY}pWoNg=Z8458iI1sgx>-b*IaEEN5PwMe?A zw;{!F14Wnx%*|&dUNKwo_N9EnO3c*Uvj~_$XjNc$LC@8=52tGGWPi z=b*SKN;-BKAPW(REVr(s7<6jXt%(Ug?pymT6kp8wENcZ69afl^uVM04cjF`*4-L4% zZ%}{C;h^fpkB@gyk!B%*e32I~loYM3vsF?lXRUZyjh^noeDQM+V_W3)62+(o{#TB$ z=Xis}3HQORikVT|DRuAF9vuAEo~=0F#xL^heW_FA$+%kI)*fFSxeFgaZ{;53Has); zQ${Lh@vn(wEBW@x2HTm5A+Jq46SIla)|DExehz2Ul zmG$2~Ps;ZjK)(RymP)jwXN4@U{)mZ=6&BtqeSk|~$f&=b^B4M+9CF5cs45$FpQcVdUWf^)_s*ALBitBJzFx}Uv@JLA{0f% zQCjj2^S<-L#e!>xWZKF(tA%mK&9yB#gINzQWENdehrmT+{e3axupEJPK?i@pK-6U5 zEQhTKzs7uZ+hLbYQUSVoT-mX)*M`VTKqp!V1_U#v?JK1HUIgk8awvc#l7BC{JczWL zD?-$4J~c?`h@ARDQInFa7=f8J)e5w>x#fhaTC!=p!Wkw2=Bxe>;y&y(xr2%0Me$2tR+6MFBN)b?l?x&~Kb6s>BHdq?_e zHPj%NyMPW`DPmTFOs4kRi_yjE$Oq*RzCD#6_(IUhn8C8o+yW>q!uvrufy&cwR1H@p zJkhd^#@d}jD=mQx{&w-tgyJF=ZA#Vz4HX{`wT0PiD}VW_zIiJl%dg^(4HIUJzG6+q zphzoVfPhx+2nc(MD4V@WL^Rd6-rA5*uPN|jVS8{wIEuxN%|$h-=vN)rRa@zc9-M zCKUum1&cf@jf*n0G$YSR7q*c&H9&2Elg$=qEFdFK1?dl0_eccIQ%rzJf8W4Yeod)= zD~*b?chQGe12&+oO_(jibpC2bhhT30Pi$X`vg4vV$;%;dc3Y?MhEXKX7#sPUbhtbH zUqaW`x))=lf#`zAyH=@isStmG^~wJJk=1}_xO>h*VZ4MVoJ24ii--G}IEYw^W~sZk z)~xg1rD(H;{V84ztwHrg9K7Z~$r-BuLcLk%=EWh$`Tf+MDW7jf;txgNa`@!uKpBq} z7%6XXCpbesyLw?q?r5zJOfV7{W$EGLUficlSMI!rS_F5(g&G4~(>v3Ym)F=}!QXz? z*>xpzPiZdt3TJo?Sb6xYyUl~d-#`I`We>vK4N(d%B<;;SI*3#Jj{v4Rt=H6)O zAC@ascG?vZkm6(A@|*@VYwWlDS(2xcY3Hkf5j8NBow{JbBMdeg%$JCB6==3)J-&{; zQSU2J<&uAN5mGAOXq6OBR99m&N%ZS#K}sQPS$SH)AZ-FyZ_HNDGU}oKs@W_gwmtnF zJIx%vY%KfqwmV`sOul*h><0&zLcBHwL>;RT;)P@kkQ&tgA?-VXXe|c9tXq4rl|0bN zljua^A;2DlBT0RfF-BE7CRXF3ZTd@rWagUoP5}PVUt-OA@FI;;oDM){*M$8R^Q9<6 zYQlJPqP08pPhld{)Jr*UG>H+12ugLIgs|_x;&zZV~ii69JK|c6V=MjLS-w<3O9#s0G&qJYP6zlP7#BwEzm7}l=Gf-F@XP}7` z8Q5m--zy}@RQb#DjS8>WFU%!@M(hAOEQWC;Ysjl)GeGl{qwv`OV((((V@TfIF-_Yg z6q#*0BRw(5U2C6H%Sx$CPVa+J`X;4v7HF=Ex@P#`Fe0+S)`KBvCi(zrGnYQtCPKpO zi22YKQ;Y4gim45))6??@PSpaAJkV3YRX1|}>4yU@cRjyvl-j|FW;8l|;aed01qARr zs)-oy?C_MpODCi>vmrxlM| z_9Vi;z3y6F07*RHTB{7gjl=%tw`0DWCaLm2F0&8 zh}{y=Z64sNBI=7$_4{<$2W=zfIAQsHZgxjI)DfdbvYF~WuN@hWho@8^3iR<;&49Y# zB@5;S>*pYqaoefsFQ4~Y+Yk!uwvB|-w)$V_@=f5FXTbj1y|RalA7=$`ynoSEEE(Dr zDXs$W39u;5fEx-vxc0mi#u{1uD!nQK9%E(kZ~?X3P~6Yr#9-Iq=R@G6(t>s4X8N2J zh=~ct{h91<>zQwKUhSXWQGyN^wU{Mg;l9N|WzS!!=mPQPYaiZA1bV* zBIWE~7gdB`z^VrjDqrb5c-kSbE^3Ya_Ug1l5>~pHBoP_PQ3@m5rRbC1TKbVXj-);w z@hH%V=?|Yzn^EULQirz*ukPb+&Hb3dalRqr$T5HA-e?ri8F#xC+2hgf*xk z8h5j~f8I<{Lq9T-T}$r@p_k*fJ5K1N(I+^novW{XO~fW2$}Vx6Dv;;a0Id1A6Afc{_Blmv@d^mjpL;_xnbRe)bZ2`yB!`l}vpPKW5{AR_r%yLT8YT8Jb> zgMbh6EdtC4y`$a~3>Y24vG#VS+!D(+Z+Q@Niw%=(SV@jd!`aGW7n`M z80i{Xvq3cA*>*fDA}xiJjeu?_)zJ~nXlUZ`z5{E?$N@|t8X5wi6Gq_y0b*+j<#?qWqxIBPbf7(RJQ)BD|e3OHnXQ>_jC?2a1MC>|cTe(x-gaMNl@Elsc`!fJ*O_2=@;$=c*880|zSs|o*21CaBS%Ol^eR}! zNnFp)KbU{%PjL^C`$(5{sk;sm*zle7-GX41s94~ny*`UQ0uq0h3Ni|Nd=t3hydHrk zQa;%8CA8JpdXgefQuiay^IFIB)A`k#RT|{c8?5qAa6%f$T zvW>=t4OqD3&PFv21V%IQ?42P$U^DUsx9`=b+bQhvSUQ1 z`^)X4&EYnDWlb(hX{_^qr|x)(iX32mCJ5@Ci#gSGLcrV`<;Mp*f9zbh zbNM5E?axiC{nhk2g+XHe#Ss0?MGv-V$0JPJb2A-*)`*C_j&hujWh7MH*iEJwnLw|{ zVXk5f@XjE7WECE+#uK!?@A15dAv$VE2MMp&7dmlE${#<@yXYzu@nrHy1CnNOZDe%G zpek1F;q|xrFLNkCk`2CeCE(dwcF*UFAV}?po642 zmXaBBbQWI5)sc@7N{fH>)HUOJ%bs+-`+4Bd=m*P6?hJ2F))+#Kd<_Q|n)%pT3W zhh|@#ti_k%uVG`t-nc&zKZ+8cb^5{5&>s%tBXFNcxIU>yBQ?if9VfwW z_YrF#9aU-Q@?}fLLC{8S*RZVJFReeCkLSI~X zY_Fgx&}!_1pj_u;mjoN0Q+mth6^X@n+SNwP}k zF6|02a>HIvWZ)Q561_Up9Ict%CXipD?rE2EK|G3IGaOm0@v|uER2hd?-vYkCB3sNz zF?Vxg;Fs(ZbG!`2Lc9>ck@2jWIP5-lV%9p&=q;4u{c45pi3U6;TE%ID91b=%{NGpt z6N$pcdTi?(EeBuVLCQitpcf-uhU5pU%Kb%}1E`WRIJ00Md}c3)(;z64B2}O#O}7Yz z4)Tn}{H#t9PGNLIdoWJeytG+hExf^YB_b17n*9q#Fy~mxn@@yu=k^xB>HC7?g!*5m zB#xsftLpPwHCm_{Zi6m+5E>={*xKSE8NhQ1pGVHg=#i;3LB}DA1@;|xUAVme*mg=H zNgrdoXtRQ3k6{Bo{#Lx! zbU&aPXtBND&=Q1-*ulNjrq?H9!RTmoFHNCIbe<+dOBWUOfbFCz-?|lk|E#TMq;YYaWlZAy0G9wqX*SaT%D!57en$EBrm1A|qyP~blBy{*O-cvyO{$XnHD;ZR`vc5Gjk(lUcx*z`u zO#_Sumj)~$EI-SBtxz=!a)!EC`U4#3AQh7)aM{{szoSAUr@Q)fcM#{)aN2~XkAKsk zja;of(1Q8&U+dXW1%zifyl$>}uxp6CEyMN6U@h^307J%v5qOs1XUv0a#DgF*-#Hjcagmt;PRy$9m&)(IKtoNfLiRd4`RKxlQeO8fmR{niPFBYk;toZ{k3%&`{8twt+Gxp|Xu`y;Vkc%+-K9#meJ;YmD zQ@4c%OIP>m1p`BITN}7HtwsZfn(#biJ5CvP?n7(T8`+mWT=JWfd8e|T(dzH=<83^n zXU^ukfQPD^SVY(OA&_V`ms6Ia_2th=xqyft%SxoFZCATKZ6nW1j;H;PCl!%%XPc zv#lb61jEx4$v#)Re{s*l#`1_H7@@<8Fc`<+0tE)s0;r>Ht$SQe&&MIT zNs$NAjq|c6b5q0pAMt5L{UlZalw&2Z7A=14Heei) zXBPY)%+uN}T{3{yx``nH#QmIyn_}$~_oJJ=9DDAP`bevN0f4$Un;|E7e4*1R)C=Ft(p`JB{Uhj;ke8wcRFH_f|tUz4$h<&81^Ompv! zxZSJkN%@8~i4JCB(i(2RsAf4apAY^Jw~Nh)=4`(~i4_5m@)VW4^Lw7sokNHMcNssu z*qd~dP8dqHP2gWat(UqSIF)Pezh_7lbQ}VVmBCkcf;TqATU7PY(HkljO0tX%(c?2# z=40bm<8Yp^zI`r(IAE%2lcnC)-P7lLpNIXth^+sz@Bbf+zT*W!yU3MVIJyi`2rKPt za%JeBgkdjFZ(1R1i3Pg48$Zfl8E9sxCTlGzuC~?BA!L`;C}KN<45YFX<0Tj`nf~W~ zOZHoWKDZiuvt+t|aH)qcfxvKa4W1gv4KxwMh6qgVx{u0i8)f7!fPTyqY8cQ#8tCC2 z?vmr}U-`xBbZ|>EBB^j06|j5nf0myzT0}AE!zq(&YamTS zzlIV&+;zeLuCd2t5%8Z|FO=gE@!llHOr_U-jeHI_@5L{w{Tr#CQl~$V@ZdOT3uPtw zs*Tq)nm|7Q(->J5{AXfYrN>|eTtOAZCCyMp_sBw`0j zOtlC4JnMLug&({l#Yd@*q@VS^qhKdt6k(>R)#~5**?;AMp^``dg~af!dE2DZV(G$V z?E{a^9n*$Q=MQn7*4KUHizl@9{Pf<^51U{pS3!i~+#D#lPEaF1;DpvrI=g>1!zqn- zn)~JIl%07h>vlJM>108z%2rz1zF&bfCPLMm`LQ|`EWWqwQM0Z)FQ9Su0B?69}M zk)uq{?>pjLg?pT$wrf>gy2*kO7{dr`&m$S6^{$FfQ@mzZd#rAV@?6lO0ztQ*6Nk;(DhrjD6OS85W4v**zUQ``PM zG@tMuTuR!aa62d4GUKMqQ>3nk*yZrA)?oHF^jRog3eCY{9jJLj8`y zg3TIfI)XuS`lyb~2rE?$INLGOw<%+Hhm`NW~2I zQ5Z_)>9_oaoFgN{L*s*luV{`)#$0cR;- zC%{8p)^avBTJQSXpuxH(hEgMbIWI`3;mI@e^h|V#>`Ru$1;w*!s`qVUGFtb>t-$-VD^ql%xyw~cu?eh?YeK7t7DZMEJPYDu-Om! zqHFg>ruan1y9#3NyrvLgK9ugZII=Q!Un9v}H^bYCyg~@%3upavBWB{#0P6@nG>bxh zCGVZRt^;Hi+%iG~sP3M~)oFTS0bP)6voNm!DKEc9J>awIJ^KAD;#&!e?%A|4S-sOpgi9tyw*T`LiW zB9wPF8sFGqh^KB4El=u}Ky~uoKMd zY0oFEZ`+uFHCpOL4luH&$!1=s>po28K>~n6f+45aD(QQ&OKvlYmGbRXOW;GT+)QW) zONzxvd~Yu7%)--Q_ho)xv|nlN$$-H%069R$zc>Z5p4@*ts48C6q;n^XP+*kAdrlx3-7(;gGb`mm+FpJbWLI!dnB|^5Yc!2@4;4 z*%*cid==v;s{nF*IuOn@br_ivFfeF|H>EBW-jhMAifyo3TUuT8=7BYsovg%P=Ypw8 zbxQhU)DmgtWuwSv)7GGsaNQ@xSiV?9;Vxo>L^SF(ks4=bSBJn&{UDLg>dd^*=%}Ah z+2pJES<3}sd4`6#Q6jcPWVo8TEr6@3PWN}K8M-+;UE_G}BYff}cRfQNCy%F&gBVtU zNbs0OnC^~bT#~)&MSeakyK$u1sniF{u_m9#BFziVZU5a!I>|o@RX)V#ux2ZzD4U>?o>qe1^BAzosJ50auOFG<`WmdEzdQiWM2x17tyd&{^ZO9L z3h0N3K16OG@a=po&lGQW1~b5=!Po5mEZi z#z~TTA6v4xZi>}4zl@e3^mYF*tnaVM3;^7gxlPU&xz846r=PdXIT8($V{ z03<+c?!#Iuow(;mo^oFHTdNF;s63@lR@jHucEhuk7FN!!j$awjZs6%O=}nSlgJF$5 zV0O1zZ)hb(a46(68~Hvh_Fa>Q6Ue6NBU1L~44)vC)vA8l?4)u|h4qQ}JVWnZ-j-H3 z16;JB#ir-e4zJmnX~UtApz^**(3fq% zFps1MmU={^$v&Fwh~Eog{qR_{_{tDT=C?k@bi z@FJE3BgN$Js>38nypUPyf2vz%%!^h9Xzz#{&Rxs8-!qZ5ggQ-75+mufH@f=xYWarf#5#_Fz6(&Md#Krq4D0%Q zc%FPWL}&2BFfU2gL9m+)sD%>aQn}W(@Yt5g)gnLDE#|M^OISW|mQ-69`oGqM@wWcX z{U?HD&g?3+DLD)6373Izg$A^h^forNpj~tCuvtgz5X9KohumVYo09=NGb8C*%r_a6M!L4r43Nqt#&04>v(-QS(TGWUixhimSX-h5{Qt_{NO@*Js zqK8kBznSSFf(~m-e{&*bkdLh&?yivT@W|t5f-$0$|&7MYiPU26ZzVa((nTs zTIX5pyP>s_jMKb2;+FgSz*Riicuf-Wt)39shNc6Vc8(ENT)*^>QJnuDQBz&9QjdHH zm2&%E$BmK~NV}+3qfb;5h$=xtcoYmm`&WdKeL=iqY}=X```^*$WVQDvoRiO&&m-6C zf#~s-l}w+P{jpgsYV%FCIfrqgk9b#Xfy$l$Q~5Vl6~IXr;OX%YaO_W$NX9lc#FMYg zpbpS1UW+-&%!3Tx3EKD|JphpS)?{^`3hmupP(k+F2^f!a9TI^YF}remBt$$^AK3@z zd?K@H(S1^);04w6z8^IKtGxyl4dHx5bIFDOKyA-yeu^z-Xq6d#7pA4aNg7uh;#iL& z&!CHVH-!*`j}PucuIl7$f}v3ORUchfwNM$pgXj#|%dxpIMj(V;DTU<)=Vm%z_2TCA zieB*l2)c&#Yo3c&@C$4$Pi0e+o%$j98$Qk*=;1GapK{n0#L?~#8zbDNk>uR@V{?6* zBm>(M(7;$BS}8R&E$df{%Jl}~6G#DALUVJ>;X*3!8Jr#E&q$4;k8pL1RX34noWULf zK3S5X-N$Q#_FfOH4A@*RW4Hs8^3<}t?^}d@G6WctT2teKuo8)_%5S8xekR6{)(Qa6 zF41_#fB=UgNPam%rewWTR~c9YtLeJIAQm>a3X4ARjy@v`#^hyNv9p>~&5?%kg6xz@ z0L~H>4`(`J6a|Oy2=@W-*`G6W_ygyyUH_0u8aH|wZwzJ=E+J_V((Y$td{$PLrFtreL{rjD-HU0|IU3yV9FXz z`!01Y4Y>g4fQ}=7>YD&WyORl47GmDup)~`otbvQ2RYvjQ0d)#ZG?Gv)+7Z~byz(C@n_`V}R#q!(= z&g5)uDlU#?wIQwOyMK2^5&_uP^< z(3n@K_SJMfNr`aB#j5Brw5P8RzMZhuhP=wqBlU$3s zTC-b@zC8wE4301Mt;~L6!zv4sGP2W21ePt|5fIM0&QO|{KbJw|&>tbTueBa!MzQ95 zacG=jLjO*i$C7z6$vvyO8FBTRzU8g>{$s;uAyr)y#{?a)SBp{957eHu(9}JV&8;~$ zogD&jZccR&A(J`3YQI1qY!~={eX8_2v;>Z#GIQgC3Oq0YPhU42k*f9F;fLk-*uiB) zMFSnmF7h0sz~KE9cWjZ}0ZsC6JN5m2?ax&rS;XjTV~`3VBmM{;!U6fYz?ju>R2nK(umIy`8mPJzD^NqEF0O`KYxh zlhTqF7bDLwO20KwsAgO3uiAZR&{-PHGC7>Tv(LChMxa9}UZH}^=`^rBCS#`FI4E4= zyJU%{KPf4AT!9fknewEWvQK7`HmwTkCr|;#GGLM~BkL#D`do>HDY#WlqGF5e$aR8EME2F$ueyv7PKt z-&;%r-%GCuKEZQQw){`U2dxM^H_1`QSI!ToDv#rYgEwAl^Z z2f_}u#UxpisIzb&bJ*cEv6kaePKH5X%&b?jb<*yiJaes6w9}{<*gM=KcYZ&zvJUMi z2VN|%Q7l0RS}o9bsZvF4QV2 z{gOI`L<_+~#f_hC4~pF*&pcYYZr|m>{POG?WD|ox9U4@0CyLJjxS}`IajmH$8!(Zo zI*n$#AZUH5Bm7Zf(ZVi%P4$qwyjQrs!VV-nby88v*dKsq)FUuXf`PO2?3W^vOm>DK zW(qRg>}C!Q{(CvhN<#!|7FCTq6uiG=p-C%S2EJF5kjdjzwu+NMGtR0r8y%)Wr|P~q;tNc<0E@0n)B_k;+_q!V1~LCwAYzX*j@bk)`9DBK&n@+HQ-1gP zVl$uUPN(JtGo-fPB-TuE9;{oQq#BQfT#TDJkH-wD561F-e!M?e)_Bi5xcXzMRCS4m zt7lS9lu&YtRMm}{deGTxBV9=xGi|FMY{a%y5kljYykOM`G3KUnAqUF+Qk3}|xE3?N&h z-lgrEGauwN}DIo2m?`wZt#W+By%lR36t5{;s&I{7P21tU9twJZm z7Y>--=gbI%Ze30?`)R|I0S#BBLJT4F*c(m7lsK|c2i!gk{;KLuBFL`qMof*en{XSoIc1M=N$pct0aS{H1{ARsxl!nWHW zk}J__`&nxFRrQJaeasM_z6Aw(PBC{TI@cQU>F?^mp}-vM{bhS5QF6jDX2gA-0*kb1 zT?!42G=@tz0q2UQKWHt^go|bfwWm@=WIJQ=26)u0<#PJ1#X=_0JWWfMH{6Cz&~ygC zc*wl^TTbo)2a`J@olPB5RR&h-sj3M8;#ol7BM(7tUm_sUvpUzDo>yZ6_+sj$l|nLZ zi17DZ@sywO{oZ!bGIbQbUoZpYJ~XU2)6#|#T<^z?_j>IR55g_*RD6%mz$yL)e&@Y_ z_Zss%3m4R>Z!6vOcfY^Ua)~5Wx;S^Qw?}NVvQ6c>8SRlg!3I zLF7)!A^Be z<4mY@oZ1^At;*sj=?cQ(~lLY z6XZVUgAyeR1yH5sNiBh>G&$KGM&3kb@YAA2h%Mwvml}L$)CJM*A0@|k*h(RD0sL!R z+wp(o?yktwfgWbbnC|{uP3I1ti`u>P%9FP`bd@v~)~UTa{&T!71GufU`lZ&f?K^sI z{90ox#4%?MczODWcp3K55tXaRTyZotx-1%4pJEwKmOTQxAdU2%xIMSF*dI|aZui$G z(BZjX_q;&>CSZUr4IC<$!zn5J2N2XX zyfZ){VYkIlN_ZNYwmoa0f^u&O9sS;sB)ve$ZrmWQUIJ}eY5NCebXd?#$Y>uYr7UL& zeSCEl%FaG4Mw4?AQmYy}2*G%JzokQ+v~sg_A~^&a*zMMOKPo&N7>A>+C5@rC6KiEg z1E|&|zOf+Qltz%q=xk#L9D{0W+LcBa2bmp>bRsR8D8C_%hcs5T?!8*dkquFxRk%YS z)?0lwB82_JTl1X=hJD@^#1Fj=d3;M`*4z?e?ttn#A&0rpM(ALForj_M+;}#2dpi}a zkf(v`hOLqi${@G}>Esvh3qP4rVB#A{VYg@{;BtQ%M;q~W@|YBJ!CnWHWu@*Ay|GXG zX)mUW`i!kAbHHXBIl|W%w=fw`Li$zMYz135z>||vPQ#-p81BPH^;PVA^(G`IvAeq= zuGu>6hl<&JRM!Y`=cH1q?bwU4uu?q7e4Gf3QSg^Ruk5*|5&Jc7EV-x%%d12WQ$qJQ0874bfiAc+UG1%WGU7v4ezGdFd@ne7I|H;}8r7 zzBC22WKcqW%b!d+XAiu}m*BflyC0)g(AlG75`4Y_1zv&ouxPckgRHBOkek21kf3KrZ!NJFiF`pzR?DRk$S{3Rzms} zXbt~y25(rgh84L9)X-vEWIlkop7<`c?Ry#0YEOC{(19(H1EFQq%$jQq_ ze8r)x9JRu`aiwRcLUYb+yQzXSE&0nF*1&{bvblM5^5nCzt_vk@!+K+o5HOa%4al#G z6+aVd@RRgy**LxsuIKLX2vrd@EtMgJ!Qn+o+4TCakSUUS-^6`uaR4YO^d#EK5a6+%5(Hn4>2eyPOM z$J_LzHsh}6hFJ{d+nfL%8E&a^#5Y&(JqF0S=Su?%U@5oQgteu;_%ZZ{V&Geq9B9FX zsI{Y@Bl~oUI3ynm$MGYDz7CmD@yPwVp#6$xa#&i*wu9p#=6S~GN);bY83M0q%4>V3 zUA9TbV~rsKx%Vwa#ndHygbl#fgUihm>vIfHoG*r)05j#TSiXsF)!^ZX-e72O}plciWind-4H zy^`Kq>#Cz*MpmPR;?Dertid3uI-*n;|{ro3={7@BRdwYTYmMKeCO)o<#$*(-e=w{TAgykq5q1o{S)2QPNsjkdXkQTgKA00q0B zGZ_sVb&vIrPe zv2F8&w12Z+K1?@1ZR~0wPX3Zswh#oUQ9>vTwa)`Olmv;}rslas$%nYee19e8bP2htR?&IO3V!xFX3##-jiuU>CF$YMa#ylC5b( z9(PcU>EFqru579IjeBxK^45aRNlJ{0SUM7O{^`>1%*`nNgSC6v8vd2~8fB0FgQey3 zBbZzW$^+`Ei)gf11oSR~Ha<#($UEeDnSqzBdv-t+StZA^>2LvOT;AVqlV6#%UmVCX z>rDveT8C))B!BQ3)#`V9&R1&LoByJ|vqFNcLMkxCb2*9P+3i<{^Cp}ujT8)y()6^- z17Og)QEANrlAUoj#U`*lln}@;46`(T+H#2TEFk_ud{)Owk`Y01rrRC95XdK1vw#FK zpqOm};3=ZMPd)urAw>ff4zGS^%VRvxMz*Jlksfg~7^8r^gG0#RmQo<60?aP5a6LK3 zaGy+{RHd{pzb<8#&t+10?|T$WB#37+Ol$_~IWd=!)NpM7rJoykS{K~C@#KW(GC z=A1aO|2bozuNHAkwG6C+&b0;l3DL~{m`@NrJITcucLq($E_sO@`tihOgw-}KCTpmo z^uDQ9L1H+X>OJD^-oE9O*fS_#@c`4{txIsG7Xa6@)rav{FdL%uk)+{v_=peoWcF2n z4jdMKhOH1q+BOt@{aHr6E3cufA3Lw9)8uphvLXE6Y4pKAaSFI>45TW-i#&*TWV6Zv zkV#$7XQmg@S7$&aCEt9Q=>&fP-!zU8e5L2Dj)%9y(IelDmL6+&w40o;WnZno?V_;_ z&iRElh=Nh~1Q%$o?6WXi?ZG-j+QhjnRYsSBaM4lC9bxRqXuAe@oa|FK;M+mA3&J*L zfFgg&SdLAqNbX>(6Fybsy2O-(Qeq_;`wDFqYWWrX1QP*At5TLg0AC|T_KTpXT|)o? z&Jd;f|7D`%&-yQ`WlV(9Ih=??_6BFX<98_Xu3_xf`;w`{@hO)8+&YaY)@1=~!l&UuzvLg+;mm7b&L&CT64(c7 z1E8pFty>eWXA?aB{>f3#R0A?p9^B|BRzw{0Aw$N7t`Od24`qqjTx=}DFiZdrMOQb& z?kSCLsnVZ;koM>eCA--GQ08D#oE;-O>dQ21(Gmvrn!O}uOSL#nH@rH)Wr1yw*%#YD z4E;akart7Re*7Gsi?}_@9w|8F&AlpGvryF0QiVco@u@v-&Ky1)#JLE9(M9g`Q|gTg z<3m0YRIi53jxh0e8I7a%uf}_XA8OM?5$v!v(KmZQC*TLf6hv+ZVi>+pj)NGIym8PQ zQT8h1gtkprOq1c59~-HCNXU1O=*{cFIAHl%qwU+vDn}!8bY`vbwrZ)ptSK!YJP3`^ ziAPNf@myor=mig$%Z+|r1IGJhKXG<#w-*w1N6qY>yXm7$G=~0 zj@Yc&!1EQ$+R~ABg_4uRdHaoWZ6GDV8$$ELri(zOXfsQ^ZVd`KBk82#XZm5jAX!(G zpp{%Jj|KIGJ;OIt=_E!u!FYO6XPXHa2uh#k*l8T4vec`r^U{R?3QC8Sd4 zB9t;Tk@3Io@^FlSR`gLm2+R~<4Nftf#U&rfb(S8y`QmLjq^K~#Xz~L;l)%Po{j>s$ z>HUNku=k3WA{-%Tb;xowff>K!RhNfRfTn4STG*o5WQX@U11`HBI&xzU~zD^f&< zyglWcN%melx2Jjx0nxWFa2?+GgfnO%yX}?DU&ZgZLeloJieU`cCjF4kr3I;UGNwws zZftOsO}@Hh*)c-B(lO5k^QZxPLiBDMGlMuu+KYzoifPnKYQ@EA*27nnu#+YF2iCy? z2z)H&0!J8pqwfw4do6ojVFWwczUD7w$ll*qO$E}LoItTfSi&~VP{Ovc4d3d6mH|)! zBoB81k$JOlpGWPc>-X5989NF>&5F2 zlszD_l%m`c4+7l`GyptQMDFV}BZN~SX80T2lml_=g4QCGY2VylC0ladVOqHiB+FSZ z&K0w&grK~R2v3&O%k>r2m6xkTkEt#tIgQA^jQi-s1D~s#rFXV}x2^4QAj`AOjkqeu z#r%Vy2CcQJI5sDt3QUI}cBD28kVy;GMBQns8GE8mH-s_7S9cEGR13@MMCgO(s?n7O zQ;oL4$KJHd+|oWCg1aSf4=nv%5?4Epd2J1_qZ_Z|U(>Uw^fiq*V_*M8{v}PZmC5^I z>G&0OJ(H1=qpwZk{o60I42bjjrT~@Lc~Q)ToMuWFO$D6p1Ac!xI(X zWKV`9J5~m~DgycYh!0TpGAxi{Ds60%?1~ey=Jit zx0!1~?4tnW`kxJ#0HC{N5DVCvScS0nR+z^UrcFkzj1=_6y9V;Z~=ay$pH9e53(K4H?4g8B=Ge-I5XvhPLjnL(Q0 zgFRI~^p%Yh_=C~v#sy8~kj=)kpvv*J0BZ0T+W1D^pkcL$RF3JKT>|QRt)PVK-$6Pk zxS1$%cfxxueBo05c$u-VkN1anh;d%H0V!Q*7H3%lEX>9=>qOY zG;+!LHnf25ec5`V*OUH5|`*CE0qWKm0r`psE4btBhv#Y-zV9euo!46;7+33fVzanmBVi zCSASo0O&a7)(VJGS0~^li&3NM=d~#7usGU%9T?l!-7J7z`=9PB){Fd8t z=BktOFh8oG_sGxuLEmQwwWC`YCf8yrW0IGYIWM zQTC9o)X0ESeH*WwP^1a5-qrPq%~!(APn{j1X8yu&4Ve zD$AC|G#ivY1i(|=8#4e{KfB$EeL3-G_|HnPQ0 zA6e!7xQ?uP9(9M@Y!$pSV%^b($dP}e*TFq6`x|M3R7bI#TQRJzBP<@cI3?=i+^Z?eoW)qMkpDefDjm$kTGS0u|E8t{ zuo^EbFo(FIfNqQG@)>zfzOXmQ#GqTZYcHxk+bKbxVF zA7KLuBDEwmKC<8k*{O&ZF>nQXI9|YvyI|m(QuM%~N!uV8U+~ZZUoZjm>>j|qUWPJ) zu*=a2%~Fc}M77{B@{oZ{C|^<9Xd9!)q2%vbhzKWogy{t^1N-IzuQ~u(&iu*lLVlaV zB?g;{+Vi*XE3L;Ym6Bzt30XShP^Ix^zVOX4fw8|QT&_kKy%G-TEJXrfSHemI-BrT^ z@O=)i%i&Q|e(e$=!l%Mjxu$ynCn_i(AxVc*hR*Axyhmi7g5SaE zj{jRY6g#-In68VE_9BI8ELIT$Kx7(#<_mZv7i099rp{g`h9<#45gsIix1ctF z!0uhLwKN6RbJV%HICZSHgYvV31C1FygD%Htw6h2YmQ@xP6w+e#n)#nj1-XX zL>UP_r*R1#D))9MQgGW)*79C=Mev{jbF#S{#qZVjk3GrEsrr{yf(pJ)&H>Z__YK~4 zq6CAb^r;KV;<0i;R{}E2M+*pJXxW^GEl|K;SAG1LY73uU{V@ zsc8o63gnLHbi}4)BAsna$`HuSV5Ftc=VHlq#)bmqpfch#-B5EosZaNH=U|3HlY zrIYC(ch_Yk$Ze3Zw%aNMiw;(Fu1lYS?W)0$f?MyEx2_^o+rQFzxNlOpy!r#TI$x5R zNqmvvA1*k0yWq4D%*@lnWhl$J?-$KQEBVso7;*=D8UG8FOrzu~3ho+CL7PDuxpZHS z8CV?_(V4!-53h~y>uZ_sXi+FZ3+%;7E%BbK;2(^R3es19zwO!isnjeX9O-9+Ws&k@ z3WCM^T@xRMZgaQ6)LkJopV--Oo&}(R$RzI!>?QA})@GH+y>c&4NqrQ-<5w(RH^TGY zx#Zg^>{gNT6Mmfp{?dle#nG&f68axIF7sqQ4+UiOFjOjV3~qEbTPBfP5!W zo<$ld5ufj~g|5msxNc?yCf|3(bE==)kIB5OYx=@If8qxFA5?7ILoi81oMt$scRvdp zU(ji33`&!?U8IyM!L8;|gh24Z!9%C%s_fD9=t@CZiMS@PQN%Ng-q`R~(FQwFrnONL zAGMI&SI0MOkbcc2ywk_*xE~55+zwbvo5{0<9BUJ%d3*RdsSQ_Y@tRoX0jpL)9fMuP z&QJu`-AuJ$dc0Hss3o4!>TiLzSqn@VBN2XDSH3J~X;)G+iDCW5tE)R?ALA>G@SGBS zK+h{In-0qO0-BG(r{D6OkQEn|WTUnJ#4ua{T!zKTRYd(9P%pLEJ+}Ph=ZpS{u3aL=UtqMz=ev*dbCxe&UD$1QHuOF7i0oTG`CIzVYM!NRUE z6!_y@OTk!+J^ij-1^P{K4eL=Gvv!_QwNqXV;5FI=(N1viBZ}NXy6`>kea(Pb8Q-1% z?^9^Eyr;8{)8!g~I6zC-oHt3Cz6~;!U%=SJIz;E|w5as{J1b67y$V$`h}h=MllPF* zFva5Jp@+6_bI2EuMjbcBDXXeq5Qj|l2s~%csI*Z@J-cLVSh+%KPEgJ^P56V|OG{D~ zTM!xn6x0;G7rIZ5#7JhIbMGxUKB|iZ;o3C0jHJ3dbU!Ral)FYb?DFBRaEl5SLPBKa znDkXgh~T8yV%@NXdS}~FX6rYZ&lTELH#Q~lJ1FjV6X3R&{rZ$CMRq5aIgM0->)#Fe zh4QEN#Vz`MfzqO}R^tOj>gQ*OTzB=lR&7HQs{@9en_#gwZr+Dq4hXJ*;KHZ#b+nnQ+#axDEgwASC^7G z-t2sEORr5Wf!ZxW9~V%hH?)Gw_#m>yV=1XAEUFK5x^yNw_CtDf>Px#(c2qMKfC?qm z2Wh2EzesfoIt9}t`l(idm~rGYJlpsfy?p8Jq7SuM8cI`1+>^5I=7l-gCnQX-2_a8Q z;#?n;tk#GJEVv2nLE75;{{u32^RfRJDwaD)_FdIooPI-(oKytLjy^%~iTI1$TzB?q z!G-n)nw2N zubTGR2C$|O=R4*qRc`%Ax*Sbe4N4+75 zV?b~FQT>~`I>2=UHzkd&p3Rpi*3y{?plEy+3+8R;)nbt4R#LlFAFARQKYKir)K5PZ z*dcc4?Q$O@jrWyCBOSY3ev<|x&PX2V%kTCBAu6r;-lXkcbhZ0C_UaqNuM48Ba3)JueLKWg+kkLU7qAk$4l#e$ALCzJh!vtQ>ctz@CMu*q~iH_HFR zq(JjC{5jE4e7+C0IQtfZg4`Eq8g1528Qv0jVRU8njaRybit>0YSV`+(5G%d0eBTql zhDlG+LKjGkzleLR<#-eGQ%U!|a2D^(T+3)(|2)1v3UL4NPlyuZXIWL5S(0pt^ff7Z zjr^4i^^jSN6^jI($u>s0?!3)2P`g=Y2R8dMvz?Fk$*_311 zPEj3?ObeaAI4a)$*(EZLp8#UDcNjak?``ia6cU87NqeVF^a=OjDsl^Y*U%X@8m7y| znj~8))3)Fd?oMk|X?kQRk@$EW5&Yypr1&n))=TXmJ+zR}-H1)Pydm4xRck%c>U zTs+eBBJ`ofZVBuiTsth0(vdBMEIFu>QgpSYo2m-K1A(6$E*>-jU#_eEo54KnKOv)B zN4IaqF5{Nk{Ngo?N$RTE$0*rwTge7S*l~)v?v77r%>%;ebPZ3$tr~`VTp#USxEi^K zf+;Qncw2d>0?YS$h*QlrR9T+E6T~UgComZvNDyG`y}-dI!v425&hAYgEX3y!;CFfcBdnd$_^S|7E5 z67GHPZ-!HjN<-p{KG}l7M7>-|?sNR7A8f`B_R%P|5sKus#}^I8ZQ;9Z%`5*>jgBAk z>fKymOvuEp!-IwBu+c*JHdj~6p1(y02E@L`g#MWawB1i#ymQ)o~}jE7~nNGCf)vU&@-=O?qoreJ$~# zE0JYyj9<;{>?_)*-Jl;c3gE=gty2FRMLbE7>2NBCcX3v6rO8F2TCT9ZO7gs4Gz;`1 z=z7=w_JsL`TNLi2N%zS~2ANULD+7*XF+Ml5J@wV}1lT*fDPTqV?&8}uaPWs_`C~9h z##U$ul|w*Ud$`?dY8RYPsoYD4PSxp=wj_keP6YwcamnFw4f}EyT<+)P)0B<4aJ~j; z9QKg-i`57_0K~iAlmCk~-5fV4;DG8FITgBktmmA%QqZZ(M+zf^WJX7o!gQTl2^y+mY+EkQg8tY$=qae$M{qz;g9lF*pOB~w;jx$@N^;jpxH!5 zL+8|`3p`(vlU9u-aQR*i!v!=C;$4uJEHP00)m4EW&)1N2#vzDPB)6g3iMK`x&O9HOAWce()(CKnULAz~6cf+T9 z|I~*FK7_LOoa;|3XBG#X?C(0smWG#A&@2@sZ z2aNXS0d42S(f|*{;5b2g$=A@No$bL|GD|4Kv9!vE!7!z|VXeOyr(H-dc`eqV<;XA8 zWlq<;Eo>mh*tN2fD;nl{q-q}gmv3JXu%vuXy{)(bNyZO_1-HeaQrTNB+(S#j*FIcv zw^NqiUIKNTdk|T-0J@R<$hrvvecs;#PqlfpyvR(t;l_3cS*akb*iSKeUx4O|?{ddl z@f}Y1E*m9aNYYxJ?Z;p7yrKmj852St4`#GZ_g-SU_-NFu2Md$dshjSUrLN?Cy*bU) zQR16G5x>a_?esrD_xQ7f+JIjvrmHS4@*PQdRHi`1#Y2FycY+NSv(Wi9eVUqT0Yw|n zhRkV-%!e(^HTaF$tnx^ZBmAAkQjB<+Ps-e?zeS`pq?XTE+~B8Mhf-RHLI;#O*+1{^ z?A}^_M|~{bPd7kJZak&lkD;u@t6;jO{J91x{xptGcgUsfcPa^`!AaP7zGpWYr%OTS z;}LVZa0wS`K{Q9~9!?46%;Y!27xv`UBrhI=lRaH{=$bK1M3neGHgTKCK@I53oXz+b z-=xKya9gRzRMld^F4vvwik5bOk6Elp*B&=Iw|b8)rHk6eLB~nJlZZsD9`V}hUKtK< z)pfNqF5_$;0GQu$0k60JHq7osB_luP1kfEd8^@5VejmBQq*5C&JiQT;>tZye5IX+DMa2z5FdB$&$@_-yE<|Wq50*1gWqx5Gr(;bjs}`YpJ^VG3)(z$b-t zGx zF)-aqAL=I`QS{#Pi~qFo!{5K zMZcLxB}W zT_r}LGVx$Ll8a6Z{N%=D4L`+c0~@WYe_s{T?+i~d~Uvfv4Wh#+f?5?Yd*M!ss0Je;@ZRTDJ07j*%2{#N^^jmxZ z!3=JW;#WGPpXlUqG+wthm&7F&9DtDZMMh@lra#mBx090Nl|B1F#4TG)Lb0?Qbm0=- zvdCfx0$g~1==p3y#Mzs%_^*wb)!r@QA`U&kz^dC!u0*J@lT1pHh1 zWAt(~JgMw7FMJiC3tFOTe&~?&FnO|4GL6wb34Q`nizh78h)?DjHpd-dOsfi>AEyA3 zS{4AnZygNKNmKm~qx%I9%;-RlOI@hFQc*I!*mszujM=nMlwf z#o88K@a+j?QVYfGI!r7g#86O6Dj}RBPrYnRBSGL-@$CD_is$5NNz3jbGH#)SkzqBw z_ffu>0&|#L^MkPXKEO!c+pZGy>p~9k3u&;d{zz_xqHu z!*LUwXPmGEmgZqklU>p7$+Yty?f{(Jf-j5=@As;5aRM12cICCxS5qD(Rke-F*Q6qu z2v{@>yR|ZWlI-4Ko#u1{=XnKN4f|dHC>55<$#1>6G605Eb97CxTZ+i7l8KTygo>N- z35-EhHVZ9u7PT*q%U-$==<~?AwsdGMGVJ&6|U}Sg99N zQQ#V2zRXIa*KS z9OhGnp}=5%AeB4%fPS~P2jFa>@UblSSSP-1i87JMU+|CDFyHX=#L7D{0B$~cUu+3Co zLgNpxD<D{(?kf9JtD`CDt)S)47e8D$wh9cq2=JiWQjhY~ zLC#Fn5<3`Z+M`^iH9Z$P@o>q!nwg&)E%=3KjDW?>Yk7k)sZ$tb@uoem()|fNdB@3s zINJgQw0kjRx>(5O#Zejj4d0o&xP(hO1rN>5HVkq!K69?G4|mqoOB-6c5u$K12hEf*Z6vs@>qEGH1nCe`rl~* z1a^`T@u%w@)tRU4p6k$;`YVR#1#dN`bJX#K_9>5R)Vu?Fyj(i-0z%oj7nzD$9Z5yS zpzUGK+;`~~4%h*#@v>25-~0yR>nveU4A10$N!%w*m?~64YbGQ z)npS*fb<(yJD5YLCnTr@%&}qbb6bpqLH?jdooTMn-4B{ z)qI}Tpo(++V3PD~N&0aAxtW#sIhMz`F>te2k}&a?NlZ=|tv{rW`p<_cLIJ3JbUm!4 zW0?du<(4K=otMFdI^rP#?O{GOy9)O~_NTOd8}7c_D~urey%iMA{i?(HhU{OuugHOA?hR*XsN@fH}mcO46Bt6<`+n%REv?{xaF_uWY%cUb50^y<3h~`Xh|w z%U)_+9Mw&?b$D)|xrwSPWK*s!e*Mdh-%w6gb0W>Xx^m)TnnZa9pV~K{7!i&`5=TSk zhYgB=?Jo%b`W7dB&xdY;GC5nBCg;(p7_&bi&^-1xP69LE$~T;`NSl03Be1-r*D_7bo8LhcCS0`=>1(mjk!~Ait#*M_ys^W!Z{O|?s z=NqeXOWsvh^aTq@;6BVucrH}Sk>z(wG#60T-(WHp6i`iE9+_n7aLBC32ANX(__L?C zeZ~aWzzt~y_I($r%lvn%9#s1R1L_`%H7lg)BD(ASG%dl5l%b&SnoJGU}F#obdpC}q04^@ zaH<({8b0?W1Yj~C?zzw7tGw5|OVjL7ji2;4lI2r@u4DH9ry&A$x)Is7OcJ!Mohy6z zOnTP+-BeI}r4X8+gQBt!q2ivi@n-5V8DQ%@-!VM5AqZH@`T3bmPE{_}DZa@~2-YLB z?>nuCtmD1dE=52TnYO&ug+45$`qZsxHTq2r1x5!(UFJEp7arfIxkMQp!k+v^(@|qe za^S&N6RB&HG4^NvT!3KA<*p~5#J9)i}k0{18U#8qSF z;;`>z5phYHAd4pMB{*@X*UmCp7#LxG9$~TnuLg9^EBUaN{5{2nVIVY3#OkX4aYaNFr#Fw6;{_I6Rps&<{ z<534pWbPEK-JT*BT83)Q_lJ9eUDR+BEfL8k!d>t@Z$1rw>P3c8B?1vNh;b7S|Nl2? zatI}RI5SNnnRfSej3U2B>Sp0ovV5LBNhv|%XkLi43{<}bGbnt5%Yd2zA2#ir1vqPC zM38%Z{tE@ZXg|(;~XTVfxNq5U0(rpM)KZH8khP^{iKV`P-6~Eu=Sl$OlB;j0) z2@e0@E=IUA9=RDC@dJe7(xMnl3KyI^D}0STd^!9`U1%B(J1!F#v)7j^h5>s;o{?8c zhW_@utX;{FjH#_sIG`yq)VDWU#(tE(inn8R46vMk{r5tf>2EtYqR%<|Fjxtk@Oh7D zUUQXS%E~NZzN{a9k;cHj?_Wu~n+VH=tvKV~cqBj4!ihyRN9+^pnn)nvg>2D}nu`(M zDv!+Hx=*-|{dFCO+Oght5vFDKh2z%NY((;r5PZ}3lpMz>t#NMMdfsz9_&pjTyr(k3 zU@8nhb_gmq803guc2N=sIiU<)G5x1G;(jcz*ZkUZ+b-Sd_Ob58wm-E^e>(piEwH$p8p!!Y#P;zhx#P|Xw{kwbSOG`!xwq0hj4dc_ z&fV0g_ns9l8KRhy$LGGXtE*G*H)JSFjievQH&Tqju9enwjIXeUrz4*)0mJAettPp4 zp3F9n0SdBL;U{2vKB`qAb8ib%$XXfJw$i^oFWoh&7*;$7k!2hEs}z_cb_2C;wou;X zlDe!uH`xazJTNSCn)pj@rpn^?i)t5W;qi>{vYXgU zVqQcG8# zpobbTu+|YDed<|9gQY=yynQwbB4R7^>3*k9l5*x26O%w2z%b<7^jarR+;O?8xSOm6 znJsXOPXp;xJ+a5jeLEf+!C;T7k2o?1A%{BAY@v+F(m7~%X*}weYP^ph&@U5 zHXK9>D$7>^han|yuHIdi*uQ}77K-T-f1+?;f4@RR#`o^+tR=vzi&uNhi=AViXpqZj zF3z%FModQ42->2p^cg2iU-+XF-`tBs!G%f-S@tk)A364$d*G?dbD9OD4rXj+PtMX> zzlB<^N8(2K)_ZrJ(j!jDk9yq-+6RbY1bc#~9b7B?>&>5I116nTqqBbHE~x|BBB*x8 zzK&J}wEt06*f)$1I4eL`W6xhuaB&%G=46*!oiCq}s0N{?0YYJHJuqCape(JSq?hvc z3clZZW>FA@*zLTYyz7zu`Y0xWdB9V;usebIVzC^s!zS95$7~9n(TXme2?BuY_sgDr zd2s4~SHy@>j<1Uiz;J#>4ZM=0&<<7Q|FK2%L?Ys--K{VrGz>?| z=ed*^t5O+Cw7QBc{#_36zn761bx|T%4uX2ow3wdcM3uE#9qr5xl6WDRd33atMebJb zj^U&*r_?WB85q>^jzqHV%4=Tpnz_^J!#H8_c?Q}aVze)^#ge9u7x5yYpFJ!|<xZ?(Fa^x!=O>jKG z-Vm_V7NL-lgsRjdG#CF=I{)`8oH-hFqOH%kwl}@8!Z){8Rdl>QRl1iOEBylg#qn*| zWRp8V>sOk?G8j=$&#)i)>FJjL9qiNK`m@lZ?#NckAtvIW%II`hy5MN`pB3-kU zMr~NZG$~}q8x}ey5vehNL^VslT&F0|(}f zy^i_cEO9Ee|4dcQRQvb(DHJ(#g){hr2m-d)Uw{`rc#5Xua!bI@ain2pw@+pIX5d?S z$O-p!eJ1YCAp1?xdihh6(}%J~K{fpKtoWihJ2rfqHtkh(HZER=4gbF|t}^3KuRilY zeXD`x2p<3?mwm;p{B389tkNI8>Z=)d{(~)aKAdf%70w>YlcZDRmGS|uA~i;VbyEzS z$q=^i!Rz`wS2LaHLt5%F7SpN=F4p`IxA4Clwqr?eo_b!&tW4rtJjjoaUsRPE+s>(zbuJr!Y8r?f-Sc z5e4yTG_<4!rul_88vVI4_{W6H{6nuh#(V>Dp1-MVUdPL_!=gl5i@CVMRju^#uLUoY zsg^u4*hi1N_+T68@+A579-TgeX&!*;SRwV`pl=WOU54q*wfNa$)Ri;(xdD=&L|Yu- zu9Xb3p1m3h=upcy=R!Xf1d)l<*XObose}FQk5-C_Z7erUf}zR}{d)Z^WJlBN-^{2R zJB9!h_3*27&xLVpr{*ZD?{e1{n+(J|_9FM%prv%((L(>N+yl{1cNUJmAUct1{{Nt! zFKB5;`Bb2bF=3osU!VW?z#seqGX^&5_% z1-br3Vs4nPs5PhjdZ+@Kv)pb%l1{V%tvHD_HAc^cIl~~kM7KBCj|iqqtsz$r710?N zk!z$?TM`Uv&ZWQq+e<#wo`gUzzPE^zZ z{#UEYH#Hp#LWctw$B9g!5N#IMR?xQ0t=hL;%bXT(sM=1g;ly1I4XShcZWm3ljr50@~YO#gJy7`-Ik9|DRV!ZE$ z*BWVQy`;UrJ`upmc3ZJG@PPRfkUVz66_K4)lwVeDB^Msb#@^OHP>l@*B-m6c$JcMi zJ%ZBRXg!+N`rkX4iQc=c+L9V)UkzogJ_7ih;V#$3Ik-nRiUw3UDpoaqRjlZNFL^Q6 z?N5M+U;8$`%P+JdH^%Fr`TLLHy4z{>YW4^KV!(XUnMrpnf#VZ#R);s4%cDx*PxoW_ zTCF&{gM3OTOZ(Ig-FZZWS&m%nIZ|ARh%CW;(sB{zQf}LVotr3|uTeyY8iBa(wC%px0x$1ZbVH3N9 z3=fjb<+ukKChxCy(ytdPY#El=NH>gq??~-PnR}0=6JPaz(g@}Gw$G~yHAMYIAKs0T zUFk7g2@Fp(6%rXZ*d>_L%{}A;X+nTg_!2efc{))hy-ENP0#T_rdoMvE>gd@xgad+z zrMDEO-tR;1pfx2Rdaw1py4-qB^V~2JTBQYm3enxuz`GhEw+2{X6Rfr=?$kKD@734< z;BeZ?oeqS(hAOm_8%cbebs~+2vEyZ%&#R5Bz|A z_2u3UY(4Y8-qbTS0t0W2^COHZ(V1;BSe~7P2FXIqH4+X*asa{fU14ZHHNL(*>q5Sc zv!K`b-U|GPqp8FYQweQu#LP`N0mb1&LIOy8es1NZEVgABX@3WAWt0IWt@p=H@gr5%GK8(M?2?%@uJ$Jg7=WUya(MdPDNdU?j*`4x~2 zw+R{k(E=*amCd)gVk-Yp4RBgRYz$lY@%-#)Gq0f!6brP`Tnjr`3jb{61p&)irelDp zHUp?RM`2esf50r67A&e3mph0&M&a*Md$tA;d&xhWNO#o83pOGe&YFdlw0)Uj(f5CF z7hoUNHN21|j3U1ydV6=>ddBb5))A;b2_iX3iMt_8W5rztj1elK3I) zI0)ragH|bpveA@sW@~vsA~B@linLP8cH_Df7DTDlsn#>rNOC8yv1%lzh-9P⋙O4IJ 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�hmi5d9>_!yQUPknbKoTaJ^U%Pk3FPT>9g7;3SNj-vLa z&J`xL&cB&$Kj{x}6|c(mddB*EkVV8!T-bN~vTLDH_7Z3`1KY2*nEuX*(wkEdVrrh2 zHq3ie57yS@5W_$$%=86kFwD4pzorrI;1#zcKc*TWvAlHM_|Syf<90e;&Lw(eaLwPX zlv#<3gxLv)Z=lDMj{ExvIGN&_R{!xl_y;s>Y79|a+FE#^nt2*Y9!?zHF&5T{XKHw? zS8y^{QoSb6ths@Y!6-7RV1oHd_O{R>x>I9f2KerGKT`}N;HDeD{%Fl_c2C32Xh;AX zN)sALo4$Ra+?BolsZi_lcd1@zo`9sHYuN zD@AFH>La*fUGUvJKgJO~FVw4&$g1CcL>?g`4CS!5ZwSkIeVQFdN!EiKdVIQxXDMLK z|FIRT=s8&lw11Fp-c}kK;=+yj{|@LeastIV$zR+&bA#fm_zNI%x|Zd^2TVA4fc92q zsZPQKeY>W;3e%kPX)PTuaC}+%m84|GilWx{^SiTJFO$tS`p)tL7iy_3Sdl<@(RDZ@ zX5JpjEq-WaE7CbRMV47NeqUKs2vc@t?#1FLm_61*pN17)!2#7%x4dbqhF+22P0aI_ z1js|p54XgTYkC*;j!M16Låm*xG7*ru(NatIilT!>12K_G_K9qAK2Uce8aHmZ` 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 E!gK)nI{*Lx literal 0 HcmV?d00001 diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak new file mode 100644 index 0000000000000000000000000000000000000000..5eb2c244e64b3e1d247387364ad50bd941ddc515 GIT binary patch literal 113792 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¬}l$e#d` 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 Click Wargroove->Properties->Local Files->Browse Local Files` and copying the path in the address bar. + - Paste the path in between the quotes next to `root_directory:` in the `host.yaml`. + - You may have to replace all single \\ with \\\\. +4. Start the Wargroove client. + +## Installing the Archipelago Wargroove Mod and Campaign files + +1. Shut down Wargroove if it is open. +2. Start the ArchipelagoWargrooveClient.exe from the Archipelago installation. +This should install the mod and campaign for you. +3. Start Wargroove. + +## Verify the campaign can be loaded + +1. Start Wargroove from Steam. +2. Go to `Story->Campaign->Custom->Archipelago` and click play. You should see the first level. + +## Starting a Multiworld game + +1. Start the Wargroove Client and connect to the server. Enter your username from your +[settings file.](/games/Wargroove/player-settings) +2. Start Wargroove and play the Archipelago campaign by going to `Story->Campaign->Custom->Archipelago`. + +## Ending a Multiworld game +It is strongly recommended that you delete your campaign progress after finishing a multiworld game. +This can be done by going to the level selection screen in the Archipelago campaign, hitting `ESC` and clicking the +`Delete Progress` button. The main menu should now be visible. + +## Updating to a new version of the Wargroove mod or downloading new campaign files +First, delete your campaign progress by going to the level selection screen in the Archipelago campaign, +hitting `ESC` and clicking the `Delete Progress` button. + +Follow the `Installing the Archipelago Wargroove Mod and Campaign files` steps again, but look for the latest version + to download. In addition, follow the steps outlined in `Wargroove crashes when trying to run the Archipelago campaign` +when attempting to update the campaign files and the mod. + +## Troubleshooting + +### The game is too hard +`Go to the campaign overview screen->Hit escape on the keyboard->Click adjust difficulty->Adjust the setttings` + +### The mod doesn't load +Double-check the mod installation under `%appdata%\Chucklefish\Wargroove\mods`. There should be 3 `.dat` files in +`%appdata%\Chucklefish\Wargroove\mods\ArchipelagoMod`. Otherwise, follow +`Installing the Archipelago Wargroove Mod and Campaign files` steps once more. + +### Wargroove crashes or there is a lua error +Wargroove is finicky, but there could be several causes for this. If it happens often or can be reproduced, +please submit a bug report in the tech-support channel on the [discord](https://discord.gg/archipelago). + +### Wargroove crashes when trying to run the Archipelago campaign +This is caused by not deleting campaign progress before updating the mod and campaign files. +1. Go to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod. +2. Wargroove will give an error message. +3. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again. +4. Wargroove crashes. +5. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again. +6. In the edit menu, hit `ESC` and click `Delete Progress`. +7. If the above steps do not allow you to start the campaign from `Story->Campaign->Custom->Archipelago` replace +`playerProgress` and `playerProgress.bak` with your previously backed up files. + +### Mod is out of date when trying to run the Archipelago campaign +Please follow the above steps in `Wargroove crashes when trying to run the Archipelago campaign`. \ No newline at end of file From 0b12d800082f9b02a653c1dd3a797dbcfb608794 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 24 Feb 2023 01:30:11 -0600 Subject: [PATCH 05/14] Tracker: get game names from slot_info instead of `multidata["games"]` and render custom game names on generic tracker (#1453) --- WebHostLib/tracker.py | 50 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index c0b83e4daf..4565f6083b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -5,6 +5,7 @@ from typing import Counter, Optional, Dict, Any, Tuple from uuid import UUID from flask import render_template +from jinja2 import pass_context, runtime from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -83,9 +84,6 @@ def get_alttp_id(item_name): return Items.item_table[item_name][2] -app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location) -app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id) - links = {"Bow": "Progressive Bow", "Silver Arrows": "Progressive Bow", "Silver Bow": "Progressive Bow", @@ -237,6 +235,22 @@ def render_timedelta(delta: datetime.timedelta): return f"{hours}:{minutes}" +@pass_context +def get_location_name(context: runtime.Context, loc: int) -> str: + context_locations = context.get("custom_locations", {}) + return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc) + + +@pass_context +def get_item_name(context: runtime.Context, item: int) -> str: + context_items = context.get("custom_items", {}) + return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item) + + +app.jinja_env.filters["location_name"] = get_location_name +app.jinja_env.filters["item_name"] = get_item_name + + _multidata_cache = {} @@ -258,10 +272,23 @@ def get_static_room_data(room: Room): # in > 100 players this can take a bit of time and is the main reason for the cache locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations'] names: Dict[int, Dict[int, str]] = multidata["names"] + games = {} groups = {} + custom_locations = {} + custom_items = {} if "slot_info" in multidata: + games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()} groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items() if slot_info.type == SlotType.group} + + for game in games.values(): + if game in multidata["datapackage"]: + custom_locations.update( + {id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()}) + custom_items.update( + {id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()}) + elif "games" in multidata: + games = multidata["games"] seed_checks_in_area = checks_in_area.copy() use_door_tracker = False @@ -282,7 +309,8 @@ def get_static_room_data(room: Room): if playernumber not in groups} saving_second = get_saving_second(multidata["seed_name"]) result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ - multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second + multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \ + custom_locations, custom_items _multidata_cache[room.seed.id] = result return result @@ -309,7 +337,8 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w # Collect seed information and pare it down to a single player locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room) + precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ + get_static_room_data(room) player_name = names[tracked_team][tracked_player - 1] location_to_area = player_location_to_area[tracked_player] inventory = collections.Counter() @@ -351,7 +380,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second) else: tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done, saving_second) + seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items) return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker @@ -1194,7 +1223,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - saving_second: int) -> str: + saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str: checked_locations = multisave.get("location_checks", {}).get((team, player), set()) player_received_items = {} @@ -1212,8 +1241,8 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic player=player, team=team, room=room, player_name=playerName, checked_locations=checked_locations, not_checked_locations=set(locations[player]) - checked_locations, - received_items=player_received_items, - saving_second=saving_second) + received_items=player_received_items, saving_second=saving_second, + custom_items=custom_items, custom_locations=custom_locations) @app.route('/tracker/') @@ -1223,7 +1252,8 @@ def getTracker(tracker: UUID): if not room: abort(404) locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room) + precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ + get_static_room_data(room) inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} From b8659d28cc9b43e3c0e9017b3815d5f42c3a5af8 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Thu, 23 Feb 2023 23:32:15 -0800 Subject: [PATCH 06/14] [OC2] DeathLink (#1470) --- worlds/overcooked2/Options.py | 30 +++++++++++++++++++++++-- worlds/overcooked2/Overcooked2Levels.py | 10 ++++----- worlds/overcooked2/__init__.py | 18 ++++++++------- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index d0de7f4c8a..400796af59 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,14 +1,20 @@ -from enum import Enum +from enum import IntEnum from typing import TypedDict from Options import DefaultOnToggle, Range, Choice -class LocationBalancingMode(Enum): +class LocationBalancingMode(IntEnum): disabled = 0 compromise = 1 full = 2 +class DeathLinkMode(IntEnum): + disabled = 0 + death_only = 1 + death_and_overcook = 2 + + class OC2OnToggle(DefaultOnToggle): @property def result(self) -> bool: @@ -31,6 +37,23 @@ class LocationBalancing(Choice): default = LocationBalancingMode.compromise.value +class DeathLink(Choice): + """DeathLink is an opt-in feature for Multiworlds where individual death events are propogated to all games with DeathLink enabled. + + - Disabled: Death will behave as it does in the original game. + + - Death Only: A DeathLink broadcast will be sent every time a chef falls into a stage hazard. All local chefs will be killed when any one perishes. + + - Death and Overcook: Same as above, but an additional broadcast will be sent whenever the kitchen catches on fire from burnt food. + """ + auto_display_name = True + display_name = "DeathLink" + option_disabled = DeathLinkMode.disabled.value + option_death_only = DeathLinkMode.death_only.value + option_death_and_overcook = DeathLinkMode.death_and_overcook.value + default = DeathLinkMode.disabled.value + + class AlwaysServeOldestOrder(OC2OnToggle): """Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster.""" @@ -131,6 +154,9 @@ overcooked_options = { # generator options "location_balancing": LocationBalancing, + # deathlink + "deathlink": DeathLink, + # randomization options "shuffle_level_order": ShuffleLevelOrder, "include_horde_levels": IncludeHordeLevels, diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index 624e274196..007be13c9e 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, IntEnum from typing import List @@ -113,7 +113,7 @@ ITEMS_TO_EXCLUDE_IF_NO_DLC = [ "Calmer Unbread", ] -class Overcooked2GameWorld(Enum): +class Overcooked2GameWorld(IntEnum): ONE = 1 TWO = 2 THREE = 3 @@ -127,7 +127,7 @@ class Overcooked2GameWorld(Enum): if self == Overcooked2GameWorld.KEVIN: return "Kevin" - return str(int(self.value)) + return str(self.value) @property def sublevel_count(self) -> int: @@ -141,7 +141,7 @@ class Overcooked2GameWorld(Enum): if self == Overcooked2GameWorld.ONE: return 1 - prev = Overcooked2GameWorld(self.value - 1) + prev = Overcooked2GameWorld(self - 1) return prev.base_id + prev.sublevel_count @property @@ -195,7 +195,7 @@ class Overcooked2Level: if self.sublevel > self.world.sublevel_count: if self.world == Overcooked2GameWorld.KEVIN: raise StopIteration - self.world = Overcooked2GameWorld(self.world.value + 1) + self.world = Overcooked2GameWorld(self.world + 1) self.sublevel = 1 return self diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index b973ebe485..63d87648e1 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import IntEnum from typing import Callable, Dict, Any, List, Optional from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, Tutorial, LocationProgressType @@ -6,7 +6,7 @@ from worlds.AutoWorld import World, WebWorld from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel, ITEMS_TO_EXCLUDE_IF_NO_DLC from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name -from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode +from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful @@ -27,7 +27,7 @@ class Overcooked2Web(WebWorld): tutorials = [setup_en] -class PrepLevelMode(Enum): +class PrepLevelMode(IntEnum): original = 0 excluded = 1 ayce = 2 @@ -179,7 +179,7 @@ class Overcooked2World(World): balancing_mode = self.get_options()["LocationBalancing"] - if balancing_mode == LocationBalancingMode.disabled.value: + if balancing_mode == LocationBalancingMode.disabled: # Location balancing is disabled, progression density is purely determined by filler return list() @@ -191,12 +191,12 @@ class Overcooked2World(World): game_progression_count += 1 game_progression_density = game_progression_count/game_item_count - if balancing_mode == LocationBalancingMode.full.value: + if balancing_mode == LocationBalancingMode.full: # Location balancing will be employed in an attempt to keep the number of # progression locations and proression items as close to equal as possible return self.get_n_random_locations(game_progression_count) - assert balancing_mode == LocationBalancingMode.compromise.value + assert balancing_mode == LocationBalancingMode.compromise # Count how many progression items are shuffled between all games total_item_count = len(self.multiworld.itempool) @@ -242,7 +242,7 @@ class Overcooked2World(World): self.level_mapping = \ level_shuffle_factory( self.multiworld.random, - self.options["PrepLevels"] != PrepLevelMode.excluded.value, + self.options["PrepLevels"] != PrepLevelMode.excluded, self.options["IncludeHordeLevels"], ) else: @@ -508,6 +508,8 @@ class Overcooked2World(World): "SaveFolderName": mod_name, "CustomOrderTimeoutPenalty": 10, "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], + "LocalDeathLink": self.options["DeathLink"] != DeathLinkMode.disabled, + "BurnTriggersDeath": self.options["DeathLink"] == DeathLinkMode.death_and_overcook, # Game Modifications "LevelPurchaseRequirements": level_purchase_requirements, @@ -560,7 +562,7 @@ class Overcooked2World(World): for bug in bugs: self.options[bug] = self.options["FixBugs"] self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] - self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value + self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 self.options["LeaderboardScoreScale"] = { "FourStars": 1.0, From 6c460bcbf7807e25bd1f7213ce2b35c291fde9f5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 24 Feb 2023 21:02:51 -0600 Subject: [PATCH 07/14] LTTP: Move LTTP spoiler writing out of core (#1467) --- BaseClasses.py | 201 +-------------------------------------- worlds/alttp/__init__.py | 125 +++++++++++++++++++++++- 2 files changed, 126 insertions(+), 200 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 75cd317a73..f91b81bdff 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1260,13 +1260,9 @@ class Spoiler(): self.multiworld = world self.hashes = {} self.entrances = OrderedDict() - self.medallions = {} self.playthrough = {} self.unreachables = set() - self.locations = {} self.paths = {} - self.shops = [] - self.bosses = OrderedDict() def set_entrance(self, entrance: str, exit_: str, direction: str, player: int): if self.multiworld.players == 1: @@ -1276,126 +1272,6 @@ class Spoiler(): self.entrances[(entrance, direction, player)] = OrderedDict( [('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)]) - def parse_data(self): - from worlds.alttp.SubClasses import LTTPRegionType - self.medallions = OrderedDict() - for player in self.multiworld.get_game_players("A Link to the Past"): - self.medallions[f'Misery Mire ({self.multiworld.get_player_name(player)})'] = \ - self.multiworld.required_medallions[player][0] - self.medallions[f'Turtle Rock ({self.multiworld.get_player_name(player)})'] = \ - self.multiworld.required_medallions[player][1] - - self.locations = OrderedDict() - listed_locations = set() - lw_locations = [] - dw_locations = [] - cave_locations = [] - for loc in self.multiworld.get_locations(): - if loc.game == "A Link to the Past": - if loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.LightWorld and loc.show_in_spoiler: - lw_locations.append(loc) - elif loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.DarkWorld and loc.show_in_spoiler: - dw_locations.append(loc) - elif loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.Cave and loc.show_in_spoiler: - cave_locations.append(loc) - - self.locations['Light World'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - lw_locations]) - listed_locations.update(lw_locations) - - self.locations['Dark World'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - dw_locations]) - listed_locations.update(dw_locations) - - self.locations['Caves'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - cave_locations]) - listed_locations.update(cave_locations) - - for dungeon in self.multiworld.dungeons.values(): - dungeon_locations = [loc for loc in self.multiworld.get_locations() if - loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler] - self.locations[str(dungeon)] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - dungeon_locations]) - listed_locations.update(dungeon_locations) - - other_locations = [loc for loc in self.multiworld.get_locations() if - loc not in listed_locations and loc.show_in_spoiler] - if other_locations: - self.locations['Other Locations'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - other_locations]) - listed_locations.update(other_locations) - - self.shops = [] - from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display - for shop in self.multiworld.shops: - if not shop.custom: - continue - shopdata = { - 'location': str(shop.region), - 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' - } - for index, item in enumerate(shop.inventory): - if item is None: - continue - my_price = item['price'] // price_rate_display.get(item['price_type'], 1) - shopdata['item_{}'.format( - index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}" - - if item['player'] > 0: - shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', - '(Player {}) — '.format( - item['player'])) - - if item['max'] == 0: - continue - shopdata['item_{}'.format(index)] += " x {}".format(item['max']) - - if item['replacement'] is None: - continue - shopdata['item_{}'.format( - index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}" - self.shops.append(shopdata) - - for player in self.multiworld.get_game_players("A Link to the Past"): - self.bosses[str(player)] = OrderedDict() - self.bosses[str(player)]["Eastern Palace"] = self.multiworld.get_dungeon("Eastern Palace", player).boss.name - self.bosses[str(player)]["Desert Palace"] = self.multiworld.get_dungeon("Desert Palace", player).boss.name - self.bosses[str(player)]["Tower Of Hera"] = self.multiworld.get_dungeon("Tower of Hera", player).boss.name - self.bosses[str(player)]["Hyrule Castle"] = "Agahnim" - self.bosses[str(player)]["Palace Of Darkness"] = self.multiworld.get_dungeon("Palace of Darkness", - player).boss.name - self.bosses[str(player)]["Swamp Palace"] = self.multiworld.get_dungeon("Swamp Palace", player).boss.name - self.bosses[str(player)]["Skull Woods"] = self.multiworld.get_dungeon("Skull Woods", player).boss.name - self.bosses[str(player)]["Thieves Town"] = self.multiworld.get_dungeon("Thieves Town", player).boss.name - self.bosses[str(player)]["Ice Palace"] = self.multiworld.get_dungeon("Ice Palace", player).boss.name - self.bosses[str(player)]["Misery Mire"] = self.multiworld.get_dungeon("Misery Mire", player).boss.name - self.bosses[str(player)]["Turtle Rock"] = self.multiworld.get_dungeon("Turtle Rock", player).boss.name - if self.multiworld.mode[player] != 'inverted': - self.bosses[str(player)]["Ganons Tower Basement"] = \ - self.multiworld.get_dungeon('Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[ - 'middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[ - 'top'].name - else: - self.bosses[str(player)]["Ganons Tower Basement"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name - - self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2" - self.bosses[str(player)]["Ganon"] = "Ganon" - def create_playthrough(self, create_paths: bool = True): """Destructive to the world while it is run, damage gets repaired afterwards.""" from itertools import chain @@ -1547,30 +1423,7 @@ class Spoiler(): self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) - def to_json(self): - self.parse_data() - out = OrderedDict() - out['Entrances'] = list(self.entrances.values()) - out.update(self.locations) - out['Special'] = self.medallions - if self.hashes: - out['Hashes'] = self.hashes - if self.shops: - out['Shops'] = self.shops - out['playthrough'] = self.playthrough - out['paths'] = self.paths - out['Bosses'] = self.bosses - - return json.dumps(out) - def to_file(self, filename: str): - self.parse_data() - - def bool_to_text(variable: Union[bool, str]) -> str: - if type(variable) == str: - return variable - return 'Yes' if variable else 'No' - def write_option(option_key: str, option_obj: type(Options.Option)): res = getattr(self.multiworld, option_key)[player] display_name = getattr(option_obj, "display_name", option_key) @@ -1600,38 +1453,6 @@ class Spoiler(): write_option(f_option, option) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) - if player in self.multiworld.get_game_players("A Link to the Past"): - outfile.write('%s%s\n' % ('Hash: ', self.hashes[player])) - - outfile.write('Logic: %s\n' % self.multiworld.logic[player]) - outfile.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[player]) - outfile.write('Mode: %s\n' % self.multiworld.mode[player]) - outfile.write('Goal: %s\n' % self.multiworld.goal[player]) - if "triforce" in self.multiworld.goal[player]: # triforce hunt - outfile.write("Pieces available for Triforce: %s\n" % - self.multiworld.triforce_pieces_available[player]) - outfile.write("Pieces required for Triforce: %s\n" % - self.multiworld.triforce_pieces_required[player]) - outfile.write('Difficulty: %s\n' % self.multiworld.difficulty[player]) - outfile.write('Item Functionality: %s\n' % self.multiworld.item_functionality[player]) - outfile.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[player]) - if self.multiworld.shuffle[player] != "vanilla": - outfile.write('Entrance Shuffle Seed %s\n' % self.multiworld.worlds[player].er_seed) - outfile.write('Shop inventory shuffle: %s\n' % - bool_to_text("i" in self.multiworld.shop_shuffle[player])) - outfile.write('Shop price shuffle: %s\n' % - bool_to_text("p" in self.multiworld.shop_shuffle[player])) - outfile.write('Shop upgrade shuffle: %s\n' % - bool_to_text("u" in self.multiworld.shop_shuffle[player])) - outfile.write('New Shop inventory: %s\n' % - bool_to_text("g" in self.multiworld.shop_shuffle[player] or - "f" in self.multiworld.shop_shuffle[player])) - outfile.write('Custom Potion Shop: %s\n' % - bool_to_text("w" in self.multiworld.shop_shuffle[player])) - outfile.write('Enemy health: %s\n' % self.multiworld.enemy_health[player]) - outfile.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[player]) - outfile.write('Prize shuffle %s\n' % - self.multiworld.shuffle_prizes[player]) if self.entrances: outfile.write('\n\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: ' @@ -1640,30 +1461,14 @@ class Spoiler(): '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) - if self.medallions: - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion}') - AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) + locations = [(str(location), str(location.item) if location.item is not None else "Nothing") + for location in self.multiworld.get_locations()] outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join( - ['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in - grouping.items()])) + ['%s: %s' % (location, item) for location, item in locations])) - if self.shops: - outfile.write('\n\nShops:\n\n') - outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join( - item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if - item)) for shop in self.shops)) - - for player in self.multiworld.get_game_players("A Link to the Past"): - if self.multiworld.boss_shuffle[player] != 'none': - bossmap = self.bosses[str(player)] if self.multiworld.players > 1 else self.bosses - outfile.write( - f'\n\nBosses{(f" ({self.multiworld.get_player_name(player)})" if self.multiworld.players > 1 else "")}:\n') - outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join( [' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [ diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d925048433..5f33b152b8 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -3,6 +3,7 @@ import os import random import threading import typing +from collections import OrderedDict import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld @@ -19,8 +20,8 @@ from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules -from .Shops import create_shops, ShopSlotFill -from .SubClasses import ALttPItem +from .Shops import create_shops, ShopSlotFill, ShopType, price_rate_display, price_type_display_name +from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin lttp_logger = logging.getLogger("A Link to the Past") @@ -520,6 +521,126 @@ class ALTTPWorld(World): else: logging.warning(f"Could not trash fill Ganon's Tower for player {player}.") + def write_spoiler_header(self, spoiler_handle: typing.TextIO) -> None: + def bool_to_text(variable: typing.Union[bool, str]) -> str: + if type(variable) == str: + return variable + return "Yes" if variable else "No" + + spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player]) + spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player]) + spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player]) + spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player]) + if "triforce" in self.multiworld.goal[self.player]: # triforce hunt + spoiler_handle.write("Pieces available for Triforce: %s\n" % + self.multiworld.triforce_pieces_available[self.player]) + spoiler_handle.write("Pieces required for Triforce: %s\n" % + self.multiworld.triforce_pieces_required[self.player]) + spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player]) + spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player]) + spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player]) + if self.multiworld.shuffle[self.player] != "vanilla": + spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed) + spoiler_handle.write('Shop inventory shuffle: %s\n' % + bool_to_text("i" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Shop price shuffle: %s\n' % + bool_to_text("p" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Shop upgrade shuffle: %s\n' % + bool_to_text("u" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('New Shop inventory: %s\n' % + bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or + "f" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Custom Potion Shop: %s\n' % + bool_to_text("w" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player]) + spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player]) + spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player]) + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + spoiler_handle.write("\n\nMedallions:\n") + spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):" + f" {self.multiworld.required_medallions[self.player][0]}") + spoiler_handle.write( + f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):" + f" {self.multiworld.required_medallions[self.player][1]}") + + if self.multiworld.boss_shuffle[self.player] != "none": + def create_boss_map() -> typing.Dict: + boss_map = { + "Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name, + "Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name, + "Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name, + "Hyrule Castle": "Agahnim", + "Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness", + self.player).boss.name, + "Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name, + "Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name, + "Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name, + "Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name, + "Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name, + "Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name, + "Ganons Tower": "Agahnim 2", + "Ganon": "Ganon" + } + if self.multiworld.mode[self.player] != 'inverted': + boss_map.update({ + "Ganons Tower Basement": + self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name, + "Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + "middle"].name, + "Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + "top"].name + }) + else: + boss_map.update({ + "Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name, + "Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name, + "Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name + }) + return boss_map + + bossmap = create_boss_map() + spoiler_handle.write( + f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n') + spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) + + def build_shop_info() -> typing.Dict: + shop = self.multiworld.shops[self.player] + if not shop.custom: + return None + + shop_data = { + "location": str(shop.region), + "type": "Take Any" if shop.type == ShopType.TakeAny else "Shop" + } + + for index, item in enumerate(shop.inventory): + if item is None: + continue + price = item["price"] // price_rate_display.get(item["price_type"], 1) + shop_data["item_{}".format(index)] = f"{item['item']} - {price} {price_type_display_name[item['price_type']]}" + if item["player"]: + shop_data["item_{}".format(index)] =\ + shop_data["item_{}".format(index)].replace("—", "(Player {}) — ".format(item["player"])) + + if item["max"] == 0: + continue + shop_data["item_{}".format(index)] += " x {}".format(item["max"]) + if item["replacement"] is None: + continue + shop_data["item_{}".format(index)] +=\ + f", {item['replacement']} - {item['replacement_price']}" \ + f" {price_type_display_name[item['replacement_price_type']]}" + + return shop_data + + shop_data = build_shop_info() + if shop_data is not None: + spoiler_handle.write('\n\nShops:\n\n') + spoiler_handle.write(''.join("{} [{}]\n {}".format(shop_data['location'], shop_data['type'], "\n ".join( + item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if + item)))) + def get_filler_item_name(self) -> str: if self.multiworld.goal[self.player] == "icerodhunt": item = "Nothing" From 062d6eeaced463003bd1509a3e0933c68c8f7535 Mon Sep 17 00:00:00 2001 From: Marech Date: Sun, 26 Feb 2023 06:35:03 +0100 Subject: [PATCH 08/14] DS3: Added DLC Items/Locations + corresponding option and added an option to enable materials/consumables/estus randomization (#1301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added more progressive locations and associated items. - Added an option to enable materials/consumables/estus randomization, some players complain about the number of locations and the randomness of those items. - Added an option to add DLC Items and Locations to the pool, the player must own both the ASHES OF ARIANDEL and the RINGED CITY DLC. Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com> Co-authored-by: Friðberg Reynir Traustason --- worlds/dark_souls_3/Items.py | 21 +- worlds/dark_souls_3/Locations.py | 7 +- worlds/dark_souls_3/Options.py | 72 +++++- worlds/dark_souls_3/__init__.py | 112 ++++++++-- worlds/dark_souls_3/data/items_data.py | 210 +++++++++++++++++- worlds/dark_souls_3/data/locations_data.py | 153 ++++++++++++- worlds/dark_souls_3/docs/en_Dark Souls III.md | 15 +- worlds/dark_souls_3/docs/setup_en.md | 4 + 8 files changed, 542 insertions(+), 52 deletions(-) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index e7ba2ecf00..140d3ba613 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -1,10 +1,18 @@ +import sys + from BaseClasses import Item -from worlds.dark_souls_3.data.items_data import item_tables +from worlds.dark_souls_3.data.items_data import item_tables, dlc_shields_table, dlc_weapons_upgrade_10_table, \ + dlc_weapons_upgrade_5_table, dlc_goods_table, dlc_spells_table, dlc_armor_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table class DarkSouls3Item(Item): game: str = "Dark Souls III" + dlc_set = {**dlc_shields_table, **dlc_weapons_upgrade_10_table, **dlc_weapons_upgrade_5_table, + **dlc_goods_table, **dlc_spells_table, **dlc_armor_table, **dlc_ring_table, **dlc_misc_table} + + dlc_progressive = {**dlc_goods_2_table} + @staticmethod def get_name_to_id() -> dict: base_id = 100000 @@ -12,6 +20,17 @@ class DarkSouls3Item(Item): output = {} for i, table in enumerate(item_tables): + if len(table) > table_offset: + raise Exception("An item table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i)) output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) return output + + @staticmethod + def is_dlc_item(name) -> bool: + return name in DarkSouls3Item.dlc_set + + @staticmethod + def is_dlc_progressive(name) -> bool: + return name in DarkSouls3Item.dlc_progressive + diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index 0ba84e365b..a63a951ccb 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -1,5 +1,8 @@ +import sys + from BaseClasses import Location -from worlds.dark_souls_3.data.locations_data import location_tables +from worlds.dark_souls_3.data.locations_data import location_tables, painted_world_table, dreg_heap_table, \ + ringed_city_table class DarkSouls3Location(Location): @@ -12,6 +15,8 @@ class DarkSouls3Location(Location): output = {} for i, table in enumerate(location_tables): + if len(table) > table_offset: + raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i)) output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) return output diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index 54c43143e0..dc9510a7f3 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Toggle, Option, DeathLink +from Options import Toggle, Option, Range, Choice, DeathLink class AutoEquipOption(Toggle): @@ -29,10 +29,57 @@ class NoEquipLoadOption(Toggle): display_name = "No Equip load" -class RandomizeWeaponsLevelOption(Toggle): - """Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value - between +1 and +5/+10""" +class RandomizeWeaponsLevelOption(Choice): + """Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and + maximum levels defined. + all: All weapons are eligible, both basic and epic + basic: Only weapons that can be upgraded to +10 + epic: Only weapons that can be upgraded to +5""" display_name = "Randomize weapons level" + option_none = 0 + option_all = 1 + option_basic = 2 + option_epic = 3 + + +class RandomizeWeaponsLevelPercentageOption(Range): + """The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled""" + display_name = "Percentage of randomized weapons" + range_start = 1 + range_end = 100 + default = 33 + + +class MinLevelsIn5WeaponPoolOption(Range): + """The minimum upgraded value of a weapon in the pool of weapons that can only reach +5""" + display_name = "Minimum level of +5 weapons" + range_start = 1 + range_end = 5 + default = 1 + + +class MaxLevelsIn5WeaponPoolOption(Range): + """The maximum upgraded value of a weapon in the pool of weapons that can only reach +5""" + display_name = "Maximum level of +5 weapons" + range_start = 1 + range_end = 5 + default = 5 + + +class MinLevelsIn10WeaponPoolOption(Range): + """The minimum upgraded value of a weapon in the pool of weapons that can reach +10""" + display_name = "Minimum level of +10 weapons" + range_start = 1 + range_end = 10 + default = 1 + + +class MaxLevelsIn10WeaponPoolOption(Range): + """The maximum upgraded value of a weapon in the pool of weapons that can reach +10""" + display_name = "Maximum level of +10 weapons" + range_start = 1 + range_end = 10 + default = 10 class LateBasinOfVowsOption(Toggle): @@ -41,14 +88,31 @@ class LateBasinOfVowsOption(Toggle): display_name = "Late Basin of Vows" +class EnableProgressiveLocationsOption(Toggle): + """Randomize upgrade materials such as the titanite shards, the estus shards and the consumables""" + display_name = "Randomize materials, Estus shards and consumables (+196 checks/items)" + + +class EnableDLCOption(Toggle): + """To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC""" + display_name = "Add the DLC Items and Locations to the pool (+81 checks/items)" + + dark_souls_options: typing.Dict[str, type(Option)] = { "auto_equip": AutoEquipOption, "lock_equip": LockEquipOption, "no_weapon_requirements": NoWeaponRequirementsOption, "randomize_weapons_level": RandomizeWeaponsLevelOption, + "randomize_weapons_percentage": RandomizeWeaponsLevelPercentageOption, + "min_levels_in_5": MinLevelsIn5WeaponPoolOption, + "max_levels_in_5": MaxLevelsIn5WeaponPoolOption, + "min_levels_in_10": MinLevelsIn10WeaponPoolOption, + "max_levels_in_10": MaxLevelsIn10WeaponPoolOption, "late_basin_of_vows": LateBasinOfVowsOption, "no_spell_requirements": NoSpellRequirementsOption, "no_equip_load": NoEquipLoadOption, "death_link": DeathLink, + "enable_progressive_locations": EnableProgressiveLocationsOption, + "enable_dlc": EnableDLCOption, } diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 6d16e562fe..d08cd3ee3e 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1,18 +1,18 @@ # world/dark_souls_3/__init__.py -import json -import os from typing import Dict from .Items import DarkSouls3Item from .Locations import DarkSouls3Location from .Options import dark_souls_options -from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list +from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list, \ + dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table from .data.locations_data import location_dictionary, fire_link_shrine_table, \ high_wall_of_lothric, \ undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \ - untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations + untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations, \ + progressive_locations_2, progressive_locations_3, painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations from ..AutoWorld import World, WebWorld from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification from ..generic.Rules import set_rule, add_item_rule @@ -52,9 +52,9 @@ class DarkSouls3World(World): option_definitions = dark_souls_options topology_present: bool = True web = DarkSouls3Web() - data_version = 4 + data_version = 5 base_id = 100000 - required_client_version = (0, 3, 6) + required_client_version = (0, 3, 7) item_name_to_id = DarkSouls3Item.get_name_to_id() location_name_to_id = DarkSouls3Location.get_name_to_id() @@ -77,7 +77,15 @@ class DarkSouls3World(World): return DarkSouls3Item(name, item_classification, data, self.player) def create_regions(self): - menu_region = self.create_region("Menu", progressive_locations) + + if self.multiworld.enable_progressive_locations[self.player].value and self.multiworld.enable_dlc[self.player].value: + menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2, + **progressive_locations_3, **dlc_progressive_locations}) + elif self.multiworld.enable_progressive_locations[self.player].value: + menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2, + **progressive_locations_3}) + else: + menu_region = self.create_region("Menu", None) # Create all Vanilla regions of Dark Souls III firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table) @@ -101,6 +109,11 @@ class DarkSouls3World(World): untended_graves_region = self.create_region("Untended Graves", untended_graves_table) archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table) kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None) + # DLC Down here + if self.multiworld.enable_dlc[self.player]: + painted_world_of_ariandel_region = self.create_region("Painted World of Ariandel", painted_world_table) + dreg_heap_region = self.create_region("Dreg Heap", dreg_heap_table) + ringed_city_region = self.create_region("Ringed City", ringed_city_table) # Create the entrance to connect those regions menu_region.exits.append(Entrance(self.player, "New Game", menu_region)) @@ -112,7 +125,8 @@ class DarkSouls3World(World): firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower", firelink_shrine_region)) self.multiworld.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region) - self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region) + self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect( + kiln_of_the_first_flame_region) self.multiworld.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region) high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement", high_wall_of_lothric_region)) @@ -133,7 +147,7 @@ class DarkSouls3World(World): catacombs_of_carthus_region)) catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake", catacombs_of_carthus_region)) - self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player).\ + self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player). \ connect(irithyll_of_the_boreal_valley_region) self.multiworld.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region) irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon", @@ -153,6 +167,16 @@ class DarkSouls3World(World): consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves", consumed_king_garden_region)) self.multiworld.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region) + # DLC Connectors Below + if self.multiworld.enable_dlc[self.player]: + cathedral_of_the_deep_region.exits.append(Entrance(self.player, "Goto Painted World of Ariandel", + cathedral_of_the_deep_region)) + self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player).connect(painted_world_of_ariandel_region) + painted_world_of_ariandel_region.exits.append(Entrance(self.player, "Goto Dreg Heap", + painted_world_of_ariandel_region)) + self.multiworld.get_entrance("Goto Dreg Heap", self.player).connect(dreg_heap_region) + dreg_heap_region.exits.append(Entrance(self.player, "Goto Ringed City", dreg_heap_region)) + self.multiworld.get_entrance("Goto Ringed City", self.player).connect(ringed_city_region) # For each region, add the associated locations retrieved from the corresponding location_table def create_region(self, region_name, location_table) -> Region: @@ -169,8 +193,18 @@ class DarkSouls3World(World): def create_items(self): for name, address in self.item_name_to_id.items(): # Specific items will be included in the item pool under certain conditions. See generate_basic - if name != "Basin of Vows": - self.multiworld.itempool += [self.create_item(name)] + if name == "Basin of Vows": + continue + # Do not add progressive_items ( containing "#" ) to the itempool if the option is disabled + if (not self.multiworld.enable_progressive_locations[self.player]) and "#" in name: + continue + # Do not add DLC items if the option is disabled + if (not self.multiworld.enable_dlc[self.player]) and DarkSouls3Item.is_dlc_item(name): + continue + # Do not add DLC Progressives if both options are disabled + if ((not self.multiworld.enable_progressive_locations[self.player]) or (not self.multiworld.enable_dlc[self.player])) and DarkSouls3Item.is_dlc_progressive(name): + continue + self.multiworld.itempool += [self.create_item(name)] def generate_early(self): pass @@ -194,15 +228,23 @@ class DarkSouls3World(World): lambda state: state.has("Grand Archives Key", self.player)) set_rule(self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player), lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and - state.has("Cinders of a Lord - Yhorm the Giant", self.player) and - state.has("Cinders of a Lord - Aldrich", self.player) and - state.has("Cinders of a Lord - Lothric Prince", self.player)) + state.has("Cinders of a Lord - Yhorm the Giant", self.player) and + state.has("Cinders of a Lord - Aldrich", self.player) and + state.has("Cinders of a Lord - Lothric Prince", self.player)) + # DLC Access Rules Below + if self.multiworld.enable_dlc[self.player]: + set_rule(self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player), + lambda state: state.has("Contraption Key", self.player)) + set_rule(self.multiworld.get_entrance("Goto Ringed City", self.player), + lambda state: state.has("Small Envoy Banner", self.player)) # Define the access rules to some specific locations set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), lambda state: state.has("Basin of Vows", self.player)) set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player), lambda state: state.has("Cell Key", self.player)) + set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player), + lambda state: state.has("Cell Key", self.player)) set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player), lambda state: state.has("Jailbreaker's Key", self.player)) set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player), @@ -242,17 +284,38 @@ class DarkSouls3World(World): # Depending on the specified option, modify items hexadecimal value to add an upgrade level item_dictionary_copy = item_dictionary.copy() - if self.multiworld.randomize_weapons_level[self.player]: - # Randomize some weapons upgrades - for name in weapons_upgrade_5_table.keys(): - if self.multiworld.random.randint(0, 100) < 33: - value = self.multiworld.random.randint(1, 5) - item_dictionary_copy[name] += value + if self.multiworld.randomize_weapons_level[self.player] > 0: + # if the user made an error and set a min higher than the max we default to the max + max_5 = self.multiworld.max_levels_in_5[self.player] + min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5) + max_10 = self.multiworld.max_levels_in_10[self.player] + min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10) + weapons_percentage = self.multiworld.randomize_weapons_percentage[self.player] - for name in weapons_upgrade_10_table.keys(): - if self.multiworld.random.randint(0, 100) < 33: - value = self.multiworld.random.randint(1, 10) - item_dictionary_copy[name] += value + # Randomize some weapons upgrades + if self.multiworld.randomize_weapons_level[self.player] in [1, 3]: # Options are either all or +5 + for name in weapons_upgrade_5_table.keys(): + if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage: + value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5) + item_dictionary_copy[name] += value + + if self.multiworld.randomize_weapons_level[self.player] in [1, 2]: # Options are either all or +10 + for name in weapons_upgrade_10_table.keys(): + if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage: + value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10) + item_dictionary_copy[name] += value + + if self.multiworld.randomize_weapons_level[self.player] in [1, 3]: + for name in dlc_weapons_upgrade_5_table.keys(): + if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage: + value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5) + item_dictionary_copy[name] += value + + if self.multiworld.randomize_weapons_level[self.player] in [1, 2]: + for name in dlc_weapons_upgrade_10_table.keys(): + if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage: + value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10) + item_dictionary_copy[name] += value # Create the mandatory lists to generate the player's output file items_id = [] @@ -281,6 +344,7 @@ class DarkSouls3World(World): "death_link": self.multiworld.death_link[self.player].value, "no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value, "no_equip_load": self.multiworld.no_equip_load[self.player].value, + "enable_dlc": self.multiworld.enable_dlc[self.player].value }, "seed": self.multiworld.seed_name, # to verify the server's multiworld "slot": self.multiworld.player_name[self.player], # to connect to server diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py index c3f2c682e5..0ecd043440 100644 --- a/worlds/dark_souls_3/data/items_data.py +++ b/worlds/dark_souls_3/data/items_data.py @@ -13,7 +13,6 @@ weapons_upgrade_5_table = { "Izalith Staff": 0x00C96A80, "Fume Ultra Greatsword": 0x0060E4B0, "Black Knight Sword": 0x005F5E10, - "Yorshka's Spear": 0x008C3A70, "Smough's Great Hammer": 0x007E30B0, "Dragonslayer Greatbow": 0x00CF8500, @@ -25,7 +24,6 @@ weapons_upgrade_5_table = { "Dragonslayer Spear": 0x008CAFA0, "Caitha's Chime": 0x00CA06C0, "Sunlight Straight Sword": 0x00203230, - "Firelink Greatsword": 0x0060BDA0, "Hollowslayer Greatsword": 0x00604870, "Arstor's Spear": 0x008BEC50, @@ -37,7 +35,6 @@ weapons_upgrade_5_table = { "Wolnir's Holy Sword": 0x005FFA50, "Demon's Greataxe": 0x006CA480, "Demon's Fist": 0x00A84DF0, - "Old King's Great Hammer": 0x007CF830, "Greatsword of Judgment": 0x005E2590, "Profaned Greatsword": 0x005E4CA0, @@ -55,6 +52,29 @@ weapons_upgrade_5_table = { "Irithyll Rapier": 0x002E8A10 } +dlc_weapons_upgrade_5_table = { + "Friede's Great Scythe": 0x009B55A0, + "Rose of Ariandel": 0x00B82C70, + "Demon's Scar": 0x003F04D0, # Assigned to "RC: Church Guardian Shiv" + "Frayed Blade": 0x004D35A0, # Assigned to "RC: Ritual Spear Fragment" + "Gael's Greatsword": 0x00227C20, # Assigned to "RC: Violet Wrappings" + "Repeating Crossbow": 0x00D885B0, # Assigned to "RC: Blood of the Dark Souls" + "Onyx Blade": 0x00222E00, # VILHELM FIGHT + "Earth Seeker": 0x006D8EE0, + "Quakestone Hammer": 0x007ECCF0, + "Millwood Greatbow": 0x00D85EA0, + "Valorheart": 0x00F646E0, + "Aquamarine Dagger": 0x00116520, + "Ringed Knight Straight Sword": 0x00225510, + "Ledo's Great Hammer": 0x007EF400, # INVADER FIGHT + "Ringed Knight Spear": 0x008CFDC0, + "Crucifix of the Mad King": 0x008D4BE0, + "Sacred Chime of Filianore": 0x00CCECF0, + "Preacher's Right Arm": 0x00CD1400, + "White Birch Bow": 0x00D77440, + "Ringed Knight Paired Greatswords": 0x00F69500 +} + weapons_upgrade_10_table = { "Broken Straight Sword": 0x001EF9B0, "Deep Battle Axe": 0x0006AFA54, @@ -73,7 +93,6 @@ weapons_upgrade_10_table = { "Red Hilted Halberd": 0x009AB960, "Saint's Talisman": 0x00CACA10, "Large Club": 0x007AFC60, - "Brigand Twindaggers": 0x00F50E60, "Butcher Knife": 0x006BE130, "Brigand Axe": 0x006B1DE0, @@ -104,9 +123,24 @@ weapons_upgrade_10_table = { "Drakeblood Greatsword": 0x00609690, "Greatlance": 0x008A8CC0, "Sniper Crossbow": 0x00D83790, - "Claw": 0x00A7D8C0, "Drang Twinspears": 0x00F5AAA0, + "Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx +} + +dlc_weapons_upgrade_10_table = { + "Follower Sabre": 0x003EDDC0, + "Millwood Battle Axe": 0x006D67D0, + "Follower Javelin": 0x008CD6B0, + "Crow Talons": 0x00A89C10, + "Pyromancer's Parting Flame": 0x00CC9ED0, + "Crow Quills": 0x00F66DF0, + "Follower Torch": 0x015F1AD0, + "Murky Hand Scythe": 0x00118C30, + "Herald Curved Greatsword": 0x006159E0, + "Lothric War Banner": 0x008D24D0, + "Splitleaf Greatsword": 0x009B2E90, # SHOP ITEM + "Murky Longstaff": 0x00CCC5E0, } shields_table = { @@ -132,7 +166,15 @@ shields_table = { "Golden Wing Crest Shield": 0x0143CAA0, "Ancient Dragon Greatshield": 0x013599D0, "Spirit Tree Crest Shield": 0x014466E0, - "Blessed Red and White Shield": 0x01343FB9, + "Blessed Red and White Shield": 0x01343FB9 +} + +dlc_shields_table = { + "Followers Shield": 0x0135C0E0, + "Ethereal Oak Shield": 0x01450320, + "Giant Door Shield": 0x00F5F8C0, + "Dragonhead Shield": 0x0135E7F0, + "Dragonhead Greatshield": 0x01452A30 } goods_table = { @@ -167,7 +209,55 @@ goods_table = { **{"Soul of a Great Champion #"+str(i): 0x400001A4 for i in range(1, 3)}, **{"Soul of a Champion #"+str(i): 0x400001A3 for i in range(1, 5)}, **{"Soul of a Venerable Old Hand #"+str(i): 0x400001A2 for i in range(1, 5)}, - **{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)}, + **{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)} +} + +goods_2_table = { # Added by Br00ty + "HWL: Gold Pine Resin #": 0x4000014B, + "US: Charcoal Pine Resin #": 0x4000014A, + "FK: Gold Pine Bundle #": 0x40000155, + "CC: Carthus Rouge #": 0x4000014F, + "ID: Pale Pine Resin #": 0x40000150, + **{"Ember #"+str(i): 0x400001F4 for i in range(1, 45)}, + **{"Titanite Shard #"+str(i): 0x400003E8 for i in range(11, 16)}, + **{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(11, 16)}, + **{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 25)} +} + +goods_3_table = { # Added by Br00ty + **{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)}, + **{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)}, + **{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)}, + **{"Green Blossom #"+str(i): 0x40000104 for i in range(1, 7)}, + **{"Human Pine Resin #"+str(i): 0x4000014E for i in range(1, 3)}, + **{"Charcoal Pine Bundle #"+str(i): 0x40000154 for i in range(1, 3)}, + **{"Rotten Pine Resin #"+str(i): 0x40000157 for i in range(1, 3)}, + **{"Alluring Skull #"+str(i): 0x40000126 for i in range(1, 9)}, + **{"Rusted Coin #"+str(i): 0x400001C7 for i in range(1, 3)}, + **{"Rusted Gold Coin #"+str(i): 0x400001C9 for i in range(1, 3)}, + **{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)}, + **{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)} +} + +dlc_goods_table = { + "Soul of Sister Friede": 0x400002E8, + "Soul of the Demon Prince": 0x400002EA, + "Soul of Darkeater Midir": 0x400002EB, + "Soul of Slave Knight Gael": 0x400002E9 +} + +dlc_goods_2_table = { #71 + **{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)}, + **{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)}, + **{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)}, + **{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)}, + **{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)}, + **{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)}, + **{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)}, + **{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)}, + **{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)}, + **{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)}, + **{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)} } armor_table = { @@ -265,6 +355,69 @@ armor_table = { "Outrider Knight Armor": 0x1328BB28, "Outrider Knight Gauntlets": 0x1328BF10, "Outrider Knight Leggings": 0x1328C2F8, + + "Cornyx's Wrap": 0x11946370, + "Cornyx's Garb": 0x11945F88, + "Cornyx's Skirt": 0x11946758 +} + +dlc_armor_table = { + "Slave Knight Hood": 0x134EDCE0, + "Slave Knight Armor": 0x134EE0C8, + "Slave Knight Gauntlets": 0x134EE4B0, + "Slave Knight Leggings": 0x134EE898, + "Vilhelm's Helm": 0x11312D00, + "Vilhelm's Armor": 0x113130E8, + "Vilhelm's Gauntlets": 0x113134D0, + "Vilhelm's Leggings": 0x113138B8, + #"Millwood Knight Helm": 0x139B2820, # SHOP ITEM + #"Millwood Knight Armor": 0x139B2C08, # SHOP ITEM + #"Millwood Knight Gauntlets": 0x139B2FF0, # SHOP ITEM + #"Millwood Knight Leggings": 0x139B33D8, # SHOP ITEM + + "Shira's Crown": 0x11C22260, + "Shira's Armor": 0x11C22648, + "Shira's Gloves": 0x11C22A30, + "Shira's Trousers": 0x11C22E18, + #"Lapp's Helm": 0x11E84800, # SHOP ITEM + #"Lapp's Armor": 0x11E84BE8, # SHOP ITEM + #"Lapp's Gauntlets": 0x11E84FD0, # SHOP ITEM + #"Lapp's Leggings": 0x11E853B8, # SHOP ITEM + #"Ringed Knight Hood": 0x13C8EEE0, # RANDOM ENEMY DROP + #"Ringed Knight Armor": 0x13C8F2C8, # RANDOM ENEMY DROP + #"Ringed Knight Gauntlets": 0x13C8F6B0, # RANDOM ENEMY DROP + #"Ringed Knight Leggings": 0x13C8FA98, # RANDOM ENEMY DROP + #"Harald Legion Armor": 0x13D83508, # RANDOM ENEMY DROP + #"Harald Legion Gauntlets": 0x13D838F0, # RANDOM ENEMY DROP + #"Harald Legion Leggings": 0x13D83CD8, # RANDOM ENEMY DROP + "Iron Dragonslayer Helm": 0x1405F7E0, + "Iron Dragonslayer Armor": 0x1405FBC8, + "Iron Dragonslayer Gauntlets": 0x1405FFB0, + "Iron Dragonslayer Leggings": 0x14060398, + + "Ruin Sentinel Helm": 0x14CC5520, + "Ruin Sentinel Armor": 0x14CC5908, + "Ruin Sentinel Gauntlets": 0x14CC5CF0, + "Ruin Sentinel Leggings": 0x14CC60D8, + "Desert Pyromancer Hood": 0x14DB9760, + "Desert Pyromancer Garb": 0x14DB9B48, + "Desert Pyromancer Gloves": 0x14DB9F30, + "Desert Pyromancer Skirt": 0x14DBA318, + + #"Follower Helm": 0x137CA3A0, # RANDOM ENEMY DROP + #"Follower Armor": 0x137CA788, # RANDOM ENEMY DROP + #"Follower Gloves": 0x137CAB70, # RANDOM ENEMY DROP + #"Follower Boots": 0x137CAF58, # RANDOM ENEMY DROP + #"Ordained Hood": 0x135E1F20, # SHOP ITEM + #"Ordained Dress": 0x135E2308, # SHOP ITEM + #"Ordained Trousers": 0x135E2AD8, # SHOP ITEM + "Black Witch Veil": 0x14FA1BE0, + "Black Witch Hat": 0x14EAD9A0, + "Black Witch Garb": 0x14EADD88, + "Black Witch Wrappings": 0x14EAE170, + "Black Witch Trousers": 0x14EAE558, + "White Preacher Head": 0x14153A20, + "Antiquated Plain Garb": 0x11B2E408 } rings_table = { @@ -314,6 +467,12 @@ rings_table = { "Dragonscale Ring": 0x2000515E, "Knight Slayer's Ring": 0x20005000, "Magic Stoneplate Ring": 0x20004E66, + "Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat +} + +dlc_ring_table = { + "Havel's Ring": 0x20004E34, + "Chillbite Ring": 0x20005208 } spells_table = { @@ -335,7 +494,21 @@ spells_table = { "Divine Pillars of Light": 0x4038C340, "Great Magic Barrier": 0x40365628, "Great Magic Shield": 0x40144F38, - "Crystal Scroll": 0x40000856, + "Crystal Scroll": 0x40000856 +} + +dlc_spells_table = { + #"Boulder Heave": 0x40282170, # KILN STRAY DEMON + #"Seething Chaos": 0x402896A0, # KILN DEMON PRINCES + #"Old Moonlight": 0x4014FF00, # KILN MIDIR + "Frozen Weapon": 0x401408E8, + "Snap Freeze": 0x401A90C8, + "Great Soul Dregs": 0x401879A0, + "Flame Fan": 0x40258190, + "Lightning Arrow": 0x40358B08, + "Way of White Corona": 0x403642A0, + "Projected Heal": 0x40364688, + "Floating Chaos": 0x40257DA8 } misc_items_table = { @@ -347,7 +520,7 @@ misc_items_table = { "Braille Divine Tome of Carim": 0x40000847, # Shop "Great Swamp Pyromancy Tome": 0x4000084F, # Shop "Farron Coal ": 0x40000837, # Shop - "Paladin's Ashes": 0x4000083D, #Shop + "Paladin's Ashes": 0x4000083D, # Shop "Deep Braille Divine Tome": 0x40000860, # Shop "Small Doll": 0x400007D5, "Golden Scroll": 0x4000085C, @@ -388,6 +561,12 @@ misc_items_table = { "Orbeck's Ashes": 0x40000840 } +dlc_misc_table = { + "Captains Ashes": 0x4000086A, + "Contraption Key": 0x4000086B, # Needed for Painted World + "Small Envoy Banner": 0x4000086C # Needed to get to Ringed City from Dreg Heap +} + key_items_list = { "Small Lothric Banner", "Basin of Vows", @@ -405,8 +584,17 @@ key_items_list = { "Prisoner Chief's Ashes", "Old Cell Key", "Jailer's Key Ring", + "Contraption Key", + "Small Envoy Banner" } -item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, armor_table, rings_table, spells_table, misc_items_table, goods_table] +item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, + armor_table, rings_table, spells_table, misc_items_table, goods_table, goods_2_table, goods_3_table, + dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table, dlc_shields_table, dlc_goods_table, + dlc_armor_table, dlc_spells_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table] + +item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, + **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table, **goods_2_table, + **goods_3_table, **dlc_weapons_upgrade_5_table, **dlc_weapons_upgrade_10_table, **dlc_shields_table, + **dlc_goods_table, **dlc_armor_table, **dlc_spells_table, **dlc_ring_table, **dlc_misc_table, **dlc_goods_2_table} -item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 654c7f0930..5a85916973 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -42,6 +42,7 @@ high_wall_of_lothric = { "HWL: Soul of the Dancer": 0x400002CA, "HWL: Way of Blue Covenant": 0x2000274C, "HWL: Greirat's Ashes": 0x4000083F, + "HWL: Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat } undead_settlement_table = { @@ -91,7 +92,11 @@ undead_settlement_table = { "US: Warrior of Sunlight Covenant": 0x20002738, "US: Blessed Red and White Shield": 0x01343FB9, "US: Irina's Ashes": 0x40000843, - "US: Cornyx's Ashes": 0x40000841 + "US: Cornyx's Ashes": 0x40000841, + "US: Cornyx's Wrap": 0x11946370, + "US: Cornyx's Garb": 0x11945F88, + "US: Cornyx's Skirt": 0x11946758, + "US: Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx } road_of_sacrifice_table = { @@ -437,6 +442,101 @@ archdragon_peak_table = { "AP: Havel's Greatshield": 0x013376F0, } +painted_world_table = { # DLC + "PW: Follower Javelin": 0x008CD6B0, + "PW: Frozen Weapon": 0x401408E8, + "PW: Millwood Greatbow": 0x00D85EA0, + "PW: Captains Ashes": 0x4000086A, + "PW: Millwood Battle Axe": 0x006D67D0, + "PW: Ethereal Oak Shield": 0x01450320, + "PW: Crow Quills": 0x00F66DF0, + "PW: Slave Knight Hood": 0x134EDCE0, + "PW: Slave Knight Armor": 0x134EE0C8, + "PW: Slave Knight Gauntlets": 0x134EE4B0, + "PW: Slave Knight Leggings": 0x134EE898, + "PW: Way of White Corona": 0x403642A0, + "PW: Crow Talons": 0x00A89C10, + "PW: Quakestone Hammer": 0x007ECCF0, + "PW: Earth Seeker": 0x006D8EE0, + "PW: Follower Torch": 0x015F1AD0, + "PW: Follower Shield": 0x0135C0E0, + "PW: Follower Sabre": 0x003EDDC0, + "PW: Snap Freeze": 0x401A90C8, + "PW: Floating Chaos": 0x40257DA8, + "PW: Pyromancer's Parting Flame": 0x00CC9ED0, + "PW: Vilhelm's Helm": 0x11312D00, + "PW: Vilhelm's Armor": 0x113130E8, + "PW: Vilhelm's Gauntlets": 0x113134D0, + "PW: Vilhelm's Leggings": 0x113138B8, + "PW: Vilhelm's Leggings": 0x113138B8, + "PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT + "PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT + "PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT + "PW: Soul of Sister Friede": 0x400002E8, + "PW: Titanite Slab": 0x400003EB, + "PW: Chillbite Ring": 0x20005208, + "PW: Contraption Key": 0x4000086B # VILHELM FIGHT/NEEDED TO PROGRESS THROUGH PW +} + +dreg_heap_table = { # DLC + "DH: Loincloth": 0x11B2EBD8, + "DH: Aquamarine Dagger": 0x00116520, + "DH: Murky Hand Scythe": 0x00118C30, + "DH: Murky Longstaff": 0x00CCC5E0, + "DH: Great Soul Dregs": 0x401879A0, + "DH: Lothric War Banner": 0x00CCC5E0, + "DH: Projected Heal": 0x40364688, + "DH: Desert Pyromancer Hood": 0x14DB9760, + "DH: Desert Pyromancer Garb": 0x14DB9B48, + "DH: Desert Pyromancer Gloves": 0x14DB9F30, + "DH: Desert Pyromancer Skirt": 0x14DBA318, + "DH: Giant Door Shield": 0x00F5F8C0, + "DH: Herald Curved Greatsword": 0x006159E0, + "DH: Flame Fan": 0x40258190, + "DH: Soul of the Demon Prince": 0x400002EA, + "DH: Small Envoy Banner": 0x4000086C # NEEDED TO TRAVEL TO RINGED CITY +} + +ringed_city_table = { # DLC + "RC: Ruin Sentinel Helm": 0x14CC5520, + "RC: Ruin Sentinel Armor": 0x14CC5908, + "RC: Ruin Sentinel Gauntlets": 0x14CC5CF0, + "RC: Ruin Sentinel Leggings": 0x14CC60D8, + "RC: Black Witch Veil": 0x14FA1BE0, + "RC: Black Witch Hat": 0x14EAD9A0, + "RC: Black Witch Garb": 0x14EADD88, + "RC: Black Witch Wrappings": 0x14EAE170, + "RC: Black Witch Trousers": 0x14EAE558, + "RC: White Preacher Head": 0x14153A20, + "RC: Havel's Ring": 0x20004E34, + "RC: Ringed Knight Spear": 0x008CFDC0, + "RC: Dragonhead Shield": 0x0135E7F0, + "RC: Ringed Knight Straight Sword": 0x00225510, + "RC: Preacher's Right Arm": 0x00CD1400, + "RC: White Birch Bow": 0x00D77440, + "RC: Church Guardian Shiv": 0x4000013B, # Assigned to "Demon's Scar" + "RC: Dragonhead Greatshield": 0x01452A30, + "RC: Ringed Knight Paired Greatswords": 0x00F69500, + "RC: Shira's Crown": 0x11C22260, + "RC: Shira's Armor": 0x11C22648, + "RC: Shira's Gloves": 0x11C22A30, + "RC: Shira's Trousers": 0x11C22E18, + "RC: Titanite Slab": 0x400003EB, # SHIRA DROP + "RC: Crucifix of the Mad King": 0x008D4BE0, # SHIRA DROP + "RC: Sacred Chime of Filianore": 0x00CCECF0, # SHIRA DROP + "RC: Iron Dragonslayer Helm": 0x1405F7E0, + "RC: Iron Dragonslayer Armor": 0x1405FBC8, + "RC: Iron Dragonslayer Gauntlets": 0x1405FFB0, + "RC: Iron Dragonslayer Leggings": 0x14060398, + "RC: Lightning Arrow": 0x40358B08, + "RC: Ritual Spear Fragment": 0x4000028A, # Assigned to "Frayed Blade" + "RC: Antiquated Plain Garb": 0x11B2E408, + "RC: Violet Wrappings": 0x11B2E7F0, # Assigned to "Gael's Greatsword" + "RC: Soul of Darkeater Midir": 0x400002EB, + "RC: Soul of Slave Knight Gael": 0x400002E9, + "RC: Blood of the Dark Souls": 0x4000086E, # Assigned to "Repeating Crossbow" +} + progressive_locations = { # Upgrade materials **{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)}, @@ -456,15 +556,60 @@ progressive_locations = { **{"Soul of a Deserted Corpse #" + str(i): 0x40000191 for i in range(1, 6)}, **{"Large Soul of a Deserted Corpse #" + str(i): 0x40000192 for i in range(1, 6)}, **{"Soul of an Unknown Traveler #" + str(i): 0x40000193 for i in range(1, 6)}, - **{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)}, + **{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)} +} + +progressive_locations_2 = { + ##Added by Br00ty + "HWL: Gold Pine Resin #": 0x4000014B, + "US: Charcoal Pine Resin #": 0x4000014A, + "FK: Gold Pine Bundle #": 0x40000155, + "CC: Carthus Rouge #": 0x4000014F, + "ID: Pale Pine Resin #": 0x40000150, + **{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 27)}, + **{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)}, + **{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)}, + **{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)}, + **{"Ember #"+str(i): 0x400001F4 for i in range(1, 46)}, +} + +progressive_locations_3 = { + **{"Green Blossom #" + str(i): 0x40000104 for i in range(1, 7)}, + **{"Human Pine Resin #" + str(i): 0x4000014E for i in range(1, 3)}, + **{"Charcoal Pine Bundle #" + str(i): 0x40000154 for i in range(1, 3)}, + **{"Rotten Pine Resin #" + str(i): 0x40000157 for i in range(1, 3)}, + **{"Pale Tongue #" + str(i): 0x40000175 for i in range(1, 3)}, + **{"Alluring Skull #" + str(i): 0x40000126 for i in range(1, 3)}, + **{"Undead Hunter Charm #" + str(i): 0x40000128 for i in range(1, 3)}, + **{"Duel Charm #" + str(i): 0x40000130 for i in range(1, 3)}, + **{"Rusted Coin #" + str(i): 0x400001C7 for i in range(1, 3)}, + **{"Rusted Gold Coin #" + str(i): 0x400001C9 for i in range(1, 4)}, + **{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)}, + **{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)} +} + +dlc_progressive_locations = { #71 + **{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)}, + **{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)}, + **{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)}, + **{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)}, + **{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)}, + **{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)}, + **{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)}, + **{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)}, + **{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)}, + **{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)}, + **{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)} } location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table, cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table, - grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations] + grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations, progressive_locations_2, progressive_locations_3, + painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations] location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, - **grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations} + **grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations, **progressive_locations_2, **progressive_locations_3, + **painted_world_table, **dreg_heap_table, **ringed_city_table, **dlc_progressive_locations} diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index 2effa5f124..3ad8236ccf 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -7,19 +7,20 @@ config file. ## What does randomization do to this game? -In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized. -This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at -the same location. I also added an option available from the settings page to randomize the level of the generated -weapons(from +0 to +10/+5) +In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are +randomized. +An option is available from the settings page to also randomize the upgrade materials, the Estus shards and the +consumables. +Another option is available to randomize the level of the generated weapons(from +0 to +10/+5) -To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld +To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld and kill the final boss "Soul of Cinder" ## What Dark Souls III items can appear in other players' worlds? -Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon +Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon, or a key item. ## What does another world's item look like in Dark Souls III? -In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone. \ No newline at end of file +In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone. diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index b4705ad5d5..8e1af8e92d 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -5,6 +5,10 @@ - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) - [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) +## Optional Software + +- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases) + ## General Concept The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command From 50425985c4c04a84938512fee2c3d368ea3891d6 Mon Sep 17 00:00:00 2001 From: Klenoa <116247993+Klenoa@users.noreply.github.com> Date: Sun, 26 Feb 2023 00:48:08 -0800 Subject: [PATCH 09/14] Subnautica: Rename location check (id 33029) (#1120) Update southwest grassy plateaus wreck to 'Grassy Plateaus Southwest Wreck - Databox' instead of 'Grassy Plateaus West Wreck - Databox' --- worlds/subnautica/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 9408094d2b..da92f8d481 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -140,7 +140,7 @@ location_table: Dict[int, LocationDict] = { 'need_laser_cutter': False, 'position': {'x': -664.4, 'y': -97.8, 'z': -8.0}}, 33029: {'can_slip_through': False, - 'name': 'Grassy Plateaus West Wreck - Databox', + 'name': 'Grassy Plateaus Southwest Wreck - Databox', 'need_laser_cutter': True, 'position': {'x': -421.4, 'y': -107.8, 'z': -266.5}}, 33030: {'can_slip_through': False, From 05e36cab1ca7508c9cb48bdae9e21e4d50f8abc0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 26 Feb 2023 09:48:41 +0100 Subject: [PATCH 10/14] Subnautica: increment version --- worlds/subnautica/__init__.py | 2 +- worlds/subnautica/test/__init__.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 worlds/subnautica/test/__init__.py diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 341af5a543..53c04fb3d8 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations option_definitions = Options.options - data_version = 8 + data_version = 9 required_client_version = (0, 3, 8) creatures_to_scan: List[str] diff --git a/worlds/subnautica/test/__init__.py b/worlds/subnautica/test/__init__.py new file mode 100644 index 0000000000..ea7fbb61d6 --- /dev/null +++ b/worlds/subnautica/test/__init__.py @@ -0,0 +1,15 @@ +import unittest +from worlds import subnautica + + +class SubnauticaTest(unittest.TestCase): + # This is an assumption in the mod side + scancutoff: int = 33999 + + def testIDRange(self): + for id, name in subnautica.SubnauticaWorld.location_name_to_id.items(): + with self.subTest(item=name): + if "Scan" in name: + self.assertGreater(self.scancutoff, id) + else: + self.assertLess(self.scancutoff, id) From 0286edf20cf08335228b8d1de07cadcf3aeba4e1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 26 Feb 2023 09:51:02 +0100 Subject: [PATCH 11/14] Subnautica: fix the test that I didn't mean to push yet --- worlds/subnautica/test/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/subnautica/test/__init__.py b/worlds/subnautica/test/__init__.py index ea7fbb61d6..b10ca2c7c5 100644 --- a/worlds/subnautica/test/__init__.py +++ b/worlds/subnautica/test/__init__.py @@ -7,9 +7,9 @@ class SubnauticaTest(unittest.TestCase): scancutoff: int = 33999 def testIDRange(self): - for id, name in subnautica.SubnauticaWorld.location_name_to_id.items(): + for name, id in subnautica.SubnauticaWorld.location_name_to_id.items(): with self.subTest(item=name): if "Scan" in name: - self.assertGreater(self.scancutoff, id) - else: self.assertLess(self.scancutoff, id) + else: + self.assertGreater(self.scancutoff, id) From af7d0dbf37014c348261d01a872fcdef3711519c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sun, 26 Feb 2023 19:19:15 -0500 Subject: [PATCH 12/14] Stardew Valley: implement new game (#1455) * Stardew Valley Archipelago implementation * fix breaking changes * - Added and Updated Documentation for the game * Removed fun * Remove entire idea of step, due to possible inconsistency with the main AP core * Commented out the desired steps, fix renaming after rebase * Fixed wording * tests now passes on 3.8 * run flake8 * remove dependency so apworld work again * remove dependency for real * - Fix Formatting in the Game Page - Removed disabled Option Descriptions for Entrance Randomizer - Improved Game Page's description of the Arcade Machine buffs - Trimmed down the text on the Options page for Arcade Machines, so that it is smaller * - Removed blankspace * remove player field * remove None check in options * document the scripts * fix pytest warning * use importlib.resources.files * fix * add version requirement to importlib_resources * remove __init__.py from data folder * increment data version * let the __init__.py for 3.9 * use sorted() instead of list() * replace frozenset from fish_data with tuples * remove dependency on pytest * - Add a bit of text to the guide to tell them about how to redeem some received items * - Added a comment about which mod version to use * change single quotes for double quotes * Minimum client version both ways * Changed version number to be more specific. The mod will handle deciding --------- Co-authored-by: Alex Gilbert --- setup.py | 1 + test/TestBase.py | 2 +- worlds/stardew_valley/__init__.py | 188 +++ worlds/stardew_valley/bundle_data.py | 414 ++++++ worlds/stardew_valley/bundles.py | 254 ++++ worlds/stardew_valley/data/__init__.py | 0 worlds/stardew_valley/data/items.csv | 312 +++++ worlds/stardew_valley/data/locations.csv | 379 ++++++ worlds/stardew_valley/data/resource_packs.csv | 39 + .../stardew_valley/docs/en_Stardew Valley.md | 71 + worlds/stardew_valley/docs/setup_en.md | 74 ++ worlds/stardew_valley/fish_data.py | 127 ++ worlds/stardew_valley/game_item.py | 26 + worlds/stardew_valley/items.py | 376 ++++++ worlds/stardew_valley/locations.py | 175 +++ worlds/stardew_valley/logic.py | 1143 +++++++++++++++++ worlds/stardew_valley/options.py | 409 ++++++ worlds/stardew_valley/regions.py | 291 +++++ worlds/stardew_valley/requirements.txt | 1 + worlds/stardew_valley/rules.py | 190 +++ worlds/stardew_valley/scripts/__init__.py | 0 worlds/stardew_valley/scripts/export_items.py | 26 + .../scripts/export_locations.py | 26 + worlds/stardew_valley/scripts/update_data.py | 88 ++ worlds/stardew_valley/test/TestAllLogic.py | 53 + worlds/stardew_valley/test/TestBundles.py | 16 + worlds/stardew_valley/test/TestData.py | 20 + worlds/stardew_valley/test/TestGeneration.py | 127 ++ worlds/stardew_valley/test/TestItems.py | 26 + worlds/stardew_valley/test/TestLogic.py | 293 +++++ .../test/TestLogicSimplification.py | 52 + worlds/stardew_valley/test/TestRegions.py | 46 + .../stardew_valley/test/TestResourcePack.py | 76 ++ worlds/stardew_valley/test/__init__.py | 14 + 34 files changed, 5334 insertions(+), 1 deletion(-) create mode 100644 worlds/stardew_valley/__init__.py create mode 100644 worlds/stardew_valley/bundle_data.py create mode 100644 worlds/stardew_valley/bundles.py create mode 100644 worlds/stardew_valley/data/__init__.py create mode 100644 worlds/stardew_valley/data/items.csv create mode 100644 worlds/stardew_valley/data/locations.csv create mode 100644 worlds/stardew_valley/data/resource_packs.csv create mode 100644 worlds/stardew_valley/docs/en_Stardew Valley.md create mode 100644 worlds/stardew_valley/docs/setup_en.md create mode 100644 worlds/stardew_valley/fish_data.py create mode 100644 worlds/stardew_valley/game_item.py create mode 100644 worlds/stardew_valley/items.py create mode 100644 worlds/stardew_valley/locations.py create mode 100644 worlds/stardew_valley/logic.py create mode 100644 worlds/stardew_valley/options.py create mode 100644 worlds/stardew_valley/regions.py create mode 100644 worlds/stardew_valley/requirements.txt create mode 100644 worlds/stardew_valley/rules.py create mode 100644 worlds/stardew_valley/scripts/__init__.py create mode 100644 worlds/stardew_valley/scripts/export_items.py create mode 100644 worlds/stardew_valley/scripts/export_locations.py create mode 100644 worlds/stardew_valley/scripts/update_data.py create mode 100644 worlds/stardew_valley/test/TestAllLogic.py create mode 100644 worlds/stardew_valley/test/TestBundles.py create mode 100644 worlds/stardew_valley/test/TestData.py create mode 100644 worlds/stardew_valley/test/TestGeneration.py create mode 100644 worlds/stardew_valley/test/TestItems.py create mode 100644 worlds/stardew_valley/test/TestLogic.py create mode 100644 worlds/stardew_valley/test/TestLogicSimplification.py create mode 100644 worlds/stardew_valley/test/TestRegions.py create mode 100644 worlds/stardew_valley/test/TestResourcePack.py create mode 100644 worlds/stardew_valley/test/__init__.py diff --git a/setup.py b/setup.py index 382d3dc599..509981da37 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ apworlds: set = { "Rogue Legacy", "Donkey Kong Country 3", "Super Mario World", + "Stardew Valley", "Timespinner", } diff --git a/test/TestBase.py b/test/TestBase.py index 5ffcb5ce4d..eea8e81a55 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -1,6 +1,6 @@ +import pathlib import typing import unittest -import pathlib from argparse import Namespace import Utils diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py new file mode 100644 index 0000000000..306a3ec7e0 --- /dev/null +++ b/worlds/stardew_valley/__init__.py @@ -0,0 +1,188 @@ +from typing import Dict, Any, Iterable, Optional, Union + +from BaseClasses import Region, Entrance, Location, Item, Tutorial +from worlds.AutoWorld import World, WebWorld +from . import rules, logic, options +from .bundles import get_all_bundles, Bundle +from .items import item_table, create_items, ItemData, Group +from .locations import location_table, create_locations, LocationData +from .logic import StardewLogic, StardewRule, _True, _And +from .options import stardew_valley_options, StardewOptions, fetch_options +from .regions import create_regions +from .rules import set_rules + +client_version = 0 + + +class StardewLocation(Location): + game: str = "Stardew Valley" + + def __init__(self, player: int, name: str, address: Optional[int], parent=None): + super().__init__(player, name, address, parent) + self.event = not address + + +class StardewItem(Item): + game: str = "Stardew Valley" + + +class StardewWebWorld(WebWorld): + theme = "dirt" + bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" + + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Stardew Valley with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["KaitoKid", "Jouramie"] + )] + + +class StardewValleyWorld(World): + """ + Stardew Valley farming simulator game where the objective is basically to spend the least possible time on your farm. + """ + game = "Stardew Valley" + option_definitions = stardew_valley_options + topology_present = False + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = {name: data.code for name, data in location_table.items()} + + data_version = 1 + required_client_version = (0, 3, 9) + + options: StardewOptions + logic: StardewLogic + + web = StardewWebWorld() + modified_bundles: Dict[str, Bundle] + randomized_entrances: Dict[str, str] + + def generate_early(self): + self.options = fetch_options(self.multiworld, self.player) + self.logic = StardewLogic(self.player, self.options) + self.modified_bundles = get_all_bundles(self.multiworld.random, + self.logic, + self.options[options.BundleRandomization], + self.options[options.BundlePrice]) + + def create_regions(self): + def create_region(name: str, exits: Iterable[str]) -> Region: + region = Region(name, self.player, self.multiworld) + region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits] + return region + + world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options) + self.multiworld.regions.extend(world_regions) + + def add_location(name: str, code: Optional[int], region: str): + region = self.multiworld.get_region(region, self.player) + location = StardewLocation(self.player, name, code, region) + location.access_rule = lambda _: True + region.locations.append(location) + + create_locations(add_location, self.options, self.multiworld.random) + + def create_items(self): + locations_count = len([location + for location in self.multiworld.get_locations(self.player) + if not location.event]) + items_to_exclude = [excluded_items + for excluded_items in self.multiworld.precollected_items[self.player] + if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, + Group.FRIENDSHIP_PACK)] + created_items = create_items(self.create_item, locations_count + len(items_to_exclude), self.options, + self.multiworld.random) + self.multiworld.itempool += created_items + + for item in items_to_exclude: + self.multiworld.itempool.remove(item) + + self.setup_season_events() + self.setup_victory() + + def set_rules(self): + set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + + def create_item(self, item: Union[str, ItemData]) -> StardewItem: + if isinstance(item, str): + item = item_table[item] + + return StardewItem(item.name, item.classification, item.code, self.player) + + def setup_season_events(self): + self.multiworld.push_precollected(self.create_item("Spring")) + self.create_event_location(location_table["Summer"], self.logic.received("Spring"), "Summer") + self.create_event_location(location_table["Fall"], self.logic.received("Summer"), "Fall") + self.create_event_location(location_table["Winter"], self.logic.received("Fall"), "Winter") + self.create_event_location(location_table["Year Two"], self.logic.received("Winter"), "Year Two") + + def setup_victory(self): + if self.options[options.Goal] == options.Goal.option_community_center: + self.create_event_location(location_table["Complete Community Center"], + self.logic.can_complete_community_center().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation: + self.create_event_location(location_table["Succeed Grandpa's Evaluation"], + self.logic.can_finish_grandpa_evaluation().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines: + self.create_event_location(location_table["Reach the Bottom of The Mines"], + self.logic.can_mine_to_floor(120).simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_cryptic_note: + self.create_event_location(location_table["Complete Quest Cryptic Note"], + self.logic.can_complete_quest("Cryptic Note").simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_master_angler: + self.create_event_location(location_table["Catch Every Fish"], + self.logic.can_catch_every_fish().simplify(), + "Victory") + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str): + region = self.multiworld.get_region(location_data.region, self.player) + location = StardewLocation(self.player, location_data.name, None, region) + location.access_rule = rule + region.locations.append(location) + location.place_locked_item(self.create_item(item)) + + def get_filler_item_name(self) -> str: + return "Joja Cola" + + def fill_slot_data(self) -> Dict[str, Any]: + + modified_bundles = {} + for bundle_key in self.modified_bundles: + key, value = self.modified_bundles[bundle_key].to_pair() + modified_bundles[key] = value + + return { + "starting_money": self.options[options.StartingMoney], + "entrance_randomization": self.options[options.EntranceRandomization], + "backpack_progression": self.options[options.BackpackProgression], + "tool_progression": self.options[options.ToolProgression], + "elevator_progression": self.options[options.TheMinesElevatorsProgression], + "skill_progression": self.options[options.SkillProgression], + "building_progression": self.options[options.BuildingProgression], + "arcade_machine_progression": self.options[options.ArcadeMachineLocations], + "help_wanted_locations": self.options[options.HelpWantedLocations], + "fishsanity": self.options[options.Fishsanity], + "death_link": self.options["death_link"], + "goal": self.options[options.Goal], + "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits + "multiple_day_sleep_enabled": self.options[options.MultipleDaySleepEnabled], + "multiple_day_sleep_cost": self.options[options.MultipleDaySleepCost], + "experience_multiplier": self.options[options.ExperienceMultiplier], + "debris_multiplier": self.options[options.DebrisMultiplier], + "quick_start": self.options[options.QuickStart], + "gifting": self.options[options.Gifting], + "gift_tax": self.options[options.GiftTax], + "modified_bundles": modified_bundles, + "randomized_entrances": self.randomized_entrances, + "client_version": "2.2.2", + } diff --git a/worlds/stardew_valley/bundle_data.py b/worlds/stardew_valley/bundle_data.py new file mode 100644 index 0000000000..cfc5d482ad --- /dev/null +++ b/worlds/stardew_valley/bundle_data.py @@ -0,0 +1,414 @@ +from dataclasses import dataclass + +from . import fish_data +from .game_item import GameItem + +quality_dict = { + 0: "", + 1: "Silver", + 2: "Gold", + 3: "Iridium" +} + + +@dataclass(frozen=True) +class BundleItem: + item: GameItem + amount: int + quality: int + + @staticmethod + def item_bundle(name: str, item_id: int, amount: int, quality: int): + return BundleItem(GameItem(name, item_id), amount, quality) + + @staticmethod + def money_bundle(amount: int): + return BundleItem.item_bundle("Money", -1, amount, amount) + + def as_amount(self, amount: int): + return BundleItem.item_bundle(self.item.name, self.item.item_id, amount, self.quality) + + def as_quality(self, quality: int): + return BundleItem.item_bundle(self.item.name, self.item.item_id, self.amount, quality) + + def __repr__(self): + return f"{self.amount} {quality_dict[self.quality]} {self.item.name}" + + def __lt__(self, other): + return self.item < other.item + + +wild_horseradish = BundleItem.item_bundle("Wild Horseradish", 16, 1, 0) +daffodil = BundleItem.item_bundle("Daffodil", 18, 1, 0) +leek = BundleItem.item_bundle("Leek", 20, 1, 0) +dandelion = BundleItem.item_bundle("Dandelion", 22, 1, 0) +morel = BundleItem.item_bundle("Morel", 257, 1, 0) +common_mushroom = BundleItem.item_bundle("Common Mushroom", 404, 1, 0) +salmonberry = BundleItem.item_bundle("Salmonberry", 296, 1, 0) +spring_onion = BundleItem.item_bundle("Spring Onion", 399, 1, 0) + +grape = BundleItem.item_bundle("Grape", 398, 1, 0) +spice_berry = BundleItem.item_bundle("Spice Berry", 396, 1, 0) +sweet_pea = BundleItem.item_bundle("Sweet Pea", 402, 1, 0) +red_mushroom = BundleItem.item_bundle("Red Mushroom", 420, 1, 0) +fiddlehead_fern = BundleItem.item_bundle("Fiddlehead Fern", 259, 1, 0) + +wild_plum = BundleItem.item_bundle("Wild Plum", 406, 1, 0) +hazelnut = BundleItem.item_bundle("Hazelnut", 408, 1, 0) +blackberry = BundleItem.item_bundle("Blackberry", 410, 1, 0) +chanterelle = BundleItem.item_bundle("Chanterelle", 281, 1, 0) + +winter_root = BundleItem.item_bundle("Winter Root", 412, 1, 0) +crystal_fruit = BundleItem.item_bundle("Crystal Fruit", 414, 1, 0) +snow_yam = BundleItem.item_bundle("Snow Yam", 416, 1, 0) +crocus = BundleItem.item_bundle("Crocus", 418, 1, 0) +holly = BundleItem.item_bundle("Holly", 283, 1, 0) + +coconut = BundleItem.item_bundle("Coconut", 88, 1, 0) +cactus_fruit = BundleItem.item_bundle("Cactus Fruit", 90, 1, 0) +cave_carrot = BundleItem.item_bundle("Cave Carrot", 78, 1, 0) +purple_mushroom = BundleItem.item_bundle("Purple Mushroom", 422, 1, 0) +maple_syrup = BundleItem.item_bundle("Maple Syrup", 724, 1, 0) +oak_resin = BundleItem.item_bundle("Oak Resin", 725, 1, 0) +pine_tar = BundleItem.item_bundle("Pine Tar", 726, 1, 0) +nautilus_shell = BundleItem.item_bundle("Nautilus Shell", 392, 1, 0) +coral = BundleItem.item_bundle("Coral", 393, 1, 0) +sea_urchin = BundleItem.item_bundle("Sea Urchin", 397, 1, 0) +rainbow_shell = BundleItem.item_bundle("Rainbow Shell", 394, 1, 0) +clam = BundleItem(fish_data.clam, 1, 0) +cockle = BundleItem(fish_data.cockle, 1, 0) +mussel = BundleItem(fish_data.mussel, 1, 0) +oyster = BundleItem(fish_data.oyster, 1, 0) +seaweed = BundleItem.item_bundle("Seaweed", 152, 1, 0) + +wood = BundleItem.item_bundle("Wood", 388, 99, 0) +stone = BundleItem.item_bundle("Stone", 390, 99, 0) +hardwood = BundleItem.item_bundle("Hardwood", 709, 10, 0) +clay = BundleItem.item_bundle("Clay", 330, 10, 0) +fiber = BundleItem.item_bundle("Fiber", 771, 99, 0) + +blue_jazz = BundleItem.item_bundle("Blue Jazz", 597, 1, 0) +cauliflower = BundleItem.item_bundle("Cauliflower", 190, 1, 0) +green_bean = BundleItem.item_bundle("Green Bean", 188, 1, 0) +kale = BundleItem.item_bundle("Kale", 250, 1, 0) +parsnip = BundleItem.item_bundle("Parsnip", 24, 1, 0) +potato = BundleItem.item_bundle("Potato", 192, 1, 0) +strawberry = BundleItem.item_bundle("Strawberry", 400, 1, 0) +tulip = BundleItem.item_bundle("Tulip", 591, 1, 0) +unmilled_rice = BundleItem.item_bundle("Unmilled Rice", 271, 1, 0) +blueberry = BundleItem.item_bundle("Blueberry", 258, 1, 0) +corn = BundleItem.item_bundle("Corn", 270, 1, 0) +hops = BundleItem.item_bundle("Hops", 304, 1, 0) +hot_pepper = BundleItem.item_bundle("Hot Pepper", 260, 1, 0) +melon = BundleItem.item_bundle("Melon", 254, 1, 0) +poppy = BundleItem.item_bundle("Poppy", 376, 1, 0) +radish = BundleItem.item_bundle("Radish", 264, 1, 0) +summer_spangle = BundleItem.item_bundle("Summer Spangle", 593, 1, 0) +sunflower = BundleItem.item_bundle("Sunflower", 421, 1, 0) +tomato = BundleItem.item_bundle("Tomato", 256, 1, 0) +wheat = BundleItem.item_bundle("Wheat", 262, 1, 0) +hay = BundleItem.item_bundle("Hay", 178, 1, 0) +amaranth = BundleItem.item_bundle("Amaranth", 300, 1, 0) +bok_choy = BundleItem.item_bundle("Bok Choy", 278, 1, 0) +cranberries = BundleItem.item_bundle("Cranberries", 282, 1, 0) +eggplant = BundleItem.item_bundle("Eggplant", 272, 1, 0) +fairy_rose = BundleItem.item_bundle("Fairy Rose", 595, 1, 0) +pumpkin = BundleItem.item_bundle("Pumpkin", 276, 1, 0) +yam = BundleItem.item_bundle("Yam", 280, 1, 0) +sweet_gem_berry = BundleItem.item_bundle("Sweet Gem Berry", 417, 1, 0) +rhubarb = BundleItem.item_bundle("Rhubarb", 252, 1, 0) +beet = BundleItem.item_bundle("Beet", 284, 1, 0) +red_cabbage = BundleItem.item_bundle("Red Cabbage", 266, 1, 0) +artichoke = BundleItem.item_bundle("Artichoke", 274, 1, 0) + +egg = BundleItem.item_bundle("Egg", 176, 1, 0) +large_egg = BundleItem.item_bundle("Large Egg", 174, 1, 0) +brown_egg = BundleItem.item_bundle("Egg (Brown)", 180, 1, 0) +large_brown_egg = BundleItem.item_bundle("Large Egg (Brown)", 182, 1, 0) +wool = BundleItem.item_bundle("Wool", 440, 1, 0) +milk = BundleItem.item_bundle("Milk", 184, 1, 0) +large_milk = BundleItem.item_bundle("Large Milk", 186, 1, 0) +goat_milk = BundleItem.item_bundle("Goat Milk", 436, 1, 0) +large_goat_milk = BundleItem.item_bundle("Large Goat Milk", 438, 1, 0) +truffle = BundleItem.item_bundle("Truffle", 430, 1, 0) +duck_feather = BundleItem.item_bundle("Duck Feather", 444, 1, 0) +duck_egg = BundleItem.item_bundle("Duck Egg", 442, 1, 0) +rabbit_foot = BundleItem.item_bundle("Rabbit's Foot", 446, 1, 0) + +truffle_oil = BundleItem.item_bundle("Truffle Oil", 432, 1, 0) +cloth = BundleItem.item_bundle("Cloth", 428, 1, 0) +goat_cheese = BundleItem.item_bundle("Goat Cheese", 426, 1, 0) +cheese = BundleItem.item_bundle("Cheese", 424, 1, 0) +honey = BundleItem.item_bundle("Honey", 340, 1, 0) +beer = BundleItem.item_bundle("Beer", 346, 1, 0) +juice = BundleItem.item_bundle("Juice", 350, 1, 0) +mead = BundleItem.item_bundle("Mead", 459, 1, 0) +pale_ale = BundleItem.item_bundle("Pale Ale", 303, 1, 0) +wine = BundleItem.item_bundle("Wine", 348, 1, 0) +jelly = BundleItem.item_bundle("Jelly", 344, 1, 0) +pickles = BundleItem.item_bundle("Pickles", 342, 1, 0) +caviar = BundleItem.item_bundle("Caviar", 445, 1, 0) +aged_roe = BundleItem.item_bundle("Aged Roe", 447, 1, 0) +apple = BundleItem.item_bundle("Apple", 613, 1, 0) +apricot = BundleItem.item_bundle("Apricot", 634, 1, 0) +orange = BundleItem.item_bundle("Orange", 635, 1, 0) +peach = BundleItem.item_bundle("Peach", 636, 1, 0) +pomegranate = BundleItem.item_bundle("Pomegranate", 637, 1, 0) +cherry = BundleItem.item_bundle("Cherry", 638, 1, 0) +lobster = BundleItem(fish_data.lobster, 1, 0) +crab = BundleItem(fish_data.crab, 1, 0) +shrimp = BundleItem(fish_data.shrimp, 1, 0) +crayfish = BundleItem(fish_data.crayfish, 1, 0) +snail = BundleItem(fish_data.snail, 1, 0) +periwinkle = BundleItem(fish_data.periwinkle, 1, 0) +trash = BundleItem.item_bundle("Trash", 168, 1, 0) +driftwood = BundleItem.item_bundle("Driftwood", 169, 1, 0) +soggy_newspaper = BundleItem.item_bundle("Soggy Newspaper", 172, 1, 0) +broken_cd = BundleItem.item_bundle("Broken CD", 171, 1, 0) +broken_glasses = BundleItem.item_bundle("Broken Glasses", 170, 1, 0) + +chub = BundleItem(fish_data.chub, 1, 0) +catfish = BundleItem(fish_data.catfish, 1, 0) +rainbow_trout = BundleItem(fish_data.rainbow_trout, 1, 0) +lingcod = BundleItem(fish_data.lingcod, 1, 0) +walleye = BundleItem(fish_data.walleye, 1, 0) +perch = BundleItem(fish_data.perch, 1, 0) +pike = BundleItem(fish_data.pike, 1, 0) +bream = BundleItem(fish_data.bream, 1, 0) +salmon = BundleItem(fish_data.salmon, 1, 0) +sunfish = BundleItem(fish_data.sunfish, 1, 0) +tiger_trout = BundleItem(fish_data.tiger_trout, 1, 0) +shad = BundleItem(fish_data.shad, 1, 0) +smallmouth_bass = BundleItem(fish_data.smallmouth_bass, 1, 0) +dorado = BundleItem(fish_data.dorado, 1, 0) +carp = BundleItem(fish_data.carp, 1, 0) +midnight_carp = BundleItem(fish_data.midnight_carp, 1, 0) +largemouth_bass = BundleItem(fish_data.largemouth_bass, 1, 0) +sturgeon = BundleItem(fish_data.sturgeon, 1, 0) +bullhead = BundleItem(fish_data.bullhead, 1, 0) +tilapia = BundleItem(fish_data.tilapia, 1, 0) +pufferfish = BundleItem(fish_data.pufferfish, 1, 0) +tuna = BundleItem(fish_data.tuna, 1, 0) +super_cucumber = BundleItem(fish_data.super_cucumber, 1, 0) +flounder = BundleItem(fish_data.flounder, 1, 0) +anchovy = BundleItem(fish_data.anchovy, 1, 0) +sardine = BundleItem(fish_data.sardine, 1, 0) +red_mullet = BundleItem(fish_data.red_mullet, 1, 0) +herring = BundleItem(fish_data.herring, 1, 0) +eel = BundleItem(fish_data.eel, 1, 0) +octopus = BundleItem(fish_data.octopus, 1, 0) +red_snapper = BundleItem(fish_data.red_snapper, 1, 0) +squid = BundleItem(fish_data.squid, 1, 0) +sea_cucumber = BundleItem(fish_data.sea_cucumber, 1, 0) +albacore = BundleItem(fish_data.albacore, 1, 0) +halibut = BundleItem(fish_data.halibut, 1, 0) +scorpion_carp = BundleItem(fish_data.scorpion_carp, 1, 0) +sandfish = BundleItem(fish_data.sandfish, 1, 0) +woodskip = BundleItem(fish_data.woodskip, 1, 0) +lava_eel = BundleItem(fish_data.lava_eel, 1, 0) +ice_pip = BundleItem(fish_data.ice_pip, 1, 0) +stonefish = BundleItem(fish_data.stonefish, 1, 0) +ghostfish = BundleItem(fish_data.ghostfish, 1, 0) + +wilted_bouquet = BundleItem.item_bundle("Wilted Bouquet", 277, 1, 0) +copper_bar = BundleItem.item_bundle("Copper Bar", 334, 2, 0) +iron_Bar = BundleItem.item_bundle("Iron Bar", 335, 2, 0) +gold_bar = BundleItem.item_bundle("Gold Bar", 336, 1, 0) +iridium_bar = BundleItem.item_bundle("Iridium Bar", 337, 1, 0) +refined_quartz = BundleItem.item_bundle("Refined Quartz", 338, 2, 0) +coal = BundleItem.item_bundle("Coal", 382, 5, 0) + +quartz = BundleItem.item_bundle("Quartz", 80, 1, 0) +fire_quartz = BundleItem.item_bundle("Fire Quartz", 82, 1, 0) +frozen_tear = BundleItem.item_bundle("Frozen Tear", 84, 1, 0) +earth_crystal = BundleItem.item_bundle("Earth Crystal", 86, 1, 0) +emerald = BundleItem.item_bundle("Emerald", 60, 1, 0) +aquamarine = BundleItem.item_bundle("Aquamarine", 62, 1, 0) +ruby = BundleItem.item_bundle("Ruby", 64, 1, 0) +amethyst = BundleItem.item_bundle("Amethyst", 66, 1, 0) +topaz = BundleItem.item_bundle("Topaz", 68, 1, 0) +jade = BundleItem.item_bundle("Jade", 70, 1, 0) + +slime = BundleItem.item_bundle("Slime", 766, 99, 0) +bug_meat = BundleItem.item_bundle("Bug Meat", 684, 10, 0) +bat_wing = BundleItem.item_bundle("Bat Wing", 767, 10, 0) +solar_essence = BundleItem.item_bundle("Solar Essence", 768, 1, 0) +void_essence = BundleItem.item_bundle("Void Essence", 769, 1, 0) + +maki_roll = BundleItem.item_bundle("Maki Roll", 228, 1, 0) +fried_egg = BundleItem.item_bundle("Fried Egg", 194, 1, 0) +omelet = BundleItem.item_bundle("Omelet", 195, 1, 0) +pizza = BundleItem.item_bundle("Pizza", 206, 1, 0) +hashbrowns = BundleItem.item_bundle("Hashbrowns", 210, 1, 0) +pancakes = BundleItem.item_bundle("Pancakes", 211, 1, 0) +bread = BundleItem.item_bundle("Bread", 216, 1, 0) +tortilla = BundleItem.item_bundle("Tortilla", 229, 1, 0) +triple_shot_espresso = BundleItem.item_bundle("Triple Shot Espresso", 253, 1, 0) +farmer_s_lunch = BundleItem.item_bundle("Farmer's Lunch", 240, 1, 0) +survival_burger = BundleItem.item_bundle("Survival Burger", 241, 1, 0) +dish_o_the_sea = BundleItem.item_bundle("Dish O' The Sea", 242, 1, 0) +miner_s_treat = BundleItem.item_bundle("Miner's Treat", 243, 1, 0) +roots_platter = BundleItem.item_bundle("Roots Platter", 244, 1, 0) +salad = BundleItem.item_bundle("Salad", 196, 1, 0) +cheese_cauliflower = BundleItem.item_bundle("Cheese Cauliflower", 197, 1, 0) +parsnip_soup = BundleItem.item_bundle("Parsnip Soup", 199, 1, 0) +fried_mushroom = BundleItem.item_bundle("Fried Mushroom", 205, 1, 0) +salmon_dinner = BundleItem.item_bundle("Salmon Dinner", 212, 1, 0) +pepper_poppers = BundleItem.item_bundle("Pepper Poppers", 215, 1, 0) +spaghetti = BundleItem.item_bundle("Spaghetti", 224, 1, 0) +sashimi = BundleItem.item_bundle("Sashimi", 227, 1, 0) +blueberry_tart = BundleItem.item_bundle("Blueberry Tart", 234, 1, 0) +algae_soup = BundleItem.item_bundle("Algae Soup", 456, 1, 0) +pale_broth = BundleItem.item_bundle("Pale Broth", 457, 1, 0) +chowder = BundleItem.item_bundle("Chowder", 727, 1, 0) +green_algae = BundleItem.item_bundle("Green Algae", 153, 1, 0) +white_algae = BundleItem.item_bundle("White Algae", 157, 1, 0) +geode = BundleItem.item_bundle("Geode", 535, 1, 0) +frozen_geode = BundleItem.item_bundle("Frozen Geode", 536, 1, 0) +magma_geode = BundleItem.item_bundle("Magma Geode", 537, 1, 0) +omni_geode = BundleItem.item_bundle("Omni Geode", 749, 1, 0) + +spring_foraging_items = [wild_horseradish, daffodil, leek, dandelion, salmonberry, spring_onion] +summer_foraging_items = [grape, spice_berry, sweet_pea, fiddlehead_fern, rainbow_shell] +fall_foraging_items = [common_mushroom, wild_plum, hazelnut, blackberry] +winter_foraging_items = [winter_root, crystal_fruit, snow_yam, crocus, holly, nautilus_shell] +exotic_foraging_items = [coconut, cactus_fruit, cave_carrot, red_mushroom, purple_mushroom, + maple_syrup, oak_resin, pine_tar, morel, coral, + sea_urchin, clam, cockle, mussel, oyster, seaweed] +construction_items = [wood, stone, hardwood, clay, fiber] + +# TODO coffee_bean, garlic, rhubarb, tea_leaves +spring_crop_items = [blue_jazz, cauliflower, green_bean, kale, parsnip, potato, strawberry, tulip, unmilled_rice] +# TODO red_cabbage, starfruit, ancient_fruit, pineapple, taro_root +summer_crops_items = [blueberry, corn, hops, hot_pepper, melon, poppy, + radish, summer_spangle, sunflower, tomato, wheat] +# TODO artichoke, beet +fall_crops_items = [corn, sunflower, wheat, amaranth, bok_choy, cranberries, + eggplant, fairy_rose, grape, pumpkin, yam, sweet_gem_berry] +all_crops_items = sorted({*spring_crop_items, *summer_crops_items, *fall_crops_items}) +quality_crops_items = [item.as_quality(2).as_amount(5) for item in all_crops_items] +# TODO void_egg, dinosaur_egg, ostrich_egg, golden_egg +animal_product_items = [egg, large_egg, brown_egg, large_brown_egg, wool, milk, large_milk, + goat_milk, large_goat_milk, truffle, duck_feather, duck_egg, rabbit_foot] +# TODO coffee, green_tea +artisan_goods_items = [truffle_oil, cloth, goat_cheese, cheese, honey, beer, juice, mead, pale_ale, wine, jelly, + pickles, caviar, aged_roe, apple, apricot, orange, peach, pomegranate, cherry] + +river_fish_items = [chub, catfish, rainbow_trout, lingcod, walleye, perch, pike, bream, + salmon, sunfish, tiger_trout, shad, smallmouth_bass, dorado] +lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, + largemouth_bass, sturgeon, bullhead, midnight_carp] +ocean_fish_items = [tilapia, pufferfish, tuna, super_cucumber, flounder, anchovy, sardine, red_mullet, + herring, eel, octopus, red_snapper, squid, sea_cucumber, albacore, halibut] +night_fish_items = [walleye, bream, super_cucumber, eel, squid, midnight_carp] +# TODO void_salmon +specialty_fish_items = [scorpion_carp, sandfish, woodskip, pufferfish, eel, octopus, + squid, lava_eel, ice_pip, stonefish, ghostfish, dorado] +crab_pot_items = [lobster, clam, crab, cockle, mussel, shrimp, oyster, crayfish, snail, + periwinkle, trash, driftwood, soggy_newspaper, broken_cd, broken_glasses] + +# TODO radioactive_bar +blacksmith_items = [wilted_bouquet, copper_bar, iron_Bar, gold_bar, iridium_bar, refined_quartz, coal] +geologist_items = [quartz, earth_crystal, frozen_tear, fire_quartz, emerald, aquamarine, ruby, amethyst, topaz, jade] +adventurer_items = [slime, bug_meat, bat_wing, solar_essence, void_essence, coal] + +chef_items = [maki_roll, fried_egg, omelet, pizza, hashbrowns, pancakes, bread, tortilla, triple_shot_espresso, + farmer_s_lunch, survival_burger, dish_o_the_sea, miner_s_treat, roots_platter, salad, + cheese_cauliflower, parsnip_soup, fried_mushroom, salmon_dinner, pepper_poppers, spaghetti, + sashimi, blueberry_tart, algae_soup, pale_broth, chowder] + +dwarf_scroll_1 = BundleItem.item_bundle("Dwarf Scroll I", 96, 1, 0) +dwarf_scroll_2 = BundleItem.item_bundle("Dwarf Scroll II", 97, 1, 0) +dwarf_scroll_3 = BundleItem.item_bundle("Dwarf Scroll III", 98, 1, 0) +dwarf_scroll_4 = BundleItem.item_bundle("Dwarf Scroll IV", 99, 1, 0) +elvish_jewelry = BundleItem.item_bundle("Elvish Jewelry", 104, 1, 0) +ancient_drum = BundleItem.item_bundle("Ancient Drum", 123, 1, 0) +dried_starfish = BundleItem.item_bundle("Dried Starfish", 116, 1, 0) + +# TODO Dye Bundle +dye_red_items = [cranberries, dwarf_scroll_1, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] +dye_yellow_items = [dried_starfish, dwarf_scroll_4, elvish_jewelry, corn, parsnip, summer_spangle, sunflower] +dye_green_items = [dwarf_scroll_2, fiddlehead_fern, kale, artichoke, bok_choy, green_bean] +dye_blue_items = [blueberry, dwarf_scroll_3, blue_jazz, blackberry, crystal_fruit] +dye_purple_items = [ancient_drum, beet, crocus, eggplant, red_cabbage, sweet_pea] +dye_items = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] +field_research_items = [purple_mushroom, nautilus_shell, chub, geode, frozen_geode, magma_geode, omni_geode, + rainbow_shell, amethyst, bream, carp] +fodder_items = [wheat.as_amount(10), hay.as_amount(10), apple.as_amount(3), kale.as_amount(3), corn.as_amount(3), + green_bean.as_amount(3), potato.as_amount(3), green_algae.as_amount(5), white_algae.as_amount(3)] +enchanter_items = [oak_resin, wine, rabbit_foot, pomegranate, purple_mushroom, solar_essence, + super_cucumber, void_essence, fire_quartz, frozen_tear, jade] + +vault_2500_items = [BundleItem.money_bundle(2500)] +vault_5000_items = [BundleItem.money_bundle(5000)] +vault_10000_items = [BundleItem.money_bundle(10000)] +vault_25000_items = [BundleItem.money_bundle(25000)] + +crafts_room_bundle_items = [ + *spring_foraging_items, + *summer_foraging_items, + *fall_foraging_items, + *winter_foraging_items, + *exotic_foraging_items, + *construction_items, +] + +pantry_bundle_items = sorted({ + *spring_crop_items, + *summer_crops_items, + *fall_crops_items, + *quality_crops_items, + *animal_product_items, + *artisan_goods_items, +}) + +fish_tank_bundle_items = sorted({ + *river_fish_items, + *lake_fish_items, + *ocean_fish_items, + *night_fish_items, + *crab_pot_items, + *specialty_fish_items, +}) + +boiler_room_bundle_items = sorted({ + *blacksmith_items, + *geologist_items, + *adventurer_items, +}) + +bulletin_board_bundle_items = sorted({ + *chef_items, + *[item for dye_color_items in dye_items for item in dye_color_items], + *field_research_items, + *fodder_items, + *enchanter_items +}) + +vault_bundle_items = [ + *vault_2500_items, + *vault_5000_items, + *vault_10000_items, + *vault_25000_items, +] + +all_bundle_items_except_money = sorted({ + *crafts_room_bundle_items, + *pantry_bundle_items, + *fish_tank_bundle_items, + *boiler_room_bundle_items, + *bulletin_board_bundle_items, +}, key=lambda x: x.item.name) + +all_bundle_items = sorted({ + *crafts_room_bundle_items, + *pantry_bundle_items, + *fish_tank_bundle_items, + *boiler_room_bundle_items, + *bulletin_board_bundle_items, + *vault_bundle_items, +}, key=lambda x: x.item.name) + +all_bundle_items_by_name = {item.item.name: item for item in all_bundle_items} +all_bundle_items_by_id = {item.item.item_id: item for item in all_bundle_items} diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py new file mode 100644 index 0000000000..f87e3d6730 --- /dev/null +++ b/worlds/stardew_valley/bundles.py @@ -0,0 +1,254 @@ +from random import Random +from typing import List, Dict, Union + +from .bundle_data import * +from .logic import StardewLogic +from .options import BundleRandomization, BundlePrice + +vanilla_bundles = { + "Pantry/0": "Spring Crops/O 465 20/24 1 0 188 1 0 190 1 0 192 1 0/0", + "Pantry/1": "Summer Crops/O 621 1/256 1 0 260 1 0 258 1 0 254 1 0/3", + "Pantry/2": "Fall Crops/BO 10 1/270 1 0 272 1 0 276 1 0 280 1 0/2", + "Pantry/3": "Quality Crops/BO 15 1/24 5 2 254 5 2 276 5 2 270 5 2/6/3", + "Pantry/4": "Animal/BO 16 1/186 1 0 182 1 0 174 1 0 438 1 0 440 1 0 442 1 0/4/5", + # 639 1 0 640 1 0 641 1 0 642 1 0 643 1 0 + "Pantry/5": "Artisan/BO 12 1/432 1 0 428 1 0 426 1 0 424 1 0 340 1 0 344 1 0 613 1 0 634 1 0 635 1 0 636 1 0 637 1 0 638 1 0/1/6", + "Crafts Room/13": "Spring Foraging/O 495 30/16 1 0 18 1 0 20 1 0 22 1 0/0", + "Crafts Room/14": "Summer Foraging/O 496 30/396 1 0 398 1 0 402 1 0/3", + "Crafts Room/15": "Fall Foraging/O 497 30/404 1 0 406 1 0 408 1 0 410 1 0/2", + "Crafts Room/16": "Winter Foraging/O 498 30/412 1 0 414 1 0 416 1 0 418 1 0/6", + "Crafts Room/17": "Construction/BO 114 1/388 99 0 388 99 0 390 99 0 709 10 0/4", + "Crafts Room/19": "Exotic Foraging/O 235 5/88 1 0 90 1 0 78 1 0 420 1 0 422 1 0 724 1 0 725 1 0 726 1 0 257 1 0/1/5", + "Fish Tank/6": "River Fish/O 685 30/145 1 0 143 1 0 706 1 0 699 1 0/6", + "Fish Tank/7": "Lake Fish/O 687 1/136 1 0 142 1 0 700 1 0 698 1 0/0", + "Fish Tank/8": "Ocean Fish/O 690 5/131 1 0 130 1 0 150 1 0 701 1 0/5", + "Fish Tank/9": "Night Fishing/R 516 1/140 1 0 132 1 0 148 1 0/1", + "Fish Tank/10": "Specialty Fish/O 242 5/128 1 0 156 1 0 164 1 0 734 1 0/4", + "Fish Tank/11": "Crab Pot/O 710 3/715 1 0 716 1 0 717 1 0 718 1 0 719 1 0 720 1 0 721 1 0 722 1 0 723 1 0 372 1 0/1/5", + "Boiler Room/20": "Blacksmith's/BO 13 1/334 1 0 335 1 0 336 1 0/2", + "Boiler Room/21": "Geologist's/O 749 5/80 1 0 86 1 0 84 1 0 82 1 0/1", + "Boiler Room/22": "Adventurer's/R 518 1/766 99 0 767 10 0 768 1 0 769 1 0/1/2", + "Vault/23": "2,500g/O 220 3/-1 2500 2500/4", + "Vault/24": "5,000g/O 369 30/-1 5000 5000/2", + "Vault/25": "10,000g/BO 9 1/-1 10000 10000/3", + "Vault/26": "25,000g/BO 21 1/-1 25000 25000/1", + "Bulletin Board/31": "Chef's/O 221 3/724 1 0 259 1 0 430 1 0 376 1 0 228 1 0 194 1 0/4", + "Bulletin Board/32": "Field Research/BO 20 1/422 1 0 392 1 0 702 1 0 536 1 0/5", + "Bulletin Board/33": "Enchanter's/O 336 5/725 1 0 348 1 0 446 1 0 637 1 0/1", + "Bulletin Board/34": "Dye/BO 25 1/420 1 0 397 1 0 421 1 0 444 1 0 62 1 0 266 1 0/6", + "Bulletin Board/35": "Fodder/BO 104 1/262 10 0 178 10 0 613 3 0/3", + # "Abandoned Joja Mart/36": "The Missing//348 1 1 807 1 0 74 1 0 454 5 2 795 1 2 445 1 0/1/5" +} + + +class Bundle: + room: str + sprite: str + original_name: str + name: str + rewards: List[str] + requirements: List[BundleItem] + color: str + number_required: int + + def __init__(self, key: str, value: str): + key_parts = key.split("/") + self.room = key_parts[0] + self.sprite = key_parts[1] + + value_parts = value.split("/") + self.original_name = value_parts[0] + self.name = value_parts[0] + self.rewards = self.parse_stardew_objects(value_parts[1]) + self.requirements = self.parse_stardew_bundle_items(value_parts[2]) + self.color = value_parts[3] + if len(value_parts) > 4: + self.number_required = int(value_parts[4]) + else: + self.number_required = len(self.requirements) + + def __repr__(self): + return f"{self.original_name} -> {repr(self.requirements)}" + + def get_name_with_bundle(self) -> str: + return f"{self.original_name} Bundle" + + def to_pair(self) -> (str, str): + key = f"{self.room}/{self.sprite}" + str_rewards = "" + for reward in self.rewards: + str_rewards += f" {reward}" + str_rewards = str_rewards.strip() + str_requirements = "" + for requirement in self.requirements: + str_requirements += f" {requirement.item.item_id} {requirement.amount} {requirement.quality}" + str_requirements = str_requirements.strip() + value = f"{self.name}/{str_rewards}/{str_requirements}/{self.color}/{self.number_required}" + return key, value + + def remove_rewards(self): + self.rewards = [] + + def change_number_required(self, difference: int): + self.number_required = min(len(self.requirements), max(1, self.number_required + difference)) + if len(self.requirements) == 1 and self.requirements[0].item.item_id == -1: + one_fifth = self.requirements[0].amount / 5 + new_amount = int(self.requirements[0].amount + (difference * one_fifth)) + self.requirements[0] = BundleItem.money_bundle(new_amount) + thousand_amount = int(new_amount / 1000) + dollar_amount = str(new_amount % 1000) + while len(dollar_amount) < 3: + dollar_amount = f"0{dollar_amount}" + self.name = f"{thousand_amount},{dollar_amount}g" + + def randomize_requirements(self, random: Random, + potential_requirements: Union[List[BundleItem], List[List[BundleItem]]]): + if not potential_requirements: + return + + number_to_generate = len(self.requirements) + self.requirements.clear() + if number_to_generate > len(potential_requirements): + choices: Union[BundleItem, List[BundleItem]] = random.choices(potential_requirements, k=number_to_generate) + else: + choices: Union[BundleItem, List[BundleItem]] = random.sample(potential_requirements, number_to_generate) + for choice in choices: + if isinstance(choice, BundleItem): + self.requirements.append(choice) + else: + self.requirements.append(random.choice(choice)) + + def assign_requirements(self, new_requirements: List[BundleItem]) -> List[BundleItem]: + number_to_generate = len(self.requirements) + self.requirements.clear() + for requirement in new_requirements: + self.requirements.append(requirement) + if len(self.requirements) >= number_to_generate: + return new_requirements[number_to_generate:] + + @staticmethod + def parse_stardew_objects(string_objects: str) -> List[str]: + objects = [] + if len(string_objects) < 5: + return objects + rewards_parts = string_objects.split(" ") + for index in range(0, len(rewards_parts), 3): + objects.append(f"{rewards_parts[index]} {rewards_parts[index + 1]} {rewards_parts[index + 2]}") + return objects + + @staticmethod + def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]: + bundle_items = [] + parts = string_objects.split(" ") + for index in range(0, len(parts), 3): + item_id = int(parts[index]) + bundle_item = BundleItem(all_bundle_items_by_id[item_id].item, + int(parts[index + 1]), + int(parts[index + 2])) + bundle_items.append(bundle_item) + return bundle_items + + # Shuffling the Vault doesn't really work with the stardew system in place + # shuffle_vault_amongst_themselves(random, bundles) + + +def get_all_bundles(random: Random, logic: StardewLogic, randomization: int, price: int) -> Dict[str, Bundle]: + bundles = {} + for bundle_key in vanilla_bundles: + bundle_value = vanilla_bundles[bundle_key] + bundle = Bundle(bundle_key, bundle_value) + bundles[bundle.get_name_with_bundle()] = bundle + + if randomization == BundleRandomization.option_thematic: + shuffle_bundles_thematically(random, bundles) + elif randomization == BundleRandomization.option_shuffled: + shuffle_bundles_completely(random, logic, bundles) + + price_difference = 0 + if price == BundlePrice.option_very_cheap: + price_difference = -2 + elif price == BundlePrice.option_cheap: + price_difference = -1 + elif price == BundlePrice.option_expensive: + price_difference = 1 + + for bundle_key in bundles: + bundles[bundle_key].remove_rewards() + bundles[bundle_key].change_number_required(price_difference) + + return bundles + + +def shuffle_bundles_completely(random: Random, logic: StardewLogic, bundles: Dict[str, Bundle]): + total_required_item_number = sum(len(bundle.requirements) for bundle in bundles.values()) + quality_crops_items_set = set(quality_crops_items) + all_bundle_items_without_quality_and_money = [item + for item in all_bundle_items_except_money + if item not in quality_crops_items_set] + \ + random.sample(quality_crops_items, 10) + choices = random.sample(all_bundle_items_without_quality_and_money, total_required_item_number - 4) + + items_sorted = sorted(choices, key=lambda x: logic.item_rules[x.item.name].get_difficulty()) + + keys = sorted(bundles.keys()) + random.shuffle(keys) + + for key in keys: + if not bundles[key].original_name.endswith("00g"): + items_sorted = bundles[key].assign_requirements(items_sorted) + + +def shuffle_bundles_thematically(random: Random, bundles: Dict[str, Bundle]): + shuffle_crafts_room_bundle_thematically(random, bundles) + shuffle_pantry_bundle_thematically(random, bundles) + shuffle_fish_tank_thematically(random, bundles) + shuffle_boiler_room_thematically(random, bundles) + shuffle_bulletin_board_thematically(random, bundles) + + +def shuffle_crafts_room_bundle_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Spring Foraging Bundle"].randomize_requirements(random, spring_foraging_items) + bundles["Summer Foraging Bundle"].randomize_requirements(random, summer_foraging_items) + bundles["Fall Foraging Bundle"].randomize_requirements(random, fall_foraging_items) + bundles["Winter Foraging Bundle"].randomize_requirements(random, winter_foraging_items) + bundles["Exotic Foraging Bundle"].randomize_requirements(random, exotic_foraging_items) + bundles["Construction Bundle"].randomize_requirements(random, construction_items) + + +def shuffle_pantry_bundle_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Spring Crops Bundle"].randomize_requirements(random, spring_crop_items) + bundles["Summer Crops Bundle"].randomize_requirements(random, summer_crops_items) + bundles["Fall Crops Bundle"].randomize_requirements(random, fall_crops_items) + bundles["Quality Crops Bundle"].randomize_requirements(random, quality_crops_items) + bundles["Animal Bundle"].randomize_requirements(random, animal_product_items) + bundles["Artisan Bundle"].randomize_requirements(random, artisan_goods_items) + + +def shuffle_fish_tank_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["River Fish Bundle"].randomize_requirements(random, river_fish_items) + bundles["Lake Fish Bundle"].randomize_requirements(random, lake_fish_items) + bundles["Ocean Fish Bundle"].randomize_requirements(random, ocean_fish_items) + bundles["Night Fishing Bundle"].randomize_requirements(random, night_fish_items) + bundles["Crab Pot Bundle"].randomize_requirements(random, crab_pot_items) + bundles["Specialty Fish Bundle"].randomize_requirements(random, specialty_fish_items) + + +def shuffle_boiler_room_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Blacksmith's Bundle"].randomize_requirements(random, blacksmith_items) + bundles["Geologist's Bundle"].randomize_requirements(random, geologist_items) + bundles["Adventurer's Bundle"].randomize_requirements(random, adventurer_items) + + +def shuffle_bulletin_board_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Chef's Bundle"].randomize_requirements(random, chef_items) + bundles["Dye Bundle"].randomize_requirements(random, dye_items) + bundles["Field Research Bundle"].randomize_requirements(random, field_research_items) + bundles["Fodder Bundle"].randomize_requirements(random, fodder_items) + bundles["Enchanter's Bundle"].randomize_requirements(random, enchanter_items) + + +def shuffle_vault_amongst_themselves(random: Random, bundles: Dict[str, Bundle]): + bundles["2,500g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["5,000g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["10,000g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["25,000g Bundle"].randomize_requirements(random, vault_bundle_items) diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv new file mode 100644 index 0000000000..425186ed4f --- /dev/null +++ b/worlds/stardew_valley/data/items.csv @@ -0,0 +1,312 @@ +id,name,classification,groups +0,Joja Cola,filler,TRASH +15,Rusty Key,progression, +16,Dwarvish Translation Guide,progression, +17,Bridge Repair,progression,COMMUNITY_REWARD +18,Greenhouse,progression,COMMUNITY_REWARD +19,Glittering Boulder Removed,progression,COMMUNITY_REWARD +20,Minecarts Repair,useful,COMMUNITY_REWARD +21,Bus Repair,progression,COMMUNITY_REWARD +22,Movie Theater,useful, +23,Stardrop,useful, +24,Progressive Backpack,progression, +25,Rusty Sword,progression,WEAPON +26,Leather Boots,progression,"FOOTWEAR,MINES_FLOOR_10" +27,Work Boots,useful,"FOOTWEAR,MINES_FLOOR_10" +28,Wooden Blade,progression,"MINES_FLOOR_10,WEAPON" +29,Iron Dirk,progression,"MINES_FLOOR_10,WEAPON" +30,Wind Spire,progression,"MINES_FLOOR_10,WEAPON" +31,Femur,progression,"MINES_FLOOR_10,WEAPON" +32,Steel Smallsword,progression,"MINES_FLOOR_20,WEAPON" +33,Wood Club,progression,"MINES_FLOOR_20,WEAPON" +34,Elf Blade,progression,"MINES_FLOOR_20,WEAPON" +35,Glow Ring,useful,"MINES_FLOOR_20,RING" +36,Magnet Ring,useful,"MINES_FLOOR_20,RING" +37,Slingshot,progression,WEAPON +38,Tundra Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +39,Thermal Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +40,Combat Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +41,Silver Saber,progression,"MINES_FLOOR_50,WEAPON" +42,Pirate's Sword,progression,"MINES_FLOOR_50,WEAPON" +43,Crystal Dagger,progression,"MINES_FLOOR_60,WEAPON" +44,Cutlass,progression,"MINES_FLOOR_60,WEAPON" +45,Iron Edge,progression,"MINES_FLOOR_60,WEAPON" +46,Burglar's Shank,progression,"MINES_FLOOR_60,WEAPON" +47,Wood Mallet,progression,"MINES_FLOOR_60,WEAPON" +48,Master Slingshot,progression,WEAPON +49,Firewalker Boots,useful,"FOOTWEAR,MINES_FLOOR_80" +50,Dark Boots,useful,"FOOTWEAR,MINES_FLOOR_80" +51,Claymore,progression,"MINES_FLOOR_80,WEAPON" +52,Templar's Blade,progression,"MINES_FLOOR_80,WEAPON" +53,Kudgel,progression,"MINES_FLOOR_80,WEAPON" +54,Shadow Dagger,progression,"MINES_FLOOR_80,WEAPON" +55,Obsidian Edge,progression,"MINES_FLOOR_90,WEAPON" +56,Tempered Broadsword,progression,"MINES_FLOOR_90,WEAPON" +57,Wicked Kris,progression,"MINES_FLOOR_90,WEAPON" +58,Bone Sword,progression,"MINES_FLOOR_90,WEAPON" +59,Ossified Blade,progression,"MINES_FLOOR_90,WEAPON" +60,Space Boots,useful,"FOOTWEAR,MINES_FLOOR_110" +61,Crystal Shoes,useful,"FOOTWEAR,MINES_FLOOR_110" +62,Steel Falchion,progression,"MINES_FLOOR_110,WEAPON" +63,The Slammer,progression,"MINES_FLOOR_110,WEAPON" +64,Skull Key,progression, +65,Progressive Hoe,progression,PROGRESSIVE_TOOLS +66,Progressive Pickaxe,progression,PROGRESSIVE_TOOLS +67,Progressive Axe,progression,PROGRESSIVE_TOOLS +68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS +69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS +70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS +71,Golden Scythe,useful, +72,Progressive Mine Elevator,progression, +73,Farming Level,progression,SKILL_LEVEL_UP +74,Fishing Level,progression,SKILL_LEVEL_UP +75,Foraging Level,progression,SKILL_LEVEL_UP +76,Mining Level,progression,SKILL_LEVEL_UP +77,Combat Level,progression,SKILL_LEVEL_UP +78,Earth Obelisk,useful, +79,Water Obelisk,useful, +80,Desert Obelisk,progression, +81,Island Obelisk,progression, +82,Junimo Hut,useful, +83,Gold Clock,useful, +84,Progressive Coop,progression, +85,Progressive Barn,progression, +86,Well,useful, +87,Silo,progression, +88,Mill,progression, +89,Progressive Shed,progression, +90,Fish Pond,progression, +91,Stable,useful, +92,Slime Hutch,useful, +93,Shipping Bin,progression, +94,Beach Bridge,progression, +95,Adventurer's Guild,progression, +96,Club Card,progression, +97,Magnifying Glass,progression, +98,Bear's Knowledge,progression, +99,Iridium Snake Milk,progression, +100,JotPK: Progressive Boots,progression,ARCADE_MACHINE_BUFFS +101,JotPK: Progressive Gun,progression,ARCADE_MACHINE_BUFFS +102,JotPK: Progressive Ammo,progression,ARCADE_MACHINE_BUFFS +103,JotPK: Extra Life,progression,ARCADE_MACHINE_BUFFS +104,JotPK: Increased Drop Rate,progression,ARCADE_MACHINE_BUFFS +105,Junimo Kart: Extra Life,progression,ARCADE_MACHINE_BUFFS +106,Galaxy Sword,progression,"GALAXY_WEAPONS,WEAPON" +107,Galaxy Dagger,progression,"GALAXY_WEAPONS,WEAPON" +108,Galaxy Hammer,progression,"GALAXY_WEAPONS,WEAPON" +109,Movement Speed Bonus,useful, +110,Luck Bonus,useful, +111,Lava Katana,progression,"MINES_FLOOR_110,WEAPON" +112,Progressive House,progression, +113,Traveling Merchant: Sunday,progression, +114,Traveling Merchant: Monday,progression, +115,Traveling Merchant: Tuesday,progression, +116,Traveling Merchant: Wednesday,progression, +117,Traveling Merchant: Thursday,progression, +118,Traveling Merchant: Friday,progression, +119,Traveling Merchant: Saturday,progression, +120,Traveling Merchant Stock Size,progression, +121,Traveling Merchant Discount,progression, +122,Return Scepter,useful, +5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +5003,Resource Pack: 2000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +5004,Resource Pack: 25 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" +5005,Resource Pack: 50 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" +5006,Resource Pack: 75 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" +5007,Resource Pack: 100 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" +5008,Resource Pack: 25 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5009,Resource Pack: 50 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5010,Resource Pack: 75 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5011,Resource Pack: 100 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5012,Resource Pack: 5 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5013,Resource Pack: 10 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5014,Resource Pack: 15 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5015,Resource Pack: 20 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5016,Resource Pack: 15 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" +5017,Resource Pack: 30 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" +5018,Resource Pack: 45 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" +5019,Resource Pack: 60 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" +5020,Resource Pack: 5 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5021,Resource Pack: 10 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5022,Resource Pack: 15 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5023,Resource Pack: 20 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5024,Resource Pack: 5 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5025,Resource Pack: 10 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5026,Resource Pack: 15 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5027,Resource Pack: 20 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5028,Resource Pack: 1 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5029,Resource Pack: 3 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5030,Resource Pack: 5 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5031,Resource Pack: 7 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5032,Resource Pack: 9 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5033,Resource Pack: 10 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5034,Resource Pack: 1 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5035,Resource Pack: 3 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5036,Resource Pack: 5 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5037,Resource Pack: 7 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5038,Resource Pack: 9 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5039,Resource Pack: 10 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5040,Resource Pack: 1 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5041,Resource Pack: 3 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5042,Resource Pack: 5 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5043,Resource Pack: 7 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5044,Resource Pack: 9 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5045,Resource Pack: 10 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5046,Resource Pack: 1 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5047,Resource Pack: 3 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5048,Resource Pack: 5 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5049,Resource Pack: 7 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5050,Resource Pack: 9 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5051,Resource Pack: 10 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5052,Resource Pack: 1 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5053,Resource Pack: 3 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5054,Resource Pack: 5 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5055,Resource Pack: 7 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5056,Resource Pack: 9 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5057,Resource Pack: 10 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5058,Resource Pack: 6 Geode,filler,"GEODE,RESOURCE_PACK" +5059,Resource Pack: 12 Geode,filler,"GEODE,RESOURCE_PACK" +5060,Resource Pack: 18 Geode,filler,"GEODE,RESOURCE_PACK" +5061,Resource Pack: 24 Geode,filler,"GEODE,RESOURCE_PACK" +5062,Resource Pack: 4 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5063,Resource Pack: 8 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5064,Resource Pack: 12 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5065,Resource Pack: 16 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5066,Resource Pack: 3 Magma Geode,filler,"GEODE,RESOURCE_PACK" +5067,Resource Pack: 6 Magma Geode,filler,"GEODE,RESOURCE_PACK" +5068,Resource Pack: 9 Magma Geode,filler,"GEODE,RESOURCE_PACK" +5069,Resource Pack: 12 Magma Geode,filler,"GEODE,RESOURCE_PACK" +5070,Resource Pack: 2 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5071,Resource Pack: 4 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5072,Resource Pack: 6 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5073,Resource Pack: 8 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5074,Resource Pack: 25 Copper Ore,filler,"ORE,RESOURCE_PACK" +5075,Resource Pack: 50 Copper Ore,filler,"ORE,RESOURCE_PACK" +5076,Resource Pack: 75 Copper Ore,filler,"ORE,RESOURCE_PACK" +5077,Resource Pack: 100 Copper Ore,filler,"ORE,RESOURCE_PACK" +5078,Resource Pack: 125 Copper Ore,filler,"ORE,RESOURCE_PACK" +5079,Resource Pack: 150 Copper Ore,filler,"ORE,RESOURCE_PACK" +5080,Resource Pack: 25 Iron Ore,filler,"ORE,RESOURCE_PACK" +5081,Resource Pack: 50 Iron Ore,filler,"ORE,RESOURCE_PACK" +5082,Resource Pack: 75 Iron Ore,filler,"ORE,RESOURCE_PACK" +5083,Resource Pack: 100 Iron Ore,filler,"ORE,RESOURCE_PACK" +5084,Resource Pack: 12 Gold Ore,useful,"ORE,RESOURCE_PACK" +5085,Resource Pack: 25 Gold Ore,useful,"ORE,RESOURCE_PACK" +5086,Resource Pack: 38 Gold Ore,useful,"ORE,RESOURCE_PACK" +5087,Resource Pack: 50 Gold Ore,useful,"ORE,RESOURCE_PACK" +5088,Resource Pack: 5 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5089,Resource Pack: 10 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5090,Resource Pack: 15 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5091,Resource Pack: 20 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5092,Resource Pack: 5 Quartz,filler,"ORE,RESOURCE_PACK" +5093,Resource Pack: 10 Quartz,filler,"ORE,RESOURCE_PACK" +5094,Resource Pack: 15 Quartz,filler,"ORE,RESOURCE_PACK" +5095,Resource Pack: 20 Quartz,filler,"ORE,RESOURCE_PACK" +5096,Resource Pack: 10 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5097,Resource Pack: 20 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5098,Resource Pack: 30 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5099,Resource Pack: 40 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5100,Resource Pack: 50 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5101,Resource Pack: 60 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5102,Resource Pack: 10 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5103,Resource Pack: 20 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5104,Resource Pack: 30 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5105,Resource Pack: 40 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5106,Resource Pack: 50 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5107,Resource Pack: 60 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5108,Resource Pack: 10 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5109,Resource Pack: 20 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5110,Resource Pack: 30 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5111,Resource Pack: 40 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5112,Resource Pack: 50 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5113,Resource Pack: 60 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5114,Resource Pack: 4 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5115,Resource Pack: 12 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5116,Resource Pack: 20 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5117,Resource Pack: 28 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5118,Resource Pack: 36 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5119,Resource Pack: 40 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5120,Resource Pack: 4 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5121,Resource Pack: 12 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5122,Resource Pack: 20 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5123,Resource Pack: 28 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5124,Resource Pack: 36 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5125,Resource Pack: 40 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5126,Resource Pack: 4 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5127,Resource Pack: 12 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5128,Resource Pack: 20 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5129,Resource Pack: 28 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5130,Resource Pack: 36 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5131,Resource Pack: 40 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5132,Resource Pack: 2 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5133,Resource Pack: 6 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5134,Resource Pack: 10 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5135,Resource Pack: 14 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5136,Resource Pack: 18 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5137,Resource Pack: 20 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" +5138,Resource Pack: 2 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5139,Resource Pack: 6 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5140,Resource Pack: 10 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5141,Resource Pack: 14 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5142,Resource Pack: 18 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5143,Resource Pack: 20 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" +5144,Resource Pack: 2 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5145,Resource Pack: 6 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5146,Resource Pack: 10 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5147,Resource Pack: 14 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5148,Resource Pack: 18 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5149,Resource Pack: 20 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5150,Resource Pack: 2 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5151,Resource Pack: 6 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5152,Resource Pack: 10 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5153,Resource Pack: 14 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5154,Resource Pack: 18 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5155,Resource Pack: 20 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5156,Resource Pack: 10 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5157,Resource Pack: 20 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5158,Resource Pack: 30 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5159,Resource Pack: 40 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5160,Resource Pack: 50 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5161,Resource Pack: 60 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5162,Resource Pack: 10 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5163,Resource Pack: 20 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5164,Resource Pack: 30 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5165,Resource Pack: 40 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5166,Resource Pack: 50 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5167,Resource Pack: 60 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5168,Resource Pack: 10 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5169,Resource Pack: 20 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5170,Resource Pack: 30 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5171,Resource Pack: 40 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5172,Resource Pack: 50 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5173,Resource Pack: 60 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5174,Resource Pack: 10 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5175,Resource Pack: 20 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5176,Resource Pack: 30 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5177,Resource Pack: 40 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5178,Resource Pack: 50 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5179,Resource Pack: 60 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5180,Resource Pack: 1 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5181,Resource Pack: 3 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5182,Resource Pack: 5 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5183,Resource Pack: 7 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5184,Resource Pack: 9 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5185,Resource Pack: 10 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5186,Resource Pack: 10 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5187,Resource Pack: 20 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5188,Resource Pack: 30 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5189,Resource Pack: 40 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5190,Resource Pack: 50 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5191,Resource Pack: 60 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5192,Resource Pack: 1 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5193,Resource Pack: 2 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5194,Resource Pack: 3 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5195,Resource Pack: 4 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5196,Resource Pack: 5 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5197,Resource Pack: 6 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5198,Friendship Bonus (1 <3),useful,FRIENDSHIP_PACK +5199,Friendship Bonus (2 <3),useful,FRIENDSHIP_PACK +5200,Friendship Bonus (3 <3),useful,FRIENDSHIP_PACK +5201,Friendship Bonus (4 <3),useful,FRIENDSHIP_PACK diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv new file mode 100644 index 0000000000..abad3c042d --- /dev/null +++ b/worlds/stardew_valley/data/locations.csv @@ -0,0 +1,379 @@ +id,region,name,tags +1,Crafts Room,Spring Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +2,Crafts Room,Summer Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +3,Crafts Room,Fall Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +4,Crafts Room,Winter Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +5,Crafts Room,Construction Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +6,Crafts Room,Exotic Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +7,Pantry,Spring Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +8,Pantry,Summer Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +9,Pantry,Fall Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +10,Pantry,Quality Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +11,Pantry,Animal Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +12,Pantry,Artisan Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +13,Fish Tank,River Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +14,Fish Tank,Lake Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +15,Fish Tank,Ocean Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +16,Fish Tank,Night Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +17,Fish Tank,Crab Pot Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +18,Fish Tank,Specialty Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +19,Boiler Room,Blacksmith's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +20,Boiler Room,Geologist's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +21,Boiler Room,Adventurer's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +22,Bulletin Board,Chef's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +23,Bulletin Board,Dye Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +24,Bulletin Board,Field Research Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +25,Bulletin Board,Fodder Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +26,Bulletin Board,Enchanter's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +27,Vault,"2,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +28,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +29,Vault,"10,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +30,Vault,"25,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +31,Abandoned JojaMart,The Missing Bundle,BUNDLE +32,Crafts Room,Complete Crafts Room,"COMMUNITY_CENTER_ROOM,MANDATORY" +33,Pantry,Complete Pantry,"COMMUNITY_CENTER_ROOM,MANDATORY" +34,Fish Tank,Complete Fish Tank,"COMMUNITY_CENTER_ROOM,MANDATORY" +35,Boiler Room,Complete Boiler Room,"COMMUNITY_CENTER_ROOM,MANDATORY" +36,Bulletin Board,Complete Bulletin Board,"COMMUNITY_CENTER_ROOM,MANDATORY" +37,Vault,Complete Vault,"COMMUNITY_CENTER_ROOM,MANDATORY" +101,Pierre's General Store,Large Pack,BACKPACK +102,Pierre's General Store,Deluxe Pack,BACKPACK +103,Clint's Blacksmith,Copper Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +104,Clint's Blacksmith,Iron Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +105,Clint's Blacksmith,Gold Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +106,Clint's Blacksmith,Iridium Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +107,Clint's Blacksmith,Copper Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +108,Clint's Blacksmith,Iron Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +109,Clint's Blacksmith,Gold Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +110,Clint's Blacksmith,Iridium Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +111,Clint's Blacksmith,Copper Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +112,Clint's Blacksmith,Iron Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +113,Clint's Blacksmith,Gold Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +114,Clint's Blacksmith,Iridium Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +115,Clint's Blacksmith,Copper Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +116,Clint's Blacksmith,Iron Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +117,Clint's Blacksmith,Gold Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +118,Clint's Blacksmith,Iridium Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +119,Clint's Blacksmith,Copper Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +120,Clint's Blacksmith,Iron Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +121,Clint's Blacksmith,Gold Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +122,Clint's Blacksmith,Iridium Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +123,Willy's Fish Shop,Purchase Training Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +124,Stardew Valley,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE" +202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE" +203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE" +204,The Mines - Floor 50,The Mines Floor 50 Treasure,"MANDATORY,THE_MINES_TREASURE" +205,The Mines - Floor 60,The Mines Floor 60 Treasure,"MANDATORY,THE_MINES_TREASURE" +206,The Mines - Floor 70,The Mines Floor 70 Treasure,"MANDATORY,THE_MINES_TREASURE" +207,The Mines - Floor 80,The Mines Floor 80 Treasure,"MANDATORY,THE_MINES_TREASURE" +208,The Mines - Floor 90,The Mines Floor 90 Treasure,"MANDATORY,THE_MINES_TREASURE" +209,The Mines - Floor 100,The Mines Floor 100 Treasure,"MANDATORY,THE_MINES_TREASURE" +210,The Mines - Floor 110,The Mines Floor 110 Treasure,"MANDATORY,THE_MINES_TREASURE" +211,The Mines - Floor 120,The Mines Floor 120 Treasure,"MANDATORY,THE_MINES_TREASURE" +212,Quarry Mine,Grim Reaper statue,MANDATORY +213,The Mines,The Mines Entrance Cutscene,MANDATORY +214,The Mines - Floor 5,Floor 5 Elevator,THE_MINES_ELEVATOR +215,The Mines - Floor 10,Floor 10 Elevator,THE_MINES_ELEVATOR +216,The Mines - Floor 15,Floor 15 Elevator,THE_MINES_ELEVATOR +217,The Mines - Floor 20,Floor 20 Elevator,THE_MINES_ELEVATOR +218,The Mines - Floor 25,Floor 25 Elevator,THE_MINES_ELEVATOR +219,The Mines - Floor 30,Floor 30 Elevator,THE_MINES_ELEVATOR +220,The Mines - Floor 35,Floor 35 Elevator,THE_MINES_ELEVATOR +221,The Mines - Floor 40,Floor 40 Elevator,THE_MINES_ELEVATOR +222,The Mines - Floor 45,Floor 45 Elevator,THE_MINES_ELEVATOR +223,The Mines - Floor 50,Floor 50 Elevator,THE_MINES_ELEVATOR +224,The Mines - Floor 55,Floor 55 Elevator,THE_MINES_ELEVATOR +225,The Mines - Floor 60,Floor 60 Elevator,THE_MINES_ELEVATOR +226,The Mines - Floor 65,Floor 65 Elevator,THE_MINES_ELEVATOR +227,The Mines - Floor 70,Floor 70 Elevator,THE_MINES_ELEVATOR +228,The Mines - Floor 75,Floor 75 Elevator,THE_MINES_ELEVATOR +229,The Mines - Floor 80,Floor 80 Elevator,THE_MINES_ELEVATOR +230,The Mines - Floor 85,Floor 85 Elevator,THE_MINES_ELEVATOR +231,The Mines - Floor 90,Floor 90 Elevator,THE_MINES_ELEVATOR +232,The Mines - Floor 95,Floor 95 Elevator,THE_MINES_ELEVATOR +233,The Mines - Floor 100,Floor 100 Elevator,THE_MINES_ELEVATOR +234,The Mines - Floor 105,Floor 105 Elevator,THE_MINES_ELEVATOR +235,The Mines - Floor 110,Floor 110 Elevator,THE_MINES_ELEVATOR +236,The Mines - Floor 115,Floor 115 Elevator,THE_MINES_ELEVATOR +237,The Mines - Floor 120,Floor 120 Elevator,THE_MINES_ELEVATOR +301,Stardew Valley,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL" +302,Stardew Valley,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL" +303,Stardew Valley,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL" +304,Stardew Valley,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL" +305,Stardew Valley,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL" +306,Stardew Valley,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL" +307,Stardew Valley,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL" +308,Stardew Valley,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL" +309,Stardew Valley,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL" +310,Stardew Valley,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL" +311,Stardew Valley,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +312,Stardew Valley,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +313,Stardew Valley,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +314,Stardew Valley,Level 4 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +315,Stardew Valley,Level 5 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +316,Stardew Valley,Level 6 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +317,Stardew Valley,Level 7 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +318,Stardew Valley,Level 8 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +319,Stardew Valley,Level 9 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +320,Stardew Valley,Level 10 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +321,Stardew Valley,Level 1 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +322,Stardew Valley,Level 2 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +323,Stardew Valley,Level 3 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +324,Stardew Valley,Level 4 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +325,Stardew Valley,Level 5 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +326,Stardew Valley,Level 6 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +327,Stardew Valley,Level 7 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +328,Stardew Valley,Level 8 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +329,Stardew Valley,Level 9 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +330,Stardew Valley,Level 10 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +331,Stardew Valley,Level 1 Mining,"MINING_LEVEL,SKILL_LEVEL" +332,Stardew Valley,Level 2 Mining,"MINING_LEVEL,SKILL_LEVEL" +333,Stardew Valley,Level 3 Mining,"MINING_LEVEL,SKILL_LEVEL" +334,Stardew Valley,Level 4 Mining,"MINING_LEVEL,SKILL_LEVEL" +335,Stardew Valley,Level 5 Mining,"MINING_LEVEL,SKILL_LEVEL" +336,Stardew Valley,Level 6 Mining,"MINING_LEVEL,SKILL_LEVEL" +337,Stardew Valley,Level 7 Mining,"MINING_LEVEL,SKILL_LEVEL" +338,Stardew Valley,Level 8 Mining,"MINING_LEVEL,SKILL_LEVEL" +339,Stardew Valley,Level 9 Mining,"MINING_LEVEL,SKILL_LEVEL" +340,Stardew Valley,Level 10 Mining,"MINING_LEVEL,SKILL_LEVEL" +341,Stardew Valley,Level 1 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +342,Stardew Valley,Level 2 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +343,Stardew Valley,Level 3 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +344,Stardew Valley,Level 4 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +345,Stardew Valley,Level 5 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +346,Stardew Valley,Level 6 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +347,Stardew Valley,Level 7 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +348,Stardew Valley,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +349,Stardew Valley,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +350,Stardew Valley,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT +402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT +403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT +404,Carpenter Shop,Barn Blueprint,BUILDING_BLUEPRINT +405,Carpenter Shop,Big Barn Blueprint,BUILDING_BLUEPRINT +406,Carpenter Shop,Deluxe Barn Blueprint,BUILDING_BLUEPRINT +407,Carpenter Shop,Well Blueprint,BUILDING_BLUEPRINT +408,Carpenter Shop,Silo Blueprint,BUILDING_BLUEPRINT +409,Carpenter Shop,Mill Blueprint,BUILDING_BLUEPRINT +410,Carpenter Shop,Shed Blueprint,BUILDING_BLUEPRINT +411,Carpenter Shop,Big Shed Blueprint,BUILDING_BLUEPRINT +412,Carpenter Shop,Fish Pond Blueprint,BUILDING_BLUEPRINT +413,Carpenter Shop,Stable Blueprint,BUILDING_BLUEPRINT +414,Carpenter Shop,Slime Hutch Blueprint,BUILDING_BLUEPRINT +415,Carpenter Shop,Shipping Bin Blueprint,BUILDING_BLUEPRINT +416,Carpenter Shop,Kitchen Blueprint,BUILDING_BLUEPRINT +417,Carpenter Shop,Kids Room Blueprint,BUILDING_BLUEPRINT +418,Carpenter Shop,Cellar Blueprint,BUILDING_BLUEPRINT +501,Town,Introductions,"MANDATORY,QUEST" +502,Town,How To Win Friends,"MANDATORY,QUEST" +503,Farm,Getting Started,"MANDATORY,QUEST" +504,Farm,Raising Animals,"MANDATORY,QUEST" +505,Farm,Advancement,"MANDATORY,QUEST" +506,Museum,Archaeology,"MANDATORY,QUEST" +507,Wizard Tower,Meet The Wizard,"MANDATORY,QUEST" +508,Farm,Forging Ahead,"MANDATORY,QUEST" +509,Farm,Smelting,"MANDATORY,QUEST" +510,The Mines - Floor 5,Initiation,"MANDATORY,QUEST" +511,Forest,Robin's Lost Axe,"MANDATORY,QUEST" +512,Sam's House,Jodi's Request,"MANDATORY,QUEST" +513,Marnie's Ranch,"Mayor's ""Shorts""","MANDATORY,QUEST" +514,Tunnel Entrance,Blackberry Basket,"MANDATORY,QUEST" +515,Marnie's Ranch,Marnie's Request,"MANDATORY,QUEST" +516,Town,Pam Is Thirsty,"MANDATORY,QUEST" +517,Wizard Tower,A Dark Reagent,"MANDATORY,QUEST" +518,Marnie's Ranch,Cow's Delight,"MANDATORY,QUEST" +519,Skull Cavern Entrance,The Skull Key,"MANDATORY,QUEST" +520,Town,Crop Research,"MANDATORY,QUEST" +521,Town,Knee Therapy,"MANDATORY,QUEST" +522,Town,Robin's Request,"MANDATORY,QUEST" +523,Skull Cavern,Qi's Challenge,"MANDATORY,QUEST" +524,The Desert,The Mysterious Qi,"MANDATORY,QUEST" +525,Town,Carving Pumpkins,"MANDATORY,QUEST" +526,Town,A Winter Mystery,"MANDATORY,QUEST" +527,Secret Woods,Strange Note,"MANDATORY,QUEST" +528,Skull Cavern,Cryptic Note,"MANDATORY,QUEST" +529,Town,Fresh Fruit,"MANDATORY,QUEST" +530,Town,Aquatic Research,"MANDATORY,QUEST" +531,Town,A Soldier's Star,"MANDATORY,QUEST" +532,Town,Mayor's Need,"MANDATORY,QUEST" +533,Saloon,Wanted: Lobster,"MANDATORY,QUEST" +534,Town,Pam Needs Juice,"MANDATORY,QUEST" +535,Sam's House,Fish Casserole,"MANDATORY,QUEST" +536,Beach,Catch A Squid,"MANDATORY,QUEST" +537,Saloon,Fish Stew,"MANDATORY,QUEST" +538,Town,Pierre's Notice,"MANDATORY,QUEST" +539,Town,Clint's Attempt,"MANDATORY,QUEST" +540,Town,A Favor For Clint,"MANDATORY,QUEST" +541,Wizard Tower,Staff Of Power,"MANDATORY,QUEST" +542,Town,Granny's Gift,"MANDATORY,QUEST" +543,Saloon,Exotic Spirits,"MANDATORY,QUEST" +544,Town,Catch a Lingcod,"MANDATORY,QUEST" +601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK" +602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK" +603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK" +604,JotPK World 2,JotPK: Gun 2,"ARCADE_MACHINE,JOTPK" +605,JotPK World 2,JotPK: Gun 3,"ARCADE_MACHINE,JOTPK" +606,JotPK World 3,JotPK: Super Gun,"ARCADE_MACHINE,JOTPK" +607,JotPK World 1,JotPK: Ammo 1,"ARCADE_MACHINE,JOTPK" +608,JotPK World 2,JotPK: Ammo 2,"ARCADE_MACHINE,JOTPK" +609,JotPK World 3,JotPK: Ammo 3,"ARCADE_MACHINE,JOTPK" +610,JotPK World 1,JotPK: Cowboy 1,"ARCADE_MACHINE,JOTPK" +611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK" +612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART" +613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART" +614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART" +615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART" +616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART" +617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART" +618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART" +619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART" +620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JUNIMO_KART" +621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART" +701,Secret Woods,Old Master Cannoli,MANDATORY +702,Beach,Beach Bridge Repair,MANDATORY +703,The Desert,Galaxy Sword Shrine,MANDATORY +801,Town,Help Wanted: Gathering 1,HELP_WANTED +802,Town,Help Wanted: Gathering 2,HELP_WANTED +803,Town,Help Wanted: Gathering 3,HELP_WANTED +804,Town,Help Wanted: Gathering 4,HELP_WANTED +805,Town,Help Wanted: Gathering 5,HELP_WANTED +806,Town,Help Wanted: Gathering 6,HELP_WANTED +807,Town,Help Wanted: Gathering 7,HELP_WANTED +808,Town,Help Wanted: Gathering 8,HELP_WANTED +811,Town,Help Wanted: Slay Monsters 1,HELP_WANTED +812,Town,Help Wanted: Slay Monsters 2,HELP_WANTED +813,Town,Help Wanted: Slay Monsters 3,HELP_WANTED +814,Town,Help Wanted: Slay Monsters 4,HELP_WANTED +815,Town,Help Wanted: Slay Monsters 5,HELP_WANTED +816,Town,Help Wanted: Slay Monsters 6,HELP_WANTED +817,Town,Help Wanted: Slay Monsters 7,HELP_WANTED +818,Town,Help Wanted: Slay Monsters 8,HELP_WANTED +821,Town,Help Wanted: Fishing 1,HELP_WANTED +822,Town,Help Wanted: Fishing 2,HELP_WANTED +823,Town,Help Wanted: Fishing 3,HELP_WANTED +824,Town,Help Wanted: Fishing 4,HELP_WANTED +825,Town,Help Wanted: Fishing 5,HELP_WANTED +826,Town,Help Wanted: Fishing 6,HELP_WANTED +827,Town,Help Wanted: Fishing 7,HELP_WANTED +828,Town,Help Wanted: Fishing 8,HELP_WANTED +841,Town,Help Wanted: Item Delivery 1,HELP_WANTED +842,Town,Help Wanted: Item Delivery 2,HELP_WANTED +843,Town,Help Wanted: Item Delivery 3,HELP_WANTED +844,Town,Help Wanted: Item Delivery 4,HELP_WANTED +845,Town,Help Wanted: Item Delivery 5,HELP_WANTED +846,Town,Help Wanted: Item Delivery 6,HELP_WANTED +847,Town,Help Wanted: Item Delivery 7,HELP_WANTED +848,Town,Help Wanted: Item Delivery 8,HELP_WANTED +849,Town,Help Wanted: Item Delivery 9,HELP_WANTED +850,Town,Help Wanted: Item Delivery 10,HELP_WANTED +851,Town,Help Wanted: Item Delivery 11,HELP_WANTED +852,Town,Help Wanted: Item Delivery 12,HELP_WANTED +853,Town,Help Wanted: Item Delivery 13,HELP_WANTED +854,Town,Help Wanted: Item Delivery 14,HELP_WANTED +855,Town,Help Wanted: Item Delivery 15,HELP_WANTED +856,Town,Help Wanted: Item Delivery 16,HELP_WANTED +857,Town,Help Wanted: Item Delivery 17,HELP_WANTED +858,Town,Help Wanted: Item Delivery 18,HELP_WANTED +859,Town,Help Wanted: Item Delivery 19,HELP_WANTED +860,Town,Help Wanted: Item Delivery 20,HELP_WANTED +861,Town,Help Wanted: Item Delivery 21,HELP_WANTED +862,Town,Help Wanted: Item Delivery 22,HELP_WANTED +863,Town,Help Wanted: Item Delivery 23,HELP_WANTED +864,Town,Help Wanted: Item Delivery 24,HELP_WANTED +865,Town,Help Wanted: Item Delivery 25,HELP_WANTED +866,Town,Help Wanted: Item Delivery 26,HELP_WANTED +867,Town,Help Wanted: Item Delivery 27,HELP_WANTED +868,Town,Help Wanted: Item Delivery 28,HELP_WANTED +869,Town,Help Wanted: Item Delivery 29,HELP_WANTED +870,Town,Help Wanted: Item Delivery 30,HELP_WANTED +871,Town,Help Wanted: Item Delivery 31,HELP_WANTED +872,Town,Help Wanted: Item Delivery 32,HELP_WANTED +901,Forest,Traveling Merchant Sunday Item 1,"MANDATORY,TRAVELING_MERCHANT" +902,Forest,Traveling Merchant Sunday Item 2,"MANDATORY,TRAVELING_MERCHANT" +903,Forest,Traveling Merchant Sunday Item 3,"MANDATORY,TRAVELING_MERCHANT" +911,Forest,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT" +912,Forest,Traveling Merchant Monday Item 2,"MANDATORY,TRAVELING_MERCHANT" +913,Forest,Traveling Merchant Monday Item 3,"MANDATORY,TRAVELING_MERCHANT" +921,Forest,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT" +922,Forest,Traveling Merchant Tuesday Item 2,"MANDATORY,TRAVELING_MERCHANT" +923,Forest,Traveling Merchant Tuesday Item 3,"MANDATORY,TRAVELING_MERCHANT" +931,Forest,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT" +932,Forest,Traveling Merchant Wednesday Item 2,"MANDATORY,TRAVELING_MERCHANT" +933,Forest,Traveling Merchant Wednesday Item 3,"MANDATORY,TRAVELING_MERCHANT" +941,Forest,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT" +942,Forest,Traveling Merchant Thursday Item 2,"MANDATORY,TRAVELING_MERCHANT" +943,Forest,Traveling Merchant Thursday Item 3,"MANDATORY,TRAVELING_MERCHANT" +951,Forest,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT" +952,Forest,Traveling Merchant Friday Item 2,"MANDATORY,TRAVELING_MERCHANT" +953,Forest,Traveling Merchant Friday Item 3,"MANDATORY,TRAVELING_MERCHANT" +961,Forest,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT" +962,Forest,Traveling Merchant Saturday Item 2,"MANDATORY,TRAVELING_MERCHANT" +963,Forest,Traveling Merchant Saturday Item 3,"MANDATORY,TRAVELING_MERCHANT" +1001,Mountain,Fishsanity: Carp,FISHSANITY +1002,Beach,Fishsanity: Herring,FISHSANITY +1003,Forest,Fishsanity: Smallmouth Bass,FISHSANITY +1004,Beach,Fishsanity: Anchovy,FISHSANITY +1005,Beach,Fishsanity: Sardine,FISHSANITY +1006,Forest,Fishsanity: Sunfish,FISHSANITY +1007,Forest,Fishsanity: Perch,FISHSANITY +1008,Forest,Fishsanity: Chub,FISHSANITY +1009,Forest,Fishsanity: Bream,FISHSANITY +1010,Beach,Fishsanity: Red Snapper,FISHSANITY +1011,Beach,Fishsanity: Sea Cucumber,FISHSANITY +1012,Forest,Fishsanity: Rainbow Trout,FISHSANITY +1013,Forest,Fishsanity: Walleye,FISHSANITY +1014,Forest,Fishsanity: Shad,FISHSANITY +1015,Mountain,Fishsanity: Bullhead,FISHSANITY +1016,Mountain,Fishsanity: Largemouth Bass,FISHSANITY +1017,Forest,Fishsanity: Salmon,FISHSANITY +1018,The Mines - Floor 20,Fishsanity: Ghostfish,FISHSANITY +1019,Beach,Fishsanity: Tilapia,FISHSANITY +1020,Secret Woods,Fishsanity: Woodskip,FISHSANITY +1021,Beach,Fishsanity: Flounder,FISHSANITY +1022,Beach,Fishsanity: Halibut,FISHSANITY +1023,Ginger Island,Fishsanity: Lionfish,FISHSANITY +1024,Mutant Bug Lair,Fishsanity: Slimejack,FISHSANITY +1025,Forest,Fishsanity: Midnight Carp,FISHSANITY +1026,Beach,Fishsanity: Red Mullet,FISHSANITY +1027,Forest,Fishsanity: Pike,FISHSANITY +1028,Forest,Fishsanity: Tiger Trout,FISHSANITY +1029,Ginger Island,Fishsanity: Blue Discus,FISHSANITY +1030,Beach,Fishsanity: Albacore,FISHSANITY +1031,The Desert,Fishsanity: Sandfish,FISHSANITY +1032,The Mines - Floor 20,Fishsanity: Stonefish,FISHSANITY +1033,Beach,Fishsanity: Tuna,FISHSANITY +1034,Beach,Fishsanity: Eel,FISHSANITY +1035,Forest,Fishsanity: Catfish,FISHSANITY +1036,Beach,Fishsanity: Squid,FISHSANITY +1037,Mountain,Fishsanity: Sturgeon,FISHSANITY +1038,Forest,Fishsanity: Dorado,FISHSANITY +1039,Beach,Fishsanity: Pufferfish,FISHSANITY +1040,Witch's Swamp,Fishsanity: Void Salmon,FISHSANITY +1041,Beach,Fishsanity: Super Cucumber,FISHSANITY +1042,Ginger Island,Fishsanity: Stingray,FISHSANITY +1043,The Mines - Floor 60,Fishsanity: Ice Pip,FISHSANITY +1044,Forest,Fishsanity: Lingcod,FISHSANITY +1045,The Desert,Fishsanity: Scorpion Carp,FISHSANITY +1046,The Mines - Floor 100,Fishsanity: Lava Eel,FISHSANITY +1047,Beach,Fishsanity: Octopus,FISHSANITY +1048,Beach,Fishsanity: Midnight Squid,FISHSANITY +1049,Beach,Fishsanity: Spook Fish,FISHSANITY +1050,Beach,Fishsanity: Blobfish,FISHSANITY +1051,Beach,Fishsanity: Crimsonfish,FISHSANITY +1052,Town,Fishsanity: Angler,FISHSANITY +1053,Mountain,Fishsanity: Legend,FISHSANITY +1054,Forest,Fishsanity: Glacierfish,FISHSANITY +1055,Sewers,Fishsanity: Mutant Carp,FISHSANITY +1056,Town,Fishsanity: Crayfish,FISHSANITY +1057,Town,Fishsanity: Snail,FISHSANITY +1058,Town,Fishsanity: Periwinkle,FISHSANITY +1059,Beach,Fishsanity: Lobster,FISHSANITY +1060,Beach,Fishsanity: Clam,FISHSANITY +1061,Beach,Fishsanity: Crab,FISHSANITY +1062,Beach,Fishsanity: Cockle,FISHSANITY +1063,Beach,Fishsanity: Mussel,FISHSANITY +1064,Beach,Fishsanity: Shrimp,FISHSANITY +1065,Beach,Fishsanity: Oyster,FISHSANITY diff --git a/worlds/stardew_valley/data/resource_packs.csv b/worlds/stardew_valley/data/resource_packs.csv new file mode 100644 index 0000000000..0508ee35de --- /dev/null +++ b/worlds/stardew_valley/data/resource_packs.csv @@ -0,0 +1,39 @@ +name,default_amount,scaling_factor,classification,groups +Money,1000,500,useful,BASE_RESOURCE +Stone,50,25,filler,BASE_RESOURCE +Wood,50,25,filler,BASE_RESOURCE +Hardwood,10,5,useful,BASE_RESOURCE +Fiber,30,15,filler,BASE_RESOURCE +Coal,10,5,filler,BASE_RESOURCE +Clay,10,5,filler,BASE_RESOURCE +Warp Totem: Beach,5,2,filler,WARP_TOTEM +Warp Totem: Desert,5,2,filler,WARP_TOTEM +Warp Totem: Farm,5,2,filler,WARP_TOTEM +Warp Totem: Island,5,2,filler,WARP_TOTEM +Warp Totem: Mountains,5,2,filler,WARP_TOTEM +Geode,12,6,filler,GEODE +Frozen Geode,8,4,filler,GEODE +Magma Geode,6,3,filler,GEODE +Omni Geode,4,2,useful,GEODE +Copper Ore,75,25,filler,ORE +Iron Ore,50,25,filler,ORE +Gold Ore,25,13,useful,ORE +Iridium Ore,10,5,useful,ORE +Quartz,10,5,filler,ORE +Basic Fertilizer,30,10,filler,FERTILIZER +Basic Retaining Soil,30,10,filler,FERTILIZER +Speed-Gro,30,10,filler,FERTILIZER +Quality Fertilizer,20,8,filler,FERTILIZER +Quality Retaining Soil,20,8,filler,FERTILIZER +Deluxe Speed-Gro,20,8,filler,FERTILIZER +Deluxe Fertilizer,10,4,useful,FERTILIZER +Deluxe Retaining Soil,10,4,useful,FERTILIZER +Hyper Speed-Gro,10,4,useful,FERTILIZER +Tree Fertilizer,10,4,filler,FERTILIZER +Spring Seeds,30,10,filler,SEED +Summer Seeds,30,10,filler,SEED +Fall Seeds,30,10,filler,SEED +Winter Seeds,30,10,filler,SEED +Mahogany Seed,5,2,filler,SEED +Bait,30,10,filler,FISHING_RESOURCE +Crab Pot,3,1,filler,FISHING_RESOURCE \ No newline at end of file diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md new file mode 100644 index 0000000000..aef280864b --- /dev/null +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -0,0 +1,71 @@ +# Stardew Valley + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +A vast number of optional objectives in stardew valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file. + +For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply a stack of an item that may be useful to the player. + +## What is the goal of Stardew Valley? + +The player can choose from a number of goals, using their YAML settings. +- Complete the Community Center +- Succeed Grandpa's Evaluation with 4 lit candles +- Reach the bottom of the Pelican Town Mineshaft +- Complete the "Cryptic Note" quest, by meeting Mr Qi on floor 100 of the Skull Cavern +- Get the achievement "Master Angler", which requires catching every fish in the game + +## What are location check in Stardew Valley? + +Location checks in Stardew Valley always include: +- Community Center Bundles +- Mineshaft chest rewards +- Story Quests +- Traveling Merchant items +- Isolated objectives such as the beach bridge, Old Master Cannoli, Grim Reaper Statue, etc + +There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: +- Tools and Fishing Rod Upgrades +- Carpenter Buildings +- Backpack Upgrades +- Mine elevator levels +- Skill Levels +- Arcade Machines +- Help Wanted quests +- Fishsanity: Catching individual fish + +## Which items can be in another player's world? + +Every normal reward from the above locations can be in another player's world. +For the locations which do not include a normal reward, Resource Packs are instead added to the pool. These can contain ores, seeds, fertilizers, warp totems, etc. +There are a few extra items, which are added to the pool but do not have a matching location. These include +- Wizard Buildings +- Return Scepter + +And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include: +- Arcade Machine buffs (Only if the arcade machines are randomized) + - Journey of the Prairie King has drop rate increases, extra lives, and equipment + - Junimo Kart has extra lives. +- Permanent Movement Speed Bonuses (customizable) +- Permanent Luck Bonuses (customizable) +- Traveling Merchant buffs + +## When the player receives an item, what happens? + +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. +If an item is received while offline, it will be in the mailbox as soon as the player logs in. + +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. + +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. + +## Multiplayer + +You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. + +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md new file mode 100644 index 0000000000..7ac9c8a814 --- /dev/null +++ b/worlds/stardew_valley/docs/setup_en.md @@ -0,0 +1,74 @@ +# Stardew Valley Randomizer Setup Guide + +## Required Software + +- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) +- SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) +- [StardewArchipelago Mod Release 2.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) + - It is important to use a mod release of version 2.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. + +## Optional Software +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) + - (Only for the TextClient) +- Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) + - It is **not** recommended to further mod Stardew Valley, altough it is possible to do so. Mod interactions can be unpredictable, and no support will be offered for related bugs. + - The more mods you have, and the bigger they are, the more likely things are to break. + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a YAML file? + +You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew Valley/player-settings) + +## Joining a MultiWorld Game + +### Installing the mod + +- Install [SMAPI](https://smapi.io/) by following the instructions on their website +- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder +- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: + - "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command% +- Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly +- Stardew Valley should launch itself alongside a console which allows you to read mod information and interact with some of them. + +### Connect to the MultiServer + +Launch Stardew Valley with SMAPI. Once you have reached the Stardew Valley title screen, create a new farm. + +On the new character creation page, you will see 3 new fields, used to link your new character to an archipelago multiworld + +![image](https://i.imgur.com/b8KZy2F.png) + +You can customize your farm and character as much as desired. + +The Server text box needs to have both the address and the port, and your slotname is the name specified in your yaml + +`archipelago.gg:38281` + +`StardewPlayer` + +The password is optional. + +Your game will connect automatically to Archipelago, and reconnect automatically when loading the save, later. + +You will never need to enter this information again for this character. + +### Interacting with the MultiWorld from in-game + +When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other Stardew-exclusive chat commands you can use. + +Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game that supports chatting. + +Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain items, or check missing locations. + +It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the full history as well and may be better suited to read older messages. +For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run Stardew-exclusive commands. + +### Multiplayer + +You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. \ No newline at end of file diff --git a/worlds/stardew_valley/fish_data.py b/worlds/stardew_valley/fish_data.py new file mode 100644 index 0000000000..270accb478 --- /dev/null +++ b/worlds/stardew_valley/fish_data.py @@ -0,0 +1,127 @@ +from typing import List, Tuple + +from .game_item import FishItem + +spring = ("Spring",) +summer = ("Summer",) +fall = ("Fall",) +winter = ("Winter",) +spring_summer = (*spring, *summer) +spring_fall = (*spring, *fall) +spring_winter = (*spring, *winter) +summer_fall = (*summer, *fall) +summer_winter = (*summer, *winter) +fall_winter = (*fall, *winter) +spring_summer_fall = (*spring, *summer, *fall) +spring_summer_winter = (*spring, *summer, *winter) +spring_fall_winter = (*spring, *fall, *winter) +all_seasons = (*spring, *summer, *fall, *winter) + +town = ("Town",) +beach = ("Beach",) +mountain = ("Mountain",) +forest = ("Forest",) +secret_woods = ("Secret Woods",) +desert = ("The Desert",) +mines_20 = ("The Mines - Floor 20",) +mines_60 = ("The Mines - Floor 60",) +mines_100 = ("The Mines - Floor 100",) +sewers = ("Sewers",) +mutant_bug_lair = ("Mutant Bug Lair",) +witch_swamp = ("Witch's Swamp",) +ginger_island = ("Ginger Island",) +ginger_island_ocean = ginger_island +ginger_island_river = ginger_island +pirate_cove = ginger_island +night_market = beach +lakes = (*mountain, *secret_woods, *sewers) +ocean = beach +rivers = (*town, *forest) +rivers_secret_woods = (*rivers, *secret_woods) +forest_mountain = (*forest, *mountain) +rivers_mountain_lake = (*town, *forest, *mountain) +mines_20_60 = (*mines_20, *mines_60) + +all_fish_items: List[FishItem] = [] + + +def fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Tuple[str, ...], difficulty: int) -> FishItem: + fish_item = FishItem(name, item_id, locations, seasons, difficulty) + all_fish_items.append(fish_item) + return fish_item + + +carp = fish("Carp", 142, lakes, all_seasons, 15) +herring = fish("Herring", 147, ocean, spring_winter, 25) +smallmouth_bass = fish("Smallmouth Bass", 137, rivers, spring_fall, 28) +anchovy = fish("Anchovy", 129, ocean, spring_fall, 30) +sardine = fish("Sardine", 131, ocean, spring_fall_winter, 30) +sunfish = fish("Sunfish", 145, rivers, spring_summer, 30) +perch = fish("Perch", 141, rivers_mountain_lake, winter, 35) +chub = fish("Chub", 702, forest_mountain, all_seasons, 35) +bream = fish("Bream", 132, rivers, all_seasons, 35) +red_snapper = fish("Red Snapper", 150, ocean, summer_fall, 40) +sea_cucumber = fish("Sea Cucumber", 154, ocean, fall_winter, 40) +rainbow_trout = fish("Rainbow Trout", 138, rivers_mountain_lake, summer, 45) +walleye = fish("Walleye", 140, rivers_mountain_lake, fall, 45) +shad = fish("Shad", 706, rivers, spring_summer_fall, 45) +bullhead = fish("Bullhead", 700, mountain, all_seasons, 46) +largemouth_bass = fish("Largemouth Bass", 136, mountain, all_seasons, 50) +salmon = fish("Salmon", 139, rivers, fall, 50) +ghostfish = fish("Ghostfish", 156, mines_20_60, all_seasons, 50) +tilapia = fish("Tilapia", 701, ocean, summer_fall, 50) +woodskip = fish("Woodskip", 734, secret_woods, all_seasons, 50) +flounder = fish("Flounder", 267, ocean, spring_summer, 50) +halibut = fish("Halibut", 708, ocean, spring_summer_winter, 50) +lionfish = fish("Lionfish", 837, ginger_island_ocean, all_seasons, 50) +slimejack = fish("Slimejack", 796, mutant_bug_lair, all_seasons, 55) +midnight_carp = fish("Midnight Carp", 269, forest_mountain, fall_winter, 55) +red_mullet = fish("Red Mullet", 146, ocean, summer_winter, 55) +pike = fish("Pike", 144, rivers, summer_winter, 60) +tiger_trout = fish("Tiger Trout", 699, rivers, fall_winter, 60) +blue_discus = fish("Blue Discus", 838, ginger_island_river, all_seasons, 60) +albacore = fish("Albacore", 705, ocean, fall_winter, 60) +sandfish = fish("Sandfish", 164, desert, all_seasons, 65) +stonefish = fish("Stonefish", 158, mines_20, all_seasons, 65) +tuna = fish("Tuna", 130, ocean, summer_winter, 70) +eel = fish("Eel", 148, ocean, spring_fall, 70) +catfish = fish("Catfish", 143, rivers_secret_woods, spring_fall, 75) +squid = fish("Squid", 151, ocean, winter, 75) +sturgeon = fish("Sturgeon", 698, mountain, summer_winter, 78) +dorado = fish("Dorado", 704, forest, summer, 78) +pufferfish = fish("Pufferfish", 128, ocean, summer, 80) +void_salmon = fish("Void Salmon", 795, witch_swamp, all_seasons, 80) +super_cucumber = fish("Super Cucumber", 155, ocean, summer_fall, 80) +stingray = fish("Stingray", 836, pirate_cove, all_seasons, 80) +ice_pip = fish("Ice Pip", 161, mines_60, all_seasons, 85) +lingcod = fish("Lingcod", 707, rivers_mountain_lake, winter, 85) +scorpion_carp = fish("Scorpion Carp", 165, desert, all_seasons, 90) +lava_eel = fish("Lava Eel", 162, mines_100, all_seasons, 90) +octopus = fish("Octopus", 149, ocean, summer, 95) + +midnight_squid = fish("Midnight Squid", 798, night_market, winter, 55) +spook_fish = fish("Spook Fish", 799, night_market, winter, 60) +blob_fish = fish("Blobfish", 800, night_market, winter, 75) + +crimsonfish = fish("Crimsonfish", 159, ocean, summer, 95) +angler = fish("Angler", 160, town, fall, 85) +legend = fish("Legend", 163, mountain, spring, 110) +glacierfish = fish("Glacierfish", 775, forest, winter, 100) +mutant_carp = fish("Mutant Carp", 682, sewers, all_seasons, 80) + +crayfish = fish("Crayfish", 716, rivers, all_seasons, -1) +snail = fish("Snail", 721, rivers, all_seasons, -1) +periwinkle = fish("Periwinkle", 722, rivers, all_seasons, -1) +lobster = fish("Lobster", 715, ocean, all_seasons, -1) +clam = fish("Clam", 372, ocean, all_seasons, -1) +crab = fish("Crab", 717, ocean, all_seasons, -1) +cockle = fish("Cockle", 718, ocean, all_seasons, -1) +mussel = fish("Mussel", 719, ocean, all_seasons, -1) +shrimp = fish("Shrimp", 720, ocean, all_seasons, -1) +oyster = fish("Oyster", 723, ocean, all_seasons, -1) + +legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp] +special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] + +all_fish_items_by_name = {fish.name: fish for fish in all_fish_items} +all_fish_items_by_id = {fish.item_id: fish for fish in all_fish_items} diff --git a/worlds/stardew_valley/game_item.py b/worlds/stardew_valley/game_item.py new file mode 100644 index 0000000000..6b8eb6c6aa --- /dev/null +++ b/worlds/stardew_valley/game_item.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from typing import Tuple + + +@dataclass(frozen=True) +class GameItem: + name: str + item_id: int + + def __repr__(self): + return f"{self.name} [{self.item_id}]" + + def __lt__(self, other): + return self.name < other.name + + +@dataclass(frozen=True) +class FishItem(GameItem): + locations: Tuple[str] + seasons: Tuple[str] + difficulty: int + + def __repr__(self): + return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ + f" Seasons: {self.seasons} |" \ + f" Difficulty: {self.difficulty}) " diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py new file mode 100644 index 0000000000..03419a1610 --- /dev/null +++ b/worlds/stardew_valley/items.py @@ -0,0 +1,376 @@ +import bisect +import csv +import enum +import itertools +import logging +import math +import typing +from collections import OrderedDict +from dataclasses import dataclass, field +from functools import cached_property +from pathlib import Path +from random import Random +from typing import Dict, List, Protocol, Union, Set, Optional, FrozenSet + +from BaseClasses import Item, ItemClassification +from . import options, data + +ITEM_CODE_OFFSET = 717000 + +logger = logging.getLogger(__name__) +world_folder = Path(__file__).parent + + +class Group(enum.Enum): + RESOURCE_PACK = enum.auto() + FRIENDSHIP_PACK = enum.auto() + COMMUNITY_REWARD = enum.auto() + TRASH = enum.auto() + MINES_FLOOR_10 = enum.auto() + MINES_FLOOR_20 = enum.auto() + MINES_FLOOR_50 = enum.auto() + MINES_FLOOR_60 = enum.auto() + MINES_FLOOR_80 = enum.auto() + MINES_FLOOR_90 = enum.auto() + MINES_FLOOR_110 = enum.auto() + FOOTWEAR = enum.auto() + HATS = enum.auto() + RING = enum.auto() + WEAPON = enum.auto() + PROGRESSIVE_TOOLS = enum.auto() + SKILL_LEVEL_UP = enum.auto() + ARCADE_MACHINE_BUFFS = enum.auto() + GALAXY_WEAPONS = enum.auto() + BASE_RESOURCE = enum.auto() + WARP_TOTEM = enum.auto() + GEODE = enum.auto() + ORE = enum.auto() + FERTILIZER = enum.auto() + SEED = enum.auto() + FISHING_RESOURCE = enum.auto() + + +@dataclass(frozen=True) +class ItemData: + code_without_offset: Optional[int] + name: str + classification: ItemClassification + groups: Set[Group] = field(default_factory=frozenset) + + def __post_init__(self): + if not isinstance(self.groups, frozenset): + super().__setattr__("groups", frozenset(self.groups)) + + @property + def code(self): + return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None + + def has_any_group(self, *group: Group) -> bool: + groups = set(group) + return bool(groups.intersection(self.groups)) + + +@dataclass(frozen=True) +class ResourcePackData: + name: str + default_amount: int = 1 + scaling_factor: int = 1 + classification: ItemClassification = ItemClassification.filler + groups: FrozenSet[Group] = frozenset() + + def as_item_data(self, counter: itertools.count) -> [ItemData]: + return [ItemData(next(counter), self.create_item_name(quantity), self.classification, + {Group.RESOURCE_PACK} | self.groups) + for quantity in self.scale_quantity.values()] + + def create_item_name(self, quantity: int) -> str: + return f"Resource Pack: {quantity} {self.name}" + + @cached_property + def scale_quantity(self) -> typing.OrderedDict[int, int]: + """Discrete scaling of the resource pack quantities. + 100 is default, 200 is double, 50 is half (if the scaling_factor allows it). + """ + levels = math.ceil(self.default_amount / self.scaling_factor) * 2 + first_level = self.default_amount % self.scaling_factor + if first_level == 0: + first_level = self.scaling_factor + quantities = sorted(set(range(first_level, self.scaling_factor * levels, self.scaling_factor)) + | {self.default_amount * 2}) + + return OrderedDict({round(quantity / self.default_amount * 100): quantity + for quantity in quantities + if quantity <= self.default_amount * 2}) + + def calculate_quantity(self, multiplier: int) -> int: + scales = list(self.scale_quantity) + left_scale = bisect.bisect_left(scales, multiplier) + closest_scale = min([scales[left_scale], scales[left_scale - 1]], + key=lambda x: abs(multiplier - x)) + return self.scale_quantity[closest_scale] + + def create_name_from_multiplier(self, multiplier: int) -> str: + return self.create_item_name(self.calculate_quantity(multiplier)) + + +class FriendshipPackData(ResourcePackData): + def create_item_name(self, quantity: int) -> str: + return f"Friendship Bonus ({quantity} <3)" + + def as_item_data(self, counter: itertools.count) -> [ItemData]: + item_datas = super().as_item_data(counter) + return [ItemData(item.code_without_offset, item.name, item.classification, {Group.FRIENDSHIP_PACK}) + for item in item_datas] + + +class StardewItemFactory(Protocol): + def __call__(self, name: Union[str, ItemData]) -> Item: + raise NotImplementedError + + +def load_item_csv(): + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + items = [] + with files(data).joinpath("items.csv").open() as file: + item_reader = csv.DictReader(file) + for item in item_reader: + id = int(item["id"]) if item["id"] else None + classification = ItemClassification[item["classification"]] + groups = {Group[group] for group in item["groups"].split(",") if group} + items.append(ItemData(id, item["name"], classification, groups)) + return items + + +def load_resource_pack_csv() -> List[ResourcePackData]: + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + resource_packs = [] + with files(data).joinpath("resource_packs.csv").open() as file: + resource_pack_reader = csv.DictReader(file) + for resource_pack in resource_pack_reader: + groups = frozenset(Group[group] for group in resource_pack["groups"].split(",") if group) + resource_packs.append(ResourcePackData(resource_pack["name"], + int(resource_pack["default_amount"]), + int(resource_pack["scaling_factor"]), + ItemClassification[resource_pack["classification"]], + groups)) + return resource_packs + + +events = [ + ItemData(None, "Victory", ItemClassification.progression), + ItemData(None, "Spring", ItemClassification.progression), + ItemData(None, "Summer", ItemClassification.progression), + ItemData(None, "Fall", ItemClassification.progression), + ItemData(None, "Winter", ItemClassification.progression), + ItemData(None, "Year Two", ItemClassification.progression), +] + +all_items: List[ItemData] = load_item_csv() + events +item_table: Dict[str, ItemData] = {} +items_by_group: Dict[Group, List[ItemData]] = {} + + +def initialize_groups(): + for item in all_items: + for group in item.groups: + item_group = items_by_group.get(group, list()) + item_group.append(item) + items_by_group[group] = item_group + + +def initialize_item_table(): + item_table.update({item.name: item for item in all_items}) + + +friendship_pack = FriendshipPackData("Friendship Bonus", default_amount=2, classification=ItemClassification.useful) +all_resource_packs = load_resource_pack_csv() + +initialize_item_table() +initialize_groups() + + +def create_items(item_factory: StardewItemFactory, locations_count: int, world_options: options.StardewOptions, + random: Random) \ + -> List[Item]: + items = create_unique_items(item_factory, world_options, random) + assert len(items) <= locations_count, \ + "There should be at least as many locations as there are mandatory items" + logger.debug(f"Created {len(items)} unique items") + + resource_pack_items = fill_with_resource_packs(item_factory, world_options, random, locations_count - len(items)) + items += resource_pack_items + logger.debug(f"Created {len(resource_pack_items)} resource packs") + + return items + + +def create_backpack_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or + world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive): + items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2) + + +def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], random: Random): + items.append(item_factory("Rusty Sword")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_10]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_20]))) + items.append(item_factory("Slingshot")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_50]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_60]))) + items.append(item_factory("Master Slingshot")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_80]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_90]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_110]))) + items.append(item_factory("Skull Key")) + + +def create_mine_elevators(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + if (world_options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive or + world_options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): + items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24]) + + +def create_tools(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + if world_options[options.ToolProgression] == options.ToolProgression.option_progressive: + items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4) + items.append(item_factory("Golden Scythe")) + + +def create_skills(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + if world_options[options.SkillProgression] == options.SkillProgression.option_progressive: + items.extend([item_factory(item) for item in items_by_group[Group.SKILL_LEVEL_UP] * 10]) + + +def create_wizard_buildings(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Earth Obelisk")) + items.append(item_factory("Water Obelisk")) + items.append(item_factory("Desert Obelisk")) + items.append(item_factory("Island Obelisk")) + items.append(item_factory("Junimo Hut")) + items.append(item_factory("Gold Clock")) + + +def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: options.StardewOptions, + items: List[Item]): + if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, + options.BuildingProgression.option_progressive_early_shipping_bin}: + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Well")) + items.append(item_factory("Silo")) + items.append(item_factory("Mill")) + items.append(item_factory("Progressive Shed")) + items.append(item_factory("Progressive Shed")) + items.append(item_factory("Fish Pond")) + items.append(item_factory("Stable")) + items.append(item_factory("Slime Hutch")) + items.append(item_factory("Shipping Bin")) + items.append(item_factory("Progressive House")) + items.append(item_factory("Progressive House")) + items.append(item_factory("Progressive House")) + + +def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Adventurer's Guild")) + items.append(item_factory("Club Card")) + items.append(item_factory("Magnifying Glass")) + items.append(item_factory("Bear's Knowledge")) + items.append(item_factory("Iridium Snake Milk")) + + +def create_stardrops(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Stardrop")) # The Mines level 100 + items.append(item_factory("Stardrop")) # Old Master Cannoli + + +def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, + items: List[Item]): + if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + items.append(item_factory("JotPK: Progressive Boots")) + items.append(item_factory("JotPK: Progressive Boots")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Extra Life")) + items.append(item_factory("JotPK: Extra Life")) + items.append(item_factory("JotPK: Increased Drop Rate")) + items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) + + +def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + number_of_buffs: int = world_options[options.NumberOfPlayerBuffs] + items.extend(item_factory(item) for item in ["Movement Speed Bonus"] * number_of_buffs) + items.extend(item_factory(item) for item in ["Luck Bonus"] * number_of_buffs) + + +def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Traveling Merchant: Sunday")) + items.append(item_factory("Traveling Merchant: Monday")) + items.append(item_factory("Traveling Merchant: Tuesday")) + items.append(item_factory("Traveling Merchant: Wednesday")) + items.append(item_factory("Traveling Merchant: Thursday")) + items.append(item_factory("Traveling Merchant: Friday")) + items.append(item_factory("Traveling Merchant: Saturday")) + items.extend(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6) + items.extend(item_factory(item) for item in ["Traveling Merchant Discount"] * 8) + + +def create_unique_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random) -> \ + List[Item]: + items = [] + + items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) + + create_backpack_items(item_factory, world_options, items) + create_mine_rewards(item_factory, items, random) + create_mine_elevators(item_factory, world_options, items) + create_tools(item_factory, world_options, items) + create_skills(item_factory, world_options, items) + create_wizard_buildings(item_factory, items) + create_carpenter_buildings(item_factory, world_options, items) + items.append(item_factory("Beach Bridge")) + create_special_quest_rewards(item_factory, items) + create_stardrops(item_factory, items) + create_arcade_machine_items(item_factory, world_options, items) + items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) + items.append( + item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier]))) + create_player_buffs(item_factory, world_options, items) + create_traveling_merchant_items(item_factory, items) + items.append(item_factory("Return Scepter")) + + return items + + +def fill_with_resource_packs(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, + required_resource_pack: int) -> List[Item]: + resource_pack_multiplier = world_options[options.ResourcePackMultiplier] + + if resource_pack_multiplier == 0: + return [item_factory(cola) for cola in ["Joja Cola"] * required_resource_pack] + + items = [] + + for i in range(required_resource_pack): + resource_pack = random.choice(all_resource_packs) + items.append(item_factory(resource_pack.create_name_from_multiplier(resource_pack_multiplier))) + + return items diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py new file mode 100644 index 0000000000..a7cb70c570 --- /dev/null +++ b/worlds/stardew_valley/locations.py @@ -0,0 +1,175 @@ +import csv +import enum +from dataclasses import dataclass +from random import Random +from typing import Optional, Dict, Protocol, List, FrozenSet + +from . import options, data +from .fish_data import legendary_fish, special_fish, all_fish_items + +LOCATION_CODE_OFFSET = 717000 + + +class LocationTags(enum.Enum): + MANDATORY = enum.auto() + BUNDLE = enum.auto() + COMMUNITY_CENTER_BUNDLE = enum.auto() + CRAFTS_ROOM_BUNDLE = enum.auto() + PANTRY_BUNDLE = enum.auto() + FISH_TANK_BUNDLE = enum.auto() + BOILER_ROOM_BUNDLE = enum.auto() + BULLETIN_BOARD_BUNDLE = enum.auto() + VAULT_BUNDLE = enum.auto() + COMMUNITY_CENTER_ROOM = enum.auto() + BACKPACK = enum.auto() + TOOL_UPGRADE = enum.auto() + HOE_UPGRADE = enum.auto() + PICKAXE_UPGRADE = enum.auto() + AXE_UPGRADE = enum.auto() + WATERING_CAN_UPGRADE = enum.auto() + TRASH_CAN_UPGRADE = enum.auto() + FISHING_ROD_UPGRADE = enum.auto() + THE_MINES_TREASURE = enum.auto() + THE_MINES_ELEVATOR = enum.auto() + SKILL_LEVEL = enum.auto() + FARMING_LEVEL = enum.auto() + FISHING_LEVEL = enum.auto() + FORAGING_LEVEL = enum.auto() + COMBAT_LEVEL = enum.auto() + MINING_LEVEL = enum.auto() + BUILDING_BLUEPRINT = enum.auto() + QUEST = enum.auto() + ARCADE_MACHINE = enum.auto() + ARCADE_MACHINE_VICTORY = enum.auto() + JOTPK = enum.auto() + JUNIMO_KART = enum.auto() + HELP_WANTED = enum.auto() + TRAVELING_MERCHANT = enum.auto() + FISHSANITY = enum.auto() + + +@dataclass(frozen=True) +class LocationData: + code_without_offset: Optional[int] + region: str + name: str + tags: FrozenSet[LocationTags] = frozenset() + + @property + def code(self) -> Optional[int]: + return LOCATION_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None + + +class StardewLocationCollector(Protocol): + def __call__(self, name: str, code: Optional[int], region: str) -> None: + raise NotImplementedError + + +def load_location_csv() -> List[LocationData]: + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + with files(data).joinpath("locations.csv").open() as file: + reader = csv.DictReader(file) + return [LocationData(int(location["id"]) if location["id"] else None, + location["region"], + location["name"], + frozenset(LocationTags[group] + for group in location["tags"].split(",") + if group)) + for location in reader] + + +events_locations = [ + LocationData(None, "Stardew Valley", "Succeed Grandpa's Evaluation"), + LocationData(None, "Community Center", "Complete Community Center"), + LocationData(None, "The Mines - Floor 120", "Reach the Bottom of The Mines"), + LocationData(None, "Skull Cavern", "Complete Quest Cryptic Note"), + LocationData(None, "Stardew Valley", "Catch Every Fish"), + LocationData(None, "Stardew Valley", "Summer"), + LocationData(None, "Stardew Valley", "Fall"), + LocationData(None, "Stardew Valley", "Winter"), + LocationData(None, "Stardew Valley", "Year Two"), +] + +all_locations = load_location_csv() + events_locations +location_table: Dict[str, LocationData] = {location.name: location for location in all_locations} +locations_by_tag: Dict[LocationTags, List[LocationData]] = {} + + +def initialize_groups(): + for location in all_locations: + for tag in location.tags: + location_group = locations_by_tag.get(tag, list()) + location_group.append(location) + locations_by_tag[tag] = location_group + + +initialize_groups() + + +def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_number_of_quests: int): + for i in range(0, desired_number_of_quests): + batch = i // 7 + index_this_batch = i % 7 + if index_this_batch < 4: + randomized_locations.append( + location_table[f"Help Wanted: Item Delivery {(batch * 4) + index_this_batch + 1}"]) + elif index_this_batch == 4: + randomized_locations.append(location_table[f"Help Wanted: Fishing {batch + 1}"]) + elif index_this_batch == 5: + randomized_locations.append(location_table[f"Help Wanted: Slay Monsters {batch + 1}"]) + elif index_this_batch == 6: + randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) + + +def extend_fishsanity_locations(randomized_locations: List[LocationData], fishsanity: int, random: Random): + prefix = "Fishsanity: " + if fishsanity == options.Fishsanity.option_none: + return + elif fishsanity == options.Fishsanity.option_legendaries: + randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish) + elif fishsanity == options.Fishsanity.option_special: + randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) + elif fishsanity == options.Fishsanity.option_random_selection: + randomized_locations.extend(location_table[f"{prefix}{fish.name}"] + for fish in all_fish_items if random.random() < 0.4) + elif fishsanity == options.Fishsanity.option_all: + randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish_items) + + +def create_locations(location_collector: StardewLocationCollector, + world_options: options.StardewOptions, + random: Random): + randomized_locations = [] + + randomized_locations.extend(locations_by_tag[LocationTags.MANDATORY]) + + if not world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.BACKPACK]) + + if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE]) + + if not world_options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.THE_MINES_ELEVATOR]) + + if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.SKILL_LEVEL]) + + if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.BUILDING_BLUEPRINT]) + + if not world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_disabled: + randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY]) + + if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) + + extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) + extend_fishsanity_locations(randomized_locations, world_options[options.Fishsanity], random) + + for location_data in randomized_locations: + location_collector(location_data.name, location_data.code, location_data.region) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py new file mode 100644 index 0000000000..79b2b63ddf --- /dev/null +++ b/worlds/stardew_valley/logic.py @@ -0,0 +1,1143 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List, FrozenSet + +from BaseClasses import CollectionState, ItemClassification +from . import options +from .bundle_data import BundleItem +from .fish_data import all_fish_items +from .game_item import FishItem +from .items import all_items, Group, item_table +from .options import StardewOptions + +MISSING_ITEM = "THIS ITEM IS MISSING" + +tool_materials = { + "Copper": 1, + "Iron": 2, + "Gold": 3, + "Iridium": 4 +} + +tool_prices = { + "Copper": 2000, + "Iron": 5000, + "Gold": 10000, + "Iridium": 25000 +} + +skill_level_per_season = { + "Spring": { + "Farming": 2, + "Fishing": 2, + "Foraging": 2, + "Mining": 2, + "Combat": 2, + }, + "Summer": { + "Farming": 4, + "Fishing": 4, + "Foraging": 4, + "Mining": 4, + "Combat": 3, + }, + "Fall": { + "Farming": 7, + "Fishing": 5, + "Foraging": 5, + "Mining": 5, + "Combat": 4, + }, + "Winter": { + "Farming": 7, + "Fishing": 7, + "Foraging": 6, + "Mining": 7, + "Combat": 5, + }, + "Year Two": { + "Farming": 10, + "Fishing": 10, + "Foraging": 10, + "Mining": 10, + "Combat": 10, + }, +} +season_per_skill_level: Dict[Tuple[str, int], str] = {} +season_per_total_level: Dict[int, str] = {} + + +def initialize_season_per_skill_level(): + current_level = { + "Farming": 0, + "Fishing": 0, + "Foraging": 0, + "Mining": 0, + "Combat": 0, + } + for season, skills in skill_level_per_season.items(): + for skill, expected_level in skills.items(): + for level_up in range(current_level[skill] + 1, expected_level + 1): + skill_level = (skill, level_up) + if skill_level not in season_per_skill_level: + season_per_skill_level[skill_level] = season + level_up = 0 + for level_up in range(level_up + 1, sum(skills.values()) + 1): + if level_up not in season_per_total_level: + season_per_total_level[level_up] = season + + +initialize_season_per_skill_level() +week_days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + + +class StardewRule: + def __call__(self, state: CollectionState) -> bool: + raise NotImplementedError + + def __or__(self, other) -> StardewRule: + if isinstance(other, _Or): + return _Or(self, *other.rules) + + return _Or(self, other) + + def __and__(self, other) -> StardewRule: + if isinstance(other, _And): + return _And(other.rules.union({self})) + + return _And(self, other) + + def get_difficulty(self): + raise NotImplementedError + + def simplify(self) -> StardewRule: + return self + + +class _True(StardewRule): + + def __new__(cls, _cache=[]): # noqa + if not _cache: + _cache.append(super(_True, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return True + + def __or__(self, other) -> StardewRule: + return self + + def __and__(self, other) -> StardewRule: + return other + + def __repr__(self): + return "True" + + def get_difficulty(self): + return 0 + + +class _False(StardewRule): + + def __new__(cls, _cache=[]): # noqa + if not _cache: + _cache.append(super(_False, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return False + + def __or__(self, other) -> StardewRule: + return other + + def __and__(self, other) -> StardewRule: + return self + + def __repr__(self): + return "False" + + def get_difficulty(self): + return 999999999 + + +class _Or(StardewRule): + rules: FrozenSet[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a Or conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, _Or): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return any(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' | '.join(repr(rule) for rule in self.rules)})" + + def __or__(self, other): + if isinstance(other, _True): + return other + if isinstance(other, _False): + return self + if isinstance(other, _Or): + return _Or(self.rules.union(other.rules)) + + return _Or(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return min(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, _True) for rule in self.rules): + return _True() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not _False()} + + if not simplified_rules: + return _False() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return _Or(simplified_rules) + + +class _And(StardewRule): + rules: frozenset[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a And conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, _And): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return all(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' & '.join(repr(rule) for rule in self.rules)})" + + def __and__(self, other): + if isinstance(other, _True): + return self + if isinstance(other, _False): + return other + if isinstance(other, _And): + return _And(self.rules.union(other.rules)) + + return _And(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return max(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, _False) for rule in self.rules): + return _False() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not _True()} + + if not simplified_rules: + return _True() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return _And(simplified_rules) + + +class _Count(StardewRule): + count: int + rules: List[StardewRule] + + def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = [] + if isinstance(rule, Iterable): + rules_list.extend(rule) + else: + rules_list.append(rule) + + if rules is not None: + rules_list.extend(rules) + + assert rules_list, "Can't create a Count conditions without rules" + assert len(rules_list) >= count, "Count need at least as many rules at the count" + + self.rules = rules_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for r in self.rules: + if r(state): + c += 1 + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {repr(self.rules)}" + + def get_difficulty(self): + rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty()) + easiest_n_rules = rules_sorted_by_difficulty[0:self.count] + return max(rule.get_difficulty() for rule in easiest_n_rules) + + def simplify(self): + return _Count(self.count, [rule.simplify() for rule in self.rules]) + + +class _TotalReceived(StardewRule): + count: int + items: Iterable[str] + player: int + + def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): + items_list = [] + if isinstance(items, Iterable): + items_list.extend(items) + else: + items_list.append(items) + + assert items_list, "Can't create a Total Received conditions without items" + for item in items_list: + assert item_table[item].classification & ItemClassification.progression, \ + "Item has to be progression to be used in logic" + + self.player = player + self.items = items_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for item in self.items: + c += state.count(item, self.player) + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {self.items}" + + def get_difficulty(self): + return self.count + + +@dataclass(frozen=True) +class _Received(StardewRule): + item: str + player: int + count: int + + def __post_init__(self): + assert item_table[self.item].classification & ItemClassification.progression, \ + "Item has to be progression to be used in logic" + + def __call__(self, state: CollectionState) -> bool: + return state.has(self.item, self.player, self.count) + + def __repr__(self): + if self.count == 1: + return f"Received {self.item}" + return f"Received {self.count} {self.item}" + + def get_difficulty(self): + if self.item == "Spring": + return 0 + if self.item == "Summer": + return 1 + if self.item == "Fall": + return 2 + if self.item == "Winter": + return 3 + if self.item == "Year Two": + return 4 + return self.count + + +@dataclass(frozen=True) +class _Reach(StardewRule): + spot: str + resolution_hint: str + player: int + + def __call__(self, state: CollectionState) -> bool: + return state.can_reach(self.spot, self.resolution_hint, self.player) + + def __repr__(self): + return f"Reach {self.resolution_hint} {self.spot}" + + def get_difficulty(self): + return 1 + + +@dataclass(frozen=True) +class _Has(StardewRule): + item: str + # For sure there is a better way than just passing all the rules everytime + other_rules: Dict[str, StardewRule] + + def __call__(self, state: CollectionState) -> bool: + if isinstance(self.item, str): + return self.other_rules[self.item](state) + + def __repr__(self): + if not self.item in self.other_rules: + return f"Has {self.item} -> {MISSING_ITEM}" + return f"Has {self.item} -> {repr(self.other_rules[self.item])}" + + def get_difficulty(self): + return self.other_rules[self.item].get_difficulty() + 1 + + def __hash__(self): + return hash(self.item) + + def simplify(self) -> StardewRule: + return self.other_rules[self.item].simplify() + + +@dataclass(frozen=True) +class StardewLogic: + player: int + options: StardewOptions + + item_rules: Dict[str, StardewRule] = field(default_factory=dict) + fish_rules: Dict[str, StardewRule] = field(default_factory=dict) + building_rules: Dict[str, StardewRule] = field(default_factory=dict) + quest_rules: Dict[str, StardewRule] = field(default_factory=dict) + + def __post_init__(self): + self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish_items}) + + self.item_rules.update({ + "Aged Roe": self.has("Preserves Jar") & self.has("Roe"), + "Algae Soup": self.can_cook() & self.has("Green Algae") & self.can_have_relationship("Clint", 3), + "Amaranth": self.received("Fall"), + "Amethyst": self.can_mine_in_the_mines_floor_1_40(), + "Ancient Drum": self.has("Frozen Geode"), + "Any Egg": self.has("Chicken Egg") | self.has("Duck Egg"), + "Apple": self.received("Fall"), + "Apricot": self.received("Year Two"), + "Aquamarine": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), + "Artichoke": self.received("Year Two") & self.received("Fall"), + "Bait": self.has_skill_level("Fishing", 2), + "Bat Wing": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), + "Battery Pack": self.has("Lightning Rod"), + "Bee House": self.has_skill_level("Farming", 3) & self.has("Iron Bar") & self.has("Maple Syrup"), + "Beer": (self.has("Keg") & self.has("Wheat")) | self.can_spend_money(400), + "Beet": self.received("Fall") & self.can_reach_region("The Desert"), + "Blackberry": self.received("Fall"), + "Blue Jazz": self.received("Spring"), + "Blueberry": self.received("Summer"), + "Blueberry Tart": self.has("Blueberry") & self.has("Any Egg") & self.can_have_relationship("Pierre", 3), + "Bok Choy": self.received("Fall"), + "Bouquet": self.can_have_relationship("Any", 8), + "Bread": self.can_spend_money(120) | (self.can_spend_money(100) & self.can_cook()), + "Broken CD": self.can_crab_pot(), + "Broken Glasses": self.can_crab_pot(), + "Bug Meat": self.can_mine_in_the_mines_floor_1_40(), + "Cactus Fruit": self.can_reach_region("The Desert"), + "Cauliflower": self.received("Spring"), + "Cave Carrot": self.has_mine_elevator_to_floor(10), + "Caviar": self.has("Preserves Jar") & self.has("Sturgeon Roe"), + "Chanterelle": self.received("Fall") & self.can_reach_region("Secret Woods"), + "Cheese Press": self.has_skill_level("Farming", 6) & self.has("Hardwood") & self.has("Copper Bar"), + "Cheese": (self.has("Cow Milk") & self.has("Cheese Press")) | + (self.can_reach_region("The Desert") & self.has("Emerald")), + "Cheese Cauliflower": self.has(["Cheese", "Cauliflower"]) & self.can_have_relationship("Pam", 3) & + self.can_cook(), + "Cherry": self.received("Year Two"), + "Chicken": self.has_building("Coop"), + "Chicken Egg": self.has(["Egg", "Egg (Brown)", "Large Egg", "Large Egg (Brown)"], 1), + "Chowder": self.can_cook() & self.can_have_relationship("Willy", 3) & self.has(["Clam", "Cow Milk"]), + "Clam": _True(), + "Clay": _True(), + "Cloth": (self.has("Wool") & self.has("Loom")) | + (self.can_reach_region("The Desert") & self.has("Aquamarine")), + "Coal": _True(), + "Cockle": _True(), + "Coconut": self.can_reach_region("The Desert"), + "Coffee": (self.has("Keg") & self.has("Coffee Bean")) | self.has("Coffee Maker") | + self.can_spend_money(300) | self.has("Hot Java Ring"), + "Coffee Bean": (self.received("Spring") | self.received("Summer")) & + (self.can_mine_in_the_mines_floor_41_80() | _True()), # Travelling merchant + "Coffee Maker": _False(), + "Common Mushroom": self.received("Fall") | + (self.received("Spring") & self.can_reach_region("Secret Woods")), + "Copper Bar": self.can_smelt("Copper Ore"), + "Copper Ore": self.can_mine_in_the_mines_floor_1_40() | self.can_mine_in_the_skull_cavern(), + "Coral": self.can_reach_region("Tide Pools") | self.received("Summer"), + "Corn": self.received("Summer") | self.received("Fall"), + "Cow": self.has_building("Barn"), + "Cow Milk": self.has("Milk") | self.has("Large Milk"), + "Crab": self.can_crab_pot(), + "Crab Pot": self.has_skill_level("Fishing", 3), + "Cranberries": self.received("Fall"), + "Crayfish": self.can_crab_pot(), + "Crocus": self.received("Winter"), + "Crystal Fruit": self.received("Winter"), + "Daffodil": self.received("Spring"), + "Dandelion": self.received("Spring"), + "Dish O' The Sea": self.can_cook() & self.has_skill_level("Fishing", 3) & + self.has(["Sardine", "Hashbrowns"]), + "Dorado": self.can_fish(78) & self.received("Summer"), + "Dried Starfish": self.can_fish() & self.can_reach_region("Beach"), + "Driftwood": self.can_crab_pot(), + "Duck Egg": self.has("Duck"), + "Duck Feather": self.has("Duck"), + "Duck": self.has_building("Big Coop"), + "Dwarf Scroll I": self.can_mine_in_the_mines_floor_1_40(), + "Dwarf Scroll II": self.can_mine_in_the_mines_floor_1_40(), + "Dwarf Scroll III": self.can_mine_in_the_mines_floor_1_40(), + "Dwarf Scroll IV": self.can_mine_in_the_mines_floor_81_120(), + "Earth Crystal": self.can_mine_in_the_mines_floor_1_40(), + "Egg": self.has("Chicken"), + "Egg (Brown)": self.has("Chicken"), + "Eggplant": self.received("Fall"), + "Elvish Jewelry": self.can_fish() & self.can_reach_region("Forest"), + "Emerald": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), + "Fairy Rose": self.received("Fall"), + "Farmer's Lunch": self.can_cook() & self.has_skill_level("Farming", 3) & self.has("Omelet") & self.has( + "Parsnip"), + "Fiber": _True(), + "Fiddlehead Fern": self.can_reach_region("Secret Woods") & self.received("Summer"), + "Fire Quartz": self.can_mine_in_the_mines_floor_81_120(), + "Fried Egg": self.can_cook() & self.has("Any Egg"), + "Fried Mushroom": self.can_cook() & self.can_have_relationship("Demetrius", 3) & self.has( + "Morel") & self.has("Common Mushroom"), + "Frozen Geode": self.can_mine_in_the_mines_floor_41_80(), + "Frozen Tear": self.can_mine_in_the_mines_floor_41_80(), + "Furnace": self.has("Stone") & self.has("Copper Ore"), + "Geode": self.can_mine_in_the_mines_floor_1_40(), + "Goat Cheese": self.has("Goat Milk") & self.has("Cheese Press"), + "Goat Milk": self.has("Goat"), + "Goat": self.has_building("Big Barn"), + "Gold Bar": self.can_smelt("Gold Ore"), + "Gold Ore": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), + "Grape": self.received("Summer"), + "Green Algae": self.can_fish(), + "Green Bean": self.received("Spring"), + "Hardwood": self.has_tool("Axe", "Copper"), + "Hashbrowns": self.can_cook() & self.can_spend_money(50) & self.has("Potato"), + "Hazelnut": self.received("Fall"), + "Holly": self.received("Winter"), + "Honey": self.can_reach_region("The Desert") | + (self.has("Bee House") & + (self.received("Spring") | self.received("Summer") | self.received("Fall"))), + "Hops": self.received("Summer"), + "Hot Java Ring": self.can_reach_region("Ginger Island"), + "Hot Pepper": self.received("Summer"), + "Iridium Bar": self.can_smelt("Iridium Ore"), + "Iridium Ore": self.can_mine_in_the_skull_cavern(), + "Iron Bar": self.can_smelt("Iron Ore"), + "Iron Ore": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), + "Jade": self.can_mine_in_the_mines_floor_41_80(), + "Jelly": self.has("Preserves Jar"), + "JotPK Small Buff": self.has_jotpk_power_level(2), + "JotPK Medium Buff": self.has_jotpk_power_level(4), + "JotPK Big Buff": self.has_jotpk_power_level(7), + "JotPK Max Buff": self.has_jotpk_power_level(9), + "Juice": self.has("Keg"), + "Junimo Kart Small Buff": self.has_junimo_kart_power_level(2), + "Junimo Kart Medium Buff": self.has_junimo_kart_power_level(4), + "Junimo Kart Big Buff": self.has_junimo_kart_power_level(6), + "Junimo Kart Max Buff": self.has_junimo_kart_power_level(8), + "Kale": self.received("Spring"), + "Keg": self.has_skill_level("Farming", 8) & self.has("Iron Bar") & self.has("Copper Bar") & self.has( + "Oak Resin"), + "Large Egg": self.has("Chicken"), + "Large Egg (Brown)": self.has("Chicken"), + "Large Goat Milk": self.has("Goat"), + "Large Milk": self.has("Cow"), + "Leek": self.received("Spring"), + "Lightning Rod": self.has_skill_level("Foraging", 6), + "Lobster": self.can_crab_pot(), + "Loom": self.has_skill_level("Farming", 7) & self.has("Pine Tar"), + "Magma Geode": self.can_mine_in_the_mines_floor_81_120() | + (self.has("Lava Eel") & self.has_building("Fish Pond")), + "Maki Roll": self.can_cook() & self.can_fish(), + "Maple Syrup": self.has("Tapper"), + "Mead": self.has("Keg") & self.has("Honey"), + "Melon": self.received("Summer"), + "Milk": self.has("Cow"), + "Miner's Treat": self.can_cook() & self.has_skill_level("Mining", 3) & self.has("Cow Milk") & self.has( + "Cave Carrot"), + "Morel": self.can_reach_region("Secret Woods") & self.received("Year Two"), + "Mussel": _True(), + "Nautilus Shell": self.received("Winter"), + "Oak Resin": self.has("Tapper"), + "Oil Maker": self.has_skill_level("Farming", 8) & self.has("Hardwood") & self.has("Gold Bar"), + "Omelet": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg") & self.has("Cow Milk"), + "Omni Geode": self.can_mine_in_the_mines_floor_41_80() | + self.can_reach_region("The Desert") | + self.can_do_panning() | + self.received("Rusty Key") | + (self.has("Octopus") & self.has_building("Fish Pond")) | + self.can_reach_region("Ginger Island"), + "Orange": self.received("Summer"), + "Ostrich": self.has_building("Barn"), + "Oyster": _True(), + "Pale Ale": self.has("Keg") & self.has("Hops"), + "Pale Broth": self.can_cook() & self.can_have_relationship("Marnie", 3) & self.has("White Algae"), + "Pancakes": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg"), + "Parsnip": self.received("Spring"), + "Parsnip Soup": self.can_cook() & self.can_have_relationship("Caroline", 3) & self.has( + "Parsnip") & self.has("Cow Milk"), + "Peach": self.received("Summer"), + "Pepper Poppers": self.can_cook() & self.has("Cheese") & self.has( + "Hot Pepper") & self.can_have_relationship("Shane", 3), + "Periwinkle": self.can_crab_pot(), + "Pickles": self.has("Preserves Jar"), + "Pig": self.has_building("Deluxe Barn"), + "Pine Tar": self.has("Tapper"), + "Pizza": self.can_spend_money(600), + "Pomegranate": self.received("Fall"), + "Poppy": self.received("Summer"), + "Potato": self.received("Spring"), + "Preserves Jar": self.has_skill_level("Farming", 4), + "Prismatic Shard": self.received("Year Two"), + "Pumpkin": self.received("Fall"), + "Purple Mushroom": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), + "Quartz": self.can_mine_in_the_mines_floor_1_40(), + "Rabbit": self.has_building("Deluxe Coop"), + "Rabbit's Foot": self.has("Rabbit"), + "Radish": self.received("Summer"), + "Rainbow Shell": self.received("Summer"), + "Rain Totem": self.has_skill_level("Foraging", 9), + "Recycling Machine": self.has_skill_level("Fishing", 4) & self.has("Wood") & + self.has("Stone") & self.has("Iron Bar"), + "Red Cabbage": self.received("Year Two"), + "Red Mushroom": self.can_reach_region("Secret Woods") & (self.received("Summer") | self.received("Fall")), + "Refined Quartz": self.has("Quartz") | self.has("Fire Quartz") | + (self.has("Recycling Machine") & (self.has("Broken CD") | self.has("Broken Glasses"))), + "Rhubarb": self.received("Spring") & self.can_reach_region("The Desert"), + "Roe": self.can_fish() & self.has_building("Fish Pond"), + "Roots Platter": self.can_cook() & self.has_skill_level("Combat", 3) & + self.has("Cave Carrot") & self.has("Winter Root"), + "Ruby": self.can_mine_in_the_mines_floor_81_120() | self.can_do_panning(), + "Salad": self.can_spend_money(220) | ( + self.can_cook() & self.can_have_relationship("Emily", 3) & self.has("Leek") & self.has( + "Dandelion")), + "Salmonberry": self.received("Spring"), + "Salmon Dinner": self.can_cook() & self.can_have_relationship("Gus", 3) & self.has("Salmon") & self.has( + "Amaranth") & self.has("Kale"), + "Sashimi": self.can_fish() & self.can_cook() & self.can_have_relationship("Linus", 3), + "Sea Urchin": self.can_reach_region("Tide Pools") | self.received("Summer"), + "Seaweed": self.can_fish() | self.can_reach_region("Tide Pools"), + "Sheep": self.has_building("Deluxe Barn"), + "Shrimp": self.can_crab_pot(), + "Slime": self.can_mine_in_the_mines_floor_1_40(), + "Snail": self.can_crab_pot(), + "Snow Yam": self.received("Winter"), + "Soggy Newspaper": self.can_crab_pot(), + "Solar Essence": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), + "Spaghetti": self.can_spend_money(240), + "Spice Berry": self.received("Summer"), + "Spring Onion": self.received("Spring"), + "Staircase": self.has_skill_level("Mining", 2), + "Starfruit": (self.received("Summer") | self.received("Greenhouse")) & self.can_reach_region("The Desert"), + "Stone": self.has_tool("Pickaxe"), + "Strawberry": self.received("Spring"), + "Sturgeon Roe": self.has("Sturgeon") & self.has_building("Fish Pond"), + "Summer Spangle": self.received("Summer"), + "Sunflower": self.received("Summer") | self.received("Fall"), + "Survival Burger": self.can_cook() & self.has_skill_level("Foraging", 2) & + self.has(["Bread", "Cave Carrot", "Eggplant"]), + "Sweet Gem Berry": (self.received("Fall") | self.received("Greenhouse")) & self.has_traveling_merchant(), + "Sweet Pea": self.received("Summer"), + "Tapper": self.has_skill_level("Foraging", 3), + "Tomato": self.received("Summer"), + "Topaz": self.can_mine_in_the_mines_floor_1_40(), + "Tortilla": self.can_cook() & self.can_spend_money(100) & self.has("Corn"), + "Trash": self.can_crab_pot(), + "Triple Shot Espresso": (self.has("Hot Java Ring") | + (self.can_cook() & self.can_spend_money(5000) & self.has("Coffee"))), + "Truffle Oil": self.has("Truffle") & self.has("Oil Maker"), + "Truffle": self.has("Pig") & self.received("Year Two"), + "Tulip": self.received("Spring"), + "Unmilled Rice": self.received("Year Two"), + "Void Essence": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), + "Wheat": self.received("Summer") | self.received("Fall"), + "White Algae": self.can_fish() & self.can_mine_in_the_mines_floor_1_40(), + "Wild Horseradish": self.received("Spring"), + "Wild Plum": self.received("Fall"), + "Wilted Bouquet": self.has("Furnace") & self.has("Bouquet") & self.has("Coal"), + "Wine": self.has("Keg"), + "Winter Root": self.received("Winter"), + "Wood": self.has_tool("Axe"), + "Wool": self.has("Rabbit") | self.has("Sheep"), + "Yam": self.received("Fall"), + "Hay": self.has_building("Silo"), + }) + self.item_rules.update(self.fish_rules) + + self.building_rules.update({ + "Barn": self.can_spend_money(6000) & self.has(["Wood", "Stone"]), + "Big Barn": self.can_spend_money(12000) & self.has(["Wood", "Stone"]) & self.has_building("Barn"), + "Deluxe Barn": self.can_spend_money(25000) & self.has(["Wood", "Stone"]) & self.has_building("Big Barn"), + "Coop": self.can_spend_money(4000) & self.has(["Wood", "Stone"]), + "Big Coop": self.can_spend_money(10000) & self.has(["Wood", "Stone"]) & self.has_building("Coop"), + "Deluxe Coop": self.can_spend_money(20000) & self.has(["Wood", "Stone"]) & self.has_building("Big Coop"), + "Fish Pond": self.can_spend_money(5000) & self.has(["Stone", "Seaweed", "Green Algae"]), + "Mill": self.can_spend_money(2500) & self.has(["Stone", "Wood", "Cloth"]), + "Shed": self.can_spend_money(15000) & self.has("Wood"), + "Big Shed": self.can_spend_money(20000) & self.has(["Wood", "Stone"]) & self.has_building("Shed"), + "Silo": self.can_spend_money(100) & self.has(["Stone", "Clay", "Copper Bar"]), + "Slime Hutch": self.can_spend_money(10000) & self.has(["Stone", "Refined Quartz", "Iridium Bar"]), + "Stable": self.can_spend_money(10000) & self.has(["Hardwood", "Iron Bar"]), + "Well": self.can_spend_money(1000) & self.has("Stone"), + "Shipping Bin": self.can_spend_money(250) & self.has("Wood"), + "Kitchen": self.can_spend_money(10000) & self.has("Wood") & self.has_house(0), + "Kids Room": self.can_spend_money(50000) & self.has("Hardwood") & self.has_house(1), + "Cellar": self.can_spend_money(100000) & self.has_house(2), + }) + + self.quest_rules.update({ + "Introductions": _True(), + "How To Win Friends": self.can_complete_quest("Introductions"), + "Getting Started": self.received("Spring") & self.has_tool("Hoe") & self.has_tool("Watering Can"), + "To The Beach": self.received("Spring"), + "Raising Animals": self.can_complete_quest("Getting Started") & self.has_building("Coop"), + "Advancement": self.can_complete_quest("Getting Started") & self.has_skill_level("Farming", 1), + "Archaeology": self.has_tool("Hoe") | self.can_mine_in_the_mines_floor_1_40() | self.can_fish(), + "Meet The Wizard": self.received("Spring") & self.can_reach_region("Community Center"), + "Forging Ahead": self.has("Copper Ore") & self.has("Furnace"), + "Smelting": self.has("Copper Bar"), + "Initiation": self.can_mine_in_the_mines_floor_1_40(), + "Robin's Lost Axe": self.received("Spring"), + "Jodi's Request": self.received("Spring") & self.has("Cauliflower"), + "Mayor's \"Shorts\"": self.received("Summer") & self.can_have_relationship("Marnie", 4), + "Blackberry Basket": self.received("Fall"), + "Marnie's Request": self.can_have_relationship("Marnie", 3) & self.has("Cave Carrot"), + "Pam Is Thirsty": self.received("Summer") & self.has("Pale Ale"), + "A Dark Reagent": self.received("Winter") & self.has("Void Essence"), + "Cow's Delight": self.received("Fall") & self.has("Amaranth"), + "The Skull Key": self.received("Skull Key") & self.can_reach_region("The Desert"), + "Crop Research": self.received("Summer") & self.has("Melon"), + "Knee Therapy": self.received("Summer") & self.has("Hot Pepper"), + "Robin's Request": self.received("Winter") & self.has("Hardwood"), + "Qi's Challenge": self.can_mine_in_the_skull_cavern(), + "The Mysterious Qi": self.has("Battery Pack") & self.can_reach_region("The Desert") & self.has( + "Rainbow Shell") & self.has("Beet") & self.has("Solar Essence"), + "Carving Pumpkins": self.received("Fall") & self.has("Pumpkin"), + "A Winter Mystery": self.received("Winter"), + "Strange Note": self.received("Magnifying Glass") & self.can_reach_region("Secret Woods") & self.has( + "Maple Syrup"), + "Cryptic Note": self.received("Magnifying Glass") & self.can_mine_perfectly_in_the_skull_cavern(), + "Fresh Fruit": self.received("Year Two") & self.has("Apricot"), + "Aquatic Research": self.received("Year Two") & self.has("Pufferfish"), + "A Soldier's Star": self.received("Year Two") & self.has("Starfruit"), + "Mayor's Need": self.received("Year Two") & self.has("Truffle Oil"), + "Wanted: Lobster": self.received("Year Two") & self.has("Lobster"), + "Pam Needs Juice": self.received("Year Two") & self.has("Battery Pack"), + "Fish Casserole": self.received("Year Two") & self.can_have_relationship("Jodi", 4) & self.has( + "Largemouth Bass"), + "Catch A Squid": self.received("Year Two") & self.has("Squid"), + "Fish Stew": self.received("Year Two") & self.has("Albacore"), + "Pierre's Notice": self.received("Year Two") & self.has("Sashimi"), + "Clint's Attempt": self.received("Year Two") & self.has("Amethyst"), + "A Favor For Clint": self.received("Year Two") & self.has("Iron Bar"), + "Staff Of Power": self.received("Year Two") & self.has("Iridium Bar"), + "Granny's Gift": self.received("Year Two") & self.has("Leek"), + "Exotic Spirits": self.received("Year Two") & self.has("Coconut"), + "Catch a Lingcod": self.received("Year Two") & self.has("Lingcod"), + }) + + def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule: + if isinstance(items, str): + return _Has(items, self.item_rules) + + if count is None or count == len(items): + return _And(self.has(item) for item in items) + + if count == 1: + return _Or(self.has(item) for item in items) + + return _Count(count, (self.has(item) for item in items)) + + def received(self, items: Union[str, Iterable[str]], count: Optional[int] = 1) -> StardewRule: + if isinstance(items, str): + return _Received(items, self.player, count) + + if count is None: + return _And(self.received(item) for item in items) + + if count == 1: + return _Or(self.received(item) for item in items) + + return _TotalReceived(count, items, self.player) + + def can_reach_region(self, spot: str) -> StardewRule: + return _Reach(spot, "Region", self.player) + + def can_reach_any_region(self, spots: Iterable[str]) -> StardewRule: + return _Or(self.can_reach_region(spot) for spot in spots) + + def can_reach_location(self, spot: str) -> StardewRule: + return _Reach(spot, "Location", self.player) + + def can_reach_entrance(self, spot: str) -> StardewRule: + return _Reach(spot, "Entrance", self.player) + + def can_have_earned_total_money(self, amount: int) -> StardewRule: + if amount <= 10000: + return self.received("Spring") + elif amount <= 30000: + return self.received("Summer") + elif amount <= 60000: + return self.received("Fall") + elif amount <= 70000: + return self.received("Winter") + return self.received("Year Two") + + def can_spend_money(self, amount: int) -> StardewRule: + if amount <= 2000: + return self.received("Spring") + elif amount <= 8000: + return self.received("Summer") + elif amount <= 15000: + return self.received("Fall") + elif amount <= 18000: + return self.received("Winter") + return self.received("Year Two") + + def has_tool(self, tool: str, material: str = "Basic") -> StardewRule: + if material == "Basic": + return _True() + + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received(f"Progressive {tool}", count=tool_materials[material]) + + return self.has(f"{material} Bar") & self.can_spend_money(tool_prices[material]) + + def has_skill_level(self, skill: str, level: int) -> StardewRule: + if level == 0: + return _True() + + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.received(f"{skill} Level", count=level) + + if skill == "Fishing" and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.can_get_fishing_xp() + + return self.received(season_per_skill_level[(skill, level)]) + + def has_total_skill_level(self, level: int) -> StardewRule: + if level == 0: + return _True() + + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + skills_items = ["Farming Level", "Mining Level", "Foraging Level", + "Fishing Level", "Combat Level"] + return self.received(skills_items, count=level) + + if level > 40 and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received(season_per_total_level[level]) & self.can_get_fishing_xp() + + return self.received(season_per_total_level[level]) + + def has_building(self, building: str) -> StardewRule: + if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + count = 1 + if building in ["Coop", "Barn", "Shed"]: + building = f"Progressive {building}" + elif building.startswith("Big"): + count = 2 + building = " ".join(["Progressive", *building.split(" ")[1:]]) + elif building.startswith("Deluxe"): + count = 3 + building = " ".join(["Progressive", *building.split(" ")[1:]]) + return self.received(f"{building}", count) + + return _Has(building, self.building_rules) + + def has_house(self, upgrade_level: int) -> StardewRule: + if upgrade_level < 1: + return _True() + + if upgrade_level > 3: + return _False() + + if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + return self.received(f"Progressive House", upgrade_level) + + if upgrade_level == 1: + return _Has("Kitchen", self.building_rules) + + if upgrade_level == 2: + return _Has("Kids Room", self.building_rules) + + # if upgrade_level == 3: + return _Has("Cellar", self.building_rules) + + def can_complete_quest(self, quest: str) -> StardewRule: + return _Has(quest, self.quest_rules) + + def can_get_fishing_xp(self) -> StardewRule: + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.can_fish() | self.can_crab_pot() + + return self.can_fish() + + def can_fish(self, difficulty: int = 0) -> StardewRule: + skill_required = max(0, int((difficulty / 10) - 1)) + if difficulty <= 40: + skill_required = 0 + skill_rule = self.has_skill_level("Fishing", skill_required) + number_fishing_rod_required = 1 if difficulty < 50 else 2 + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule + + return skill_rule + + def can_catch_fish(self, fish: FishItem) -> StardewRule: + region_rule = self.can_reach_any_region(fish.locations) + season_rule = self.received(fish.seasons) + difficulty_rule = self.can_fish(fish.difficulty) + if fish.difficulty == -1: + difficulty_rule = self.can_crab_pot() + return region_rule & season_rule & difficulty_rule + + def can_catch_every_fish(self) -> StardewRule: + rules = [self.has_skill_level("Fishing", 10), self.received("Progressive Fishing Rod", 4)] + for fish in all_fish_items: + rules.append(self.can_catch_fish(fish)) + return _And(rules) + + def can_cook(self) -> StardewRule: + return self.has_house(1) or self.has_skill_level("Foraging", 9) + + def can_smelt(self, item: str) -> StardewRule: + return self.has("Furnace") & self.has(item) + + def can_crab_pot(self) -> StardewRule: + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.has("Crab Pot") + + return _True() + + def can_do_panning(self) -> StardewRule: + return self.received("Glittering Boulder Removed") + + # Regions + def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: + return self.can_reach_region("The Mines - Floor 5") + + def can_mine_in_the_mines_floor_41_80(self) -> StardewRule: + return self.can_reach_region("The Mines - Floor 45") + + def can_mine_in_the_mines_floor_81_120(self) -> StardewRule: + return self.can_reach_region("The Mines - Floor 85") + + def can_mine_in_the_skull_cavern(self) -> StardewRule: + return (self.can_progress_in_the_mines_from_floor(120) & + self.can_reach_region("Skull Cavern")) + + def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule: + return (self.can_progress_in_the_mines_from_floor(160) & + self.can_reach_region("Skull Cavern")) + + def get_weapon_rule_for_floor_tier(self, tier: int): + if tier >= 4: + return self.has_galaxy_weapon() + if tier >= 3: + return self.has_great_weapon() + if tier >= 2: + return self.has_good_weapon() + if tier >= 1: + return self.has_decent_weapon() + return self.has_any_weapon() + + def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: + tier = int(floor / 40) + rules = [] + weapon_rule = self.get_weapon_rule_for_floor_tier(tier) + rules.append(weapon_rule) + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + rules.append(self.received("Progressive Pickaxe", tier)) + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + combat_tier = min(10, max(0, tier * 2)) + rules.append(self.has_skill_level("Combat", combat_tier)) + return _And(rules) + + def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule: + tier = int(floor / 40) + 1 + rules = [] + weapon_rule = self.get_weapon_rule_for_floor_tier(tier) + rules.append(weapon_rule) + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + rules.append(self.received("Progressive Pickaxe", count=tier)) + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + combat_tier = min(10, max(0, tier * 2)) + rules.append(self.has_skill_level("Combat", combat_tier)) + return _And(rules) + + def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: + if (self.options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive or + self.options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): + return self.received("Progressive Mine Elevator", count=int(floor / 5)) + return _True() + + def can_mine_to_floor(self, floor: int) -> StardewRule: + previous_elevator = max(floor - 5, 0) + previous_previous_elevator = max(floor - 10, 0) + return ((self.has_mine_elevator_to_floor(previous_elevator) & + self.can_progress_in_the_mines_from_floor(previous_elevator)) | + (self.has_mine_elevator_to_floor(previous_previous_elevator) & + self.can_progress_easily_in_the_mines_from_floor(previous_previous_elevator))) + + def has_jotpk_power_level(self, power_level: int) -> StardewRule: + if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + return _True() + jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", + "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] + return self.received(jotpk_buffs, power_level) + + def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: + if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + return _True() + return self.received("Junimo Kart: Extra Life", power_level) + + def has_traveling_merchant(self, tier: int = 1): + traveling_merchant_days = [f"Traveling Merchant: {day}" for day in week_days] + return self.received(traveling_merchant_days, tier) + + def can_get_married(self) -> StardewRule: + return self.can_reach_region("Tide Pools") & self.can_have_relationship("Bachelor", 10) & self.has_house(1) + + def can_have_relationship(self, npc: str, hearts: int) -> StardewRule: + if npc == "Leo": + return self.can_reach_region("Ginger Island") + + if npc == "Sandy": + return self.can_reach_region("The Desert") + + if npc == "Kent": + return self.received("Year Two") + + if hearts <= 3: + return self.received("Spring") + if hearts <= 6: + return self.received("Summer") + if hearts <= 9: + return self.received("Fall") + return self.received("Winter") + + def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule: + item_rules = [] + for bundle_item in bundle_requirements: + if bundle_item.item.item_id == -1: + return self.can_spend_money(bundle_item.amount) + else: + item_rules.append(bundle_item.item.name) + return self.has(item_rules, number_required) + + def can_complete_community_center(self) -> StardewRule: + return (self.can_reach_location("Complete Crafts Room") & + self.can_reach_location("Complete Pantry") & + self.can_reach_location("Complete Fish Tank") & + self.can_reach_location("Complete Bulletin Board") & + self.can_reach_location("Complete Vault") & + self.can_reach_location("Complete Boiler Room")) + + def can_finish_grandpa_evaluation(self) -> StardewRule: + # https://stardewvalleywiki.com/Grandpa + rules_worth_a_point = [self.can_have_earned_total_money(50000), # 50 000g + self.can_have_earned_total_money(100000), # 100 000g + self.can_have_earned_total_money(200000), # 200 000g + self.can_have_earned_total_money(300000), # 300 000g + self.can_have_earned_total_money(500000), # 500 000g + self.can_have_earned_total_money(1000000), # 1 000 000g first point + self.can_have_earned_total_money(1000000), # 1 000 000g second point + self.has_total_skill_level(30), # Total Skills: 30 + self.has_total_skill_level(50), # Total Skills: 50 + # Completing the museum not expected + # Catching every fish not expected + # Shipping every item not expected + self.can_get_married() & self.has_house(2), + self.received("Fall"), # 5 Friends (TODO) + self.received("Winter"), # 10 friends (TODO) + self.received("Fall"), # Max Pet takes 56 days min + self.can_complete_community_center(), # Community Center Completion + self.can_complete_community_center(), # CC Ceremony first point + self.can_complete_community_center(), # CC Ceremony second point + self.received("Skull Key"), # Skull Key obtained + # Rusty key not expected + ] + return _Count(12, rules_worth_a_point) + + def has_any_weapon(self) -> StardewRule: + return self.has_decent_weapon() | self.received(item.name for item in all_items if Group.WEAPON in item.groups) + + def has_decent_weapon(self) -> StardewRule: + return (self.has_good_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and + (Group.MINES_FLOOR_50 in item.groups or Group.MINES_FLOOR_60 in item.groups))) + + def has_good_weapon(self) -> StardewRule: + return ((self.has_great_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and + (Group.MINES_FLOOR_80 in item.groups or Group.MINES_FLOOR_90 in item.groups))) & + self.received("Adventurer's Guild")) + + def has_great_weapon(self) -> StardewRule: + return ((self.has_galaxy_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and Group.MINES_FLOOR_110 in item.groups)) & + self.received("Adventurer's Guild")) + + def has_galaxy_weapon(self) -> StardewRule: + return (self.received(item.name for item in all_items + if Group.WEAPON in item.groups and Group.GALAXY_WEAPONS in item.groups) & + self.received("Adventurer's Guild")) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py new file mode 100644 index 0000000000..e7478c7dad --- /dev/null +++ b/worlds/stardew_valley/options.py @@ -0,0 +1,409 @@ +from dataclasses import dataclass +from typing import Dict, Union, Protocol, runtime_checkable + +from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice + + +@runtime_checkable +class StardewOption(Protocol): + internal_name: str + + +@dataclass +class StardewOptions: + options: Dict[str, Union[bool, int]] + + def __getitem__(self, item: Union[str, StardewOption]) -> Union[bool, int]: + if isinstance(item, StardewOption): + item = item.internal_name + + return self.options.get(item, None) + + +class Goal(Choice): + """What's your goal with this play-through? + With Community Center, the world will be completed once you complete the Community Center. + With Grandpa's Evaluation, the world will be completed once 4 candles are lit around Grandpa's Shrine. + With Bottom of the Mines, the world will be completed once you reach level 120 in the local mineshaft. + With Cryptic Note, the world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern + With Master Angler, the world will be completed once you have caught every fish in the game. Pairs well with Fishsanity""" + internal_name = "goal" + display_name = "Goal" + option_community_center = 0 + option_grandpa_evaluation = 1 + option_bottom_of_the_mines = 2 + option_cryptic_note = 3 + option_master_angler = 4 + + @classmethod + def get_option_name(cls, value) -> str: + if value == cls.option_grandpa_evaluation: + return "Grandpa's Evaluation" + + return super().get_option_name(value) + + +class StartingMoney(SpecialRange): + """Amount of gold when arriving at the farm. + Set to -1 or unlimited for infinite money in this playthrough""" + internal_name = "starting_money" + display_name = "Starting Gold" + range_start = -1 + range_end = 50000 + default = 5000 + + special_range_names = { + "unlimited": -1, + "vanilla": 500, + "extra": 2000, + "rich": 5000, + "very rich": 20000, + "filthy rich": 50000, + } + + +class ResourcePackMultiplier(SpecialRange): + """How many items will be in the resource pack. A lower setting mean fewer resources in each pack. + A higher setting means more resources in each pack. Easy (200) doubles the default quantity. + This also include Friendship bonuses that replace the one from the Bulletin Board.""" + internal_name = "resource_pack_multiplier" + default = 100 + range_start = 0 + range_end = 200 + # step = 25 + display_name = "Resource Pack Multiplier" + + special_range_names = { + "resource packs disabled": 0, + "half packs": 50, + "normal packs": 100, + "double packs": 200, + } + + +class BundleRandomization(Choice): + """What items are needed for the community center bundles? + With Vanilla, you get the standard bundles from the game + With Thematic, every bundle will require random items within their original category + With Shuffled, every bundle will require random items without logic""" + internal_name = "bundle_randomization" + display_name = "Bundle Randomization" + default = 1 + option_vanilla = 0 + option_thematic = 1 + option_shuffled = 2 + + +class BundlePrice(Choice): + """How many items are needed for the community center bundles? + With Very Cheap, every bundle will require two items fewer than usual + With Cheap, every bundle will require 1 item fewer than usual + With Normal, every bundle will require the vanilla number of items + With Expensive, every bundle will require 1 extra item""" + internal_name = "bundle_price" + display_name = "Bundle Price" + default = 2 + option_very_cheap = 0 + option_cheap = 1 + option_normal = 2 + option_expensive = 3 + + +class EntranceRandomization(Choice): + """Should area entrances be randomized? + With Disabled, no entrance randomization is done + With Pelican Town, only buildings in the main town area are randomized with each other + With Non Progression, only buildings that are always available are randomized with each other + """ + # With Buildings, All buildings in the world are randomized with each other + # With Everything, All buildings and areas are randomized with each other + # With Chaos, same as everything, but the buildings are shuffled again every in-game day. You can't learn it! + + internal_name = "entrance_randomization" + display_name = "Entrance Randomization" + default = 0 + option_disabled = 0 + option_pelican_town = 1 + option_non_progression = 2 + # option_buildings = 3 + # option_everything = 4 + # option_chaos = 4 + + +class BackpackProgression(Choice): + """How is the backpack progression handled? + With Vanilla, you can buy them at Pierre's. + With Progressive, you will randomly find Progressive Backpack to upgrade. + With Early Progressive, you can expect you first Backpack before the second season, and the third before the forth + season. + """ + internal_name = "backpack_progression" + display_name = "Backpack Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_early_progressive = 2 + + +class ToolProgression(Choice): + """How is the tool progression handled? + With Vanilla, Clint will upgrade your tools with ore. + With Progressive, you will randomly find Progressive Tool to upgrade. + With World Checks, the tools of different quality will be found in the world.""" + internal_name = "tool_progression" + display_name = "Tool Progression" + default = 1 + option_vanilla = 0 + option_progressive = 1 + + +class TheMinesElevatorsProgression(Choice): + """How is The Mines' Elevator progression handled? + With Vanilla, you will unlock a new elevator floor every 5 floor in the mine. + With Progressive, you will randomly find Progressive Mine Elevator to go deeper. Location are sent for reaching + every level multiple of 5. + With Progressive from previous floor, you will randomly find Progressive Mine Elevator to go deeper. Location are + sent for taking the ladder or stair to every level multiple of 5, taking the elevator does not count.""" + internal_name = "elevator_progression" + display_name = "Elevator Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_progressive_from_previous_floor = 2 + + +class SkillProgression(Choice): + """How is the skill progression handled? + With Vanilla, you will level up and get the normal reward at each level. + With Progressive, the xp will be counted internally, locations will be sent when you gain a virtual level. Your real + levels will be scattered around the world.""" + internal_name = "skill_progression" + display_name = "Skill Progression" + default = 1 + option_vanilla = 0 + option_progressive = 1 + + +class BuildingProgression(Choice): + """How is the building progression handled? + With Vanilla, you will buy each building and upgrade one at the time. + With Progressive, you will receive the buildings and will be able to build the first one of each building for free, + once it is received. If you want more of the same building, it will cost the vanilla price. + This option INCLUDES the shipping bin as a building you need to receive. + With Progressive early shipping bin, you can expect to receive the shipping bin before the end of the first season. + """ + internal_name = "building_progression" + display_name = "Building Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_progressive_early_shipping_bin = 2 + + +class ArcadeMachineLocations(Choice): + """How are the Arcade Machines handled? + With Vanilla, the arcade machines are not included in the Archipelago shuffling. + With Victories, each Arcade Machine will contain one check on victory + With Victories Easy, the arcade machines are both made considerably easier to be more accessible for the average + player. + With Full Shuffling, the arcade machines will contain multiple checks each, and different buffs that make the game + easier are in the item pool. Junimo Kart has one check at the end of each level. + Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment. + """ + internal_name = "arcade_machine_locations" + display_name = "Arcade Machine Locations" + default = 3 + option_disabled = 0 + option_victories = 1 + option_victories_easy = 2 + option_full_shuffling = 3 + + +class HelpWantedLocations(SpecialRange): + """How many "Help Wanted" quests need to be completed as ArchipelagoLocations + Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. + Choosing a multiple of 7 is recommended.""" + internal_name = "help_wanted_locations" + default = 7 + range_start = 0 + range_end = 56 + # step = 7 + display_name = "Number of Help Wanted locations" + + special_range_names = { + "none": 0, + "minimum": 7, + "normal": 14, + "lots": 28, + "maximum": 56, + } + + +class Fishsanity(Choice): + """Locations for catching fish? + With None, there are no locations for catching fish + With Legendaries, each of the 5 legendary fish are locations that contain items + With Special, a curated selection of strong fish are locations that contain items + With Random Selection, a random selection of fish are locations that contain items + With All, every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal + """ + internal_name = "fishsanity" + display_name = "Fishsanity" + default = 0 + option_none = 0 + option_legendaries = 1 + option_special = 2 + option_random_selection = 3 + option_all = 4 + + +class NumberOfPlayerBuffs(Range): + """Number of buffs to the player of each type that exist as items in the pool. + Buffs include movement speed (+25% multiplier, stacks additively) + and daily luck bonus (0.025 flat value per buff)""" + internal_name = "player_buff_number" + display_name = "Number of Player Buffs" + range_start = 0 + range_end = 12 + default = 4 + # step = 1 + + +class MultipleDaySleepEnabled(Toggle): + """Should you be able to sleep automatically multiple day strait?""" + internal_name = "multiple_day_sleep_enabled" + display_name = "Multiple Day Sleep Enabled" + default = 1 + + +class MultipleDaySleepCost(SpecialRange): + """How must gold it cost to sleep through multiple days? You will have to pay that amount for each day slept.""" + internal_name = "multiple_day_sleep_cost" + display_name = "Multiple Day Sleep Cost" + range_start = 0 + range_end = 200 + # step = 25 + + special_range_names = { + "free": 0, + "cheap": 25, + "medium": 50, + "expensive": 100, + } + + +class ExperienceMultiplier(SpecialRange): + """How fast do you want to level up. A lower setting mean less experience. + A higher setting means more experience.""" + internal_name = "experience_multiplier" + display_name = "Experience Multiplier" + range_start = 25 + range_end = 400 + # step = 25 + default = 200 + + special_range_names = { + "half": 50, + "vanilla": 100, + "double": 200, + "triple": 300, + "quadruple": 400, + } + + +class DebrisMultiplier(Choice): + """How much debris spawn on the player's farm? + With Vanilla, debris spawns normally + With Half, debris will spawn at half the normal rate + With Quarter, debris will spawn at one quarter of the normal rate + With None, No debris will spawn on the farm, ever + With Start Clear, debris will spawn at the normal rate, but the farm will be completely clear when starting the game + """ + internal_name = "debris_multiplier" + display_name = "Debris Multiplier" + default = 1 + option_vanilla = 0 + option_half = 1 + option_quarter = 2 + option_none = 3 + option_start_clear = 4 + + +class QuickStart(Toggle): + """Do you want the quick start package? You will get a few items to help early game automation, + so you can use the multiple day sleep at its maximum.""" + internal_name = "quick_start" + display_name = "Quick Start" + default = 1 + + +class Gifting(Toggle): + """Do you want to enable gifting items to and from other Stardew Valley worlds?""" + internal_name = "gifting" + display_name = "Gifting" + default = 1 + + +class GiftTax(SpecialRange): + """Joja Prime will deliver gifts within one business day, for a price! + Sending a gift will cost a percentage of the item's monetary value as a tax on the sender""" + internal_name = "gift_tax" + display_name = "Gift Tax" + range_start = 0 + range_end = 400 + # step = 20 + default = 20 + + special_range_names = { + "no tax": 0, + "soft tax": 20, + "rough tax": 40, + "full tax": 100, + "oppressive tax": 200, + "nightmare tax": 400, + } + + +stardew_valley_options: Dict[str, type(Option)] = { + option.internal_name: option + for option in [ + StartingMoney, + ResourcePackMultiplier, + BundleRandomization, + BundlePrice, + EntranceRandomization, + BackpackProgression, + ToolProgression, + SkillProgression, + BuildingProgression, + TheMinesElevatorsProgression, + ArcadeMachineLocations, + HelpWantedLocations, + Fishsanity, + NumberOfPlayerBuffs, + Goal, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + ExperienceMultiplier, + DebrisMultiplier, + QuickStart, + Gifting, + GiftTax, + ] +} +default_options = {option.internal_name: option.default for option in stardew_valley_options.values()} +stardew_valley_options["death_link"] = DeathLink + + +def fetch_options(world, player: int) -> StardewOptions: + return StardewOptions({option: get_option_value(world, player, option) for option in stardew_valley_options}) + + +def get_option_value(world, player: int, name: str) -> Union[bool, int]: + assert name in stardew_valley_options, f"{name} is not a valid option for Stardew Valley." + + value = getattr(world, name) + + if issubclass(stardew_valley_options[name], Toggle): + return bool(value[player].value) + return value[player].value diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py new file mode 100644 index 0000000000..0979d7f883 --- /dev/null +++ b/worlds/stardew_valley/regions.py @@ -0,0 +1,291 @@ +from dataclasses import dataclass, field +from enum import IntFlag +from random import Random +from typing import Iterable, Dict, Protocol, Optional, List, Tuple + +from BaseClasses import Region, Entrance +from . import options +from .options import StardewOptions + + +class RegionFactory(Protocol): + def __call__(self, name: str, regions: Iterable[str]) -> Region: + raise NotImplementedError + + +class RandomizationFlag(IntFlag): + NOT_RANDOMIZED = 0b0 + PELICAN_TOWN = 0b11111 + NON_PROGRESSION = 0b11110 + BUILDINGS = 0b11100 + EVERYTHING = 0b11000 + CHAOS = 0b10000 + + +@dataclass(frozen=True) +class RegionData: + name: str + exits: List[str] = field(default_factory=list) + + +@dataclass(frozen=True) +class ConnectionData: + name: str + destination: str + reverse: Optional[str] = None + flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED + + def __post_init__(self): + if self.reverse is None and " to " in self.name: + origin, destination = self.name.split(" to ") + super().__setattr__("reverse", f"{destination} to {origin}") + + +stardew_valley_regions = [ + RegionData("Menu", ["To Stardew Valley"]), + RegionData("Stardew Valley", ["To Farmhouse"]), + RegionData("Farmhouse", ["Outside to Farm", "Downstairs to Cellar"]), + RegionData("Cellar"), + RegionData("Farm", ["Farm to Backwoods", "Farm to Bus Stop", "Farm to Forest", "Farm to Farmcave", "Enter Greenhouse", + "Use Desert Obelisk", "Use Island Obelisk"]), + RegionData("Backwoods", ["Backwoods to Mountain"]), + RegionData("Bus Stop", ["Bus Stop to Town", "Take Bus to Desert", "Bus Stop to Tunnel Entrance"]), + RegionData("Forest", ["Forest to Town", "Enter Secret Woods", "Forest to Wizard Tower", "Forest to Marnie's Ranch", + "Forest to Leah's Cottage", "Forest to Sewers"]), + RegionData("Farmcave"), + RegionData("Greenhouse"), + RegionData("Mountain", ["Mountain to Railroad", "Mountain to Tent", "Mountain to Carpenter Shop", "Mountain to The Mines", + "Enter Quarry", "Mountain to Adventurer's Guild", "Mountain to Town"]), + RegionData("Tunnel Entrance", ["Enter Tunnel"]), + RegionData("Tunnel"), + RegionData("Town", ["Town to Community Center", "Town to Beach", "Town to Hospital", + "Town to Pierre's General Store", "Town to Saloon", "Town to Alex's House", "Town to Trailer", "Town to Mayor's Manor", + "Town to Sam's House", "Town to Haley's House", "Town to Sewers", "Town to Clint's Blacksmith", "Town to Museum", + "Town to JojaMart"]), + RegionData("Beach", ["Beach to Willy's Fish Shop", "Enter Elliott's House", "Enter Tide Pools"]), + RegionData("Railroad", ["Enter Bathhouse Entrance", "Enter Witch Warp Cave"]), # "Enter Perfection Cutscene Area" + RegionData("Marnie's Ranch"), + RegionData("Leah's Cottage"), + RegionData("Sewers", ["Enter Mutant Bug Lair"]), + RegionData("Mutant Bug Lair"), + RegionData("Wizard Tower", ["Enter Wizard Basement"]), + RegionData("Wizard Basement"), + RegionData("Tent"), + RegionData("Carpenter Shop", ["Enter Sebastian's Room"]), + RegionData("Sebastian's Room"), + RegionData("Adventurer's Guild"), + RegionData("Community Center", + ["Access Crafts Room", "Access Pantry", "Access Fish Tank", "Access Boiler Room", "Access Bulletin Board", + "Access Vault"]), + RegionData("Crafts Room"), + RegionData("Pantry"), + RegionData("Fish Tank"), + RegionData("Boiler Room"), + RegionData("Bulletin Board"), + RegionData("Vault"), + RegionData("Hospital", ["Enter Harvey's Room"]), + RegionData("Harvey's Room"), + RegionData("Pierre's General Store", ["Enter Sunroom"]), + RegionData("Sunroom"), + RegionData("Saloon", ["Play Journey of the Prairie King", "Play Junimo Kart"]), + RegionData("Alex's House"), + RegionData("Trailer"), + RegionData("Mayor's Manor"), + RegionData("Sam's House"), + RegionData("Haley's House"), + RegionData("Clint's Blacksmith"), + RegionData("Museum"), + RegionData("JojaMart"), + RegionData("Willy's Fish Shop"), + RegionData("Elliott's House"), + RegionData("Tide Pools"), + RegionData("Bathhouse Entrance", ["Enter Locker Room"]), + RegionData("Locker Room", ["Enter Public Bath"]), + RegionData("Public Bath"), + RegionData("Witch Warp Cave", ["Enter Witch's Swamp"]), + RegionData("Witch's Swamp"), + RegionData("Quarry", ["Enter Quarry Mine Entrance"]), + RegionData("Quarry Mine Entrance", ["Enter Quarry Mine"]), + RegionData("Quarry Mine"), + RegionData("Secret Woods"), + RegionData("The Desert", ["Enter Skull Cavern Entrance"]), + RegionData("Skull Cavern Entrance", ["Enter Skull Cavern"]), + RegionData("Skull Cavern"), + RegionData("Ginger Island"), + RegionData("JotPK World 1", ["Reach JotPK World 2"]), + RegionData("JotPK World 2", ["Reach JotPK World 3"]), + RegionData("JotPK World 3"), + RegionData("Junimo Kart 1", ["Reach Junimo Kart 2"]), + RegionData("Junimo Kart 2", ["Reach Junimo Kart 3"]), + RegionData("Junimo Kart 3"), + RegionData("The Mines", ["Dig to The Mines - Floor 5", "Dig to The Mines - Floor 10", "Dig to The Mines - Floor 15", + "Dig to The Mines - Floor 20", "Dig to The Mines - Floor 25", "Dig to The Mines - Floor 30", + "Dig to The Mines - Floor 35", "Dig to The Mines - Floor 40", "Dig to The Mines - Floor 45", + "Dig to The Mines - Floor 50", "Dig to The Mines - Floor 55", "Dig to The Mines - Floor 60", + "Dig to The Mines - Floor 65", "Dig to The Mines - Floor 70", "Dig to The Mines - Floor 75", + "Dig to The Mines - Floor 80", "Dig to The Mines - Floor 85", "Dig to The Mines - Floor 90", + "Dig to The Mines - Floor 95", "Dig to The Mines - Floor 100", "Dig to The Mines - Floor 105", + "Dig to The Mines - Floor 110", "Dig to The Mines - Floor 115", "Dig to The Mines - Floor 120"]), + RegionData("The Mines - Floor 5"), + RegionData("The Mines - Floor 10"), + RegionData("The Mines - Floor 15"), + RegionData("The Mines - Floor 20"), + RegionData("The Mines - Floor 25"), + RegionData("The Mines - Floor 30"), + RegionData("The Mines - Floor 35"), + RegionData("The Mines - Floor 40"), + RegionData("The Mines - Floor 45"), + RegionData("The Mines - Floor 50"), + RegionData("The Mines - Floor 55"), + RegionData("The Mines - Floor 60"), + RegionData("The Mines - Floor 65"), + RegionData("The Mines - Floor 70"), + RegionData("The Mines - Floor 75"), + RegionData("The Mines - Floor 80"), + RegionData("The Mines - Floor 85"), + RegionData("The Mines - Floor 90"), + RegionData("The Mines - Floor 95"), + RegionData("The Mines - Floor 100"), + RegionData("The Mines - Floor 105"), + RegionData("The Mines - Floor 110"), + RegionData("The Mines - Floor 115"), + RegionData("The Mines - Floor 120"), +] + +# Exists and where they lead +mandatory_connections = [ + ConnectionData("To Stardew Valley", "Stardew Valley"), + ConnectionData("To Farmhouse", "Farmhouse"), + ConnectionData("Outside to Farm", "Farm"), + ConnectionData("Downstairs to Cellar", "Cellar"), + ConnectionData("Farm to Backwoods", "Backwoods"), + ConnectionData("Farm to Bus Stop", "Bus Stop"), + ConnectionData("Farm to Forest", "Forest"), + ConnectionData("Farm to Farmcave", "Farmcave", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Greenhouse", "Greenhouse"), + ConnectionData("Use Desert Obelisk", "The Desert"), + ConnectionData("Use Island Obelisk", "Ginger Island"), + ConnectionData("Backwoods to Mountain", "Mountain"), + ConnectionData("Bus Stop to Town", "Town"), + ConnectionData("Bus Stop to Tunnel Entrance", "Tunnel Entrance"), + ConnectionData("Take Bus to Desert", "The Desert"), + ConnectionData("Enter Tunnel", "Tunnel"), + ConnectionData("Forest to Town", "Town"), + ConnectionData("Forest to Wizard Tower", "Wizard Tower", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Wizard Basement", "Wizard Basement"), + ConnectionData("Forest to Marnie's Ranch", "Marnie's Ranch", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Forest to Leah's Cottage", "Leah's Cottage"), + ConnectionData("Enter Secret Woods", "Secret Woods"), + ConnectionData("Forest to Sewers", "Sewers"), + ConnectionData("Town to Sewers", "Sewers"), + ConnectionData("Enter Mutant Bug Lair", "Mutant Bug Lair"), + ConnectionData("Mountain to Railroad", "Railroad"), + ConnectionData("Mountain to Tent", "Tent", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Mountain to Carpenter Shop", "Carpenter Shop", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Sebastian's Room", "Sebastian's Room"), + ConnectionData("Mountain to Adventurer's Guild", "Adventurer's Guild"), + ConnectionData("Enter Quarry", "Quarry"), + ConnectionData("Enter Quarry Mine Entrance", "Quarry Mine Entrance"), + ConnectionData("Enter Quarry Mine", "Quarry Mine"), + ConnectionData("Mountain to Town", "Town"), + ConnectionData("Town to Community Center", "Community Center", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Access Crafts Room", "Crafts Room"), + ConnectionData("Access Pantry", "Pantry"), + ConnectionData("Access Fish Tank", "Fish Tank"), + ConnectionData("Access Boiler Room", "Boiler Room"), + ConnectionData("Access Bulletin Board", "Bulletin Board"), + ConnectionData("Access Vault", "Vault"), + ConnectionData("Town to Hospital", "Hospital", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Harvey's Room", "Harvey's Room"), + ConnectionData("Town to Pierre's General Store", "Pierre's General Store", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Sunroom", "Sunroom"), + ConnectionData("Town to Clint's Blacksmith", "Clint's Blacksmith", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Saloon", "Saloon", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Play Journey of the Prairie King", "JotPK World 1"), + ConnectionData("Reach JotPK World 2", "JotPK World 2"), + ConnectionData("Reach JotPK World 3", "JotPK World 3"), + ConnectionData("Play Junimo Kart", "Junimo Kart 1"), + ConnectionData("Reach Junimo Kart 2", "Junimo Kart 2"), + ConnectionData("Reach Junimo Kart 3", "Junimo Kart 3"), + ConnectionData("Town to Sam's House", "Sam's House", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Haley's House", "Haley's House", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Mayor's Manor", "Mayor's Manor", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Alex's House", "Alex's House", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Trailer", "Trailer", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Museum", "Museum", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to JojaMart", "JojaMart", flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Beach", "Beach"), + ConnectionData("Enter Elliott's House", "Elliott's House"), + ConnectionData("Beach to Willy's Fish Shop", "Willy's Fish Shop", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Tide Pools", "Tide Pools"), + ConnectionData("Mountain to The Mines", "The Mines", flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Dig to The Mines - Floor 5", "The Mines - Floor 5"), + ConnectionData("Dig to The Mines - Floor 10", "The Mines - Floor 10"), + ConnectionData("Dig to The Mines - Floor 15", "The Mines - Floor 15"), + ConnectionData("Dig to The Mines - Floor 20", "The Mines - Floor 20"), + ConnectionData("Dig to The Mines - Floor 25", "The Mines - Floor 25"), + ConnectionData("Dig to The Mines - Floor 30", "The Mines - Floor 30"), + ConnectionData("Dig to The Mines - Floor 35", "The Mines - Floor 35"), + ConnectionData("Dig to The Mines - Floor 40", "The Mines - Floor 40"), + ConnectionData("Dig to The Mines - Floor 45", "The Mines - Floor 45"), + ConnectionData("Dig to The Mines - Floor 50", "The Mines - Floor 50"), + ConnectionData("Dig to The Mines - Floor 55", "The Mines - Floor 55"), + ConnectionData("Dig to The Mines - Floor 60", "The Mines - Floor 60"), + ConnectionData("Dig to The Mines - Floor 65", "The Mines - Floor 65"), + ConnectionData("Dig to The Mines - Floor 70", "The Mines - Floor 70"), + ConnectionData("Dig to The Mines - Floor 75", "The Mines - Floor 75"), + ConnectionData("Dig to The Mines - Floor 80", "The Mines - Floor 80"), + ConnectionData("Dig to The Mines - Floor 85", "The Mines - Floor 85"), + ConnectionData("Dig to The Mines - Floor 90", "The Mines - Floor 90"), + ConnectionData("Dig to The Mines - Floor 95", "The Mines - Floor 95"), + ConnectionData("Dig to The Mines - Floor 100", "The Mines - Floor 100"), + ConnectionData("Dig to The Mines - Floor 105", "The Mines - Floor 105"), + ConnectionData("Dig to The Mines - Floor 110", "The Mines - Floor 110"), + ConnectionData("Dig to The Mines - Floor 115", "The Mines - Floor 115"), + ConnectionData("Dig to The Mines - Floor 120", "The Mines - Floor 120"), + ConnectionData("Enter Skull Cavern Entrance", "Skull Cavern Entrance"), + ConnectionData("Enter Skull Cavern", "Skull Cavern"), + ConnectionData("Enter Witch Warp Cave", "Witch Warp Cave"), + ConnectionData("Enter Witch's Swamp", "Witch's Swamp"), + ConnectionData("Enter Bathhouse Entrance", "Bathhouse Entrance"), + ConnectionData("Enter Locker Room", "Locker Room"), + ConnectionData("Enter Public Bath", "Public Bath"), +] + + +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[Iterable[Region], Dict[str, str]]: + regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in stardew_valley_regions} + entrances: Dict[str: Entrance] = {entrance.name: entrance + for region in regions.values() + for entrance in region.exits} + + connections, randomized_data = randomize_connections(random, world_options) + + for connection in connections: + if connection.name not in entrances: + continue + entrances[connection.name].connect(regions[connection.destination]) + + return regions.values(), randomized_data + + +def randomize_connections(random: Random, world_options: StardewOptions) -> Tuple[List[ConnectionData], Dict[str, str]]: + connections_to_randomize = [] + if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town: + connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.PELICAN_TOWN in connection.flag] + elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression: + connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.NON_PROGRESSION in connection.flag] + random.shuffle(connections_to_randomize) + + destination_pool = list(connections_to_randomize) + random.shuffle(destination_pool) + + randomized_connections = [] + randomized_data = {} + for connection in connections_to_randomize: + destination = destination_pool.pop() + randomized_connections.append(ConnectionData(connection.name, destination.destination, destination.reverse)) + randomized_data[connection.name] = destination.name + randomized_data[destination.reverse] = connection.reverse + + return mandatory_connections, randomized_data diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt new file mode 100644 index 0000000000..b0922176e4 --- /dev/null +++ b/worlds/stardew_valley/requirements.txt @@ -0,0 +1 @@ +importlib_resources; python_version <= '3.8' diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py new file mode 100644 index 0000000000..f9ba31cc19 --- /dev/null +++ b/worlds/stardew_valley/rules.py @@ -0,0 +1,190 @@ +import itertools +from typing import Dict + +from BaseClasses import MultiWorld +from worlds.generic import Rules as MultiWorldRules +from . import options, locations +from .bundles import Bundle +from .locations import LocationTags +from .logic import StardewLogic, _And, season_per_skill_level, tool_prices, week_days + +help_wanted_per_season = { + 1: "Spring", + 2: "Summer", + 3: "Fall", + 4: "Winter", + 5: "Year Two", + 6: "Year Two", + 7: "Year Two", + 8: "Year Two", + 9: "Year Two", + 10: "Year Two", +} + + +def set_rules(multi_world: MultiWorld, player: int, world_options: options.StardewOptions, logic: StardewLogic, + current_bundles: Dict[str, Bundle]): + summer = multi_world.get_location("Summer", player) + all_location_names = list(location.name for location in multi_world.get_locations(player)) + + for floor in range(5, 120 + 5, 5): + MultiWorldRules.add_rule(multi_world.get_entrance(f"Dig to The Mines - Floor {floor}", player), + logic.can_mine_to_floor(floor).simplify()) + + MultiWorldRules.add_rule(multi_world.get_entrance("Enter Quarry", player), + logic.received("Bridge Repair").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Enter Secret Woods", player), + logic.has_tool("Axe", "Iron").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Take Bus to Desert", player), + logic.received("Bus Repair").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Enter Skull Cavern", player), + logic.received("Skull Key").simplify()) + + MultiWorldRules.add_rule(multi_world.get_entrance("Use Desert Obelisk", player), + logic.received("Desert Obelisk").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Use Island Obelisk", player), + logic.received("Island Obelisk").simplify()) + + # Those checks do not exist if ToolProgression is vanilla + if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: + MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player), + (logic.has_skill_level("Fishing", 2) & logic.can_spend_money(1800)).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Purchase Iridium Rod", player), + (logic.has_skill_level("Fishing", 6) & logic.can_spend_money(7500)).simplify()) + + materials = [None, "Copper", "Iron", "Gold", "Iridium"] + tool = ["Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can"] + for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): + if previous is None: + MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + (logic.has(f"{material} Ore") & + logic.can_spend_money(tool_prices[material])).simplify()) + else: + MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + (logic.has(f"{material} Ore") & logic.has_tool(tool, previous) & + logic.can_spend_money(tool_prices[material])).simplify()) + + # Skills + if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla: + for i in range(1, 11): + MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Farming", player), + (logic.received(season_per_skill_level["Farming", i])).simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Fishing", player), + (logic.can_get_fishing_xp() & + logic.received(season_per_skill_level["Fishing", i])).simplify()) + MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), + logic.received(season_per_skill_level["Foraging", i]).simplify()) + if i >= 6: + MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), + logic.has_tool("Axe", "Iron").simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Mining", player), + logic.received(season_per_skill_level["Mining", i]).simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Combat", player), + (logic.received(season_per_skill_level["Combat", i]) & + logic.has_any_weapon()).simplify()) + + # Bundles + for bundle in current_bundles.values(): + MultiWorldRules.set_rule(multi_world.get_location(bundle.get_name_with_bundle(), player), + logic.can_complete_bundle(bundle.requirements, bundle.number_required).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player), + _And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) + + # Buildings + if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: + for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: + MultiWorldRules.set_rule(multi_world.get_location(building.name, player), + logic.building_rules[building.name.replace(" Blueprint", "")].simplify()) + + # Story Quests + for quest in locations.locations_by_tag[LocationTags.QUEST]: + MultiWorldRules.set_rule(multi_world.get_location(quest.name, player), + logic.quest_rules[quest.name].simplify()) + + # Help Wanted Quests + desired_number_help_wanted: int = world_options[options.HelpWantedLocations] // 7 + for i in range(1, desired_number_help_wanted + 1): + prefix = "Help Wanted:" + delivery = "Item Delivery" + rule = logic.received(help_wanted_per_season[min(5, i)]) + fishing_rule = rule & logic.can_fish() + slay_rule = rule & logic.has_any_weapon() + for j in range(i, i + 4): + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} {delivery} {j}", player), + rule.simplify()) + + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Gathering {i}", player), + rule.simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Fishing {i}", player), + fishing_rule.simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i}", player), + slay_rule.simplify()) + + fish_prefix = "Fishsanity: " + for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]: + if fish_location.name in all_location_names: + fish_name = fish_location.name[len(fish_prefix):] + MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player), + logic.has(fish_name).simplify()) + + if world_options[options.BuildingProgression] == options.BuildingProgression.option_progressive_early_shipping_bin: + summer.access_rule = summer.access_rule & logic.received("Shipping Bin") + + # Backpacks + if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: + MultiWorldRules.add_rule(multi_world.get_location("Large Pack", player), + logic.can_spend_money(2000).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Deluxe Pack", player), + logic.can_spend_money(10000).simplify()) + + if world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: + summer.access_rule = summer.access_rule & logic.received("Progressive Backpack") + MultiWorldRules.add_rule(multi_world.get_location("Winter", player), + logic.received("Progressive Backpack", 2).simplify()) + + MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), + logic.has("Sweet Gem Berry").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), + logic.has("Prismatic Shard").simplify()) + + # Traveling Merchant + for day in week_days: + item_for_day = f"Traveling Merchant: {day}" + for i in range(1, 4): + location_name = f"Traveling Merchant {day} Item {i}" + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), + logic.received(item_for_day)) + + if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + MultiWorldRules.add_rule(multi_world.get_entrance("Play Junimo Kart", player), + (logic.received("Skull Key") & logic.has("Junimo Kart Small Buff")).simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach Junimo Kart 2", player), + logic.has("Junimo Kart Medium Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach Junimo Kart 3", player), + logic.has("Junimo Kart Big Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + logic.has("Junimo Kart Max Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Play Journey of the Prairie King", player), + logic.has("JotPK Small Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach JotPK World 2", player), + logic.has("JotPK Medium Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach JotPK World 3", player), + logic.has("JotPK Big Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player), + logic.has("JotPK Max Buff").simplify()) diff --git a/worlds/stardew_valley/scripts/__init__.py b/worlds/stardew_valley/scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/scripts/export_items.py b/worlds/stardew_valley/scripts/export_items.py new file mode 100644 index 0000000000..6d929226d9 --- /dev/null +++ b/worlds/stardew_valley/scripts/export_items.py @@ -0,0 +1,26 @@ +"""Items export script +This script can be used to export all the AP items into a json file in the output folder. This file is used by the tests +of the mod to ensure it can handle all possible items. + +To run the script, use `python -m worlds.stardew_valley.scripts.export_items` from the repository root. +""" + +import json +import os.path + +from worlds.stardew_valley import item_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/stardew_valley_item_table.json", "w+") as f: + items = { + item.name: { + "code": item.code, + "classification": item.classification.name + } + for item in item_table.values() + if item.code is not None + } + json.dump({"items": items}, f) diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py new file mode 100644 index 0000000000..1dc60f79b1 --- /dev/null +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -0,0 +1,26 @@ +"""Locations export script +This script can be used to export all the AP locations into a json file in the output folder. This file is used by the +tests of the mod to ensure it can handle all possible locations. + +To run the script, use `python -m worlds.stardew_valley.scripts.export_locations` from the repository root. +""" + +import json +import os + +from worlds.stardew_valley import location_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/stardew_valley_location_table.json", "w+") as f: + locations = { + location.name: { + "code": location.code, + "region": location.region, + } + for location in location_table.values() + if location.code is not None + } + json.dump({"locations": locations}, f) diff --git a/worlds/stardew_valley/scripts/update_data.py b/worlds/stardew_valley/scripts/update_data.py new file mode 100644 index 0000000000..4b7b6be201 --- /dev/null +++ b/worlds/stardew_valley/scripts/update_data.py @@ -0,0 +1,88 @@ +"""Update data script +This script can be used to assign new ids for the items and locations in the CSV file. It also regenerates the items +based on the resource packs. + +To run the script, use `python -m worlds.stardew_valley.scripts.update_data` from the repository root. +""" + +import csv +import itertools +from pathlib import Path +from typing import List + +from worlds.stardew_valley import LocationData +from worlds.stardew_valley.items import load_item_csv, Group, ItemData, load_resource_pack_csv, friendship_pack +from worlds.stardew_valley.locations import load_location_csv + +RESOURCE_PACK_CODE_OFFSET = 5000 +script_folder = Path(__file__) + + +def write_item_csv(items: List[ItemData]): + with open((script_folder.parent.parent / "data/items.csv").resolve(), "w", newline="") as file: + writer = csv.DictWriter(file, ["id", "name", "classification", "groups"]) + writer.writeheader() + for item in items: + item_dict = { + "id": item.code_without_offset, + "name": item.name, + "classification": item.classification.name, + "groups": ",".join(sorted(group.name for group in item.groups)) + } + writer.writerow(item_dict) + + +def write_location_csv(locations: List[LocationData]): + with open((script_folder.parent.parent / "data/locations.csv").resolve(), "w", newline="") as file: + write = csv.DictWriter(file, ["id", "region", "name", "tags"]) + write.writeheader() + for location in locations: + location_dict = { + "id": location.code_without_offset, + "name": location.name, + "region": location.region, + "tags": ",".join(sorted(group.name for group in location.tags)) + } + write.writerow(location_dict) + + +if __name__ == "__main__": + loaded_items = load_item_csv() + + item_counter = itertools.count(max(item.code_without_offset + for item in loaded_items + if Group.RESOURCE_PACK not in item.groups + and item.code_without_offset is not None) + 1) + items_to_write = [] + for item in loaded_items: + if item.has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK): + continue + + if item.code_without_offset is None: + items_to_write.append(ItemData(next(item_counter), item.name, item.classification, item.groups)) + continue + + items_to_write.append(item) + + all_resource_packs = load_resource_pack_csv() + [friendship_pack] + resource_pack_counter = itertools.count(RESOURCE_PACK_CODE_OFFSET) + items_to_write.extend( + item for resource_pack in all_resource_packs for item in resource_pack.as_item_data(resource_pack_counter)) + + write_item_csv(items_to_write) + + loaded_locations = load_location_csv() + location_counter = itertools.count(max(location.code_without_offset + for location in loaded_locations + if location.code_without_offset is not None) + 1) + + locations_to_write = [] + for location in loaded_locations: + if location.code_without_offset is None: + locations_to_write.append( + LocationData(next(location_counter), location.region, location.name, location.tags)) + continue + + locations_to_write.append(location) + + write_location_csv(locations_to_write) diff --git a/worlds/stardew_valley/test/TestAllLogic.py b/worlds/stardew_valley/test/TestAllLogic.py new file mode 100644 index 0000000000..de1c004913 --- /dev/null +++ b/worlds/stardew_valley/test/TestAllLogic.py @@ -0,0 +1,53 @@ +import unittest + +from test.general import setup_solo_multiworld +from .. import StardewValleyWorld +from ..bundle_data import all_bundle_items_except_money +from ..logic import MISSING_ITEM, _False + + +class TestAllLogicalItem(unittest.TestCase): + multi_world = setup_solo_multiworld(StardewValleyWorld) + world = multi_world.worlds[1] + logic = world.logic + + def setUp(self) -> None: + for item in self.multi_world.get_items(): + self.multi_world.state.collect(item, event=True) + + def test_given_bundle_item_then_is_available_in_logic(self): + for bundle_item in all_bundle_items_except_money: + with self.subTest(bundle_item=bundle_item): + assert bundle_item.item.name in self.logic.item_rules + + def test_given_item_rule_then_can_be_resolved(self): + for item in self.logic.item_rules.keys(): + with self.subTest(item=item): + rule = self.logic.item_rules[item] + + assert MISSING_ITEM not in repr(rule) + assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" + + def test_given_building_rule_then_can_be_resolved(self): + for item in self.logic.building_rules.keys(): + with self.subTest(item=item): + rule = self.logic.building_rules[item] + + assert MISSING_ITEM not in repr(rule) + assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" + + def test_given_quest_rule_then_can_be_resolved(self): + for item in self.logic.quest_rules.keys(): + with self.subTest(item=item): + rule = self.logic.quest_rules[item] + + assert MISSING_ITEM not in repr(rule) + assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" + + def test_given_location_rule_then_can_be_resolved(self): + for location in self.multi_world.get_locations(1): + with self.subTest(location=location): + rule = location.access_rule + + assert MISSING_ITEM not in repr(rule) + assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py new file mode 100644 index 0000000000..5801737709 --- /dev/null +++ b/worlds/stardew_valley/test/TestBundles.py @@ -0,0 +1,16 @@ +import unittest + +from ..bundle_data import all_bundle_items + + +class TestBundles(unittest.TestCase): + def test_all_bundle_items_have_3_parts(self): + for bundle_item in all_bundle_items: + name = bundle_item.item.name + assert len(name) > 0 + id = bundle_item.item.item_id + assert (id > 0 or id == -1) + amount = bundle_item.amount + assert amount > 0 + quality = bundle_item.quality + assert quality >= 0 diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py new file mode 100644 index 0000000000..c08cef0e74 --- /dev/null +++ b/worlds/stardew_valley/test/TestData.py @@ -0,0 +1,20 @@ +import unittest + +from ..items import load_item_csv +from ..locations import load_location_csv + + +class TestCsvIntegrity(unittest.TestCase): + def test_items_integrity(self): + items = load_item_csv() + + for item in items: + assert item.code_without_offset is not None, \ + "Some item do not have an id. Run the script `update_data.py` to generate them." + + def test_locations_integrity(self): + locations = load_location_csv() + + for location in locations: + assert location.code_without_offset is not None, \ + "Some location do not have an id. Run the script `update_data.py` to generate them." diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py new file mode 100644 index 0000000000..840052d30b --- /dev/null +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -0,0 +1,127 @@ +from BaseClasses import ItemClassification +from . import SVTestBase +from .. import locations, items, location_table, options +from ..items import items_by_group, Group +from ..locations import LocationTags + + +class TestBaseItemGeneration(SVTestBase): + + def test_all_progression_items_are_added_to_the_pool(self): + for classification in [ItemClassification.progression, ItemClassification.useful]: + with self.subTest(classification=classification): + + all_classified_items = {self.world.create_item(item) + for item in items.items_by_group[items.Group.COMMUNITY_REWARD] + if item.classification is classification} + + for item in all_classified_items: + assert item in self.multiworld.itempool + + def test_creates_as_many_item_as_non_event_locations(self): + non_event_locations = [location for location in self.multiworld.get_locations(self.player) if + not location.event] + + assert len(non_event_locations), len(self.multiworld.itempool) + + +class TestGivenProgressiveBackpack(SVTestBase): + options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} + + def test_when_generate_world_then_two_progressive_backpack_are_added(self): + assert self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")) == 2 + + def test_when_generate_world_then_backpack_locations_are_added(self): + created_locations = {location.name for location in self.multiworld.get_locations(1)} + assert all(location.name in created_locations for location in locations.locations_by_tag[LocationTags.BACKPACK]) + + +class TestRemixedMineRewards(SVTestBase): + def test_when_generate_world_then_one_reward_is_added_per_chest(self): + # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_10]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_20]) + assert self.world.create_item("Slingshot") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_50]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_60]) + assert self.world.create_item("Master Slingshot") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_80]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_90]) + assert self.world.create_item("Stardrop") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_110]) + assert self.world.create_item("Skull Key") in self.multiworld.itempool + + # This test as 1 over 90,000 changes to fail... Sorry in advance + def test_when_generate_world_then_rewards_are_not_all_vanilla(self): + assert not all(self.world.create_item(item) in self.multiworld.itempool + for item in + ["Leather Boots", "Steel Smallsword", "Tundra Boots", "Crystal Dagger", "Firewalker Boots", + "Obsidian Edge", "Space Boots"]) + + +class TestProgressiveElevator(SVTestBase): + options = { + options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_given_access_to_floor_115_when_find_another_elevator_then_has_access_to_floor_120(self): + self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2) + self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22) + self.collect(self.multiworld.create_item("Bone Sword", self.player)) + self.collect([self.get_item_by_name("Combat Level")] * 4) + self.collect(self.get_item_by_name("Adventurer's Guild")) + + assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + self.collect(self.get_item_by_name("Progressive Mine Elevator")) + + assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + def test_given_access_to_floor_115_when_find_another_pickaxe_and_sword_then_has_access_to_floor_120(self): + self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2) + self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22) + self.collect(self.multiworld.create_item("Bone Sword", self.player)) + self.collect([self.get_item_by_name("Combat Level")] * 4) + self.collect(self.get_item_by_name("Adventurer's Guild")) + + assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + self.collect(self.get_item_by_name("Progressive Pickaxe")) + self.collect(self.multiworld.create_item("Steel Falchion", self.player)) + self.collect(self.get_item_by_name("Combat Level")) + self.collect(self.get_item_by_name("Combat Level")) + + assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + +class TestLocationGeneration(SVTestBase): + + def test_all_location_created_are_in_location_table(self): + for location in self.multiworld.get_locations(self.player): + if not location.event: + assert location.name in location_table + + +class TestLocationAndItemCount(SVTestBase): + options = { + options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, + options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_vanilla, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.HelpWantedLocations.internal_name: 0, + options.NumberOfPlayerBuffs.internal_name: 12, + } + + def test_minimal_location_maximal_items_still_valid(self): + assert len(self.multiworld.get_locations()) >= len(self.multiworld.get_items()) diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py new file mode 100644 index 0000000000..98d251eb58 --- /dev/null +++ b/worlds/stardew_valley/test/TestItems.py @@ -0,0 +1,26 @@ +import unittest + +from BaseClasses import MultiWorld +from .. import StardewValleyWorld +from ..items import item_table + + +class TestItems(unittest.TestCase): + def test_can_create_item_of_resource_pack(self): + item_name = "Resource Pack: 500 Money" + + multi_world = MultiWorld(1) + multi_world.game[1] = "Stardew Valley" + multi_world.player_name = {1: "Tester"} + world = StardewValleyWorld(multi_world, 1) + item = world.create_item(item_name) + + assert item.name == item_name + + def test_items_table_footprint_is_between_717000_and_727000(self): + item_with_lowest_id = min((item for item in item_table.values() if item.code is not None), key=lambda x: x.code) + item_with_highest_id = max((item for item in item_table.values() if item.code is not None), + key=lambda x: x.code) + + assert item_with_lowest_id.code >= 717000 + assert item_with_highest_id.code < 727000 diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py new file mode 100644 index 0000000000..83129a56b5 --- /dev/null +++ b/worlds/stardew_valley/test/TestLogic.py @@ -0,0 +1,293 @@ +from . import SVTestBase +from .. import options + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + } + + def test_sturgeon(self): + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + summer = self.get_item_by_name("Summer") + self.multiworld.state.collect(summer, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_rod = self.get_item_by_name("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod, event=True) + self.multiworld.state.collect(fishing_rod, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_level = self.get_item_by_name("Fishing Level") + self.multiworld.state.collect(fishing_level, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(summer) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + winter = self.get_item_by_name("Winter") + self.multiworld.state.collect(winter, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(fishing_rod) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) + self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) + + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + fall = self.get_item_by_name("Fall") + self.multiworld.state.collect(fall, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + tuesday = self.get_item_by_name("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday, event=True) + assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + self.remove(fall) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(tuesday) + + green_house = self.get_item_by_name("Greenhouse") + self.multiworld.state.collect(green_house, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + friday = self.get_item_by_name("Traveling Merchant: Friday") + self.multiworld.state.collect(friday, event=True) + assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + self.remove(green_house) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(friday) + + +class TestBundlesLogic(SVTestBase): + options = { + } + + def test_vault_2500g_bundle(self): + assert not self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) + + summer = self.get_item_by_name("Summer") + self.multiworld.state.collect(summer, event=True) + assert self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) + + +class TestBuildingLogic(SVTestBase): + options = { + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin + } + + def test_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + summer = self.get_item_by_name("Summer") + self.multiworld.state.collect(summer, event=True) + assert self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + def test_big_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.get_item_by_name("Fall"), event=True) + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) + assert self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + def test_deluxe_big_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) + assert self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + def test_big_shed_blueprint(self): + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.get_item_by_name("Progressive Shed"), event=True) + assert self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + +class TestArcadeMachinesLogic(SVTestBase): + options = { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + } + + def test_prairie_king(self): + assert not self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + + boots = self.get_item_by_name("JotPK: Progressive Boots") + gun = self.get_item_by_name("JotPK: Progressive Gun") + ammo = self.get_item_by_name("JotPK: Progressive Ammo") + life = self.get_item_by_name("JotPK: Extra Life") + drop = self.get_item_by_name("JotPK: Increased Drop Rate") + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(boots) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + self.remove(ammo) + self.remove(life) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + +class TestWeaponsLogic(SVTestBase): + options = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.collect(self.get_item_by_name("Adventurer's Guild")) + self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) + self.collect([self.get_item_by_name("Combat Level")] * 10) + self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 24) + self.multiworld.state.collect(self.get_item_by_name("Bus Repair"), event=True) + self.multiworld.state.collect(self.get_item_by_name("Skull Key"), event=True) + + self.GiveItemAndCheckReachableMine("Rusty Sword", 1) + self.GiveItemAndCheckReachableMine("Wooden Blade", 1) + self.GiveItemAndCheckReachableMine("Elf Blade", 1) + + self.GiveItemAndCheckReachableMine("Silver Saber", 2) + self.GiveItemAndCheckReachableMine("Crystal Dagger", 2) + + self.GiveItemAndCheckReachableMine("Claymore", 3) + self.GiveItemAndCheckReachableMine("Obsidian Edge", 3) + self.GiveItemAndCheckReachableMine("Bone Sword", 3) + + self.GiveItemAndCheckReachableMine("The Slammer", 4) + self.GiveItemAndCheckReachableMine("Lava Katana", 4) + + self.GiveItemAndCheckReachableMine("Galaxy Sword", 5) + self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5) + self.GiveItemAndCheckReachableMine("Galaxy Dagger", 5) + + def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.state.collect(item, event=True) + if reachable_level > 0: + assert self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + + if reachable_level > 1: + assert self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + + if reachable_level > 2: + assert self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + + if reachable_level > 3: + assert self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + + if reachable_level > 4: + assert self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + + self.remove(item) diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py new file mode 100644 index 0000000000..1a3d5a1dca --- /dev/null +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -0,0 +1,52 @@ +import unittest + +from .. import _True +from ..logic import _Received, _Has, _False, _And, _Or + + +class TestLogicSimplification(unittest.TestCase): + def test_simplify_true_in_and(self): + rules = { + "Wood": _True(), + "Rock": _True(), + } + summer = _Received("Summer", 0, 1) + assert (_Has("Wood", rules) & summer & _Has("Rock", rules)).simplify() == summer + + def test_simplify_false_in_or(self): + rules = { + "Wood": _False(), + "Rock": _False(), + } + summer = _Received("Summer", 0, 1) + assert (_Has("Wood", rules) | summer | _Has("Rock", rules)).simplify() == summer + + def test_simplify_and_in_and(self): + rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), + _And(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) + assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), + _Received("Spring", 0, 1)) + + def test_simplify_duplicated_and(self): + rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), + _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) + assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) + + def test_simplify_or_in_or(self): + rule = _Or(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), + _Or(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) + assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), + _Received("Spring", 0, 1)) + + def test_simplify_duplicated_or(self): + rule = _And(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), + _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) + assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) + + def test_simplify_true_in_or(self): + rule = _Or(_True(), _Received("Summer", 0, 1)) + assert rule.simplify() == _True() + + def test_simplify_false_in_and(self): + rule = _And(_False(), _Received("Summer", 0, 1)) + assert rule.simplify() == _False() diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py new file mode 100644 index 0000000000..3aadc7e49b --- /dev/null +++ b/worlds/stardew_valley/test/TestRegions.py @@ -0,0 +1,46 @@ +import random +import sys +import unittest + +from .. import StardewOptions, options +from ..regions import stardew_valley_regions, mandatory_connections, randomize_connections, RandomizationFlag + +connections_by_name = {connection.name for connection in mandatory_connections} +regions_by_name = {region.name for region in stardew_valley_regions} + + +class TestRegions(unittest.TestCase): + def test_region_exits_lead_somewhere(self): + for region in stardew_valley_regions: + with self.subTest(region=region): + for exit in region.exits: + assert exit in connections_by_name, f"{region.name} is leading to {exit} but it does not exist." + + def test_connection_lead_somewhere(self): + for connection in mandatory_connections: + with self.subTest(connection=connection): + assert connection.destination in regions_by_name, \ + f"{connection.name} is leading to {connection.destination} but it does not exist." + + +class TestEntranceRando(unittest.TestCase): + + def test_pelican_town_entrance_randomization(self): + for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), + (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]: + with self.subTest(option=option, flag=flag): + seed = random.randrange(sys.maxsize) + rand = random.Random(seed) + world_options = StardewOptions({options.EntranceRandomization.internal_name: option}) + + _, randomized_connections = randomize_connections(rand, world_options) + + for connection in mandatory_connections: + if flag in connection.flag: + assert connection.name in randomized_connections, \ + f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}" + assert connection.reverse in randomized_connections, \ + f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}" + + assert len(set(randomized_connections.values())) == len( + randomized_connections.values()), f"Connections are duplicated in randomization. Seed = {seed}" diff --git a/worlds/stardew_valley/test/TestResourcePack.py b/worlds/stardew_valley/test/TestResourcePack.py new file mode 100644 index 0000000000..d25505bbdc --- /dev/null +++ b/worlds/stardew_valley/test/TestResourcePack.py @@ -0,0 +1,76 @@ +import itertools +import math +import unittest + +from BaseClasses import ItemClassification +from .. import ItemData +from ..items import Group, ResourcePackData + + +class TestResourcePack(unittest.TestCase): + + def test_can_transform_resource_pack_data_into_idem_data(self): + resource_pack = ResourcePackData("item name", 1, 1, ItemClassification.filler, frozenset()) + + items = resource_pack.as_item_data(itertools.count()) + + assert ItemData(0, "Resource Pack: 1 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert ItemData(1, "Resource Pack: 2 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert len(items) == 2 + + def test_when_scale_quantity_then_generate_a_possible_quantity_from_minimal_scaling_to_double(self): + resource_pack = ResourcePackData("item name", default_amount=4, scaling_factor=2) + + quantities = resource_pack.scale_quantity.items() + + assert (50, 2) in quantities + assert (100, 4) in quantities + assert (150, 6) in quantities + assert (200, 8) in quantities + assert len(quantities) == (4 / 2) * 2 + + def test_given_scaling_not_multiple_of_default_amount_when_scale_quantity_then_double_is_added_at_200_scaling(self): + resource_pack = ResourcePackData("item name", default_amount=5, scaling_factor=3) + + quantities = resource_pack.scale_quantity.items() + + assert (40, 2) in quantities + assert (100, 5) in quantities + assert (160, 8) in quantities + assert (200, 10) in quantities + assert len(quantities) == math.ceil(5 / 3) * 2 + + def test_given_large_default_amount_multiple_of_scaling_factor_when_scale_quantity_then_scaled_amount_multiple( + self): + resource_pack = ResourcePackData("item name", default_amount=500, scaling_factor=50) + + quantities = resource_pack.scale_quantity.items() + + assert (10, 50) in quantities + assert (20, 100) in quantities + assert (30, 150) in quantities + assert (40, 200) in quantities + assert (50, 250) in quantities + assert (60, 300) in quantities + assert (70, 350) in quantities + assert (80, 400) in quantities + assert (90, 450) in quantities + assert (100, 500) in quantities + assert (110, 550) in quantities + assert (120, 600) in quantities + assert (130, 650) in quantities + assert (140, 700) in quantities + assert (150, 750) in quantities + assert (160, 800) in quantities + assert (170, 850) in quantities + assert (180, 900) in quantities + assert (190, 950) in quantities + assert (200, 1000) in quantities + assert len(quantities) == math.ceil(500 / 50) * 2 + + def test_given_smallest_multiplier_possible_when_generate_resource_pack_name_then_quantity_is_not_0(self): + resource_pack = ResourcePackData("item name", default_amount=10, scaling_factor=5) + + name = resource_pack.create_name_from_multiplier(1) + + assert name == "Resource Pack: 5 item name" diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py new file mode 100644 index 0000000000..1ddf037641 --- /dev/null +++ b/worlds/stardew_valley/test/__init__.py @@ -0,0 +1,14 @@ +from typing import ClassVar + +from test.TestBase import WorldTestBase +from .. import StardewValleyWorld + + +class SVTestBase(WorldTestBase): + game = "Stardew Valley" + world: StardewValleyWorld + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs): + super().world_setup(*args, **kwargs) + self.world = self.multiworld.worlds[self.player] From f3bdf0c5ed3b6099e33cc17a095b27ee19712d3a Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 26 Feb 2023 18:24:54 -0600 Subject: [PATCH 13/14] Tests: test all state and empty state on world test bases (#1476) * Tests: test all state and empty state on world test bases * actually add the test methods to the dict * only test if the world test base has non default options * remove temp logging * ditch the meta class and document methods * Tests: WorldTestBase comment and docstring cleanup * skip default tests if setUp or world_setup are modified and use a property * negation hurts my head * docstring * use a better name for the property --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/TestBase.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/TestBase.py b/test/TestBase.py index eea8e81a55..fb8031a900 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -112,6 +112,8 @@ class WorldTestBase(unittest.TestCase): self.world_setup() def world_setup(self, seed: typing.Optional[int] = None) -> None: + if type(self) is WorldTestBase: + return # setUp gets called for tests defined in the base class. We skip world_setup here. if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") self.multiworld = MultiWorld(1) @@ -128,7 +130,9 @@ class WorldTestBase(unittest.TestCase): for step in gen_steps: call_all(self.multiworld, step) + # methods that can be called within tests def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: + """Collects all pre-placed items and items in the multiworld itempool except those provided""" if isinstance(item_names, str): item_names = (item_names,) for item in self.multiworld.get_items(): @@ -136,12 +140,14 @@ class WorldTestBase(unittest.TestCase): self.multiworld.state.collect(item) def get_item_by_name(self, item_name: str) -> Item: + """Returns the first item found in placed items, or in the itempool with the matching name""" for item in self.multiworld.get_items(): if item.name == item_name: return item raise ValueError("No such item") def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """Returns actual items from the itempool that match the provided name(s)""" if isinstance(item_names, str): item_names = (item_names,) return [item for item in self.multiworld.itempool if item.name in item_names] @@ -153,12 +159,14 @@ class WorldTestBase(unittest.TestCase): return items def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Collects the provided item(s) into state""" if isinstance(items, Item): items = (items,) for item in items: self.multiworld.state.collect(item) def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Removes the provided item(s) from state""" if isinstance(items, Item): items = (items,) for item in items: @@ -167,17 +175,22 @@ class WorldTestBase(unittest.TestCase): self.multiworld.state.remove(item) def can_reach_location(self, location: str) -> bool: + """Determines if the current state can reach the provide location name""" return self.multiworld.state.can_reach(location, "Location", 1) def can_reach_entrance(self, entrance: str) -> bool: + """Determines if the current state can reach the provided entrance name""" return self.multiworld.state.can_reach(entrance, "Entrance", 1) def count(self, item_name: str) -> int: + """Returns the amount of an item currently in state""" return self.multiworld.state.count(item_name, 1) def assertAccessDependency(self, locations: typing.List[str], possible_items: typing.Iterable[typing.Iterable[str]]) -> None: + """Asserts that the provided locations can't be reached without the listed items but can be reached with any + one of the provided combinations""" all_items = [item_name for item_names in possible_items for item_name in item_names] self.collect_all_but(all_items) @@ -190,4 +203,36 @@ class WorldTestBase(unittest.TestCase): self.remove(items) def assertBeatable(self, beatable: bool): + """Asserts that the game can be beaten with the current state""" self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable) + + # following tests are automatically run + @property + def skip_default_tests(self) -> bool: + """Not possible or identical to the base test that's always being run already""" + constructed = hasattr(self, "game") and hasattr(self, "multiworld") + return not constructed or (not self.options + and self.setUp is WorldTestBase.setUp + and self.world_setup is WorldTestBase.world_setup) + + def testAllStateCanReachEverything(self): + """Ensure all state can reach everything with the defined options""" + if self.skip_default_tests: + return + with self.subTest("Game", game=self.game): + excluded = self.multiworld.exclude_locations[1].value + state = self.multiworld.get_all_state(False) + for location in self.multiworld.get_locations(): + if location.name not in excluded: + with self.subTest("Location should be reached", location=location): + self.assertTrue(location.can_reach(state), f"{location.name} unreachable") + + def testEmptyStateCanReachSomething(self): + """Ensure empty state can reach at least one location with the defined options""" + if self.skip_default_tests: + return + with self.subTest("Game", game=self.game): + state = CollectionState(self.multiworld) + locations = self.multiworld.get_reachable_locations(state, 1) + self.assertGreater(len(locations), 0, + "Need to be able to reach at least one location to get started.") From 7cad53c31a54c1fc9b684c3d5a36933ec1e2c558 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 18 Feb 2023 12:24:43 -0600 Subject: [PATCH 14/14] Docs: add docstrings to the World class --- worlds/AutoWorld.py | 73 +++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 7985d47001..f2a639eebf 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -131,54 +131,69 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} # link your Options mapping - game: ClassVar[str] # name the game - topology_present: ClassVar[bool] = False # indicate if world type has any meaningful layout/pathing + option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} + """link your Options mapping""" + game: ClassVar[str] + """name the game""" + topology_present: ClassVar[bool] = False + """indicate if world type has any meaningful layout/pathing""" - # gets automatically populated with all item and item group names all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset() + """gets automatically populated with all item and item group names""" - # map names to their IDs item_name_to_id: ClassVar[Dict[str, int]] = {} + """map item names to their IDs""" location_name_to_id: ClassVar[Dict[str, int]] = {} + """map location names to their IDs""" - # maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"} item_name_groups: ClassVar[Dict[str, Set[str]]] = {} + """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" - # increment this every time something in your world's names/id mappings changes. - # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be - # retrieved by clients on every connection. data_version: ClassVar[int] = 1 + """ + increment this every time something in your world's names/id mappings changes. + While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata + and retrieved by clients on every connection. + """ - # override this if changes to a world break forward-compatibility of the client - # The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the - # future. Protocol level compatibility check moved to MultiServer.min_client_version. required_client_version: Tuple[int, int, int] = (0, 1, 6) + """ + override this if changes to a world break forward-compatibility of the client + The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the + future. Protocol level compatibility check moved to MultiServer.min_client_version. + """ - # update this if the resulting multidata breaks forward-compatibility of the server required_server_version: Tuple[int, int, int] = (0, 2, 4) + """update this if the resulting multidata breaks forward-compatibility of the server""" - hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable + hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() + """any names that should not be hintable""" - # Hide World Type from various views. Does not remove functionality. hidden: ClassVar[bool] = False + """Hide World Type from various views. Does not remove functionality.""" - # see WebWorld for options web: ClassVar[WebWorld] = WebWorld() + """see WebWorld for options""" - # autoset on creation: multiworld: "MultiWorld" + """autoset on creation. The MultiWorld object for the currently generating multiworld.""" player: int + """autoset on creation. The player number for this World""" - # automatically generated item_id_to_name: ClassVar[Dict[int, str]] + """automatically generated reverse lookup of item id to name""" location_id_to_name: ClassVar[Dict[int, str]] + """automatically generated reverse lookup of location id to name""" - item_names: ClassVar[Set[str]] # set of all potential item names - location_names: ClassVar[Set[str]] # set of all potential location names + item_names: ClassVar[Set[str]] + """set of all potential item names""" + location_names: ClassVar[Set[str]] + """set of all potential location names""" - zip_path: ClassVar[Optional[pathlib.Path]] = None # If loaded from a .apworld, this is the Path to it. - __file__: ClassVar[str] # path it was loaded from + zip_path: ClassVar[Optional[pathlib.Path]] = None + """If loaded from a .apworld, this is the Path to it.""" + __file__: ClassVar[str] + """path it was loaded from""" def __init__(self, multiworld: "MultiWorld", player: int): self.multiworld = multiworld @@ -196,18 +211,32 @@ class World(metaclass=AutoWorldRegister): pass def generate_early(self) -> None: + """ + Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option + results and determining layouts for entrance rando etc. start inventory gets pushed after this step. + """ pass def create_regions(self) -> None: + """Method for creating and connecting regions for the World.""" pass def create_items(self) -> None: + """ + Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted + to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`. + """ pass def set_rules(self) -> None: + """Method for setting the rules on the World's regions and locations.""" pass def generate_basic(self) -> None: + """ + Useful for randomizing things that don't affect logic but are better to be determined before the output stage. + i.e. checking what the player has marked as priority or randomizing enemies + """ pass def pre_fill(self) -> None: