from __future__ import annotations import functools import numbers import random from dataclasses import dataclass from itertools import accumulate, chain, combinations from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \ TextChoice, Toggle from .Enemies import enemy_name_to_sprite from .Items import ItemType, l2ac_item_table if TYPE_CHECKING: from BaseClasses import PlandoOptions from worlds.AutoWorld import World class AssembleCustomizableChoices(AssembleOptions): def __new__(mcs, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]) -> AssembleCustomizableChoices: cls: AssembleOptions = super().__new__(mcs, name, bases, attrs) if "extra_options" in attrs: cls.name_lookup.update(enumerate(attrs["extra_options"], start=max(cls.name_lookup) + 1)) return cast(AssembleCustomizableChoices, cls) class RandomGroupsChoice(Choice, metaclass=AssembleCustomizableChoices): extra_options: Optional[Set[str]] random_groups: Dict[str, List[str]] @classmethod def get_option_name(cls, value: int) -> str: if value in cls.options.values(): return next(k for k, v in cls.options.items() if v == value) else: return super().get_option_name(value) @classmethod def from_text(cls, text: str) -> Choice: key: str = text.lower() if key == "random": text = random.choice([o for o in cls.options if o not in cls.random_groups]) elif key in cls.random_groups: text = random.choice(cls.random_groups[key]) return super().from_text(text) class EnemyChoice(TextChoice): _valid_sprites: Dict[str, int] = {enemy_name.lower(): sprite for enemy_name, sprite in enemy_name_to_sprite.items()} def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: if isinstance(self.value, int): return if str(self.value).lower() in self._valid_sprites: return raise ValueError(f"Could not find option '{self.value}' for '{self.__class__.__name__}', known options are:\n" f"{', '.join(self.options)}, {', '.join(enemy_name_to_sprite)}.") @property def sprite(self) -> Optional[int]: return self._valid_sprites.get(str(self.value).lower()) class LevelMixin: xp_coefficients: List[int] = sorted([191, 65, 50, 32, 18, 14, 6, 3, 3, 2, 2, 2, 2] * 8, reverse=True) @classmethod def _to_xp(cls, level: int, *, capsule: bool) -> int: if level == 1: return 0 if level == 99: return 9999999 increment: int = 20 << 8 total: int = increment for lv in range(2, level): increment += (increment * cls.xp_coefficients[lv]) >> 8 total += increment if capsule: total &= 0xFFFFFF00 return (total >> 8) - 10 class BlueChestChance(Range): """The chance of a chest being a blue chest. It is given in units of 1/256, i.e., a value of 25 corresponds to 25/256 ~ 9.77%. If you increase the blue chest chance, then the red chest chance is decreased in return. Supported values: 5 – 75 Default value: 25 (five times as much as in an unmodified game) """ display_name = "Blue chest chance" range_start = 5 range_end = 75 default = 25 @property def chest_type_thresholds(self) -> bytes: ratio: float = (256 - self.value) / (256 - 5) # unmodified chances are: consumable (mostly non-restorative) = 36/256, consumable (restorative) = 58/256, # blue chest = 5/256, spell = 30/256, gear = 45/256 (and the remaining part, weapon = 82/256) chest_type_chances: List[float] = [36 * ratio, 58 * ratio, float(self.value), 30 * ratio, 45 * ratio] return bytes(round(threshold) for threshold in reversed(tuple(accumulate(chest_type_chances)))) class BlueChestCount(Range): """The number of blue chest items that will be in your item pool. The number of blue chests in your world that count as multiworld location checks will be equal this amount plus one more for each party member or capsule monster if you have shuffle_party_members/shuffle_capsule_monsters enabled. (You will still encounter blue chests in your world after all the multiworld location checks have been exhausted, but these chests will then generate items for yourself only.) Supported values: 10 – 100 Default value: 25 """ display_name = "Blue chest count" range_start = 10 range_end = 100 default = 25 overall_max = range_end + 7 + 6 # Have to account for capsule monster and party member items class Boss(RandomGroupsChoice): """Which boss to fight on the final floor. Supported values: lizard_man, big_catfish, regal_goblin, follower_x2, camu, tarantula, pierre, daniele, gades_a, mummy_x4, troll_x3, gades_b, idura_a, lion_x2, idura_b, idura_c, rogue_flower, soldier_x4, gargoyle_x4, venge_ghost, white_dragon_x3, fire_dragon, ghost_ship, tank, gades_c, amon, erim, daos, egg_dragon, master random-low — select a random regular boss, from lizard_man to troll_x3 random-middle — select a random regular boss, from idura_a to gargoyle_x4 random-high — select a random regular boss, from venge_ghost to tank random-sinistral — select a random Sinistral boss Default value: master (same as in an unmodified game) """ display_name = "Boss" option_lizard_man = 0x01 option_big_catfish = 0x02 # 0x03 = Goblin + Skeleton; regular monsters # 0x04 = Goblin; regular monster option_regal_goblin = 0x05 option_follower_x2 = 0x06 option_camu = 0x07 option_tarantula = 0x08 option_pierre = 0x09 option_daniele = 0x0A option_gades_a = 0x0B option_mummy_x4 = 0x0C option_troll_x3 = 0x0D option_gades_b = 0x0E option_idura_a = 0x0F # 0x10 = Pierre; Maxim + Tia only # 0x11 = Daniele; Guy + Selan only option_lion_x2 = 0x12 option_idura_b = 0x13 option_idura_c = 0x14 option_rogue_flower = 0x15 option_soldier_x4 = 0x16 option_gargoyle_x4 = 0x17 option_venge_ghost = 0x18 option_white_dragon_x3 = 0x19 option_fire_dragon = 0x1A option_ghost_ship = 0x1B # 0x1C = Soldier x4; same as 0x16 # 0x1D = Soldier x4; same as 0x16 option_tank = 0x1E option_gades_c = 0x1F option_amon = 0x20 # 0x21 = Gades; same as 0x1F # 0x22 = Amon; same as 0x20 option_erim = 0x23 option_daos = 0x24 option_egg_dragon = 0x25 option_master = 0x26 default = option_master _sprite: Dict[int, int] = { option_lizard_man: 0x9E, option_big_catfish: 0xC5, option_regal_goblin: 0x9D, option_follower_x2: 0x76, option_camu: 0x75, option_tarantula: 0xC6, option_pierre: 0x77, option_daniele: 0x78, option_gades_a: 0x7A, option_mummy_x4: 0xA8, option_troll_x3: 0xA9, option_gades_b: 0x7A, option_idura_a: 0x74, option_lion_x2: 0xB7, option_idura_b: 0x74, option_idura_c: 0x74, option_rogue_flower: 0x96, option_soldier_x4: 0x18, option_gargoyle_x4: 0xC4, option_venge_ghost: 0xD0, option_white_dragon_x3: 0xC3, option_fire_dragon: 0xC0, option_ghost_ship: 0xC8, option_tank: 0xC7, option_gades_c: 0x7A, option_amon: 0x79, option_erim: 0x38, option_daos: 0x7B, option_egg_dragon: 0xC0, option_master: 0x94, } random_groups = { "random-low": ["lizard_man", "big_catfish", "regal_goblin", "follower_x2", "camu", "tarantula", "pierre", "daniele", "mummy_x4", "troll_x3"], "random-middle": ["idura_a", "lion_x2", "idura_b", "idura_c", "rogue_flower", "soldier_x4", "gargoyle_x4"], "random-high": ["venge_ghost", "white_dragon_x3", "fire_dragon", "ghost_ship", "tank"], "random-sinistral": ["gades_c", "amon", "erim", "daos"], } extra_options = set(random_groups) @property def flag(self) -> int: return 0xFE if self.value == Boss.option_master else 0xFF @property def music(self) -> int: return 0x1B if self.value in {Boss.option_master, Boss.option_gades_a, Boss.option_gades_b, Boss.option_gades_c, Boss.option_amon, Boss.option_erim, Boss.option_daos} else 0x19 @property def sprite(self) -> int: return Boss._sprite[self.value] class CapsuleCravingsJPStyle(Toggle): """Make capsule monster cravings behave as in the Japanese version. In the US version, the data that determines which items a capsule monster can request is a mess. It allows only for a very limited selection of items to be requested, and the quality of the selected item is almost always either too low or too high (compared to the capsule monsters current quality preference). This means that, if fed, the requested item will either be rejected by the capsule monster or lead to an unreasonable increase of the quality preference, making further feeding more difficult. This setting provides a fix for the bug described above. If enabled, the capsule monster feeding behavior will be changed to behave analogous to the JP (and EU) version. This means that requests become more varied, while the requested item will be guaranteed to be of the same quality as the capsule monsters current preference. Thus, it can no longer happen that the capsule monster dislikes eating the very item it just requested. Supported values: false, true Default value: false (same as in an unmodified game) """ display_name = "Capsule cravings JP style" class CapsuleStartingForm(NamedRange): """The starting form of your capsule monsters. Supported values: 1 – 4, m Default value: 1 (same as in an unmodified game) """ display_name = "Capsule monster starting form" range_start = 1 range_end = 5 default = 1 special_range_names = { "default": 1, "m": 5, } @property def unlock(self) -> int: if self.value == self.special_range_names["m"]: return 0x0B else: return self.value - 1 class CapsuleStartingLevel(LevelMixin, NamedRange): """The starting level of your capsule monsters. Can be set to the special value party_starting_level to make it the same value as the party_starting_level option. Supported values: 1 – 99, party_starting_level Default value: 1 (same as in an unmodified game) """ display_name = "Capsule monster starting level" range_start = 1 range_end = 99 default = 1 special_range_names = { "default": 1, "party_starting_level": 0, } @property def xp(self) -> int: return self._to_xp(self.value, capsule=True) class CrowdedFloorChance(Range): """The chance of a floor being a crowded floor. It is given in units of 1/256, i.e., a value of 16 corresponds to 16/256 = 6.25%. A crowded floor is a floor where most of the chests are grouped in one room together with many enemies. Supported values: 0 – 255 Default value: 16 (same as in an unmodified game) """ display_name = "Crowded floor chance" range_start = 0 range_end = 255 default = 16 class CustomItemPool(ItemDict, Mapping[str, int]): """Customize your multiworld item pool. Using this option you can place any cave item in your multiworld item pool. (By default, the pool is filled with blue chest items.) Here you can add any valid item from the Lufia II Ancient Cave section of the datapackage (see https://archipelago.gg/datapackage). The value of this option has to be a mapping of item name to count, e.g., to add two Deadly rods and one Dekar Blade: {Deadly rod: 2, Dekar blade: 1} The maximum total amount of custom items you can place is limited by the chosen blue_chest_count; any remaining, non-customized space in the pool will be occupied by random blue chest items. """ display_name = "Custom item pool" value: Dict[str, int] @property def count(self) -> int: return sum(self.values()) def __getitem__(self, key: str) -> int: return self.value.__getitem__(key) def __iter__(self) -> Iterator[str]: return self.value.__iter__() def __len__(self) -> int: return self.value.__len__() class DefaultCapsule(Choice): """Preselect the active capsule monster. (Only has an effect if shuffle_capsule_monsters is set to false.) Supported values: jelze, flash, gusto, zeppy, darbi, sully, blaze Default value: jelze """ display_name = "Default capsule monster" option_jelze = 0x00 option_flash = 0x01 option_gusto = 0x02 option_zeppy = 0x03 option_darbi = 0x04 option_sully = 0x05 option_blaze = 0x06 default = option_jelze class DefaultParty(RandomGroupsChoice, TextChoice): """Preselect the party lineup. (Only has an effect if shuffle_party_members is set to false.) Supported values: Can be set to any valid combination of up to 4 party member initials, e.g.: M — Maxim DGMA — Dekar, Guy, Maxim, and Arty MSTL — Maxim, Selan, Tia, and Lexis random-2p — a random 2-person party random-3p — a random 3-person party random-4p — a random 4-person party Default value: M """ display_name = "Default party lineup" default: Union[str, int] = "M" value: Union[str, int] random_groups = { "random-2p": ["M" + "".join(p) for p in combinations("ADGLST", 1)], "random-3p": ["M" + "".join(p) for p in combinations("ADGLST", 2)], "random-4p": ["M" + "".join(p) for p in combinations("ADGLST", 3)], } vars().update({f"option_{party}": party for party in (*random_groups, "M", *chain(*random_groups.values()))}) _valid_sorted_parties: List[List[str]] = [sorted(party) for party in ("M", *chain(*random_groups.values()))] _members_to_bytes: bytes = bytes.maketrans(b"MSGATDL", bytes(range(7))) def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: if str(self.value).lower() in self.random_groups: return if sorted(str(self.value).upper()) in self._valid_sorted_parties: return raise ValueError(f"Could not find option '{self.value}' for '{self.__class__.__name__}', known options are:\n" f"{', '.join(self.random_groups)}, {', '.join(('M', *chain(*self.random_groups.values())))} " "as well as all permutations of these.") @staticmethod def _flip(i: int) -> int: return {4: 5, 5: 4}.get(i, i) @property def event_script(self) -> bytes: return bytes((*(b for i in bytes(self) if i != 0 for b in (0x2B, i, 0x2E, i + 0x65, 0x1A, self._flip(i) + 1)), 0x1E, 0x0B, len(self) - 1, 0x1C, 0x86, 0x03, *(0x00,) * (6 * (4 - len(self))))) @property def roster(self) -> bytes: return bytes((len(self), *bytes(self), *(0xFF,) * (4 - len(self)))) def __bytes__(self) -> bytes: return str(self.value).upper().encode("ASCII").translate(self._members_to_bytes) def __len__(self) -> int: return len(str(self.value)) class EnemyFloorNumbers(Choice): """Change which enemy types are encountered at which floor numbers. Supported values: vanilla Ninja, e.g., is allowed to appear on the 3 floors B44-B46 shuffle — The existing enemy types are redistributed among nearby floors. Shifts by up to 6 floors are possible. Ninja, e.g., will be allowed to appear on exactly 3 consecutive floors somewhere from B38-B40 to B50-B52 randomize — For each floor, new enemy types are chosen randomly from the set usually possible on floors [-6, +6]. Ninja, e.g., is among the various possible selections for any enemy slot affecting the floors from B38 to B52 Default value: vanilla (same as in an unmodified game) """ display_name = "Enemy floor numbers" option_vanilla = 0 option_shuffle = 1 option_randomize = 2 default = option_vanilla class EnemyMovementPatterns(EnemyChoice): """Change the movement patterns of enemies. Supported values: vanilla shuffle_by_pattern — The existing movement patterns are redistributed among each other. Sprites that usually share a movement pattern will still share movement patterns after shuffling randomize_by_pattern — For each movement pattern, a new one is chosen randomly from the set of existing patterns. Sprites that usually share a movement pattern will still share movement patterns after randomizing shuffle_by_sprite — The existing movement patterns of sprites are redistributed among the enemy sprites. Sprites that usually share a movement pattern can end up with different movement patterns after shuffling randomize_by_sprite — For each sprite, a new movement is chosen randomly from the set of existing patterns. Sprites that usually share a movement pattern can end up with different movement patterns after randomizing singularity — All enemy sprites use the same, randomly selected movement pattern Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. In that case, the movement pattern usually associated with this sprite will be used by all enemy sprites Default value: vanilla (same as in an unmodified game) """ display_name = "Enemy movement patterns" option_vanilla = 0 option_shuffle_by_pattern = 1 option_randomize_by_pattern = 2 option_shuffle_by_sprite = 3 option_randomize_by_sprite = 4 option_singularity = 5 default = option_vanilla class EnemySprites(EnemyChoice): """Change the appearance of enemies. Supported values: vanilla shuffle — The existing sprites are redistributed among the enemy types. This means that, after shuffling, exactly 1 enemy type will be dressing up as the "Red Jelly" sprite randomize — For each enemy type, a new sprite is chosen randomly from the set of existing sprites. This means that, after randomizing, any number of enemy types could end up using the "Red Jelly" sprite singularity — All enemies use the same, randomly selected sprite Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. In this case, the sprite usually associated with that enemy will be used by all enemies Default value: vanilla (same as in an unmodified game) """ display_name = "Enemy sprites" option_vanilla = 0 option_shuffle = 1 option_randomize = 2 option_singularity = 3 default = option_vanilla class ExpModifier(Range): """Percentage modifier for EXP gained from enemies. Supported values: 100 – 500 Default value: 100 (same as in an unmodified game) """ display_name = "EXP modifier" range_start = 100 range_end = 500 default = 100 def __call__(self, exp: bytes) -> bytes: try: return (int.from_bytes(exp, "little") * self.value // 100).to_bytes(2, "little") except OverflowError: return b"\xFF\xFF" class FinalFloor(Range): """The final floor, where the boss resides. Supported values: 2 – 99 Default value: 99 (same as in an unmodified game) """ display_name = "Final floor" range_start = 2 range_end = 99 default = 99 class GearVarietyAfterB9(Toggle): """Fixes a bug that prevents various gear from appearing after B9. Due to an overflow bug in the game, the distribution of red chest gear is impaired after B9. Starting with B10, the number of items available from red chests is severely limited, meaning that red chests will no longer contain any shields, headgear, rings, or jewels (and the selection of body armor is reduced as well). This setting provides a fix for the bug described above. If enabled, red chests beyond B9 will continue to produce shields, headgear, rings, and jewels as intended, while the odds of finding body armor in red chests are decreased as a result. The distributions of red chest weapons, spells, and consumables as well as blue chests are unaffected. Supported values: false, true Default value: false (same as in an unmodified game) """ display_name = "Increase gear variety after B9" class Goal(Choice): """The objective you have to fulfill in order to complete the game. Supported values: boss — defeat the boss on the final floor iris_treasure_hunt — gather the required number of Iris treasures and leave the cave boss_iris_treasure_hunt — complete both the "boss" and the "iris_treasure_hunt" objective (in any order) final_floor — merely reach the final floor Default value: boss """ display_name = "Goal" option_boss = 0x01 option_iris_treasure_hunt = 0x02 option_boss_iris_treasure_hunt = 0x03 option_final_floor = 0x04 default = option_boss class GoldModifier(Range): """Percentage modifier for gold gained from enemies. Supported values: 25 – 400 Default value: 100 (same as in an unmodified game) """ display_name = "Gold modifier" range_start = 25 range_end = 400 default = 100 def __call__(self, gold: bytes) -> bytes: try: return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little") except OverflowError: return b"\xFF\xFF" class HealingFloorChance(Range): """The chance of a floor having a healing tile hidden under a bush. It is given in units of 1/256, i.e., a value of 16 corresponds to 16/256 = 6.25%. Supported values: 0 – 255 Default value: 16 (same as in an unmodified game) """ display_name = "Healing tile floor chance" range_start = 0 range_end = 255 default = 16 class InactiveExpGain(Choice): """The rate at which characters not currently in the active party gain EXP. Supported values: disabled, half, full Default value: disabled (same as in an unmodified game) """ display_name = "Inactive character EXP gain" option_disabled = 0 option_half = 50 option_full = 100 default = option_disabled class InitialFloor(Range): """The initial floor, where you begin your journey. (If this value isn't smaller than the value of final_floor, it will automatically be set to final_floor - 1) Supported values: 1 – 98 Default value: 1 (same as in an unmodified game) """ display_name = "Initial floor" range_start = 1 range_end = 98 default = 1 class IrisFloorChance(Range): """The chance of a floor being able to generate an Iris treasure. It is given in units of 1/256, i.e., a value of 5 corresponds to 5/256 ~ 1.95%. The true chance of a floor holding an Iris treasure you need is usually lower than the chance specified here, e.g., if you have already found 8 of 9 Iris items then the chance of generating the last one is only 1/9 of this value. Supported values: 5 – 255 Default value: 5 (same as in an unmodified game) """ display_name = "Iris treasure floor chance" range_start = 5 range_end = 255 default = 5 class IrisTreasuresRequired(Range): """The number of Iris treasures required to complete the goal. This setting only has an effect if the "iris_treasure_hunt" or "boss_iris_treasure_hunt" goal is active. Supported values: 1 – 9 Default value: 9 """ display_name = "Iris treasures required" range_start = 1 range_end = 9 default = 9 class MasterHp(Range): """The number of hit points of the Master (Only has an effect if boss is set to master.) Supported values: 1 – 9980 Default value: 9980 (same as in an unmodified game) """ display_name = "Master HP" range_start = 1 range_end = 9980 default = 9980 class PartyStartingLevel(LevelMixin, Range): """The starting level of your party members. Supported values: 1 – 99 Default value: 1 (same as in an unmodified game) """ display_name = "Party starting level" range_start = 1 range_end = 99 default = 1 @property def xp(self) -> int: return self._to_xp(self.value, capsule=False) class RunSpeed(Choice): """Modifies the game to allow you to move faster than normal when pressing the Y button. Supported values: disabled, double, triple, quadruple Default value: disabled (same as in an unmodified game) """ display_name = "Run speed" option_disabled = 0x08 option_double = 0x10 option_triple = 0x16 option_quadruple = 0x20 default = option_disabled class ShopInterval(NamedRange): """Place shops after a certain number of floors. E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc., whereas if you set it to 1, then there will be a shop after every single completed floor. Shops will offer a random selection of wares; on deeper floors, more expensive items might appear. You can customize the stock that can appear in shops using the shop_inventory option. You can control how much gold you will be obtaining from enemies using the gold_multiplier option. Supported values: disabled, 1 – 10 Default value: disabled (same as in an unmodified game) """ display_name = "Shop interval" range_start = 1 range_end = 10 default = 0 special_range_names = { "disabled": 0, } class ShopInventory(OptionDict): """Determine the item types that can appear in shops. The value of this option should be a mapping of item categories (or individual items) to weights (non-negative integers), which are used as relative probabilities when it comes to including these things in shops. (The actual contents of the generated shops are selected randomly and are subject to additional constraints such as more expensive things being allowed only on later floors.) Supported keys: non_restorative — a selection of mostly non-restorative red chest consumables restorative — all HP- or MP-restoring red chest consumables blue_chest — all blue chest items spell — all red chest spells gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option, meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you also enabled that option) weapon — all red chest weapons Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5". You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing separate weights for each item you want to include. (Spells, however, cannot be weighted individually and are only available as part of the "spell" category.) Default value: {spell: 30, gear: 45, weapon: 82} """ display_name = "Shop inventory" _special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"} valid_keys = _special_keys | {item for item, data in l2ac_item_table.items() if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST, ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}} default: Dict[str, int] = { "spell": 30, "gear": 45, "weapon": 82, } value: Dict[str, int] def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: super().verify(world, player_name, plando_options) for item, weight in self.value.items(): if not isinstance(weight, numbers.Integral) or weight < 0: raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, " f"but was \"{weight}\".") @property def total(self) -> int: return sum(self.value.values()) @property def non_restorative(self) -> int: return self.value.get("non_restorative", 0) @property def restorative(self) -> int: return self.value.get("restorative", 0) @property def blue_chest(self) -> int: return self.value.get("blue_chest", 0) @property def spell(self) -> int: return self.value.get("spell", 0) @property def gear(self) -> int: return self.value.get("gear", 0) @property def weapon(self) -> int: return self.value.get("weapon", 0) @functools.cached_property def custom(self) -> Dict[int, int]: return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items() if item not in self._special_keys} class ShuffleCapsuleMonsters(Toggle): """Shuffle the capsule monsters into the multiworld. Supported values: false — all 7 capsule monsters are available in the menu and can be selected right away true — you start without capsule monster; 7 new "items" are added to your pool and shuffled into the multiworld; when one of these items is found, the corresponding capsule monster is unlocked for you to use Default value: false (same as in an unmodified game) """ display_name = "Shuffle capsule monsters" @property def unlock(self) -> int: return 0b00000000 if self.value else 0b01111111 class ShufflePartyMembers(Toggle): """Shuffle the party members into the multiworld. Supported values: false — all 6 optional party members are present in the cafe and can be recruited right away true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the multiworld; when one of these items is found, the corresponding party member is unlocked for you to use. While cave diving, you can add or remove unlocked party members by using the character items from the inventory Default value: false (same as in an unmodified game) """ display_name = "Shuffle party members" @property def unlock(self) -> int: return 0b00000000 if self.value else 0b11111100 @dataclass class L2ACOptions(PerGameCommonOptions): blue_chest_chance: BlueChestChance blue_chest_count: BlueChestCount boss: Boss capsule_cravings_jp_style: CapsuleCravingsJPStyle capsule_starting_form: CapsuleStartingForm capsule_starting_level: CapsuleStartingLevel crowded_floor_chance: CrowdedFloorChance custom_item_pool: CustomItemPool death_link: DeathLink default_capsule: DefaultCapsule default_party: DefaultParty enemy_floor_numbers: EnemyFloorNumbers enemy_movement_patterns: EnemyMovementPatterns enemy_sprites: EnemySprites exp_modifier: ExpModifier final_floor: FinalFloor gear_variety_after_b9: GearVarietyAfterB9 goal: Goal gold_modifier: GoldModifier healing_floor_chance: HealingFloorChance inactive_exp_gain: InactiveExpGain initial_floor: InitialFloor iris_floor_chance: IrisFloorChance iris_treasures_required: IrisTreasuresRequired master_hp: MasterHp party_starting_level: PartyStartingLevel run_speed: RunSpeed shop_interval: ShopInterval shop_inventory: ShopInventory shuffle_capsule_monsters: ShuffleCapsuleMonsters shuffle_party_members: ShufflePartyMembers