Super Mario 64: Add painting passability as items (#5294)

This commit is contained in:
Will Morrow
2026-01-21 09:12:53 -05:00
committed by GitHub
parent 8f261bb27c
commit 94492c45cb
5 changed files with 85 additions and 21 deletions

View File

@@ -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}

View File

@@ -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

View File

@@ -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")

View File

@@ -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(" & ")

View File

@@ -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