Skip to content

Commit 5732ce8

Browse files
DeamonHunterqwint
authored andcommitted
Muse Dash: Add filler items and rework generation balance (ArchipelagoMW#2809)
1 parent 585af0d commit 5732ce8

File tree

6 files changed

+77
-61
lines changed

6 files changed

+77
-61
lines changed

worlds/musedash/MuseDashCollection.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,19 @@ class MuseDashCollections:
6666
"Error SFX Trap": STARTING_CODE + 9,
6767
}
6868

69-
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
69+
filler_items: Dict[str, int] = {
70+
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
71+
"Miss To Great (5 Pack)": STARTING_CODE + 31,
72+
"Extra Life": STARTING_CODE + 32,
73+
}
74+
75+
filler_item_weights: Dict[str, int] = {
76+
"Great To Perfect (10 Pack)": 10,
77+
"Miss To Great (5 Pack)": 3,
78+
"Extra Life": 1,
79+
}
80+
81+
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
7082
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
7183

7284
def __init__(self) -> None:

worlds/musedash/Options.py

+5-14
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
from .MuseDashCollection import MuseDashCollections
66

7+
78
class AllowJustAsPlannedDLCSongs(Toggle):
89
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
910
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
1011
display_name = "Allow [Muse Plus] DLC Songs"
1112

13+
1214
class DLCMusicPacks(OptionSet):
1315
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
1416
display_name = "DLC Packs"
@@ -101,20 +103,10 @@ class GradeNeeded(Choice):
101103
default = 0
102104

103105

104-
class AdditionalItemPercentage(Range):
105-
"""The percentage of songs that will have 2 items instead of 1 when completing them.
106-
- Starting Songs will always have 2 items.
107-
- Locations will be filled with duplicate songs if there are not enough items.
108-
"""
109-
display_name = "Additional Item %"
110-
range_start = 50
111-
default = 80
112-
range_end = 100
113-
114-
115106
class MusicSheetCountPercentage(Range):
116-
"""Collecting enough Music Sheets will unlock the goal song needed for completion.
117-
This option controls how many are in the item pool, based on the total number of songs."""
107+
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
108+
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
109+
"""
118110
range_start = 10
119111
range_end = 40
120112
default = 20
@@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
175167
streamer_mode_enabled: StreamerModeEnabled
176168
starting_song_count: StartingSongs
177169
additional_song_count: AdditionalSongs
178-
additional_item_percentage: AdditionalItemPercentage
179170
song_difficulty_mode: DifficultyMode
180171
song_difficulty_min: DifficultyModeOverrideMin
181172
song_difficulty_max: DifficultyModeOverrideMax

worlds/musedash/Presets.py

-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"allow_just_as_planned_dlc_songs": False,
77
"starting_song_count": 5,
88
"additional_song_count": 34,
9-
"additional_item_percentage": 80,
109
"music_sheet_count_percentage": 20,
1110
"music_sheet_win_count_percentage": 90,
1211
},
@@ -15,7 +14,6 @@
1514
"allow_just_as_planned_dlc_songs": True,
1615
"starting_song_count": 5,
1716
"additional_song_count": 34,
18-
"additional_item_percentage": 80,
1917
"music_sheet_count_percentage": 20,
2018
"music_sheet_win_count_percentage": 90,
2119
},
@@ -24,7 +22,6 @@
2422
"allow_just_as_planned_dlc_songs": True,
2523
"starting_song_count": 8,
2624
"additional_song_count": 91,
27-
"additional_item_percentage": 80,
2825
"music_sheet_count_percentage": 20,
2926
"music_sheet_win_count_percentage": 90,
3027
},

worlds/musedash/__init__.py

+55-39
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class MuseDashWorld(World):
5757

5858
# Necessary Data
5959
md_collection = MuseDashCollections()
60+
filler_item_names = list(md_collection.filler_item_weights.keys())
61+
filler_item_weights = list(md_collection.filler_item_weights.values())
6062

6163
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
6264
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
@@ -70,7 +72,7 @@ class MuseDashWorld(World):
7072

7173
def generate_early(self):
7274
dlc_songs = {key for key in self.options.dlc_packs.value}
73-
if (self.options.allow_just_as_planned_dlc_songs.value):
75+
if self.options.allow_just_as_planned_dlc_songs.value:
7476
dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
7577

7678
streamer_mode = self.options.streamer_mode_enabled
@@ -84,7 +86,7 @@ def generate_early(self):
8486
while True:
8587
# In most cases this should only need to run once
8688
available_song_keys = self.md_collection.get_songs_with_settings(
87-
dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold)
89+
dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
8890

8991
available_song_keys = self.handle_plando(available_song_keys)
9092

@@ -161,19 +163,17 @@ def create_song_pool(self, available_song_keys: List[str]):
161163
break
162164
self.included_songs.append(available_song_keys.pop())
163165

164-
self.location_count = len(self.starting_songs) + len(self.included_songs)
165-
location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0)
166-
self.location_count = floor(self.location_count * location_multiplier)
167-
168-
minimum_location_count = len(self.included_songs) + self.get_music_sheet_count()
169-
if self.location_count < minimum_location_count:
170-
self.location_count = minimum_location_count
166+
self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs))
171167

172168
def create_item(self, name: str) -> Item:
173169
if name == self.md_collection.MUSIC_SHEET_NAME:
174170
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
175171
self.md_collection.MUSIC_SHEET_CODE, self.player)
176172

173+
filler = self.md_collection.filler_items.get(name)
174+
if filler:
175+
return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
176+
177177
trap = self.md_collection.vfx_trap_items.get(name)
178178
if trap:
179179
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
@@ -189,6 +189,9 @@ def create_item(self, name: str) -> Item:
189189
song = self.md_collection.song_items.get(name)
190190
return MuseDashSongItem(name, self.player, song)
191191

192+
def get_filler_item_name(self) -> str:
193+
return self.random.choices(self.filler_item_names, self.filler_item_weights)[0]
194+
192195
def create_items(self) -> None:
193196
song_keys_in_pool = self.included_songs.copy()
194197

@@ -199,8 +202,13 @@ def create_items(self) -> None:
199202
for _ in range(0, item_count):
200203
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
201204

202-
# Then add all traps
203-
trap_count = self.get_trap_count()
205+
# Then add 1 copy of every song
206+
item_count += len(self.included_songs)
207+
for song in self.included_songs:
208+
self.multiworld.itempool.append(self.create_item(song))
209+
210+
# Then add all traps, making sure we don't over fill
211+
trap_count = min(self.location_count - item_count, self.get_trap_count())
204212
trap_list = self.get_available_traps()
205213
if len(trap_list) > 0 and trap_count > 0:
206214
for _ in range(0, trap_count):
@@ -209,23 +217,38 @@ def create_items(self) -> None:
209217

210218
item_count += trap_count
211219

212-
# Next fill all remaining slots with song items
213-
needed_item_count = self.location_count
214-
while item_count < needed_item_count:
215-
# If we have more items needed than keys, just iterate the list and add them all
216-
if len(song_keys_in_pool) <= needed_item_count - item_count:
217-
for key in song_keys_in_pool:
218-
self.multiworld.itempool.append(self.create_item(key))
220+
# At this point, if a player is using traps, it's possible that they have filled all locations
221+
items_left = self.location_count - item_count
222+
if items_left <= 0:
223+
return
224+
225+
# When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
226+
# First fill 50% with the filler. The rest is to be duplicate songs.
227+
filler_count = floor(0.5 * items_left)
228+
items_left -= filler_count
219229

220-
item_count += len(song_keys_in_pool)
221-
continue
230+
for _ in range(0, filler_count):
231+
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
222232

223-
# Otherwise add a random assortment of songs
224-
self.random.shuffle(song_keys_in_pool)
225-
for i in range(0, needed_item_count - item_count):
226-
self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
233+
# All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression
234+
# to cut down on the number of progression items that Muse Dash puts into the pool.
227235

228-
item_count = needed_item_count
236+
# This is for the extraordinary case of needing to fill a lot of items.
237+
while items_left > len(song_keys_in_pool):
238+
for key in song_keys_in_pool:
239+
item = self.create_item(key)
240+
item.classification = ItemClassification.useful
241+
self.multiworld.itempool.append(item)
242+
243+
items_left -= len(song_keys_in_pool)
244+
continue
245+
246+
# Otherwise add a random assortment of songs
247+
self.random.shuffle(song_keys_in_pool)
248+
for i in range(0, items_left):
249+
item = self.create_item(song_keys_in_pool[i])
250+
item.classification = ItemClassification.useful
251+
self.multiworld.itempool.append(item)
229252

230253
def create_regions(self) -> None:
231254
menu_region = Region("Menu", self.player, self.multiworld)
@@ -245,19 +268,18 @@ def create_regions(self) -> None:
245268
self.random.shuffle(included_song_copy)
246269
all_selected_locations.extend(included_song_copy)
247270

248-
two_item_location_count = self.location_count - len(all_selected_locations)
249-
250271
# Make a region per song/album, then adds 1-2 item locations to them
251272
for i in range(0, len(all_selected_locations)):
252273
name = all_selected_locations[i]
253274
region = Region(name, self.player, self.multiworld)
254275
self.multiworld.regions.append(region)
255276
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
256277

257-
# Up to 2 Locations are defined per song
258-
region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation)
259-
if i < two_item_location_count:
260-
region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation)
278+
# Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
279+
region.add_locations({
280+
name + "-0": self.md_collection.song_locations[name + "-0"],
281+
name + "-1": self.md_collection.song_locations[name + "-1"]
282+
}, MuseDashLocation)
261283

262284
def set_rules(self) -> None:
263285
self.multiworld.completion_condition[self.player] = lambda state: \
@@ -276,19 +298,14 @@ def get_available_traps(self) -> List[str]:
276298

277299
return trap_list
278300

279-
def get_additional_item_percentage(self) -> int:
280-
trap_count = self.options.trap_count_percentage.value
281-
song_count = self.options.music_sheet_count_percentage.value
282-
return max(trap_count + song_count, self.options.additional_item_percentage.value)
283-
284301
def get_trap_count(self) -> int:
285302
multiplier = self.options.trap_count_percentage.value / 100.0
286-
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
303+
trap_count = len(self.starting_songs) + len(self.included_songs)
287304
return max(0, floor(trap_count * multiplier))
288305

289306
def get_music_sheet_count(self) -> int:
290307
multiplier = self.options.music_sheet_count_percentage.value / 100.0
291-
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
308+
song_count = len(self.starting_songs) + len(self.included_songs)
292309
return max(1, floor(song_count * multiplier))
293310

294311
def get_music_sheet_win_count(self) -> int:
@@ -329,5 +346,4 @@ def fill_slot_data(self):
329346
"deathLink": self.options.death_link.value,
330347
"musicSheetWinCount": self.get_music_sheet_win_count(),
331348
"gradeNeeded": self.options.grade_needed.value,
332-
"hasFiller": True,
333349
}

worlds/musedash/test/TestDifficultyRanges.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
55
def test_all_difficulty_ranges(self) -> None:
66
muse_dash_world = self.multiworld.worlds[1]
77
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
8-
difficulty_choice = self.multiworld.song_difficulty_mode[1]
9-
difficulty_min = self.multiworld.song_difficulty_min[1]
10-
difficulty_max = self.multiworld.song_difficulty_max[1]
8+
difficulty_choice = muse_dash_world.options.song_difficulty_mode
9+
difficulty_min = muse_dash_world.options.song_difficulty_min
10+
difficulty_max = muse_dash_world.options.song_difficulty_max
1111

1212
def test_range(inputRange, lower, upper):
1313
self.assertEqual(inputRange[0], lower)

worlds/musedash/test/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from test.TestBase import WorldTestBase
1+
from test.bases import WorldTestBase
22

33

44
class MuseDashTestBase(WorldTestBase):

0 commit comments

Comments
 (0)