diff --git a/randomizer/Enums/Plandomizer.py b/randomizer/Enums/Plandomizer.py index f3301cdae..f44cb7849 100644 --- a/randomizer/Enums/Plandomizer.py +++ b/randomizer/Enums/Plandomizer.py @@ -105,6 +105,122 @@ class PlandoItems(IntEnum): TinyBlueprint = auto() ChunkyBlueprint = auto() + # Group items to represent random selections from a specific group. + RandomKong = auto() + RandomMove = auto() + RandomKongMove = auto() + RandomSharedMove = auto() + RandomKey = auto() + RandomItem = auto() + + +PlandoGroupMap = { + PlandoItems.RandomKong: [ + PlandoItems.Donkey, + PlandoItems.Diddy, + PlandoItems.Lanky, + PlandoItems.Tiny, + PlandoItems.Chunky, + ], + PlandoItems.RandomMove: [ + PlandoItems.Vines, + PlandoItems.Swim, + PlandoItems.Oranges, + PlandoItems.Barrels, + PlandoItems.ProgressiveSlam, + PlandoItems.BaboonBlast, + PlandoItems.StrongKong, + PlandoItems.GorillaGrab, + PlandoItems.ChimpyCharge, + PlandoItems.RocketbarrelBoost, + PlandoItems.SimianSpring, + PlandoItems.Orangstand, + PlandoItems.BaboonBalloon, + PlandoItems.OrangstandSprint, + PlandoItems.MiniMonkey, + PlandoItems.PonyTailTwirl, + PlandoItems.Monkeyport, + PlandoItems.HunkyChunky, + PlandoItems.PrimatePunch, + PlandoItems.GorillaGone, + PlandoItems.Coconut, + PlandoItems.Peanut, + PlandoItems.Grape, + PlandoItems.Feather, + PlandoItems.Pineapple, + PlandoItems.HomingAmmo, + PlandoItems.SniperSight, + PlandoItems.ProgressiveAmmoBelt, + PlandoItems.Bongos, + PlandoItems.Guitar, + PlandoItems.Trombone, + PlandoItems.Saxophone, + PlandoItems.Triangle, + PlandoItems.ProgressiveInstrumentUpgrade, + PlandoItems.Camera, + PlandoItems.Shockwave, + ], + PlandoItems.RandomKongMove: [ + PlandoItems.BaboonBlast, + PlandoItems.StrongKong, + PlandoItems.GorillaGrab, + PlandoItems.ChimpyCharge, + PlandoItems.RocketbarrelBoost, + PlandoItems.SimianSpring, + PlandoItems.Orangstand, + PlandoItems.BaboonBalloon, + PlandoItems.OrangstandSprint, + PlandoItems.MiniMonkey, + PlandoItems.PonyTailTwirl, + PlandoItems.Monkeyport, + PlandoItems.HunkyChunky, + PlandoItems.PrimatePunch, + PlandoItems.GorillaGone, + PlandoItems.Coconut, + PlandoItems.Peanut, + PlandoItems.Grape, + PlandoItems.Feather, + PlandoItems.Pineapple, + PlandoItems.Bongos, + PlandoItems.Guitar, + PlandoItems.Trombone, + PlandoItems.Saxophone, + PlandoItems.Triangle, + ], + PlandoItems.RandomSharedMove: [ + PlandoItems.Vines, + PlandoItems.Swim, + PlandoItems.Oranges, + PlandoItems.Barrels, + PlandoItems.ProgressiveSlam, + PlandoItems.HomingAmmo, + PlandoItems.SniperSight, + PlandoItems.ProgressiveAmmoBelt, + PlandoItems.ProgressiveInstrumentUpgrade, + PlandoItems.Camera, + PlandoItems.Shockwave, + ], + PlandoItems.RandomKey: [ + PlandoItems.JungleJapesKey, + PlandoItems.AngryAztecKey, + PlandoItems.FranticFactoryKey, + PlandoItems.GloomyGalleonKey, + PlandoItems.FungiForestKey, + PlandoItems.CrystalCavesKey, + PlandoItems.CreepyCastleKey, + PlandoItems.HideoutHelmKey, + ], + PlandoItems.RandomItem: [ + PlandoItems.GoldenBanana, + PlandoItems.BananaFairy, + PlandoItems.BananaMedal, + PlandoItems.BattleCrown, + PlandoItems.Bean, + PlandoItems.Pearl, + PlandoItems.RainbowCoin, + ], +} + ItemToPlandoItemMap = { Items.NoItem: PlandoItems.NoItem, @@ -336,6 +452,110 @@ class PlandoItems(IntEnum): Items.CreepyCastleChunkyBlueprint, Items.DKIslesChunkyBlueprint, ], + PlandoItems.RandomKong: [ + Items.Donkey, + Items.Diddy, + Items.Lanky, + Items.Tiny, + Items.Chunky, + ], + PlandoItems.RandomMove: [ + Items.Vines, + Items.Swim, + Items.Oranges, + Items.Barrels, + Items.ProgressiveSlam, + Items.BaboonBlast, + Items.StrongKong, + Items.GorillaGrab, + Items.ChimpyCharge, + Items.RocketbarrelBoost, + Items.SimianSpring, + Items.Orangstand, + Items.BaboonBalloon, + Items.OrangstandSprint, + Items.MiniMonkey, + Items.PonyTailTwirl, + Items.Monkeyport, + Items.HunkyChunky, + Items.PrimatePunch, + Items.GorillaGone, + Items.Coconut, + Items.Peanut, + Items.Grape, + Items.Feather, + Items.Pineapple, + Items.HomingAmmo, + Items.SniperSight, + Items.ProgressiveAmmoBelt, + Items.Bongos, + Items.Guitar, + Items.Trombone, + Items.Saxophone, + Items.Triangle, + Items.ProgressiveInstrumentUpgrade, + Items.Camera, + Items.Shockwave, + ], + PlandoItems.RandomKongMove: [ + Items.BaboonBlast, + Items.StrongKong, + Items.GorillaGrab, + Items.ChimpyCharge, + Items.RocketbarrelBoost, + Items.SimianSpring, + Items.Orangstand, + Items.BaboonBalloon, + Items.OrangstandSprint, + Items.MiniMonkey, + Items.PonyTailTwirl, + Items.Monkeyport, + Items.HunkyChunky, + Items.PrimatePunch, + Items.GorillaGone, + Items.Coconut, + Items.Peanut, + Items.Grape, + Items.Feather, + Items.Pineapple, + Items.Bongos, + Items.Guitar, + Items.Trombone, + Items.Saxophone, + Items.Triangle, + ], + PlandoItems.RandomSharedMove: [ + Items.Vines, + Items.Swim, + Items.Oranges, + Items.Barrels, + Items.ProgressiveSlam, + Items.HomingAmmo, + Items.SniperSight, + Items.ProgressiveAmmoBelt, + Items.ProgressiveInstrumentUpgrade, + Items.Camera, + Items.Shockwave, + ], + PlandoItems.RandomKey: [ + Items.JungleJapesKey, + Items.AngryAztecKey, + Items.FranticFactoryKey, + Items.GloomyGalleonKey, + Items.FungiForestKey, + Items.CrystalCavesKey, + Items.CreepyCastleKey, + Items.HideoutHelmKey, + ], + PlandoItems.RandomItem: [ + Items.GoldenBanana, + Items.BananaFairy, + Items.BananaMedal, + Items.BattleCrown, + Items.Bean, + Items.Pearl, + Items.RainbowCoin, + ], } diff --git a/randomizer/Lists/Plandomizer.py b/randomizer/Lists/Plandomizer.py index f9c687a5b..c79b76dce 100644 --- a/randomizer/Lists/Plandomizer.py +++ b/randomizer/Lists/Plandomizer.py @@ -285,6 +285,12 @@ def isMinigameLocation(locationEnum: Locations) -> bool: PlannableItems.append({"name": "Blueprint (Tiny)", "value": "TinyBlueprint"}) PlannableItems.append({"name": "Blueprint (Chunky)", "value": "ChunkyBlueprint"}) PlannableItems.append({"name": "Junk Item", "value": "JunkItem"}) +PlannableItems.append({"name": "Random Kong", "value": "RandomKong"}) +PlannableItems.append({"name": "Random Move", "value": "RandomMove"}) +PlannableItems.append({"name": "Random Kong Move", "value": "RandomKongMove"}) +PlannableItems.append({"name": "Random Shared Move", "value": "RandomSharedMove"}) +PlannableItems.append({"name": "Random Key", "value": "RandomKey"}) +PlannableItems.append({"name": "Random Collectible", "value": "RandomItem"}) # The maximum amount of each item that the user is allowed to place. # If a plando item is not here, that item has no limit. @@ -357,6 +363,17 @@ def isMinigameLocation(locationEnum: Locations) -> bool: PlandoItems.ChunkyBlueprint: 8, } +# The maximum amount of items from each group that the user is allowed to +# place. This includes the individual items as well as the "random" item. +PlannableGroupLimits = { + PlandoItems.RandomKong: 5, + PlandoItems.RandomMove: 41, + PlandoItems.RandomKongMove: 25, + PlandoItems.RandomSharedMove: 16, + PlandoItems.RandomKey: 8, + PlandoItems.RandomItem: 293, +} + ############# # MINIGAMES # ############# diff --git a/randomizer/PlandoUtils.py b/randomizer/PlandoUtils.py index bfdb50e29..6164195cb 100644 --- a/randomizer/PlandoUtils.py +++ b/randomizer/PlandoUtils.py @@ -79,6 +79,12 @@ PlandoItems.TinyBlueprint: "Blueprint (Tiny)", PlandoItems.ChunkyBlueprint: "Blueprint (Chunky)", PlandoItems.JunkItem: "Junk Item", + PlandoItems.RandomKong: "Random Kong", + PlandoItems.RandomMove: "Random Move", + PlandoItems.RandomKongMove: "Random Kong Move", + PlandoItems.RandomSharedMove: "Random Shared Move", + PlandoItems.RandomKey: "Random Key", + PlandoItems.RandomItem: "Random Collectible", } @@ -234,6 +240,11 @@ def GetNameFromPlandoItem(plandoItem: PlandoItems) -> str: PlandoItems.LankyBlueprint.name, PlandoItems.TinyBlueprint.name, PlandoItems.ChunkyBlueprint.name, + PlandoItems.RandomMove.name, + PlandoItems.RandomKongMove.name, + PlandoItems.RandomSharedMove.name, + PlandoItems.RandomKey.name, + PlandoItems.RandomItem.name, } kongLocationList = [Locations.DiddyKong.name, Locations.TinyKong.name, Locations.LankyKong.name, Locations.ChunkyKong.name] @@ -278,6 +289,7 @@ def GetNameFromPlandoItem(plandoItem: PlandoItems) -> str: PlandoItems.Trombone.name, PlandoItems.Saxophone.name, PlandoItems.Triangle.name, + PlandoItems.RandomKongMove.name, } # Kong-specific shops have a handful of banned items. @@ -338,6 +350,7 @@ def GetNameFromPlandoItem(plandoItem: PlandoItems) -> str: # This one rock can't have Kongs as a reward. ItemRestrictionsPerLocation[Locations.IslesDonkeyJapesRock.name].update(KongSet) +ItemRestrictionsPerLocation[Locations.IslesDonkeyJapesRock.name].add(PlandoItems.RandomKong.name) # These specific locations cannot have fake items on them. badFakeItemLocationList = [ diff --git a/ui/plando_settings.py b/ui/plando_settings.py index e75a99f6f..b9c9c1e53 100644 --- a/ui/plando_settings.py +++ b/ui/plando_settings.py @@ -14,6 +14,7 @@ lock_key_8_in_helm, populate_plando_options, reset_plando_options_no_prompt, + validate_group_limits, validate_helm_order_no_duplicates, validate_hint_count, validate_hint_text, @@ -124,6 +125,7 @@ async def import_plando_options(jsonString): plando_hide_krool_options(None) lock_key_8_in_helm(None) validate_item_limits(None) + validate_group_limits(None) validate_hint_count(None) validate_smaller_shops_no_conflict(None) validate_shuffle_shops_no_conflict(None) diff --git a/ui/plando_validation.py b/ui/plando_validation.py index ae774449a..2ca161e37 100644 --- a/ui/plando_validation.py +++ b/ui/plando_validation.py @@ -5,9 +5,10 @@ import js from randomizer.Enums.Items import Items +from randomizer.Enums.Kongs import Kongs from randomizer.Enums.Locations import Locations from randomizer.Enums.Minigames import Minigames -from randomizer.Enums.Plandomizer import ItemToPlandoItemMap, PlandoItems +from randomizer.Enums.Plandomizer import ItemToPlandoItemMap, PlandoGroupMap, PlandoItems from randomizer.Enums.Settings import KasplatRandoSetting from randomizer.Lists.Item import StartingMoveOptions from randomizer.Lists.Location import LocationListOriginal as LocationList @@ -20,6 +21,7 @@ KasplatLocationList, MelonCrateLocationList, MinigameLocationList, + PlannableGroupLimits, PlannableItemLimits, ShopLocationKongMap, ShopLocationList, @@ -33,6 +35,7 @@ class ValidationError(IntEnum): """Specific validation failures associated with an element.""" exceeds_item_limits = auto() + exceeds_group_limits = auto() shop_has_shared_and_solo_rewards = auto() smaller_shops_conflict = auto() invalid_hint_text = auto() @@ -294,6 +297,84 @@ def validate_item_limits(evt): mark_option_valid(js.document.getElementById(loc), ValidationError.exceeds_item_limits) +@bindList("click", startingMoveValues, prefix="none-") +@bindList("click", startingMoveValues, prefix="start-") +@bindList("click", startingMoveValues, prefix="random-") +@bindList("change", ItemLocationList, prefix="plando_", suffix="_item") +@bindList("change", ShopLocationList, prefix="plando_", suffix="_item") +@bind("click", "starting_moves_reset") +@bind("click", "starting_moves_start_all") +@bind("change", "plando_starting_kongs_selected") +@bind("change", "select_keys") +@bind("click", "starting_keys_list_selected") +def validate_group_limits(evt): + """Raise an error if a group of items had too many of its members placed.""" + count_dict = count_items() + # Add in starting moves, which also count toward the totals. + startingMoveSet = set() + for startingMove in StartingMoveOptions: + startingMoveElem = js.document.getElementById(f"start-{str(startingMove.value)}") + if startingMoveElem.checked: + plandoMove = ItemToPlandoItemMap[startingMove] + startingMoveSet.add(plandoMove) + if plandoMove in count_dict: + # Add in None, so we don't attempt to mark a nonexistent + # element. + count_dict[plandoMove].append(None) + else: + count_dict[plandoMove] = [None] + # Add in starting Kongs, which also count toward the totals. + startingKongs = js.document.getElementById("plando_starting_kongs_selected") + for kong in startingKongs.selectedOptions: + if kong.value == "": + continue + selectedKong = PlandoItems[kong.value.capitalize()] + if selectedKong in count_dict: + # Add in None, so we don't attempt to mark a nonexistent element. + count_dict[selectedKong].append(None) + else: + count_dict[selectedKong] = [None] + # Check to see if any groups exceeded their limits. + groupTypeNameMap = { + PlandoItems.RandomKong: "Kongs", + PlandoItems.RandomMove: "moves", + PlandoItems.RandomKongMove: "Kong moves", + PlandoItems.RandomSharedMove: "shared moves", + PlandoItems.RandomKey: "keys", + PlandoItems.RandomItem: "collectibles", + } + for plandoGroup, groupItems in PlandoGroupMap.items(): + # For each group, add up the number of times the "random" item was + # placed, and all of the items in that group. If it exceeds the limit, + # that's an error. + groupLocations = count_dict[plandoGroup] if plandoGroup in count_dict else [] + for groupItem in groupItems: + if groupItem in count_dict: + groupLocations.extend(count_dict[groupItem]) + # If we're dealing with keys, add in the number of starting keys. + if plandoGroup == PlandoItems.RandomKey: + startingKeyCount = 0 + if js.document.getElementById("select_keys").checked: + for _ in js.document.getElementById("starting_keys_list_selected").selectedOptions: + startingKeyCount += 1 + else: + startingKeyCount = 8 - int(js.document.getElementById("krool_key_count").value) + for _ in range(0, startingKeyCount): + groupLocations.append(None) + limitExceeded = len(groupLocations) > PlannableGroupLimits[plandoGroup] + for loc in groupLocations: + if loc is not None: + # Throw an error if we've exceeded the limit and we've placed + # the "random" item anywhere. + if limitExceeded and plandoGroup in count_dict: + itemTypeName = groupTypeNameMap[plandoGroup] + maybeStartingItems = f" (This includes starting {itemTypeName}.)" if None in groupLocations else "" + errString = f"A total of {len(groupLocations)} {itemTypeName} have been placed, but the maximum allowed is {PlannableGroupLimits[plandoGroup]} {itemTypeName}.{maybeStartingItems}" + mark_option_invalid(js.document.getElementById(loc), ValidationError.exceeds_group_limits, errString) + else: + mark_option_valid(js.document.getElementById(loc), ValidationError.exceeds_group_limits) + + @bindList("change", ShopLocationList, prefix="plando_", suffix="_item") def validate_shop_kongs(evt): """Raise an error if a shop has both individual and shared rewards.""" @@ -897,6 +978,54 @@ def validate_plando_options(settings_dict: dict) -> list[str]: errString += " (40 Golden Bananas are always allocated to blueprint rewards.)" errList.append(errString) + # Check groups of items, using the same dictionary. + # Add in starting Kongs, which also count toward the totals. + startingKongCount = 0 + for kong in plando_dict["plando_starting_kongs_selected"]: + if kong < 0: + continue + plandoKong = PlandoItems[Kongs(kong).name.capitalize()] + startingKongCount += 1 + if plandoKong in count_dict: + count_dict[plandoKong] += 1 + else: + count_dict[plandoKong] = 1 + groupTypeNameMap = { + PlandoItems.RandomKong: "Kongs", + PlandoItems.RandomMove: "moves", + PlandoItems.RandomKongMove: "Kong moves", + PlandoItems.RandomSharedMove: "shared moves", + PlandoItems.RandomKey: "keys", + PlandoItems.RandomItem: "collectibles", + } + for plandoGroup, groupItems in PlandoGroupMap.items(): + # For each group, add up the number of times the "random" item was + # placed, and all of the items in that group. If it exceeds the limit, + # that's an error. + groupCount = count_dict[plandoGroup] if plandoGroup in count_dict else 0 + for groupItem in groupItems: + if groupItem in count_dict: + groupCount += count_dict[groupItem] + # If we're dealing with keys, add in the number of starting keys. + startingKeyCount = 0 + if plandoGroup == PlandoItems.RandomKey: + startingKeyCount = 0 + if js.document.getElementById("select_keys").checked: + for _ in js.document.getElementById("starting_keys_list_selected").selectedOptions: + startingKeyCount += 1 + else: + startingKeyCount = 8 - int(js.document.getElementById("krool_key_count").value) + groupCount += startingKeyCount + limitExceeded = groupCount > PlannableGroupLimits[plandoGroup] + # Throw an error if we've exceeded the limit and we've placed + # the "random" item anywhere. + if limitExceeded and plandoGroup in count_dict: + itemTypeName = groupTypeNameMap[plandoGroup] + errString = f"A total of {groupCount} {itemTypeName} have been placed, but the maximum allowed is {PlannableGroupLimits[plandoGroup]} {itemTypeName}." + if (plandoGroup == PlandoItems.RandomKong and startingKongCount > 0) or (plandoGroup == PlandoItems.RandomKey and startingKeyCount > 0): + errString += f" (This includes starting {itemTypeName}.)" + errList.append(errString) + # Ensure that no shop has both a shared reward and an individual reward. errString = "Shop vendors cannot have both shared rewards and Kong rewards assigned in the same level." for _, vendors in ShopLocationKongMap.items():