Muse Dash: Add filler items and rework generation balance (#2809)
This commit is contained in:
parent
7c44d749d4
commit
d1274c12b9
|
@ -66,7 +66,19 @@ class MuseDashCollections:
|
||||||
"Error SFX Trap": STARTING_CODE + 9,
|
"Error SFX Trap": STARTING_CODE + 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
|
filler_items: Dict[str, int] = {
|
||||||
|
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
|
||||||
|
"Miss To Great (5 Pack)": STARTING_CODE + 31,
|
||||||
|
"Extra Life": STARTING_CODE + 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
filler_item_weights: Dict[str, int] = {
|
||||||
|
"Great To Perfect (10 Pack)": 10,
|
||||||
|
"Miss To Great (5 Pack)": 3,
|
||||||
|
"Extra Life": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
|
||||||
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
|
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
|
@ -4,11 +4,13 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from .MuseDashCollection import MuseDashCollections
|
from .MuseDashCollection import MuseDashCollections
|
||||||
|
|
||||||
|
|
||||||
class AllowJustAsPlannedDLCSongs(Toggle):
|
class AllowJustAsPlannedDLCSongs(Toggle):
|
||||||
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
|
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
|
||||||
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
|
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
|
||||||
display_name = "Allow [Muse Plus] DLC Songs"
|
display_name = "Allow [Muse Plus] DLC Songs"
|
||||||
|
|
||||||
|
|
||||||
class DLCMusicPacks(OptionSet):
|
class DLCMusicPacks(OptionSet):
|
||||||
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
|
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
|
||||||
display_name = "DLC Packs"
|
display_name = "DLC Packs"
|
||||||
|
@ -101,20 +103,10 @@ class GradeNeeded(Choice):
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class AdditionalItemPercentage(Range):
|
|
||||||
"""The percentage of songs that will have 2 items instead of 1 when completing them.
|
|
||||||
- Starting Songs will always have 2 items.
|
|
||||||
- Locations will be filled with duplicate songs if there are not enough items.
|
|
||||||
"""
|
|
||||||
display_name = "Additional Item %"
|
|
||||||
range_start = 50
|
|
||||||
default = 80
|
|
||||||
range_end = 100
|
|
||||||
|
|
||||||
|
|
||||||
class MusicSheetCountPercentage(Range):
|
class MusicSheetCountPercentage(Range):
|
||||||
"""Collecting enough Music Sheets will unlock the goal song needed for completion.
|
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
|
||||||
This option controls how many are in the item pool, based on the total number of songs."""
|
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
|
||||||
|
"""
|
||||||
range_start = 10
|
range_start = 10
|
||||||
range_end = 40
|
range_end = 40
|
||||||
default = 20
|
default = 20
|
||||||
|
@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
|
||||||
streamer_mode_enabled: StreamerModeEnabled
|
streamer_mode_enabled: StreamerModeEnabled
|
||||||
starting_song_count: StartingSongs
|
starting_song_count: StartingSongs
|
||||||
additional_song_count: AdditionalSongs
|
additional_song_count: AdditionalSongs
|
||||||
additional_item_percentage: AdditionalItemPercentage
|
|
||||||
song_difficulty_mode: DifficultyMode
|
song_difficulty_mode: DifficultyMode
|
||||||
song_difficulty_min: DifficultyModeOverrideMin
|
song_difficulty_min: DifficultyModeOverrideMin
|
||||||
song_difficulty_max: DifficultyModeOverrideMax
|
song_difficulty_max: DifficultyModeOverrideMax
|
||||||
|
|
|
@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||||
"allow_just_as_planned_dlc_songs": False,
|
"allow_just_as_planned_dlc_songs": False,
|
||||||
"starting_song_count": 5,
|
"starting_song_count": 5,
|
||||||
"additional_song_count": 34,
|
"additional_song_count": 34,
|
||||||
"additional_item_percentage": 80,
|
|
||||||
"music_sheet_count_percentage": 20,
|
"music_sheet_count_percentage": 20,
|
||||||
"music_sheet_win_count_percentage": 90,
|
"music_sheet_win_count_percentage": 90,
|
||||||
},
|
},
|
||||||
|
@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||||
"allow_just_as_planned_dlc_songs": True,
|
"allow_just_as_planned_dlc_songs": True,
|
||||||
"starting_song_count": 5,
|
"starting_song_count": 5,
|
||||||
"additional_song_count": 34,
|
"additional_song_count": 34,
|
||||||
"additional_item_percentage": 80,
|
|
||||||
"music_sheet_count_percentage": 20,
|
"music_sheet_count_percentage": 20,
|
||||||
"music_sheet_win_count_percentage": 90,
|
"music_sheet_win_count_percentage": 90,
|
||||||
},
|
},
|
||||||
|
@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||||
"allow_just_as_planned_dlc_songs": True,
|
"allow_just_as_planned_dlc_songs": True,
|
||||||
"starting_song_count": 8,
|
"starting_song_count": 8,
|
||||||
"additional_song_count": 91,
|
"additional_song_count": 91,
|
||||||
"additional_item_percentage": 80,
|
|
||||||
"music_sheet_count_percentage": 20,
|
"music_sheet_count_percentage": 20,
|
||||||
"music_sheet_win_count_percentage": 90,
|
"music_sheet_win_count_percentage": 90,
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,6 +57,8 @@ class MuseDashWorld(World):
|
||||||
|
|
||||||
# Necessary Data
|
# Necessary Data
|
||||||
md_collection = MuseDashCollections()
|
md_collection = MuseDashCollections()
|
||||||
|
filler_item_names = list(md_collection.filler_item_weights.keys())
|
||||||
|
filler_item_weights = list(md_collection.filler_item_weights.values())
|
||||||
|
|
||||||
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
|
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
|
||||||
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
|
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
|
||||||
|
@ -70,7 +72,7 @@ class MuseDashWorld(World):
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
dlc_songs = {key for key in self.options.dlc_packs.value}
|
dlc_songs = {key for key in self.options.dlc_packs.value}
|
||||||
if (self.options.allow_just_as_planned_dlc_songs.value):
|
if self.options.allow_just_as_planned_dlc_songs.value:
|
||||||
dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
|
dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
|
||||||
|
|
||||||
streamer_mode = self.options.streamer_mode_enabled
|
streamer_mode = self.options.streamer_mode_enabled
|
||||||
|
@ -84,7 +86,7 @@ class MuseDashWorld(World):
|
||||||
while True:
|
while True:
|
||||||
# In most cases this should only need to run once
|
# In most cases this should only need to run once
|
||||||
available_song_keys = self.md_collection.get_songs_with_settings(
|
available_song_keys = self.md_collection.get_songs_with_settings(
|
||||||
dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold)
|
dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
|
||||||
|
|
||||||
available_song_keys = self.handle_plando(available_song_keys)
|
available_song_keys = self.handle_plando(available_song_keys)
|
||||||
|
|
||||||
|
@ -161,19 +163,17 @@ class MuseDashWorld(World):
|
||||||
break
|
break
|
||||||
self.included_songs.append(available_song_keys.pop())
|
self.included_songs.append(available_song_keys.pop())
|
||||||
|
|
||||||
self.location_count = len(self.starting_songs) + len(self.included_songs)
|
self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs))
|
||||||
location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0)
|
|
||||||
self.location_count = floor(self.location_count * location_multiplier)
|
|
||||||
|
|
||||||
minimum_location_count = len(self.included_songs) + self.get_music_sheet_count()
|
|
||||||
if self.location_count < minimum_location_count:
|
|
||||||
self.location_count = minimum_location_count
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
if name == self.md_collection.MUSIC_SHEET_NAME:
|
if name == self.md_collection.MUSIC_SHEET_NAME:
|
||||||
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
|
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
|
||||||
self.md_collection.MUSIC_SHEET_CODE, self.player)
|
self.md_collection.MUSIC_SHEET_CODE, self.player)
|
||||||
|
|
||||||
|
filler = self.md_collection.filler_items.get(name)
|
||||||
|
if filler:
|
||||||
|
return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
|
||||||
|
|
||||||
trap = self.md_collection.vfx_trap_items.get(name)
|
trap = self.md_collection.vfx_trap_items.get(name)
|
||||||
if trap:
|
if trap:
|
||||||
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
|
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
|
||||||
|
@ -189,6 +189,9 @@ class MuseDashWorld(World):
|
||||||
song = self.md_collection.song_items.get(name)
|
song = self.md_collection.song_items.get(name)
|
||||||
return MuseDashSongItem(name, self.player, song)
|
return MuseDashSongItem(name, self.player, song)
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
return self.random.choices(self.filler_item_names, self.filler_item_weights)[0]
|
||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
song_keys_in_pool = self.included_songs.copy()
|
song_keys_in_pool = self.included_songs.copy()
|
||||||
|
|
||||||
|
@ -199,8 +202,13 @@ class MuseDashWorld(World):
|
||||||
for _ in range(0, item_count):
|
for _ in range(0, item_count):
|
||||||
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
|
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
|
||||||
|
|
||||||
# Then add all traps
|
# Then add 1 copy of every song
|
||||||
trap_count = self.get_trap_count()
|
item_count += len(self.included_songs)
|
||||||
|
for song in self.included_songs:
|
||||||
|
self.multiworld.itempool.append(self.create_item(song))
|
||||||
|
|
||||||
|
# Then add all traps, making sure we don't over fill
|
||||||
|
trap_count = min(self.location_count - item_count, self.get_trap_count())
|
||||||
trap_list = self.get_available_traps()
|
trap_list = self.get_available_traps()
|
||||||
if len(trap_list) > 0 and trap_count > 0:
|
if len(trap_list) > 0 and trap_count > 0:
|
||||||
for _ in range(0, trap_count):
|
for _ in range(0, trap_count):
|
||||||
|
@ -209,23 +217,38 @@ class MuseDashWorld(World):
|
||||||
|
|
||||||
item_count += trap_count
|
item_count += trap_count
|
||||||
|
|
||||||
# Next fill all remaining slots with song items
|
# At this point, if a player is using traps, it's possible that they have filled all locations
|
||||||
needed_item_count = self.location_count
|
items_left = self.location_count - item_count
|
||||||
while item_count < needed_item_count:
|
if items_left <= 0:
|
||||||
# If we have more items needed than keys, just iterate the list and add them all
|
return
|
||||||
if len(song_keys_in_pool) <= needed_item_count - item_count:
|
|
||||||
for key in song_keys_in_pool:
|
|
||||||
self.multiworld.itempool.append(self.create_item(key))
|
|
||||||
|
|
||||||
item_count += len(song_keys_in_pool)
|
# When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
|
||||||
continue
|
# First fill 50% with the filler. The rest is to be duplicate songs.
|
||||||
|
filler_count = floor(0.5 * items_left)
|
||||||
|
items_left -= filler_count
|
||||||
|
|
||||||
# Otherwise add a random assortment of songs
|
for _ in range(0, filler_count):
|
||||||
self.random.shuffle(song_keys_in_pool)
|
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||||
for i in range(0, needed_item_count - item_count):
|
|
||||||
self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
|
|
||||||
|
|
||||||
item_count = needed_item_count
|
# All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression
|
||||||
|
# to cut down on the number of progression items that Muse Dash puts into the pool.
|
||||||
|
|
||||||
|
# This is for the extraordinary case of needing to fill a lot of items.
|
||||||
|
while items_left > len(song_keys_in_pool):
|
||||||
|
for key in song_keys_in_pool:
|
||||||
|
item = self.create_item(key)
|
||||||
|
item.classification = ItemClassification.useful
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
|
||||||
|
items_left -= len(song_keys_in_pool)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Otherwise add a random assortment of songs
|
||||||
|
self.random.shuffle(song_keys_in_pool)
|
||||||
|
for i in range(0, items_left):
|
||||||
|
item = self.create_item(song_keys_in_pool[i])
|
||||||
|
item.classification = ItemClassification.useful
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
menu_region = Region("Menu", self.player, self.multiworld)
|
menu_region = Region("Menu", self.player, self.multiworld)
|
||||||
|
@ -245,8 +268,6 @@ class MuseDashWorld(World):
|
||||||
self.random.shuffle(included_song_copy)
|
self.random.shuffle(included_song_copy)
|
||||||
all_selected_locations.extend(included_song_copy)
|
all_selected_locations.extend(included_song_copy)
|
||||||
|
|
||||||
two_item_location_count = self.location_count - len(all_selected_locations)
|
|
||||||
|
|
||||||
# Make a region per song/album, then adds 1-2 item locations to them
|
# Make a region per song/album, then adds 1-2 item locations to them
|
||||||
for i in range(0, len(all_selected_locations)):
|
for i in range(0, len(all_selected_locations)):
|
||||||
name = all_selected_locations[i]
|
name = all_selected_locations[i]
|
||||||
|
@ -254,10 +275,11 @@ class MuseDashWorld(World):
|
||||||
self.multiworld.regions.append(region)
|
self.multiworld.regions.append(region)
|
||||||
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
|
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
|
||||||
|
|
||||||
# Up to 2 Locations are defined per song
|
# Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
|
||||||
region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation)
|
region.add_locations({
|
||||||
if i < two_item_location_count:
|
name + "-0": self.md_collection.song_locations[name + "-0"],
|
||||||
region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation)
|
name + "-1": self.md_collection.song_locations[name + "-1"]
|
||||||
|
}, MuseDashLocation)
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||||
|
@ -276,19 +298,14 @@ class MuseDashWorld(World):
|
||||||
|
|
||||||
return trap_list
|
return trap_list
|
||||||
|
|
||||||
def get_additional_item_percentage(self) -> int:
|
|
||||||
trap_count = self.options.trap_count_percentage.value
|
|
||||||
song_count = self.options.music_sheet_count_percentage.value
|
|
||||||
return max(trap_count + song_count, self.options.additional_item_percentage.value)
|
|
||||||
|
|
||||||
def get_trap_count(self) -> int:
|
def get_trap_count(self) -> int:
|
||||||
multiplier = self.options.trap_count_percentage.value / 100.0
|
multiplier = self.options.trap_count_percentage.value / 100.0
|
||||||
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
trap_count = len(self.starting_songs) + len(self.included_songs)
|
||||||
return max(0, floor(trap_count * multiplier))
|
return max(0, floor(trap_count * multiplier))
|
||||||
|
|
||||||
def get_music_sheet_count(self) -> int:
|
def get_music_sheet_count(self) -> int:
|
||||||
multiplier = self.options.music_sheet_count_percentage.value / 100.0
|
multiplier = self.options.music_sheet_count_percentage.value / 100.0
|
||||||
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
song_count = len(self.starting_songs) + len(self.included_songs)
|
||||||
return max(1, floor(song_count * multiplier))
|
return max(1, floor(song_count * multiplier))
|
||||||
|
|
||||||
def get_music_sheet_win_count(self) -> int:
|
def get_music_sheet_win_count(self) -> int:
|
||||||
|
@ -329,5 +346,4 @@ class MuseDashWorld(World):
|
||||||
"deathLink": self.options.death_link.value,
|
"deathLink": self.options.death_link.value,
|
||||||
"musicSheetWinCount": self.get_music_sheet_win_count(),
|
"musicSheetWinCount": self.get_music_sheet_win_count(),
|
||||||
"gradeNeeded": self.options.grade_needed.value,
|
"gradeNeeded": self.options.grade_needed.value,
|
||||||
"hasFiller": True,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
|
||||||
def test_all_difficulty_ranges(self) -> None:
|
def test_all_difficulty_ranges(self) -> None:
|
||||||
muse_dash_world = self.multiworld.worlds[1]
|
muse_dash_world = self.multiworld.worlds[1]
|
||||||
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
|
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
|
||||||
difficulty_choice = self.multiworld.song_difficulty_mode[1]
|
difficulty_choice = muse_dash_world.options.song_difficulty_mode
|
||||||
difficulty_min = self.multiworld.song_difficulty_min[1]
|
difficulty_min = muse_dash_world.options.song_difficulty_min
|
||||||
difficulty_max = self.multiworld.song_difficulty_max[1]
|
difficulty_max = muse_dash_world.options.song_difficulty_max
|
||||||
|
|
||||||
def test_range(inputRange, lower, upper):
|
def test_range(inputRange, lower, upper):
|
||||||
self.assertEqual(inputRange[0], lower)
|
self.assertEqual(inputRange[0], lower)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from test.TestBase import WorldTestBase
|
from test.bases import WorldTestBase
|
||||||
|
|
||||||
|
|
||||||
class MuseDashTestBase(WorldTestBase):
|
class MuseDashTestBase(WorldTestBase):
|
||||||
|
|
Loading…
Reference in New Issue