diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 55523542d7df..70c606e78baa 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -34,6 +34,7 @@ class MuseDashCollections: "Rush-Hour", "Find this Month's Featured Playlist", "PeroPero in the Universe", + "umpopoff" ] album_items: Dict[str, AlbumData] = {} @@ -56,7 +57,19 @@ class MuseDashCollections: "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 (5 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 (5 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) def __init__(self) -> None: @@ -81,11 +94,22 @@ def __init__(self) -> None: steamer_mode = sections[3] == "True" if song_name in self.DIFF_OVERRIDES: - # Note: These difficulties may not actually be representative of these songs. - # The game does not provide these difficulties so they have to be filled in. - diff_of_easy = 4 - diff_of_hard = 7 - diff_of_master = 10 + # These songs use non-standard difficulty values. Which are being overriden with standard values. + # But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value. + if sections[4] != '0': + diff_of_easy = 4 + else: + diff_of_easy = None + + if sections[5] != '0': + diff_of_hard = 7 + else: + diff_of_hard = None + + if sections[6] != '0': + diff_of_master = 10 + else: + diff_of_master = None else: diff_of_easy = self.parse_song_difficulty(sections[4]) diff_of_hard = self.parse_song_difficulty(sections[5]) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 54a0124474c6..fe3574f31b67 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11| Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11| Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7| Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8| -Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7| +Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11 Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8| Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10| Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9| @@ -501,4 +501,12 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9| Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11 Swing Edge|68-3|Gambler's Tricks|True|4|8|10| Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11 -Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| \ No newline at end of file +Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| +Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8| +YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9| +Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9| +INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10| +Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9| +HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10| +Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11 +Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8| \ No newline at end of file diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 3fe28187fae6..d5ce313f8f03 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -36,7 +36,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 500 # Note will probably not reach this high if any other settings are done. + range_end = 508 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index a68fd2853def..1d86642d65b5 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -57,6 +57,8 @@ class MuseDashWorld(World): # Necessary Data 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()} location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()} @@ -174,6 +176,10 @@ def create_item(self, name: str) -> Item: return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing, 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) if trap: return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) @@ -189,6 +195,9 @@ def create_item(self, name: str) -> Item: song = self.md_collection.song_items.get(name) 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: song_keys_in_pool = self.included_songs.copy() @@ -199,8 +208,13 @@ def create_items(self) -> None: for _ in range(0, item_count): self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME)) - # Then add all traps - trap_count = self.get_trap_count() + # Then add 1 copy of every song + 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() if len(trap_list) > 0 and trap_count > 0: for _ in range(0, trap_count): @@ -209,23 +223,38 @@ def create_items(self) -> None: item_count += trap_count - # Next fill all remaining slots with song items - needed_item_count = self.location_count - while item_count < needed_item_count: - # If we have more items needed than keys, just iterate the list and add them all - 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)) + # At this point, if a player is using traps, it's possible that they have filled all locations + items_left = self.location_count - item_count + if items_left <= 0: + return + + # When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs. + # First fill 50% with the filler. The rest is to be duplicate songs. + filler_count = floor(0.5 * items_left) + items_left -= filler_count + + for _ in range(0, filler_count): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) + + # 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. - item_count += len(song_keys_in_pool) - continue + # 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) - # Otherwise add a random assortment of songs - self.random.shuffle(song_keys_in_pool) - for i in range(0, needed_item_count - item_count): - self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i])) + items_left -= len(song_keys_in_pool) + continue - item_count = needed_item_count + # 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: menu_region = Region("Menu", self.player, self.multiworld) @@ -328,5 +357,6 @@ def fill_slot_data(self): "victoryLocation": self.victory_song_name, "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), - "gradeNeeded": self.options.grade_needed.value + "gradeNeeded": self.options.grade_needed.value, + "hasFiller": True, } diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index 01420347af15..af3469aa080f 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -66,5 +66,11 @@ def test_songs_have_difficulty(self) -> None: for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: song = muse_dash_world.md_collection.song_items[song_name] - self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, + # umpopoff is a one time weird song. Its currently the only song in the game + # with non-standard difficulties and also doesn't have 3 or more difficulties. + if song_name == 'umpopoff': + self.assertTrue(song.easy is None and song.hard is not None and song.master is None, + f"Song '{song_name}' difficulty not set when it should be.") + else: + self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, f"Song '{song_name}' difficulty not set when it should be.")