From 94492c45cb045060def543eb8097f214e1fe609b Mon Sep 17 00:00:00 2001 From: Will Morrow Date: Wed, 21 Jan 2026 09:12:53 -0500 Subject: [PATCH] Super Mario 64: Add painting passability as items (#5294) --- worlds/sm64ex/Items.py | 17 +++++++++- worlds/sm64ex/Options.py | 15 +++++++++ worlds/sm64ex/Regions.py | 1 + worlds/sm64ex/Rules.py | 65 +++++++++++++++++++++++++++------------ worlds/sm64ex/__init__.py | 8 ++++- 5 files changed, 85 insertions(+), 21 deletions(-) diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py index 28fcd74484..6cd9b5eb46 100644 --- a/worlds/sm64ex/Items.py +++ b/worlds/sm64ex/Items.py @@ -49,10 +49,25 @@ cannon_item_data_table: dict[str, SM64ItemData] = { "Cannon Unlock RR": SM64ItemData(sm64ex_base_id + 214), } +painting_unlock_item_data_table: dict[str, SM64ItemData] = { + "Painting Unlock WF": SM64ItemData(sm64ex_base_id + 231), + "Painting Unlock JRB": SM64ItemData(sm64ex_base_id + 232), + "Painting Unlock CCM": SM64ItemData(sm64ex_base_id + 233), + "Painting Unlock LLL": SM64ItemData(sm64ex_base_id + 236), + "Painting Unlock SSL": SM64ItemData(sm64ex_base_id + 237), + "Painting Unlock DDD": SM64ItemData(sm64ex_base_id + 238), + "Painting Unlock SL": SM64ItemData(sm64ex_base_id + 239), + "Painting Unlock WDW": SM64ItemData(sm64ex_base_id + 240), + "Painting Unlock TTM": SM64ItemData(sm64ex_base_id + 241), + "Painting Unlock THI": SM64ItemData(sm64ex_base_id + 242), + "Painting Unlock TTC": SM64ItemData(sm64ex_base_id + 243), +} + item_data_table = { **generic_item_data_table, **action_item_data_table, - **cannon_item_data_table + **cannon_item_data_table, + **painting_unlock_item_data_table } item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None} diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 47cda7507d..8bbd647f06 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -19,6 +19,19 @@ class EnableCoinStars(Choice): option_on = 1 option_vanilla = 2 +class EnableLockedPaintings(Toggle): + """ + Determine how paintings are treated. + + Off - Paintings are not locked, as long as you can access them you can enter them (Vanilla behavior). + + On - Paintings (other than BoB) start off locked and 11 stars are replaced in the pool with items to allow access to them. + Attempting to enter a locked painting will simply kick Mario out. + Does not affect secrets and levels that don't have a painting (BBH, HMC, RR). + This only affects the ability for Mario to enter a painting, the destination of the painting may change due to Entrance Randomization, if it is enabled. + """ + display_name = "Enable Locked Paintings" + class StrictCapRequirements(DefaultOnToggle): """If disabled, Stars that expect special caps may have to be acquired without the caps""" @@ -145,6 +158,7 @@ sm64_options_groups = [ ExclamationBoxes, ProgressiveKeys, EnableCoinStars, + EnableLockedPaintings, StrictCapRequirements, StrictCannonRequirements, ]), @@ -171,6 +185,7 @@ class SM64Options(PerGameCommonOptions): exclamation_boxes: ExclamationBoxes progressive_keys: ProgressiveKeys enable_coin_stars: EnableCoinStars + enable_locked_paintings: EnableLockedPaintings enable_move_rando: EnableMoveRandomizer move_rando_actions: MoveRandomizerActions strict_cap_requirements: StrictCapRequirements diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 52126bcf9f..44a0636f1b 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -100,6 +100,7 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int): if options.enable_coin_stars: create_locs(regWhomp, "WF: 100 Coins") + regJRBDoor = create_region("Jolly Roger Bay Door", player, world) regJRB = create_region("Jolly Roger Bay", player, world) create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave", "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy") diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index f5305dab6c..a6cc8dad6d 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -71,12 +71,17 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, rf = RuleFactory(world, options, player, move_rando_bitvec) connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) - connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], + rf.build_rule("", painting_lvl_name="WF", star_num_req=1)) + # JRB door is separated from JRB itself because the secret aquarium can be accessed without entering the painting + connect_regions(world, player, "Menu", "Jolly Roger Bay Door", rf.build_rule("", star_num_req=3)) + connect_regions(world, player, "Jolly Roger Bay Door", randomized_entrances_s["Jolly Roger Bay"], + rf.build_rule("", painting_lvl_name="JRB")) + connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], + rf.build_rule("", painting_lvl_name="CCM", star_num_req=3)) connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12)) connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, randomized_entrances_s["Jolly Roger Bay"], randomized_entrances_s["The Secret Aquarium"], + connect_regions(world, player, "Jolly Roger Bay Door", randomized_entrances_s["The Secret Aquarium"], rf.build_rule("SF/BF | TJ & LG | MOVELESS & TJ")) connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10)) connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], @@ -85,10 +90,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"], + rf.build_rule("", painting_lvl_name="LLL")) + connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"], + rf.build_rule("", painting_lvl_name="SSL")) connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], - lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"])) + rf.build_rule("", painting_lvl_name="DDD", star_num_req=star_costs["BasementDoorCost"])) connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], rf.build_rule("GP")) @@ -101,17 +108,23 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) - connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"]) - connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"]) - connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"]) - connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"]) - connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"]) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"], + rf.build_rule("", painting_lvl_name="SL")) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"], + rf.build_rule("", painting_lvl_name="WDW")) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"], + rf.build_rule("", painting_lvl_name="TTM")) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"], + rf.build_rule("", painting_lvl_name="THI")) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"], + rf.build_rule("", painting_lvl_name="THI")) connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island") connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island") connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"])) - connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"], rf.build_rule("LG/TJ/SF/BF/WK")) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"], + rf.build_rule("LG/TJ/SF/BF/WK", painting_lvl_name="TTC")) connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"], rf.build_rule("TJ/SF/BF")) connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"], rf.build_rule("TJ/SF/BF")) connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"])) @@ -272,6 +285,7 @@ class RuleFactory: self.player = player self.move_rando_bitvec = move_rando_bitvec self.area_randomizer = options.area_rando > 0 + self.painting_randomizer = options.enable_locked_paintings self.capless = not options.strict_cap_requirements self.cannonless = not options.strict_cannon_requirements self.moveless = not options.strict_move_requirements @@ -287,22 +301,35 @@ class RuleFactory: if rule: set_rule(target, rule) - def build_rule(self, rule_expr: str, cannon_name: str = '') -> Callable: - expressions = rule_expr.split(" | ") + def build_rule(self, rule_expr: str, cannon_name: str = '', painting_lvl_name: str = None, star_num_req: int = None) -> Callable: + # Star/painting requirements are outer and'd requirements, logically (painting? star? and (rule_expr)) + base_rule = self.build_star_painting_entry_requirements(painting_lvl_name, star_num_req) + expressions = rule_expr.split(" | ") if len(rule_expr) > 0 else [] rules = [] for expression in expressions: or_clause = self.combine_and_clauses(expression, cannon_name) if or_clause is True: - return None + return base_rule if or_clause is not False: rules.append(or_clause) if rules: if len(rules) == 1: - return rules[0] + return lambda state: base_rule(state) and rules[0](state) else: - return lambda state: any(rule(state) for rule in rules) + return lambda state: base_rule(state) and any(rule(state) for rule in rules) else: - return None + return base_rule + + def build_star_painting_entry_requirements(self, painting_lvl_name: str = None, star_num_req: int = None) -> Callable: + nop_condition = lambda state: True + star_rule = nop_condition + painting_rule = nop_condition + if painting_lvl_name is not None and self.painting_randomizer: + painting_item_name = f"Painting Unlock {painting_lvl_name}" + painting_rule = lambda state: state.has(painting_item_name, self.player) + if star_num_req is not None: + star_rule = lambda state: state.has("Power Star", self.player, star_num_req) + return lambda state: star_rule(state) and painting_rule(state) def combine_and_clauses(self, rule_expr: str, cannon_name: str) -> Union[Callable, bool]: expressions = rule_expr.split(" & ") diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 33aaa003ad..f1208d2059 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -1,7 +1,7 @@ import typing import os import json -from .Items import item_data_table, action_item_data_table, cannon_item_data_table, item_table, SM64Item +from .Items import item_data_table, action_item_data_table, cannon_item_data_table, painting_unlock_item_data_table, item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options_groups, SM64Options from .Rules import set_rules @@ -71,6 +71,8 @@ class SM64World(World): self.move_rando_bitvec |= (1 << (action_item_data_table[action].code - double_jump_bitvec_offset)) if self.options.exclamation_boxes: max_stars += 29 + if self.options.enable_locked_paintings: + max_stars -= len(painting_unlock_item_data_table) # free up space for the required paintings self.number_of_stars = min(self.options.amount_of_stars, max_stars) self.filler_count = max_stars - self.number_of_stars self.star_costs = { @@ -127,6 +129,9 @@ class SM64World(World): # Cannons if (self.options.buddy_checks): self.multiworld.itempool += [self.create_item(cannon_name) for cannon_name in cannon_item_data_table.keys()] + # Paintings + if (self.options.enable_locked_paintings): + self.multiworld.itempool += [self.create_item(painting_name) for painting_name in painting_unlock_item_data_table.keys()] # Moves double_jump_bitvec_offset = action_item_data_table['Double Jump'].code self.multiworld.itempool += [self.create_item(action) @@ -201,6 +206,7 @@ class SM64World(World): return { "AreaRando": self.area_connections, "MoveRandoVec": self.move_rando_bitvec, + "PaintingRando": self.options.enable_locked_paintings.value, "DeathLink": self.options.death_link.value, "CompletionType": self.options.completion_type.value, **self.star_costs