SMW: v2.0 Content Update (#2762)

Changelog:

Features:
- New optional Location Checks
  - 3-Up Moons
  - Hidden 1-Ups
  - Bonus Blocks
  - Blocksanity
    - All blocks that contain coins or items are included, with the exception of:
      - Blocks in Top Secret Area & Front Door/Bowser Castle
      - Blocks that are unreachable without glitches/unreasonable movement
- New Items
  - Special Zone Clear
  - New Filler Items
    - 1 Coin
    - 5 Coins
    - 10 Coins
    - 50 Coins
  - New Trap Items
    - Reverse Trap
    - Thwimp Trap
- SFX Shuffle
- Palette Shuffle Overhaul
  - New Curated Palette can now be used for the Overworld and Level Palette Shuffle options
  - Foreground and Background Shuffle options have been merged into a single setting
- Max possible Yoshi Egg value is 255
  - UI in-game is updated to handle 3-digits
  - New `Display Received Item Popups` option: `progression_minus_yoshi_eggs`

Quality of Life:
- In-Game Indicators are now displayed on the map screen for location checks and received items
- In-level sprites are displayed upon receiving certain items
- The Camera Scroll unlocking is now only enabled on levels where it needs to be
- SMW can now handle receiving more than 255 items
- Significant World Code cleanup
  - New Options API
  - Removal of `world: MultiWorld` across the world
- The PopTracker pack now has tabs for every level/sublevel, and can automatically swap tabs while playing if connected to the server

Bug Fixes:
- Several logic tweaks/fixes

"Major credit to @TheLX5 for being the driving force for almost all of this update. We've been collaborating on design and polish of the features for the last few months, but all of the heavy lifting was all @TheLX5."
This commit is contained in:
PoryGone
2024-03-12 17:00:13 -04:00
committed by GitHub
parent b6b88070be
commit f8d5fe0e1e
395 changed files with 8433 additions and 775 deletions

View File

@@ -1,3 +1,4 @@
import dataclasses
import os
import typing
import math
@@ -5,9 +6,9 @@ import settings
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import SMWItem, ItemData, item_table
from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names
from .Options import smw_options
from .Items import SMWItem, ItemData, item_table, junk_table
from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
from .Options import SMWOptions
from .Regions import create_regions, connect_regions
from .Levels import full_level_list, generate_level_list, location_id_to_level_id
from .Rules import set_rules
@@ -50,11 +51,14 @@ class SMWWorld(World):
lost all of his abilities. Can he get them back in time to save the Princess?
"""
game: str = "Super Mario World"
option_definitions = smw_options
settings: typing.ClassVar[SMWSettings]
options_dataclass = SMWOptions
options: SMWOptions
topology_present = False
data_version = 3
required_client_version = (0, 3, 5)
required_client_version = (0, 4, 4)
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = all_locations
@@ -62,9 +66,9 @@ class SMWWorld(World):
active_level_dict: typing.Dict[int,int]
web = SMWWeb()
def __init__(self, world: MultiWorld, player: int):
def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
super().__init__(multiworld, player)
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
@@ -72,37 +76,34 @@ class SMWWorld(World):
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def _get_slot_data(self):
return {
#"death_link": self.multiworld.death_link[self.player].value,
"active_levels": self.active_level_dict,
}
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in smw_options:
option = getattr(self.multiworld, option_name)[self.player]
slot_data[option_name] = option.value
slot_data = self.options.as_dict(
"dragon_coin_checks",
"moon_checks",
"hidden_1up_checks",
"bonus_block_checks",
"blocksanity",
)
slot_data["active_levels"] = self.active_level_dict
return slot_data
def generate_early(self):
if self.multiworld.early_climb[self.player]:
if self.options.early_climb:
self.multiworld.local_early_items[self.player][ItemName.mario_climb] = 1
def create_regions(self):
location_table = setup_locations(self.multiworld, self.player)
create_regions(self.multiworld, self.player, location_table)
location_table = setup_locations(self)
create_regions(self, location_table)
# Not generate basic
itempool: typing.List[SMWItem] = []
self.active_level_dict = dict(zip(generate_level_list(self.multiworld, self.player), full_level_list))
self.topology_present = self.multiworld.level_shuffle[self.player]
self.active_level_dict = dict(zip(generate_level_list(self), full_level_list))
self.topology_present = self.options.level_shuffle
connect_regions(self, self.active_level_dict)
connect_regions(self.multiworld, self.player, self.active_level_dict)
# Add Boss Token amount requirements for Worlds
add_rule(self.multiworld.get_region(LocationName.donut_plains_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 1))
add_rule(self.multiworld.get_region(LocationName.vanilla_dome_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 2))
@@ -110,18 +111,29 @@ class SMWWorld(World):
add_rule(self.multiworld.get_region(LocationName.chocolate_island_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 5))
add_rule(self.multiworld.get_region(LocationName.valley_of_bowser_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 6))
if self.multiworld.exclude_special_zone[self.player]:
exclusion_pool = set()
if self.multiworld.dragon_coin_checks[self.player]:
exclusion_pool.update(special_zone_level_names)
exclusion_pool = set()
if self.options.exclude_special_zone:
exclusion_pool.update(special_zone_level_names)
if self.options.dragon_coin_checks:
exclusion_pool.update(special_zone_dragon_coin_names)
elif self.multiworld.number_of_yoshi_eggs[self.player].value <= 72:
exclusion_pool.update(special_zone_level_names)
if self.options.hidden_1up_checks:
exclusion_pool.update(special_zone_hidden_1up_names)
if self.options.blocksanity:
exclusion_pool.update(special_zone_blocksanity_names)
exclusion_rules(self.multiworld, self.player, exclusion_pool)
total_required_locations = 96
if self.multiworld.dragon_coin_checks[self.player]:
if self.options.dragon_coin_checks:
total_required_locations += 49
if self.options.moon_checks:
total_required_locations += 7
if self.options.hidden_1up_checks:
total_required_locations += 14
if self.options.bonus_block_checks:
total_required_locations += 4
if self.options.blocksanity:
total_required_locations += 582
itempool += [self.create_item(ItemName.mario_run)]
itempool += [self.create_item(ItemName.mario_carry)]
@@ -137,31 +149,53 @@ class SMWWorld(World):
itempool += [self.create_item(ItemName.green_switch_palace)]
itempool += [self.create_item(ItemName.red_switch_palace)]
itempool += [self.create_item(ItemName.blue_switch_palace)]
itempool += [self.create_item(ItemName.special_world_clear)]
if self.multiworld.goal[self.player] == "yoshi_egg_hunt":
itempool += [self.create_item(ItemName.yoshi_egg)
for _ in range(self.multiworld.number_of_yoshi_eggs[self.player])]
if self.options.goal == "yoshi_egg_hunt":
raw_egg_count = total_required_locations - len(itempool) - len(exclusion_pool)
total_egg_count = min(raw_egg_count, self.options.max_yoshi_egg_cap.value)
self.required_egg_count = max(math.floor(total_egg_count * (self.options.percentage_of_yoshi_eggs.value / 100.0)), 1)
extra_egg_count = total_egg_count - self.required_egg_count
removed_egg_count = math.floor(extra_egg_count * (self.options.junk_fill_percentage.value / 100.0))
self.actual_egg_count = total_egg_count - removed_egg_count
itempool += [self.create_item(ItemName.yoshi_egg) for _ in range(self.actual_egg_count)]
self.multiworld.get_location(LocationName.yoshis_house, self.player).place_locked_item(self.create_item(ItemName.victory))
else:
self.actual_egg_count = 0
self.required_egg_count = 0
self.multiworld.get_location(LocationName.bowser, self.player).place_locked_item(self.create_item(ItemName.victory))
junk_count = total_required_locations - len(itempool)
trap_weights = []
trap_weights += ([ItemName.ice_trap] * self.multiworld.ice_trap_weight[self.player].value)
trap_weights += ([ItemName.stun_trap] * self.multiworld.stun_trap_weight[self.player].value)
trap_weights += ([ItemName.literature_trap] * self.multiworld.literature_trap_weight[self.player].value)
trap_weights += ([ItemName.timer_trap] * self.multiworld.timer_trap_weight[self.player].value)
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0))
trap_weights += ([ItemName.ice_trap] * self.options.ice_trap_weight.value)
trap_weights += ([ItemName.stun_trap] * self.options.stun_trap_weight.value)
trap_weights += ([ItemName.literature_trap] * self.options.literature_trap_weight.value)
trap_weights += ([ItemName.timer_trap] * self.options.timer_trap_weight.value)
trap_weights += ([ItemName.reverse_controls_trap] * self.options.reverse_trap_weight.value)
trap_weights += ([ItemName.thwimp_trap] * self.options.thwimp_trap_weight.value)
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.options.trap_fill_percentage.value / 100.0))
junk_count -= trap_count
trap_pool = []
for i in range(trap_count):
trap_item = self.multiworld.random.choice(trap_weights)
trap_item = self.random.choice(trap_weights)
trap_pool.append(self.create_item(trap_item))
itempool += trap_pool
itempool += [self.create_item(ItemName.one_up_mushroom) for _ in range(junk_count)]
junk_weights = []
junk_weights += ([ItemName.one_coin] * 15)
junk_weights += ([ItemName.five_coins] * 15)
junk_weights += ([ItemName.ten_coins] * 25)
junk_weights += ([ItemName.fifty_coins] * 25)
junk_weights += ([ItemName.one_up_mushroom] * 20)
junk_pool = [self.create_item(self.random.choice(junk_weights)) for _ in range(junk_count)]
itempool += junk_pool
boss_location_names = [LocationName.yoshis_island_koopaling, LocationName.donut_plains_koopaling, LocationName.vanilla_dome_koopaling,
LocationName.twin_bridges_koopaling, LocationName.forest_koopaling, LocationName.chocolate_koopaling,
@@ -176,18 +210,18 @@ class SMWWorld(World):
def generate_output(self, output_directory: str):
rompath = "" # if variable is not declared finally clause may fail
try:
world = self.multiworld
multiworld = self.multiworld
player = self.player
rom = LocalRom(get_base_rom_path())
patch_rom(self.multiworld, rom, self.player, self.active_level_dict)
patch_rom(self, rom, self.player, self.active_level_dict)
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath)
self.rom_name = rom.name
patch = SMWDeltaPatch(os.path.splitext(rompath)[0]+SMWDeltaPatch.patch_file_ending, player=player,
player_name=world.player_name[player], patched_path=rompath)
player_name=multiworld.player_name[player], patched_path=rompath)
patch.write()
except:
raise
@@ -243,7 +277,15 @@ class SMWWorld(World):
if level_index >= world_cutoffs[i]:
continue
if self.multiworld.dragon_coin_checks[self.player].value == 0 and "Dragon Coins" in loc_name:
if not self.options.dragon_coin_checks and "Dragon Coins" in loc_name:
continue
if not self.options.moon_checks and "3-Up Moon" in loc_name:
continue
if not self.options.hidden_1up_checks and "Hidden 1-Up" in loc_name:
continue
if not self.options.bonus_block_checks and "1-Up from Bonus Block" in loc_name:
continue
if not self.options.blocksanity and "Block #" in loc_name:
continue
location = self.multiworld.get_location(loc_name, self.player)
@@ -271,7 +313,7 @@ class SMWWorld(World):
return created_item
def get_filler_item_name(self) -> str:
return ItemName.one_up_mushroom
return self.random.choice(list(junk_table.keys()))
def set_rules(self):
set_rules(self.multiworld, self.player)
set_rules(self)