diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 35d9d630..23a688b8 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -53,6 +53,75 @@ class BossRando(Choice): option_unscaled = 2 alias_true = 1 +class BossRandoType(Choice): + """ + Sets what type of boss shuffling occurs. + Shuffle: Bosses will be shuffled amongst each other + Chaos: Bosses will be randomized with the chance of duplicate bosses + Singularity: All bosses will be replaced with a single boss + Manual: Bosses will be placed according to the Boss Rando Overrides setting + """ + display_name = "Boss Randomization Type" + option_shuffle = 0 + option_chaos = 1 + option_singularity = 2 + option_manual = 3 + +class BossRandoOverrides(OptionDict): + """ + Manual mapping of bosses to the boss they will be replaced with. + Bosses that you don't specify will be the vanilla boss. + """ + bosses = [ + "FelineSentry", + "Varndagroth", + "AzureQueen", + "GoldenIdol", + "Aelana", + "Maw", + "Cantoran", + "Genza", + "Nuvius", + "Vol", + "Prince", + "Xarion", + "Ravenlord", + "Ifrit", + "Sandman", + "Nightmare", + ] + + schema = Schema( + { + Optional(Or(*bosses)): Or( + And( + {Optional(boss): And(int, lambda n: n >= 0) for boss in bosses}, + lambda d: any(v > 0 for v in d.values()), + ), + *bosses + ) + } + ) + display_name = "Boss Rando Overrides" + default = { + "FelineSentry": "FelineSentry", + "Varndagroth": "Varndagroth", + "AzureQueen": "AzureQueen", + "GoldenIdol": "GoldenIdol", + "Aelana": "Aelana", + "Maw": "Maw", + "Cantoran": "Cantoran", + "Genza": "Genza", + "Nuvius": "Nuvius", + "Vol": "Vol", + "Prince": "Prince", + "Xarion": "Xarion", + "Ravenlord": "Ravenlord", + "Ifrit": "Ifrit", + "Sandman": "Sandman", + "Nightmare": "Nightmare" + } + class EnemyRando(Choice): "Wheter enemies will be randomized, and if their damage/hp should be scaled." display_name = "Enemy Randomization" @@ -420,6 +489,8 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): cantoran: Cantoran lore_checks: LoreChecks boss_rando: BossRando + boss_rando_type: BossRandoType + boss_rando_overrides: BossRandoOverrides enemy_rando: EnemyRando damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 96551ea7..916de346 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -21,6 +21,8 @@ class PreCalculatedWeights: flood_lake_serene_bridge: bool flood_lab: bool + boss_rando_overrides: Dict[str, str] + def __init__(self, options: TimespinnerOptions, random: Random): if options.rising_tides: weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options) @@ -51,6 +53,26 @@ class PreCalculatedWeights: self.flood_lake_serene_bridge = False self.flood_lab = False + boss_rando_weights_overrides: Dict[str, Union[str, Dict[str, int]]] = self.get_boss_rando_weights_overrides(options) + self.boss_rando_overrides = { + "FelineSentry": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "FelineSentry"), + "Varndagroth": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Varndagroth"), + "AzureQueen": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "AzureQueen"), + "GoldenIdol": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "GoldenIdol"), + "Aelana": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Aelana"), + "Maw": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Maw"), + "Cantoran": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Cantoran"), + "Genza": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Genza"), + "Nuvius": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Nuvius"), + "Vol": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Vol"), + "Prince": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Prince"), + "Xarion": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Xarion"), + "Ravenlord": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Ravenlord"), + "Ifrit": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Ifrit"), + "Sandman": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Sandman"), + "Nightmare": self.roll_boss_rando_setting(random, boss_rando_weights_overrides, "Nightmare") + } + self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion, self.flood_lab) @@ -142,3 +164,32 @@ class PreCalculatedWeights: return True, True elif result == "FloodedWithSavePointAvailable": return True, False + + @staticmethod + def get_boss_rando_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]: + weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ + options.boss_rando_overrides.value + + default_weights: Dict[str, Dict[str, int]] = options.boss_rando_overrides.default + + if not weights_overrides_option: + weights_overrides_option = default_weights + else: + for key, weights in default_weights.items(): + if not key in weights_overrides_option: + weights_overrides_option[key] = weights + + return weights_overrides_option + + @staticmethod + def roll_boss_rando_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]], + key: str) -> str: + + weights: Union[Dict[str, int], str] = all_weights[key] + + if isinstance(weights, dict): + result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + else: + result: str = weights + + return result diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 0bd9c7d1..3f117837 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Item, Tutorial, ItemClassification from .Items import get_item_names_per_category from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items, pyramid_start_starter_progression_items from .Locations import get_location_datas, EventId -from .Options import BackwardsCompatiableTimespinnerOptions, Toggle +from .Options import BackwardsCompatiableTimespinnerOptions, Toggle, BossRandoType from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld @@ -104,6 +104,8 @@ class TimespinnerWorld(World): "Cantoran": self.options.cantoran.value, "LoreChecks": self.options.lore_checks.value, "BossRando": self.options.boss_rando.value, + "BossRandoType": self.options.boss_rando_type.value, + "BossRandoOverrides": self.precalculated_weights.boss_rando_overrides, "EnemyRando": self.options.enemy_rando.value, "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, @@ -181,6 +183,8 @@ class TimespinnerWorld(World): self.options.cantoran.value = slot_data["Cantoran"] self.options.lore_checks.value = slot_data["LoreChecks"] self.options.boss_rando.value = slot_data["BossRando"] + self.options.boss_rando_type.value = slot_data["BossRandoType"] + self.precalculated_weights.boss_rando_overrides = slot_data["BossRandoOverrides"] self.options.damage_rando.value = slot_data["DamageRando"] self.options.damage_rando_overrides.value = slot_data["DamageRandoOverrides"] self.options.hp_cap.value = slot_data["HpCap"] @@ -201,6 +205,7 @@ class TimespinnerWorld(World): self.options.rising_tides.value = slot_data["RisingTides"] self.options.unchained_keys.value = slot_data["UnchainedKeys"] self.options.back_to_the_future.value = slot_data["PresentAccessWithWheelAndSpindle"] + self.options.prism_break.value = slot_data["PrismBreak"] self.options.traps.value = slot_data["Traps"] self.options.death_link.value = slot_data["DeathLink"] # Readonly slot_data["StinkyMaw"] @@ -237,7 +242,10 @@ class TimespinnerWorld(World): spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - + + if self.options.boss_rando.value and self.options.boss_rando_type.value == BossRandoType.option_manual: + spoiler_handle.write(f'Selected bosses: {self.precalculated_weights.boss_rando_overrides}\n') + if self.options.rising_tides: flooded_areas: List[str] = []