Timespinner: Add Boss Rando Type Options (#4466)

* adding in boss rando type options for Timespinner

* removing new options from the backwards compatible section

* adding in boss rando type options for Timespinner

* removing new options from the backwards compatible section

* re-adding accidentally deleted line

* better documenting the different boss rando types

* adding missing options to the interpret_slot_data function

* making boss override schema more strict and allow for weights

* now actually rolling using the weights for boss rando overrides

* adding boss rando overrides to the spoiler header

* simplifying the schema for the manual boss mappings
This commit is contained in:
Ben Dixon
2025-09-10 16:56:04 -05:00
committed by GitHub
parent 1322ce866e
commit aaaceebd91
3 changed files with 132 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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] = []