diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 14ea4f04..47b183c8 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -242,6 +242,81 @@ class CrowdedFloorChance(Range): default = 16 +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 = "M" + + 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, *args, **kwargs) -> 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 FinalFloor(Range): """The final floor, where the boss resides. @@ -439,81 +514,6 @@ class ShufflePartyMembers(Toggle): return 0b00000000 if self.value else 0b11111100 -class StartingCapsule(Choice): - """The capsule monster you start the game with. - - 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 = "Starting 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 StartingParty(RandomGroupsChoice, TextChoice): - """The party you start the game with. - - 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 — start with Maxim - DGMA — start with Dekar, Guy, Maxim, and Arty - MSTL — start with 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 = "Starting party" - default = "M" - - 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, *args, **kwargs) -> 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)) - - l2ac_option_definitions: Dict[str, type(Option)] = { "blue_chest_chance": BlueChestChance, "blue_chest_count": BlueChestCount, @@ -523,6 +523,8 @@ l2ac_option_definitions: Dict[str, type(Option)] = { "capsule_starting_level": CapsuleStartingLevel, "crowded_floor_chance": CrowdedFloorChance, "death_link": DeathLink, + "default_capsule": DefaultCapsule, + "default_party": DefaultParty, "final_floor": FinalFloor, "gear_variety_after_b9": GearVarietyAfterB9, "goal": Goal, @@ -535,6 +537,4 @@ l2ac_option_definitions: Dict[str, type(Option)] = { "run_speed": RunSpeed, "shuffle_capsule_monsters": ShuffleCapsuleMonsters, "shuffle_party_members": ShufflePartyMembers, - "starting_capsule": StartingCapsule, - "starting_party": StartingParty, } diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 4b2268ff..3ff7c50b 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -12,8 +12,8 @@ from worlds.generic.Rules import add_rule, set_rule from .Client import L2ACSNIClient # noqa: F401 from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id from .Locations import l2ac_location_name_to_id, L2ACLocation -from .Options import Boss, CapsuleStartingForm, CapsuleStartingLevel, Goal, l2ac_option_definitions, MasterHp, \ - PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers, StartingParty +from .Options import Boss, CapsuleStartingForm, CapsuleStartingLevel, DefaultParty, Goal, l2ac_option_definitions, \ + MasterHp, PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch from .basepatch import apply_basepatch @@ -64,6 +64,8 @@ class L2ACWorld(World): capsule_starting_level: Optional[CapsuleStartingLevel] crowded_floor_chance: Optional[int] death_link: Optional[int] + default_capsule: Optional[int] + default_party: Optional[DefaultParty] final_floor: Optional[int] gear_variety_after_b9: Optional[int] goal: Optional[int] @@ -76,8 +78,6 @@ class L2ACWorld(World): run_speed: Optional[int] shuffle_capsule_monsters: Optional[ShuffleCapsuleMonsters] shuffle_party_members: Optional[ShufflePartyMembers] - starting_capsule: Optional[int] - starting_party: Optional[StartingParty] @classmethod def stage_assert_generate(cls, _multiworld: MultiWorld) -> None: @@ -103,6 +103,8 @@ class L2ACWorld(World): self.capsule_starting_level = self.multiworld.capsule_starting_level[self.player] self.crowded_floor_chance = self.multiworld.crowded_floor_chance[self.player].value self.death_link = self.multiworld.death_link[self.player].value + self.default_capsule = self.multiworld.default_capsule[self.player].value + self.default_party = self.multiworld.default_party[self.player] self.final_floor = self.multiworld.final_floor[self.player].value self.gear_variety_after_b9 = self.multiworld.gear_variety_after_b9[self.player].value self.goal = self.multiworld.goal[self.player].value @@ -115,8 +117,6 @@ class L2ACWorld(World): self.run_speed = self.multiworld.run_speed[self.player].value self.shuffle_capsule_monsters = self.multiworld.shuffle_capsule_monsters[self.player] self.shuffle_party_members = self.multiworld.shuffle_party_members[self.player] - self.starting_capsule = self.multiworld.starting_capsule[self.player].value - self.starting_party = self.multiworld.starting_party[self.player] if self.capsule_starting_level.value == CapsuleStartingLevel.special_range_names["party_starting_level"]: self.capsule_starting_level.value = self.party_starting_level.value @@ -125,7 +125,7 @@ class L2ACWorld(World): if self.master_hp == MasterHp.special_range_names["scale"]: self.master_hp = MasterHp.scale(self.final_floor) if self.shuffle_party_members: - self.starting_party.value = StartingParty.default + self.default_party.value = DefaultParty.default def create_regions(self) -> None: menu = Region("Menu", RegionType.Generic, "Menu", self.player, self.multiworld) @@ -234,13 +234,13 @@ class L2ACWorld(World): rom_bytearray[0x019E82:0x019E82 + 1] = self.final_floor.to_bytes(1, "little") rom_bytearray[0x01FC75:0x01FC75 + 1] = self.run_speed.to_bytes(1, "little") rom_bytearray[0x01FC81:0x01FC81 + 1] = self.run_speed.to_bytes(1, "little") - rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.starting_party.roster + rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.default_party.roster for offset in range(0x02B395, 0x02B452, 0x1B): rom_bytearray[offset:offset + 1] = self.party_starting_level.value.to_bytes(1, "little") for offset in range(0x02B39A, 0x02B457, 0x1B): rom_bytearray[offset:offset + 3] = self.party_starting_level.xp.to_bytes(3, "little") rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes() - rom_bytearray[0x056AA3:0x056AA3 + 24] = self.starting_party.event_script + rom_bytearray[0x056AA3:0x056AA3 + 24] = self.default_party.event_script rom_bytearray[0x072742:0x072742 + 1] = self.boss.value.to_bytes(1, "little") rom_bytearray[0x072748:0x072748 + 1] = self.boss.flag.to_bytes(1, "little") rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table() @@ -248,7 +248,7 @@ class L2ACWorld(World): rom_bytearray[0x280010:0x280010 + 2] = self.blue_chest_count.to_bytes(2, "little") rom_bytearray[0x280012:0x280012 + 3] = self.capsule_starting_level.xp.to_bytes(3, "little") rom_bytearray[0x280015:0x280015 + 1] = self.initial_floor.to_bytes(1, "little") - rom_bytearray[0x280016:0x280016 + 1] = self.starting_capsule.to_bytes(1, "little") + rom_bytearray[0x280016:0x280016 + 1] = self.default_capsule.to_bytes(1, "little") rom_bytearray[0x280017:0x280017 + 1] = self.iris_treasures_required.to_bytes(1, "little") rom_bytearray[0x280018:0x280018 + 1] = self.shuffle_party_members.unlock.to_bytes(1, "little") rom_bytearray[0x280019:0x280019 + 1] = self.shuffle_capsule_monsters.unlock.to_bytes(1, "little") diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index d7175d47..375c6732 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -42,9 +42,9 @@ Your Party Leader will hold up the item they received when not in a fight or in Retrieve a (customizable) number of iris treasures from the cave; 4) Retrieve the iris treasures *and* defeat the boss - You can also randomize the goal; The blue-haired NPC in front of the cafe can tell you about the selected objective - Customize (or randomize) the chances of encountering blue chests, healing tiles, iris treasures, etc. -- Customize (or randomize) your starting party members and/or party level -- Customize (or randomize) your starting capsule monster and/or capsule monster level as well as form -- Customize (or randomize) the initial and/or final floor numbers +- Customize (or randomize) the default party lineup and capsule monster +- Customize (or randomize) the party starting level as well as capsule monster level and form +- Customize (or randomize) the initial and final floor numbers - Customize (or randomize) the boss that resides on the final floor - Customize start inventory, i.e., begin every run with certain items or spells of your choice - Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to