diff --git a/CommonClient.py b/CommonClient.py index 87fa59cbf2c9..b91294d6ab5d 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -184,6 +184,10 @@ class CommonContext: server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations locations_info: typing.Dict[int, NetworkItem] + # data storage + stored_data: typing.Dict[str, typing.Any] + stored_data_notification_keys: typing.Set[str] + # internals # current message box through kvui _messagebox: typing.Optional["kvui.MessageBox"] = None @@ -219,6 +223,9 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option self.server_locations = set() # all locations the server knows of, missing_location | checked_locations self.locations_info = {} + self.stored_data = {} + self.stored_data_notification_keys = set() + self.input_queue = asyncio.Queue() self.input_requests = 0 @@ -460,6 +467,21 @@ def consume_network_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): Utils.store_data_package_for_checksum(game, game_data) + # data storage + + def set_notify(self, *keys: str) -> None: + """Subscribe to be notified of changes to selected data storage keys. + + The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the + names of the data storage keys to the latest values received from the server. + """ + if new_keys := (set(keys) - self.stored_data_notification_keys): + self.stored_data_notification_keys.update(new_keys) + async_start(self.send_msgs([{"cmd": "Get", + "keys": list(new_keys)}, + {"cmd": "SetNotify", + "keys": list(new_keys)}])) + # DeathLink hooks def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: @@ -728,6 +750,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict): if ctx.locations_scouted: msgs.append({"cmd": "LocationScouts", "locations": list(ctx.locations_scouted)}) + if ctx.stored_data_notification_keys: + msgs.append({"cmd": "Get", + "keys": list(ctx.stored_data_notification_keys)}) + msgs.append({"cmd": "SetNotify", + "keys": list(ctx.stored_data_notification_keys)}) if msgs: await ctx.send_msgs(msgs) if ctx.finished_game: @@ -791,7 +818,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict): # we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]: ctx.on_deathlink(args["data"]) + + elif cmd == "Retrieved": + ctx.stored_data.update(args["keys"]) + elif cmd == "SetReply": + ctx.stored_data[args["key"]] = args["value"] if args["key"] == "EnergyLink": ctx.current_energy_link_value = args["value"] if ctx.ui: diff --git a/Utils.py b/Utils.py index 8635df7367ac..65a97bb2c28a 100644 --- a/Utils.py +++ b/Utils.py @@ -42,7 +42,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.4.1" +__version__ = "0.4.2" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -766,10 +766,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: return buffer -_faf_tasks: "Set[asyncio.Task[None]]" = set() +_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set() -def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None: +def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None: """ Use this to start a task when you don't keep a reference to it or immediately await it, to prevent early garbage collection. "fire-and-forget" @@ -782,6 +782,6 @@ def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] # ``` # This implementation follows the pattern given in that documentation. - task = asyncio.create_task(co, name=name) + task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name) _faf_tasks.add(task) task.add_done_callback(_faf_tasks.discard) diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 58ee7f87f971..bc0cb6a7d8dd 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -1,14 +1,16 @@ import logging import time import typing +import uuid from logging import Logger -from typing import Optional +from typing import Dict, List, Optional from NetUtils import ClientStatus, NetworkItem from worlds.AutoSNIClient import SNIClient from .Enemies import enemy_id_to_name from .Items import start_id as items_start_id from .Locations import start_id as locations_start_id +from .Options import BlueChestCount if typing.TYPE_CHECKING: from SNIClient import SNIContext @@ -59,6 +61,18 @@ async def game_watcher(self, ctx: SNIContext) -> None: if signature != b"ArchipelagoLufia": return + uuid_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 16, 16) + if uuid_data is None: + return + + coop_uuid: uuid.UUID = uuid.UUID(bytes=uuid_data) + if coop_uuid.version != 4: + coop_uuid = uuid.uuid4() + snes_buffered_write(ctx, L2AC_TX_ADDR + 16, coop_uuid.bytes) + + blue_chests_key: str = f"lufia2ac_blue_chests_checked_T{ctx.team}_P{ctx.slot}" + ctx.set_notify(blue_chests_key) + # Goal if not ctx.finished_game: goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10) @@ -78,29 +92,47 @@ async def game_watcher(self, ctx: SNIContext) -> None: await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.") # TX - tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 8) + tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 12) if tx_data is not None: - snes_items_sent = int.from_bytes(tx_data[:2], "little") - client_items_sent = int.from_bytes(tx_data[2:4], "little") - client_ap_items_found = int.from_bytes(tx_data[4:6], "little") - - if client_items_sent < snes_items_sent: - location_id: int = locations_start_id + client_items_sent - location: str = ctx.location_names[location_id] - client_items_sent += 1 - + snes_blue_chests_checked: int = int.from_bytes(tx_data[:2], "little") + snes_ap_items_found: int = int.from_bytes(tx_data[6:8], "little") + snes_other_locations_checked: int = int.from_bytes(tx_data[10:12], "little") + + blue_chests_checked: Dict[str, int] = ctx.stored_data.get(blue_chests_key) or {} + if blue_chests_checked.get(str(coop_uuid), 0) < snes_blue_chests_checked: + blue_chests_checked[str(coop_uuid)] = snes_blue_chests_checked + if blue_chests_key in ctx.stored_data: + await ctx.send_msgs([{ + "cmd": "Set", + "key": blue_chests_key, + "default": {}, + "want_reply": True, + "operations": [{ + "operation": "update", + "value": {str(coop_uuid): snes_blue_chests_checked}, + }], + }]) + + total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end) + snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little")) + location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)] + + loc_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 32, snes_other_locations_checked * 2) + if loc_data is not None: + location_ids.extend(locations_start_id + int.from_bytes(loc_data[2 * i:2 * i + 2], "little") + for i in range(snes_other_locations_checked)) + + if new_location_ids := [loc_id for loc_id in location_ids if loc_id not in ctx.locations_checked]: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_location_ids}]) + for location_id in new_location_ids: ctx.locations_checked.add(location_id) - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location_id]}]) - - snes_logger.info("New Check: %s (%d/%d)" % ( - location, - len(ctx.locations_checked), - len(ctx.missing_locations) + len(ctx.checked_locations))) - snes_buffered_write(ctx, L2AC_TX_ADDR + 2, client_items_sent.to_bytes(2, "little")) + snes_logger.info("%d/%d blue chests" % ( + len(list(loc for loc in ctx.locations_checked if not loc & 0x100)), + len(list(loc for loc in ctx.missing_locations | ctx.checked_locations if not loc & 0x100)))) - ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values()) - if client_ap_items_found < ap_items_found: - snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little")) + client_ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values()) + if client_ap_items_found > snes_ap_items_found: + snes_buffered_write(ctx, L2AC_TX_ADDR + 4, client_ap_items_found.to_bytes(2, "little")) # RX rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4) diff --git a/worlds/lufia2ac/Items.py b/worlds/lufia2ac/Items.py index cfdc0b8c05fc..20159f480a9c 100644 --- a/worlds/lufia2ac/Items.py +++ b/worlds/lufia2ac/Items.py @@ -9,9 +9,11 @@ class ItemType(Enum): BLUE_CHEST = auto() + BOSS = auto() CAPSULE_MONSTER = auto() ENEMY_DROP = auto() ENTRANCE_CHEST = auto() + IRIS_TREASURE = auto() PARTY_MEMBER = auto() RED_CHEST = auto() RED_CHEST_PATCH = auto() @@ -451,15 +453,15 @@ def __init__(self, name: str, classification: ItemClassification, code: Optional # 0x0199: "Bunnysuit" # 0x019A: "Seethru cape" # 0x019B: "Seethru silk" - # 0x019C: "Iris sword" - # 0x019D: "Iris shield" - # 0x019E: "Iris helmet" - # 0x019F: "Iris armor" - # 0x01A0: "Iris ring" - # 0x01A1: "Iris jewel" - # 0x01A2: "Iris staff" - # 0x01A3: "Iris pot" - # 0x01A4: "Iris tiara" + "Iris sword": ItemData(0x039C, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris shield": ItemData(0x039D, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris helmet": ItemData(0x039E, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris armor": ItemData(0x039F, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris ring": ItemData(0x03A0, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris jewel": ItemData(0x03A1, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris staff": ItemData(0x03A2, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris pot": ItemData(0x03A3, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), + "Iris tiara": ItemData(0x03A4, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing), # 0x01A5: "Power jelly" # 0x01A6: "Jewel sonar" # 0x01A7: "Hook" @@ -489,7 +491,7 @@ def __init__(self, name: str, classification: ItemClassification, code: Optional # 0x01BF: "Truth key" # 0x01C0: "Mermaid jade" # 0x01C1: "Engine" - # 0x01C2: "Ancient key" + "Ancient key": ItemData(0x01C2, ItemType.BOSS, ItemClassification.progression_skip_balancing), # 0x01C3: "Pretty flwr." # 0x01C4: "Glass angel" # 0x01C5: "VIP card" diff --git a/worlds/lufia2ac/Locations.py b/worlds/lufia2ac/Locations.py index 7c15bee2deaf..2f433f72e2ae 100644 --- a/worlds/lufia2ac/Locations.py +++ b/worlds/lufia2ac/Locations.py @@ -1,9 +1,15 @@ from typing import Dict from BaseClasses import Location +from .Options import BlueChestCount start_id: int = 0xAC0000 -l2ac_location_name_to_id: Dict[str, int] = {f"Blue chest {i + 1}": (start_id + i) for i in range(88)} + +l2ac_location_name_to_id: Dict[str, int] = { + **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)}, + **{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)}, + "Boss": start_id + 0x01C2, +} class L2ACLocation(Location): diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index df71ef44a90d..f3076e81bf71 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -109,13 +109,13 @@ class BlueChestCount(Range): 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 – 75 + Supported values: 10 – 100 Default value: 25 """ display_name = "Blue chest count" range_start = 10 - range_end = 75 + range_end = 100 default = 25 diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 587792a58df1..e8ae99af56fe 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -5,7 +5,7 @@ from random import Random from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple -from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial +from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial from Options import AssembleOptions from Utils import __version__ from worlds.AutoWorld import WebWorld, World @@ -50,10 +50,11 @@ class L2ACWorld(World): item_name_groups: ClassVar[Dict[str, Set[str]]] = { "Blue chest items": {name for name, data in l2ac_item_table.items() if data.type is ItemType.BLUE_CHEST}, "Capsule monsters": {name for name, data in l2ac_item_table.items() if data.type is ItemType.CAPSULE_MONSTER}, + "Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } - data_version: ClassVar[int] = 1 - required_client_version: Tuple[int, int, int] = (0, 3, 6) + data_version: ClassVar[int] = 2 + required_client_version: Tuple[int, int, int] = (0, 4, 2) # L2ACWorld specific properties rom_name: bytearray @@ -107,17 +108,20 @@ def create_regions(self) -> None: L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon) chest_access.place_locked_item(prog_chest_access) ancient_dungeon.locations.append(chest_access) - treasures = L2ACLocation(self.player, "Iris Treasures", None, ancient_dungeon) - treasures.place_locked_item(L2ACItem("Treasures collected", ItemClassification.progression, None, self.player)) - ancient_dungeon.locations.append(treasures) + for iris in self.item_name_groups["Iris treasures"]: + treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}" + iris_treasure: Location = \ + L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon) + iris_treasure.place_locked_item(self.create_item(iris)) + ancient_dungeon.locations.append(iris_treasure) self.multiworld.regions.append(ancient_dungeon) final_floor = Region("FinalFloor", self.player, self.multiworld, "Ancient Cave Final Floor") ff_reached = L2ACLocation(self.player, "Final Floor reached", None, final_floor) ff_reached.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player)) final_floor.locations.append(ff_reached) - boss = L2ACLocation(self.player, "Boss", None, final_floor) - boss.place_locked_item(L2ACItem("Boss victory", ItemClassification.progression, None, self.player)) + boss: Location = L2ACLocation(self.player, "Boss", self.location_name_to_id["Boss"], final_floor) + boss.place_locked_item(self.create_item("Ancient key")) final_floor.locations.append(boss) self.multiworld.regions.append(final_floor) @@ -155,8 +159,9 @@ def set_rules(self) -> None: set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) - set_rule(self.multiworld.get_location("Iris Treasures", self.player), - lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) + for i in range(9): + set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player), + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) if self.o.shuffle_capsule_monsters: @@ -170,13 +175,14 @@ def set_rules(self) -> None: lambda state: state.has("Final Floor access", self.player) elif self.o.goal == Goal.option_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Treasures collected", self.player) + lambda state: state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required)) elif self.o.goal == Goal.option_boss: self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Boss victory", self.player) + lambda state: state.has("Ancient key", self.player) elif self.o.goal == Goal.option_boss_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player) + lambda state: (state.has("Ancient key", self.player) and + state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required))) def generate_output(self, output_directory: str) -> None: rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm index a2ea539fd4b6..d5275c744320 100644 --- a/worlds/lufia2ac/basepatch/basepatch.asm +++ b/worlds/lufia2ac/basepatch/basepatch.asm @@ -45,8 +45,8 @@ org $8EA721 ; skip master fight dialogue DB $1C,$45,$01 ; L2SASM JMP $8EA5FA+$0145 org $8EA74B ; skip master victory dialogue DB $1C,$AC,$01 ; L2SASM JMP $8EA5FA+$01AC -org $8EA7AA ; skip master key dialogue - DB $1C,$CA,$01 ; L2SASM JMP $8EA5FA+$01CA +org $8EA7AA ; skip master key dialogue and animation + DB $1C,$EE,$01 ; L2SASM JMP $8EA5FA+$01EE org $8EA7F4 ; skip master goodbye dialogue DB $1C,$05,$02 ; L2SASM JMP $8EA5FA+$0205 org $8EA807 ; skip master not fight dialogue @@ -126,7 +126,7 @@ Init: -; transmit checks +; transmit checks from chests pushpc org $8EC1EB JML TX ; overwrites JSL $83F559 @@ -136,11 +136,17 @@ TX: JSL $83F559 ; (overwritten instruction) chest opening animation REP #$20 LDA $7FD4EF ; read chest item ID - BIT.w #$4000 ; test for blue chest flag + BIT.w #$0200 ; test for iris item flag BEQ + - LDA $F02040 ; load check counter + JSR ReportLocationCheck + SEP #$20 + JML $8EC331 ; skip item get process ++: BIT.w #$4200 ; test for blue chest flag + BEQ + + LDA $F02048 ; load total blue chests checked CMP $D08010 ; compare against max AP item number BPL + + LDA $F02040 ; load check counter INC ; increment check counter STA $F02040 ; store check counter SEP #$20 @@ -150,6 +156,41 @@ TX: +; transmit checks from script events +pushpc +org $80A435 + ; DB=$8E, x=0, m=1 + JML ScriptTX ; overwrites STA $7FD4F1 +pullpc + +ScriptTX: + STA $7FD4F1 ; (overwritten instruction) + REP #$20 + LDA $7FD4EF ; read script item id + CMP.w #$01C2 ; test for ancient key + BNE + + JSR ReportLocationCheck + SEP #$20 + JML $80A47F ; skip item get process ++: SEP #$20 + JML $80A439 ; continue item get process + + + +ReportLocationCheck: + PHA ; remember item id + LDA $F0204A ; load other locations count + INC ; increment check counter + STA $F0204A ; store other locations count + DEC + ASL + TAX + PLA + STA $F02060,X ; store item id in checked locations list + RTS + + + ; report event flag based goal completion pushpc org $D09000 @@ -173,9 +214,9 @@ pullpc Goal: TDC - LDA $0797 ; load some event flags (iris sword, iris shield, ..., iris pot) + LDA $0797 ; load EV flags $C8-$CF (iris sword, iris shield, ..., iris pot) TAX - LDA $0798 ; load some event flags (iris tiara, boss, others...) + LDA $0798 ; load EV flags $D0-$D7 (iris tiara, boss, others...) TAY AND.b #$02 ; test boss victory LSR @@ -223,16 +264,32 @@ RX: SpecialItemGet: BPL + ; spells have high bit set JSR LearnSpell ++: BIT.w #$0200 ; iris items + BEQ + + SEC + SBC.w #$039C + ASL + TAX + LDA $8ED8C3,X ; load predefined bitmask with a single bit set + ORA $0797 + STA $0797 ; set iris item EV flag ($C8-$D0) + BRA ++ ++: CMP.w #$01C2 ; ancient key + BNE + + LDA.w #$0200 + ORA $0797 + STA $0797 ; set boss item EV flag ($D1) + BRA ++ +: CMP.w #$01BF ; capsule monster items range from $01B8 to $01BE - BPL + + BPL ++ SBC.w #$01B1 ; party member items range from $01B2 to $01B7 - BMI + + BMI ++ ASL TAX LDA $8ED8C7,X ; load predefined bitmask with a single bit set ORA $F02018 ; set unlock bit for party member/capsule monster STA $F02018 -+: RTS +++: RTS LearnSpell: STA $0A0B @@ -634,7 +691,7 @@ StartInventory: PHX JSR LearnSpell PLX -+: BIT.w #$C000 ; ignore blue chest items (and spells) ++: BIT.w #$C200 ; ignore spells, blue chest items, and iris items BNE + PHX STA $09CF ; specify item ID @@ -1025,12 +1082,16 @@ pullpc ; $F0203D 1 death link enabled ; $F0203E 1 death link sent (monster id + 1) ; $F0203F 1 death link received -; $F02040 2 check counter (snes_items_sent) -; $F02042 2 check counter (client_items_sent) +; $F02040 2 check counter for this save file (snes_blue_chests_checked) +; $F02042 2 RESERVED ; $F02044 2 check counter (client_ap_items_found) ; $F02046 2 check counter (snes_ap_items_found) +; $F02048 2 check counter for the slot (total_blue_chests_checked) +; $F0204A 2 check counter for this save file (snes_other_locations_checked) +; $F02050 16 coop uuid +; $F02060 var list of checked locations ; $F027E0 16 saved RX counters ; $F02800 2 received counter ; $F02802 2 processed counter -; $F02804 inf list of received items -; $F06000 inf architect mode RNG state backups +; $F02804 var list of received items +; $F06000 var architect mode RNG state backups diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 index 7d622537c1c2..33a8452210ae 100644 Binary files a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 and b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 differ diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index d1247a9e200d..64658a7d2746 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -39,9 +39,9 @@ Your Party Leader will hold up the item they received when not in a fight or in ###### Customization options: - Choose a goal for your world. Possible goals are: 1) Reach the final floor; 2) Defeat the boss on the final floor; 3) - Retrieve a (customizable) number of iris treasures from the cave; 4) Retrieve the iris treasures *and* defeat the boss + 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 the chances of encountering blue chests, healing tiles, iris treasures, etc. +- Customize the chances of encountering blue chests, healing tiles, Iris treasures, etc. - Customize the default party lineup and capsule monster - Customize the party starting level as well as capsule monster level and form - Customize the initial and final floor numbers @@ -61,6 +61,7 @@ Your Party Leader will hold up the item they received when not in a fight or in - You can elect to lock the cave layout for the next run, giving you exactly the same floors and red chest contents as on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave Entrance +- Multiple people can connect to the same slot and collaboratively search for Iris treasures and blue chests - Always start with Providence already in your inventory. (It is no longer obtained from red chests) - (optional) Run button that allows you to move at faster than normal speed diff --git a/worlds/lufia2ac/test/TestGoal.py b/worlds/lufia2ac/test/TestGoal.py index 6dc78e66d279..1eaf5a151584 100644 --- a/worlds/lufia2ac/test/TestGoal.py +++ b/worlds/lufia2ac/test/TestGoal.py @@ -4,7 +4,7 @@ class TestDefault(L2ACTestBase): def test_everything(self) -> None: - self.collect_all_but(["Boss victory"]) + self.collect_all_but(["Ancient key"]) self.assertBeatable(True) def test_nothing(self) -> None: @@ -17,7 +17,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase): } def test_everything(self) -> None: - self.collect_all_but(["Boss victory"]) + self.collect_all_but(["Ancient key"]) self.assertBeatable(True) def test_best_party(self) -> None: @@ -25,7 +25,7 @@ def test_best_party(self) -> None: self.assertBeatable(True) def test_no_darbi(self) -> None: - self.collect_all_but(["Boss victory", "DARBI"]) + self.collect_all_but(["Ancient key", "DARBI"]) self.assertBeatable(False) @@ -35,7 +35,7 @@ class TestShufflePartyMembers(L2ACTestBase): } def test_everything(self) -> None: - self.collect_all_but(["Boss victory"]) + self.collect_all_but(["Ancient key"]) self.assertBeatable(True) def test_best_party(self) -> None: @@ -43,15 +43,15 @@ def test_best_party(self) -> None: self.assertBeatable(True) def test_no_dekar(self) -> None: - self.collect_all_but(["Boss victory", "Dekar"]) + self.collect_all_but(["Ancient key", "Dekar"]) self.assertBeatable(False) def test_no_guy(self) -> None: - self.collect_all_but(["Boss victory", "Guy"]) + self.collect_all_but(["Ancient key", "Guy"]) self.assertBeatable(False) def test_no_arty(self) -> None: - self.collect_all_but(["Boss victory", "Arty"]) + self.collect_all_but(["Ancient key", "Arty"]) self.assertBeatable(False) @@ -62,7 +62,7 @@ class TestShuffleBoth(L2ACTestBase): } def test_everything(self) -> None: - self.collect_all_but(["Boss victory"]) + self.collect_all_but(["Ancient key"]) self.assertBeatable(True) def test_best_party(self) -> None: @@ -70,17 +70,17 @@ def test_best_party(self) -> None: self.assertBeatable(True) def test_no_dekar(self) -> None: - self.collect_all_but(["Boss victory", "Dekar"]) + self.collect_all_but(["Ancient key", "Dekar"]) self.assertBeatable(False) def test_no_guy(self) -> None: - self.collect_all_but(["Boss victory", "Guy"]) + self.collect_all_but(["Ancient key", "Guy"]) self.assertBeatable(False) def test_no_arty(self) -> None: - self.collect_all_but(["Boss victory", "Arty"]) + self.collect_all_but(["Ancient key", "Arty"]) self.assertBeatable(False) def test_no_darbi(self) -> None: - self.collect_all_but(["Boss victory", "DARBI"]) + self.collect_all_but(["Ancient key", "DARBI"]) self.assertBeatable(False)