From e33a9991ef381b27ed09a075925254b3f7620527 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Fri, 19 Jul 2024 01:37:59 -0500 Subject: [PATCH 01/13] CommonClient: Escape markup sent in chat messages (#3659) * escape markup in uncolored text * Fix comment to allign with style guide Fixes the comment so it follows the style guide, along with making it better explain the code. * Make more concise --- kvui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvui.py b/kvui.py index 500203a881..1409e2dc0d 100644 --- a/kvui.py +++ b/kvui.py @@ -836,6 +836,10 @@ class KivyJSONtoTextParser(JSONtoTextParser): return self._handle_text(node) def _handle_text(self, node: JSONMessagePart): + # All other text goes through _handle_color, and we don't want to escape markup twice, + # or mess up text that already has intentional markup applied to it + if node.get("type", "text") == "text": + node["text"] = escape_markup(node["text"]) for ref in node.get("refs", []): node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" self.ref_count += 1 From 34e7748f23083d26b126a52ca447afe3d93ff4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:24:24 -0400 Subject: [PATCH 02/13] Stardew Valley: Make sure number of month in time logic is a int to improve performance by ~20% (#3665) * make sure number of month is actually a int * improve rule explain like in pr * remove redundant if in can_complete_bundle * assert number is int so cache is not bloated --- worlds/stardew_valley/logic/bundle_logic.py | 6 ++-- worlds/stardew_valley/logic/museum_logic.py | 2 +- worlds/stardew_valley/logic/time_logic.py | 2 ++ worlds/stardew_valley/stardew_rule/base.py | 2 +- .../stardew_rule/rule_explain.py | 33 +++++++++++++++++-- worlds/stardew_valley/stardew_rule/state.py | 2 +- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 4ca5fd81fc..98fda1c73c 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -27,8 +27,8 @@ class BundleLogicMixin(BaseLogicMixin): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, -QuestLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, +SkillLogicMixin, QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] @@ -45,7 +45,7 @@ QuestLogicMixin]]): qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + time_rule = self.logic.time.has_lived_months(time_to_grind) return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 4ba5364f55..36ba62b31f 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -41,7 +41,7 @@ class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMi else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - time_needed_to_grind = (20 - item.difficulty) / 2 + time_needed_to_grind = int((20 - item.difficulty) // 2) time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 94e0e277c8..2ba76579ff 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -26,8 +26,10 @@ class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): @cache_self1 def has_lived_months(self, number: int) -> StardewRule: + assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing." if number <= 0: return self.logic.true_ + number = min(number, MAX_MONTHS) return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 576cd36851..3e6eb327ea 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -431,7 +431,7 @@ class Count(BaseStardewRule): return len(self.rules) def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" + return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]" @dataclass(frozen=True) diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py index 61a88ceb69..a9767c7b72 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -34,7 +34,7 @@ class RuleExplanation: if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__str__(depth + 1) if i.result is not self.expected else i.summary(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @@ -42,7 +42,7 @@ class RuleExplanation: if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @cached_property @@ -61,6 +61,33 @@ class RuleExplanation: return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] +@dataclass +class CountSubRuleExplanation(RuleExplanation): + count: int = 1 + + @staticmethod + def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation: + return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count) + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + +@dataclass +class CountExplanation(RuleExplanation): + rule: Count + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + return [ + CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count) + for rule, count in self.rule.counter.items() + ] + + def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: if isinstance(rule, StardewRule): return _explain(rule, state, expected, explored_spots=set()) @@ -80,7 +107,7 @@ def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, expl @_explain.register def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) @_explain.register diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index cf0996a63b..5f5e61b3d4 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -122,4 +122,4 @@ class HasProgressionPercent(CombinableStardewRule): return self, self(state) def __repr__(self): - return f"Received {self.percent}% progression items." + return f"Received {self.percent}% progression items" From 7039b17bf6e00735b5698e149c5cbce0df729e2b Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 21 Jul 2024 18:12:11 -0500 Subject: [PATCH 03/13] CommonClient: fix bug when using Connect button without a disconnect (#3609) * makes the kivy connect button do the same username forgetting that /connect does to fix an issue where losing connection would make you unable to connect to a different server * extract duplicate code --- kvui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index 1409e2dc0d..a63d636960 100644 --- a/kvui.py +++ b/kvui.py @@ -595,8 +595,8 @@ class GameManager(App): "!help for server commands.") def connect_button_action(self, button): + self.ctx.username = None if self.ctx.server: - self.ctx.username = None async_start(self.ctx.disconnect()) else: async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", ""))) From d7d45654290f0c04e9f601c097d8c1d160a18908 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:27:10 +0200 Subject: [PATCH 04/13] YGO06: fixes non-deterministic bug by changing sets to lists (#3674) --- worlds/yugioh06/boosterpacks.py | 186 +++++++++++++++--------------- worlds/yugioh06/structure_deck.py | 50 ++++---- 2 files changed, 120 insertions(+), 116 deletions(-) diff --git a/worlds/yugioh06/boosterpacks.py b/worlds/yugioh06/boosterpacks.py index f6f4ec7732..645977d28d 100644 --- a/worlds/yugioh06/boosterpacks.py +++ b/worlds/yugioh06/boosterpacks.py @@ -1,13 +1,13 @@ -from typing import Dict, Set +from typing import Dict, List -booster_contents: Dict[str, Set[str]] = { - "LEGEND OF B.E.W.D.": { +booster_contents: Dict[str, List[str]] = { + "LEGEND OF B.E.W.D.": [ "Exodia", "Dark Magician", "Polymerization", "Skull Servant" - }, - "METAL RAIDERS": { + ], + "METAL RAIDERS": [ "Petit Moth", "Cocoon of Evolution", "Time Wizard", @@ -30,8 +30,8 @@ booster_contents: Dict[str, Set[str]] = { "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ booster_contents: Dict[str, Set[str]] = { "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ booster_contents: Dict[str, Set[str]] = { "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ booster_contents: Dict[str, Set[str]] = { "Senju of the Thousand Hands", "Sonic Bird", "Mystical Space Typhoon" - }, - "LABYRINTH OF NIGHTMARE": { + ], + "LABYRINTH OF NIGHTMARE": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -119,8 +119,8 @@ booster_contents: Dict[str, Set[str]] = { "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ booster_contents: Dict[str, Set[str]] = { "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ booster_contents: Dict[str, Set[str]] = { "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ booster_contents: Dict[str, Set[str]] = { "Spell Reproduction", "Contract with the Abyss", "Dark Master - Zorc" - }, - "INVASION OF CHAOS": { + ], + "INVASION OF CHAOS": [ "Ojama Delta Hurricane", "Ojama Yellow", "Ojama Black", @@ -241,8 +241,8 @@ booster_contents: Dict[str, Set[str]] = { "Cursed Seal of the Forbidden Spell", "Stray Lambs", "Manju of the Ten Thousand Hands" - }, - "ANCIENT SANCTUARY": { + ], + "ANCIENT SANCTUARY": [ "Monster Gate", "Wall of Revealing Light", "Mystik Wok", @@ -255,8 +255,8 @@ booster_contents: Dict[str, Set[str]] = { "King of the Swamp", "Enemy Controller", "Enchanting Fitting Room" - }, - "SOUL OF THE DUELIST": { + ], + "SOUL OF THE DUELIST": [ "Ninja Grandmaster Sasuke", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -272,8 +272,8 @@ booster_contents: Dict[str, Set[str]] = { "Level Up!", "Howling Insect", "Mobius the Frost Monarch" - }, - "RISE OF DESTINY": { + ], + "RISE OF DESTINY": [ "Homunculus the Alchemic Being", "Thestalos the Firestorm Monarch", "Roc from the Valley of Haze", @@ -283,8 +283,8 @@ booster_contents: Dict[str, Set[str]] = { "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ booster_contents: Dict[str, Set[str]] = { "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ booster_contents: Dict[str, Set[str]] = { "Elemental Hero Thunder Giant", "Aussa the Earth Charmer", "Brain Control" - }, - "CYBERNETIC REVOLUTION": { + ], + "CYBERNETIC REVOLUTION": [ "Power Bond", "Cyber Dragon", "Cyber Twin Dragon", @@ -322,8 +322,8 @@ booster_contents: Dict[str, Set[str]] = { "Miracle Fusion", "Elemental Hero Bubbleman", "Jerry Beans Man" - }, - "ELEMENTAL ENERGY": { + ], + "ELEMENTAL ENERGY": [ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult", @@ -344,8 +344,8 @@ booster_contents: Dict[str, Set[str]] = { "Elemental Hero Bladedge", "Pot of Avarice", "B.E.S. Tetran" - }, - "SHADOW OF INFINITY": { + ], + "SHADOW OF INFINITY": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -357,8 +357,8 @@ booster_contents: Dict[str, Set[str]] = { "Gokipon", "Demise, King of Armageddon", "Anteatereatingant" - }, - "GAME GIFT COLLECTION": { + ], + "GAME GIFT COLLECTION": [ "Ritual", "Valkyrion the Magna Warrior", "Alpha the Magnet Warrior", @@ -383,8 +383,8 @@ booster_contents: Dict[str, Set[str]] = { "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ booster_contents: Dict[str, Set[str]] = { "Curse of Vampire", "Elemental Hero Flame Wingman", "Magician of Black Chaos" - }, - "Fairy Collection": { + ], + "Fairy Collection": [ "Silpheed", "Dunames Dark Witch", "Hysteric Fairy", @@ -416,8 +416,8 @@ booster_contents: Dict[str, Set[str]] = { "Asura Priest", "Manju of the Ten Thousand Hands", "Senju of the Thousand Hands" - }, - "Dragon Collection": { + ], + "Dragon Collection": [ "Victory D.", "Chaos Emperor Dragon - Envoy of the End", "Kaiser Glider", @@ -434,16 +434,16 @@ booster_contents: Dict[str, Set[str]] = { "Troop Dragon", "Horus the Black Flame Dragon LV4", "Pitch-Dark Dragon" - }, - "Warrior Collection A": { + ], + "Warrior Collection A": [ "Gate Guardian", "Gearfried the Iron Knight", "Dimensional Warrior", "Command Knight", "The Last Warrior from Another Planet", "Dream Clown" - }, - "Warrior Collection B": { + ], + "Warrior Collection B": [ "Don Zaloog", "Dark Scorpion - Chick the Yellow", "Dark Scorpion - Meanae the Thorn", @@ -467,8 +467,8 @@ booster_contents: Dict[str, Set[str]] = { "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ booster_contents: Dict[str, Set[str]] = { "Spear Cretin", "Versago the Destroyer", "Toon Summoned Skull" - }, - "Fiend Collection B": { + ], + "Fiend Collection B": [ "Raviel, Lord of Phantasms", "Yata-Garasu", "Helpoemer", @@ -505,15 +505,15 @@ booster_contents: Dict[str, Set[str]] = { "Jowls of Dark Demise", "D. D. Trainer", "Earthbound Spirit" - }, - "Machine Collection A": { + ], + "Machine Collection A": [ "Cyber-Stein", "Mechanicalchaser", "Jinzo", "UFO Turtle", "Cyber-Tech Alligator" - }, - "Machine Collection B": { + ], + "Machine Collection B": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -531,8 +531,8 @@ booster_contents: Dict[str, Set[str]] = { "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ booster_contents: Dict[str, Set[str]] = { "Injection Fairy Lily", "Cosmo Queen", "Magician of Black Chaos" - }, - "Spellcaster Collection B": { + ], + "Spellcaster Collection B": [ "Jowgen the Spiritualist", "Tsukuyomi", "Manticore of Darkness", @@ -574,8 +574,8 @@ booster_contents: Dict[str, Set[str]] = { "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ booster_contents: Dict[str, Set[str]] = { "Des Lacooda", "Wandering Mummy", "Royal Keeper" - }, - "Special Monsters A": { + ], + "Special Monsters A": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -626,8 +626,8 @@ booster_contents: Dict[str, Set[str]] = { "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ booster_contents: Dict[str, Set[str]] = { "Level Up!", "Ultimate Insect Lv3", "Ultimate Insect Lv5" - }, - "Reverse Collection": { + ], + "Reverse Collection": [ "Magical Merchant", "Castle of Dark Illusions", "Magician of Faith", @@ -675,8 +675,8 @@ booster_contents: Dict[str, Set[str]] = { "Spear Cretin", "Nobleman of Crossout", "Aussa the Earth Charmer" - }, - "LP Recovery Collection": { + ], + "LP Recovery Collection": [ "Mystik Wok", "Poison of the Old Man", "Hysteric Fairy", @@ -691,8 +691,8 @@ booster_contents: Dict[str, Set[str]] = { "Elemental Hero Steam Healer", "Fushi No Tori", "Emergency Provisions" - }, - "Special Summon Collection A": { + ], + "Special Summon Collection A": [ "Perfectly Ultimate Great Moth", "Dark Sage", "Polymerization", @@ -726,8 +726,8 @@ booster_contents: Dict[str, Set[str]] = { "Morphing Jar #2", "Spear Cretin", "Dark Magic Curtain" - }, - "Special Summon Collection B": { + ], + "Special Summon Collection B": [ "Monster Gate", "Chaos Emperor Dragon - Envoy of the End", "Ojama Trio", @@ -756,8 +756,8 @@ booster_contents: Dict[str, Set[str]] = { "Tribute Doll", "Enchanting Fitting Room", "Stray Lambs" - }, - "Special Summon Collection C": { + ], + "Special Summon Collection C": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -782,13 +782,13 @@ booster_contents: Dict[str, Set[str]] = { "Ultimate Insect Lv5", "Rescue Cat", "Anteatereatingant" - }, - "Equipment Collection": { + ], + "Equipment Collection": [ "Megamorph", "Cestus of Dagla", "United We Stand" - }, - "Continuous Spell/Trap A": { + ], + "Continuous Spell/Trap A": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -801,8 +801,8 @@ booster_contents: Dict[str, Set[str]] = { "Solemn Wishes", "Embodiment of Apophis", "Toon World" - }, - "Continuous Spell/Trap B": { + ], + "Continuous Spell/Trap B": [ "Hamon, Lord of Striking Thunder", "Uria, Lord of Searing Flames", "Wave-Motion Cannon", @@ -815,8 +815,8 @@ booster_contents: Dict[str, Set[str]] = { "Skull Zoma", "Pitch-Black Power Stone", "Metal Reflect Slime" - }, - "Quick/Counter Collection": { + ], + "Quick/Counter Collection": [ "Mystik Wok", "Poison of the Old Man", "Scapegoat", @@ -841,8 +841,8 @@ booster_contents: Dict[str, Set[str]] = { "Book of Moon", "Serial Spell", "Mystical Space Typhoon" - }, - "Direct Damage Collection": { + ], + "Direct Damage Collection": [ "Hamon, Lord of Striking Thunder", "Chaos Emperor Dragon - Envoy of the End", "Dark Snake Syndrome", @@ -868,8 +868,8 @@ booster_contents: Dict[str, Set[str]] = { "Jowls of Dark Demise", "Stealth Bird", "Elemental Hero Bladedge", - }, - "Direct Attack Collection": { + ], + "Direct Attack Collection": [ "Victory D.", "Dark Scorpion Combination", "Spirit Reaper", @@ -880,8 +880,8 @@ booster_contents: Dict[str, Set[str]] = { "Toon Mermaid", "Toon Summoned Skull", "Toon Dark Magician Girl" - }, - "Monster Destroy Collection": { + ], + "Monster Destroy Collection": [ "Hamon, Lord of Striking Thunder", "Inferno", "Ninja Grandmaster Sasuke", @@ -912,12 +912,12 @@ booster_contents: Dict[str, Set[str]] = { "Offerings to the Doomed", "Divine Wrath", "Dream Clown" - }, + ], } def get_booster_locations(booster: str) -> Dict[str, str]: return { f"{booster} {i}": content - for i, content in enumerate(booster_contents[booster]) + for i, content in enumerate(booster_contents[booster], 1) } diff --git a/worlds/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py index d58223f2e2..3559e7c515 100644 --- a/worlds/yugioh06/structure_deck.py +++ b/worlds/yugioh06/structure_deck.py @@ -1,7 +1,7 @@ -from typing import Dict, Set +from typing import Dict, List -structure_contents: Dict[str, Set] = { - "dragons_roar": { +structure_contents: Dict[str, List[str]] = { + "dragons_roar": [ "Luster Dragon", "Armed Dragon LV3", "Armed Dragon LV5", @@ -14,9 +14,9 @@ structure_contents: Dict[str, Set] = { "Stamping Destruction", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "zombie_madness": { + "Mystical Space Typhoon" + ], + "zombie_madness": [ "Pyramid Turtle", "Regenerating Mummy", "Ryu Kokki", @@ -26,9 +26,9 @@ structure_contents: Dict[str, Set] = { "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "blazing_destruction": { + "Mystical Space Typhoon" + ], + "blazing_destruction": [ "Inferno", "Solar Flare Dragon", "UFO Turtle", @@ -38,9 +38,9 @@ structure_contents: Dict[str, Set] = { "Level Limit - Area B", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "fury_from_the_deep": { + "Mystical Space Typhoon" + ], + "fury_from_the_deep": [ "Mother Grizzly", "Water Beaters", "Gravity Bind", @@ -48,9 +48,9 @@ structure_contents: Dict[str, Set] = { "Mobius the Frost Monarch", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "warriors_triumph": { + "Mystical Space Typhoon" + ], + "warriors_triumph": [ "Gearfried the Iron Knight", "D.D. Warrior Lady", "Marauding Captain", @@ -60,9 +60,9 @@ structure_contents: Dict[str, Set] = { "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "spellcasters_judgement": { + "Mystical Space Typhoon" + ], + "spellcasters_judgement": [ "Dark Magician", "Apprentice Magician", "Breaker the Magical Warrior", @@ -70,14 +70,18 @@ structure_contents: Dict[str, Set] = { "Skilled Dark Magician", "Tsukuyomi", "Magical Dimension", - "Mage PowerSpell-Counter Cards", + "Mage Power", + "Spell-Counter Cards", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "none": {}, + "Mystical Space Typhoon" + ], + "none": [], } def get_deck_content_locations(deck: str) -> Dict[str, str]: - return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])} + return { + f"{deck} {i}": content + for i, content in enumerate(structure_contents[deck], 1) + } From 12f1ef873c2442ae923fd0585c2252c6033c1075 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:47:46 -0500 Subject: [PATCH 05/13] A Short Hike: Fix Boat Rental purchase being incorrectly calculated (#3639) --- worlds/shorthike/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py index 319ad8f20e..657035a030 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ location_table: List[LocationInfo] = [ {"name": "Boat Rental", "id": base_id + 55, "inGameId": "DadDeer[0]", - "needsShovel": False, "purchase": True, + "needsShovel": False, "purchase": 100, "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, {"name": "Boat Challenge Reward", "id": base_id + 56, From 48a0fb05a2e4d5da7727f576e0b54725950b8ee3 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 02:52:44 +0300 Subject: [PATCH 06/13] Stardew Valley: Removed Stardrop Tea from Full Shipment (#3655) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index bb2ed2e2ce..0c5a12fb57 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2221,7 +2221,7 @@ id,region,name,tags,mod_name 3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", -3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY", 3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", 3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", 3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", From e59bec36ec792937d2916f0256c3655cc27a39fa Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 09:32:40 +0300 Subject: [PATCH 07/13] Stardew Valley: Add gourmand frog rules for completing his tasks sequentially (#3652) --- worlds/stardew_valley/rules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index c30d04c8a6..62a5cc5218 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -482,8 +482,10 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) From f7989780fa023d7c8be2ad7ed956db9a52483af9 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:17:34 -0600 Subject: [PATCH 08/13] Bomb Rush Cyberfunk: Fix final graffiti location being unobtainable (#3669) --- worlds/bomb_rush_cyberfunk/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/bomb_rush_cyberfunk/Locations.py b/worlds/bomb_rush_cyberfunk/Locations.py index 863e2ad020..7ea9590190 100644 --- a/worlds/bomb_rush_cyberfunk/Locations.py +++ b/worlds/bomb_rush_cyberfunk/Locations.py @@ -762,7 +762,7 @@ location_table: List[LocationDict] = [ 'game_id': "graf385"}, {'name': "Tagged 389 Graffiti Spots", 'stage': Stages.Misc, - 'game_id': "graf379"}, + 'game_id': "graf389"}, ] From c12d3dd6ade3b0d2ad65095e2b7058741376ad91 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 23 Jul 2024 01:36:42 +0300 Subject: [PATCH 09/13] Stardew valley: Fix Queen of Sauce Cookbook conditions (#3651) * - Extracted walnut logic to a Mixin so it can be used in content pack requirements * - Add 100 walnut requirements to the Queen of Sauce Cookbook * - Woops a file wasn't added to previous commits * - Make the queen of sauce cookbook a ginger island only thing, due to the walnut requirement * - Moved the book in the correct content pack * - Removed an empty class that I'm not sure where it came from --- worlds/stardew_valley/__init__.py | 2 +- .../content/vanilla/ginger_island.py | 6 +- .../content/vanilla/pelican_town.py | 3 - worlds/stardew_valley/data/locations.csv | 4 +- worlds/stardew_valley/data/requirement.py | 5 + worlds/stardew_valley/logic/logic.py | 116 +-------------- .../stardew_valley/logic/requirement_logic.py | 9 +- worlds/stardew_valley/logic/walnut_logic.py | 135 ++++++++++++++++++ worlds/stardew_valley/rules.py | 24 ++-- 9 files changed, 171 insertions(+), 133 deletions(-) create mode 100644 worlds/stardew_valley/logic/walnut_logic.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 07235ad298..1aba9af7ab 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -255,7 +255,7 @@ class StardewValleyWorld(World): Event.victory) elif self.options.goal == Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[GoalName.greatest_walnut_hunter], - self.logic.has_walnut(130), + self.logic.walnut.has_walnut(130), Event.victory) elif self.options.goal == Goal.option_protector_of_the_valley: self.create_event_location(location_table[GoalName.protector_of_the_valley], diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py index d824deff39..2fbcb03279 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -3,6 +3,7 @@ from ..game_content import ContentPack, StardewContent from ...data import villagers_data, fish_data from ...data.game_item import ItemTag, Tag from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.requirement import WalnutRequirement from ...data.shop import ShopSource from ...strings.book_names import Book from ...strings.crop_names import Fruit, Vegetable @@ -10,7 +11,7 @@ from ...strings.fish_names import Fish from ...strings.forageable_names import Forageable, Mushroom from ...strings.fruit_tree_names import Sapling from ...strings.metal_names import Fossil, Mineral -from ...strings.region_names import Region +from ...strings.region_names import Region, LogicRegion from ...strings.season_names import Season from ...strings.seed_names import Seed @@ -62,6 +63,9 @@ ginger_island_content_pack = GingerIslandContentPack( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), ), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever }, fishes=( diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 2c687eacbd..917e8cca22 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -290,9 +290,6 @@ pelican_town = ContentPack( Book.woodcutters_weekly: ( Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), - Book.queen_of_sauce_cookbook: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2),), # Worst book ever }, fishes=( fish_data.albacore, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 0c5a12fb57..6e30d2b8c8 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2252,7 +2252,7 @@ id,region,name,tags,mod_name 3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", 3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", 3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", -3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND", 3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", 3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", 3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", @@ -2292,7 +2292,7 @@ id,region,name,tags,mod_name 4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", 4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", 4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", -4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND", 4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", 4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", 4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 7e9466630f..4744f9dffd 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -29,3 +29,8 @@ class SeasonRequirement(Requirement): @dataclass(frozen=True) class YearRequirement(Requirement): year: int + + +@dataclass(frozen=True) +class WalnutRequirement(Requirement): + amount: int diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 74cdaf2374..fb0d938fbb 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from functools import cached_property from typing import Collection, Callable from .ability_logic import AbilityLogicMixin @@ -43,6 +42,7 @@ from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin +from .walnut_logic import WalnutLogicMixin from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes from ..data.museum_data import all_museum_items @@ -50,16 +50,14 @@ from ..data.recipe_data import all_cooking_recipes from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_option_names import OptionName from ..strings.ap_names.community_upgrade_names import CommunityUpgrade -from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building -from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds +from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration @@ -96,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -461,32 +459,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - @cached_property - def can_start_field_office(self) -> StardewRule: - field_office = self.region.can_reach(Region.field_office) - professor_snail = self.received("Open Professor Snail Cave") - return field_office & professor_snail - - def can_complete_large_animal_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) - return self.can_start_field_office & fossils - - def can_complete_snake_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) - return self.can_start_field_office & fossils - - def can_complete_frog_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_frog) - return self.can_start_field_office & fossils - - def can_complete_bat_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_bat) - return self.can_start_field_office & fossils - - def can_complete_field_office(self) -> StardewRule: - return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ - self.can_complete_frog_collection() & self.can_complete_bat_collection() - def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa rules_worth_a_point = [ @@ -566,86 +538,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin return False_() return self.region.can_reach(Region.island_trader) - def has_walnut(self, number: int) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if number <= 0: - return True_() - - if self.options.walnutsanity == Walnutsanity.preset_none: - return self.can_get_walnuts(number) - if self.options.walnutsanity == Walnutsanity.preset_all: - return self.has_received_walnuts(number) - puzzle_walnuts = 61 - bush_walnuts = 25 - dig_walnuts = 18 - repeatable_walnuts = 33 - total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts - walnuts_to_receive = 0 - walnuts_to_collect = number - if OptionName.walnutsanity_puzzles in self.options.walnutsanity: - puzzle_walnut_rate = puzzle_walnuts / total_walnuts - puzzle_walnuts_required = round(puzzle_walnut_rate * number) - walnuts_to_receive += puzzle_walnuts_required - walnuts_to_collect -= puzzle_walnuts_required - if OptionName.walnutsanity_bushes in self.options.walnutsanity: - bush_walnuts_rate = bush_walnuts / total_walnuts - bush_walnuts_required = round(bush_walnuts_rate * number) - walnuts_to_receive += bush_walnuts_required - walnuts_to_collect -= bush_walnuts_required - if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: - dig_walnuts_rate = dig_walnuts / total_walnuts - dig_walnuts_required = round(dig_walnuts_rate * number) - walnuts_to_receive += dig_walnuts_required - walnuts_to_collect -= dig_walnuts_required - if OptionName.walnutsanity_repeatables in self.options.walnutsanity: - repeatable_walnuts_rate = repeatable_walnuts / total_walnuts - repeatable_walnuts_required = round(repeatable_walnuts_rate * number) - walnuts_to_receive += repeatable_walnuts_required - walnuts_to_collect -= repeatable_walnuts_required - return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) - - def has_received_walnuts(self, number: int) -> StardewRule: - return self.received(Event.received_walnuts, number) - - def can_get_walnuts(self, number: int) -> StardewRule: - # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations - reach_south = self.region.can_reach(Region.island_south) - reach_north = self.region.can_reach(Region.island_north) - reach_west = self.region.can_reach(Region.island_west) - reach_hut = self.region.can_reach(Region.leo_hut) - reach_southeast = self.region.can_reach(Region.island_south_east) - reach_field_office = self.region.can_reach(Region.field_office) - reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) - reach_volcano_regions = [self.region.can_reach(Region.volcano), - self.region.can_reach(Region.volcano_secret_beach), - self.region.can_reach(Region.volcano_floor_5), - self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = self.logic.or_(*reach_volcano_regions) - reach_all_volcano = self.logic.and_(*reach_volcano_regions) - reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = self.logic.and_(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.combat.has_slingshot) - reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) - if number <= 5: - return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) - if number <= 10: - return self.count(2, *reach_walnut_regions) - if number <= 15: - return self.count(3, *reach_walnut_regions) - if number <= 20: - return self.logic.and_(*reach_walnut_regions) - if number <= 50: - return reach_entire_island - gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) - return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \ - self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ - self.can_complete_field_office() - def has_all_stardrops(self) -> StardewRule: other_rules = [] number_of_stardrops_to_receive = 0 diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 87d9ee0215..9356440ac6 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -9,8 +9,9 @@ from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin +from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -20,7 +21,7 @@ class RequirementLogicMixin(BaseLogicMixin): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -50,3 +51,7 @@ SeasonLogicMixin, TimeLogicMixin]]): @meet_requirement.register def _(self, requirement: YearRequirement): return self.logic.time.has_year(requirement.year) + + @meet_requirement.register + def _(self, requirement: WalnutRequirement): + return self.logic.walnut.has_walnut(requirement.amount) diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py new file mode 100644 index 0000000000..14fe1c3390 --- /dev/null +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -0,0 +1,135 @@ +from functools import cached_property +from typing import Union + +from .ability_logic import AbilityLogicMixin +from .base_logic import BaseLogic, BaseLogicMixin +from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from ..strings.ap_names.event_names import Event +from ..options import ExcludeGingerIsland, Walnutsanity +from ..stardew_rule import StardewRule, False_, True_ +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.craftable_names import Furniture +from ..strings.crop_names import Fruit +from ..strings.metal_names import Mineral, Fossil +from ..strings.region_names import Region +from ..strings.seed_names import Seed + + +class WalnutLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.walnut = WalnutLogic(*args, **kwargs) + + +class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, + AbilityLogicMixin]]): + + def has_walnut(self, number: int) -> StardewRule: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: + return False_() + if number <= 0: + return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if OptionName.walnutsanity_bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.logic.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: + # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations + reach_south = self.logic.region.can_reach(Region.island_south) + reach_north = self.logic.region.can_reach(Region.island_north) + reach_west = self.logic.region.can_reach(Region.island_west) + reach_hut = self.logic.region.can_reach(Region.leo_hut) + reach_southeast = self.logic.region.can_reach(Region.island_south_east) + reach_field_office = self.logic.region.can_reach(Region.field_office) + reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) + reach_volcano_regions = [self.logic.region.can_reach(Region.volcano), + self.logic.region.can_reach(Region.volcano_secret_beach), + self.logic.region.can_reach(Region.volcano_floor_5), + self.logic.region.can_reach(Region.volcano_floor_10)] + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) + reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] + reach_caves = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site), + self.logic.region.can_reach(Region.gourmand_frog_cave), + self.logic.region.can_reach(Region.colored_crystals_cave), + self.logic.region.can_reach(Region.shipwreck), self.logic.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + if number <= 5: + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) + if number <= 10: + return self.logic.count(2, *reach_walnut_regions) + if number <= 15: + return self.logic.count(3, *reach_walnut_regions) + if number <= 20: + return self.logic.and_(*reach_walnut_regions) + if number <= 50: + return reach_entire_island + gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) + return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() + + @cached_property + def can_start_field_office(self) -> StardewRule: + field_office = self.logic.region.can_reach(Region.field_office) + professor_snail = self.logic.received("Open Professor Snail Cave") + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 62a5cc5218..7c1fdbda3c 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -375,7 +375,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), - logic.can_complete_field_office()) + logic.walnut.can_complete_field_office()) set_walnut_rules(logic, multiworld, player, world_options) @@ -432,10 +432,10 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" - has_walnut = logic.has_walnut(5) - has_5_walnut = logic.has_walnut(15) - has_10_walnut = logic.has_walnut(40) - has_20_walnut = logic.has_walnut(60) + has_walnut = logic.walnut.has_walnut(5) + has_5_walnut = logic.walnut.has_walnut(15) + has_10_walnut = logic.walnut.has_walnut(40) + has_20_walnut = logic.walnut.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -471,7 +471,7 @@ def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: Sta set_walnut_repeatable_rules(logic, multiworld, player, world_options) -def set_walnut_puzzle_rules(logic, multiworld, player, world_options): +def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: return @@ -487,12 +487,12 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) - MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.can_complete_frog_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.can_complete_bat_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.can_start_field_office) - MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) From b840c3fe1a609466c3b103a1972d5ec9d862df54 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 22 Jul 2024 18:43:41 -0400 Subject: [PATCH 10/13] TUNIC: Move 3 locations to Quarry Back (#3649) * Move 3 locations to Quarry Back * Change the non-er region too --- worlds/tunic/locations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 2d87140fe5..0991622816 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -208,15 +208,15 @@ location_table: Dict[str, TunicLocationData] = { "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), - "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), From 9c2933f8033c8e8b9d9acdd341b043d7eca89d76 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 22 Jul 2024 18:45:49 -0400 Subject: [PATCH 11/13] Lingo: Fix Early Color Hallways painting in pilgrimages (#3645) --- worlds/lingo/data/LL1.yaml | 1 - worlds/lingo/data/generated.dat | Bin 136017 -> 136017 bytes worlds/lingo/regions.py | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4d6771a735..970063d585 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -3265,7 +3265,6 @@ door: Traveled Entrance Color Hallways: door: Color Hallways Entrance - warp: True panels: Achievement: id: Countdown Panels/Panel_traveled_traveled diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 6c8c925138aa5ac61edb22d194cfb8e9b9e4a492..065308628b9fc15f0d6001d6d8b30382ba8278d1 100644 GIT binary patch delta 131 zcmcb(jN{@mjtS}tCO}|joR*qqY;0j+WS(edkZ5X{Vqk7*kerreVUe`aJ None: RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) if early_color_hallways: - connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways", + connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", None, EntranceType.PAINTING, False, world) if painting_shuffle: From 51883757367e7ee859442ea2f55d59cc565f1704 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 23 Jul 2024 02:34:47 -0400 Subject: [PATCH 12/13] Lingo: Add pilgrimage logic through Starting Room (#3654) * Lingo: Add pilgrimage logic through Starting Room * Added unit test * Reverse order of two doors in unit test * Remove print statements from TestPilgrimage * Update generated.dat --- worlds/lingo/data/LL1.yaml | 9 ++++++++ worlds/lingo/data/generated.dat | Bin 136017 -> 136277 bytes worlds/lingo/test/TestPilgrimage.py | 32 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 970063d585..e12ca02297 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -140,6 +140,15 @@ painting: True The Colorful: painting: True + Welcome Back Area: + room: Welcome Back Area + door: Shortcut to Starting Room + Second Room: + door: Main Door + Hidden Room: + door: Back Right Door + Rhyme Room (Looped Square): + door: Rhyme Room Entrance panels: HI: id: Entry Room/Panel_hi_hi diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 065308628b9fc15f0d6001d6d8b30382ba8278d1..3ed6cb24f7d289c38189932fe5ccf705bdaf0262 100644 GIT binary patch delta 30433 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 Date: Tue, 23 Jul 2024 03:04:24 -0400 Subject: [PATCH 13/13] TUNIC: Add setting to disable local spoiler to host yaml (#3661) * Add TunicSettings class for host yaml options * Update __init__.py * Update worlds/tunic/__init__.py Co-authored-by: Scipio Wright * Use self.settings * Remove unused import --------- Co-authored-by: Scipio Wright --- worlds/tunic/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index f63193e6ae..9b28d1d451 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Tuple, TypedDict +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names @@ -12,6 +12,14 @@ from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_opt from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP +from settings import Group, Bool + + +class TunicSettings(Group): + class DisableLocalSpoiler(Bool): + """Disallows the TUNIC client from creating a local spoiler log.""" + + disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False class TunicWeb(WebWorld): @@ -57,6 +65,7 @@ class TunicWorld(World): options: TunicOptions options_dataclass = TunicOptions + settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups @@ -373,7 +382,8 @@ class TunicWorld(World): "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], "Hexagon Quest Goal": self.options.hexagon_goal.value, - "Entrance Rando": self.tunic_portal_pairs + "Entrance Rando": self.tunic_portal_pairs, + "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):