From 80d7ac416493a540548aad67981202a1483b5e53 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:41:32 -0500 Subject: [PATCH] KDL3: RC1 Fixes and Enhancement (#3022) * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs --- worlds/kdl3/Client.py | 17 +++-- worlds/kdl3/Regions.py | 85 +++++++++++---------- worlds/kdl3/Rules.py | 2 +- worlds/kdl3/docs/en_Kirby's Dream Land 3.md | 5 +- worlds/kdl3/docs/setup_en.md | 6 +- worlds/kdl3/test/test_locations.py | 3 +- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index a1e68f8b67e..e33a680bc02 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -36,8 +36,10 @@ KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020 KDL3_IS_DEMO = SRAM_1_START + 0x5AD5 -KDL3_GAME_STATE = SRAM_1_START + 0x36D0 KDL3_GAME_SAVE = SRAM_1_START + 0x3617 +KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F +KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641 +KDL3_GAME_STATE = SRAM_1_START + 0x36D0 KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF KDL3_KIRBY_HP = SRAM_1_START + 0x39D1 KDL3_BOSS_HP = SRAM_1_START + 0x39D5 @@ -46,8 +48,6 @@ KDL3_HEART_STARS = SRAM_1_START + 0x53A7 KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD -KDL3_CURRENT_WORLD = SRAM_1_START + 0x53CF -KDL3_CURRENT_LEVEL = SRAM_1_START + 0x53D3 KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5 KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1 KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4 @@ -74,7 +74,9 @@ 0x0202: " was out-numbered by Pon & Con.", 0x0203: " was defeated by Ado's powerful paintings.", 0x0204: " was clobbered by King Dedede.", - 0x0205: " lost their battle against Dark Matter." + 0x0205: " lost their battle against Dark Matter.", + 0x0300: " couldn't overcome the Boss Butch.", + 0x0400: " is bad at jumping.", }) @@ -281,6 +283,11 @@ async def game_watcher(self, ctx) -> None: for i in range(5): level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) self.levels[i] = unpack("HHHHHHH", level_data) + self.levels[5] = [0x0205, # Hyper Zone + 0, # MG-5, can't send from here + 0x0300, # Boss Butch + 0x0400, # Jumping + 0, 0, 0] if self.consumables is None: consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1) @@ -314,7 +321,7 @@ async def game_watcher(self, ctx) -> None: current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0] current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0] currently_dead = current_hp[0] == 0x00 - message = deathlink_messages[self.levels[current_world][current_level - 1]] + message = deathlink_messages[self.levels[current_world][current_level]] await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}") recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2) diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ed0d8658661..794a565e0a5 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -28,16 +28,30 @@ 0x77001C, # 5-4 needs Burning } +first_world_limit = { + # We need to limit the number of very restrictive stages in level 1 on solo gens + *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks + 0x770007, + 0x770008, + 0x770013, + 0x77001E, -def generate_valid_level(level, stage, possible_stages, slot_random): - new_stage = slot_random.choice(possible_stages) - if level == 1 and stage == 0 and new_stage in first_stage_blacklist: - return generate_valid_level(level, stage, possible_stages, slot_random) - else: - return new_stage +} + + +def generate_valid_level(world: "KDL3World", level, stage, possible_stages, placed_stages): + new_stage = world.random.choice(possible_stages) + if level == 1: + if stage == 0 and new_stage in first_stage_blacklist: + return generate_valid_level(world, level, stage, possible_stages, placed_stages) + elif not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and \ + new_stage in first_world_limit and \ + sum(p_stage in first_world_limit for p_stage in placed_stages) >= 2: + return generate_valid_level(world, level, stage, possible_stages, placed_stages) + return new_stage -def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing.Dict[int, Region]): +def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]): level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) rooms: typing.Dict[str, KDL3Room] = dict() @@ -49,8 +63,8 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else None for location in room_entry["locations"] if (not any(x in location for x in ["1-Up", "Maxim"]) or - world.options.consumables.value) and ("Star" not in location - or world.options.starsanity.value)}, + world.options.consumables.value) and ("Star" not in location + or world.options.starsanity.value)}, KDL3Location) rooms[room.name] = room for location in room.locations: @@ -62,33 +76,25 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing world.multiworld.regions.extend(world.rooms) first_rooms: typing.Dict[int, KDL3Room] = dict() - if door_shuffle: - # first, we need to generate the notable edge cases - # 5-6 is the first, being the most restrictive - # half of its rooms are required to be vanilla, but can be in different orders - # the room before it *must* contain the copy ability required to unlock the room's goal - - raise NotImplementedError() - else: - for name, room in rooms.items(): - if room.room == 0: - if room.stage == 7: - first_rooms[0x770200 + room.level - 1] = room - else: - first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room - exits = dict() - for def_exit in room.default_exits: - target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" - access_rule = tuple(def_exit["access_rule"]) - exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player) - room.add_exits( - exits.keys(), - exits - ) - if world.options.open_world: - if any("Complete" in location.name for location in room.locations): - room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None}, - KDL3Location) + for name, room in rooms.items(): + if room.room == 0: + if room.stage == 7: + first_rooms[0x770200 + room.level - 1] = room + else: + first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room + exits = dict() + for def_exit in room.default_exits: + target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" + access_rule = tuple(def_exit["access_rule"]) + exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player) + room.add_exits( + exits.keys(), + exits + ) + if world.options.open_world: + if any("Complete" in location.name for location in room.locations): + room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None}, + KDL3Location) for level in world.player_levels: for stage in range(6): @@ -102,7 +108,7 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing if world.options.open_world or stage == 0: level_regions[level].add_exits([first_rooms[proper_stage].name]) else: - world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage-1]], + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]], world.player).parent_region.add_exits([first_rooms[proper_stage].name]) level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) @@ -141,8 +147,7 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) or (enforce_pattern == enforce_world) ] - new_stage = generate_valid_level(level, stage, stage_candidates, - world.random) + new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level]) possible_stages.remove(new_stage) levels[level][stage] = new_stage except Exception: @@ -218,7 +223,7 @@ def create_levels(world: "KDL3World") -> None: level_shuffle == 1, level_shuffle == 2) - generate_rooms(world, False, levels) + generate_rooms(world, levels) level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location) diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/Rules.py index 91abc21d062..6a85ef84f05 100644 --- a/worlds/kdl3/Rules.py +++ b/worlds/kdl3/Rules.py @@ -264,7 +264,7 @@ def set_rules(world: "KDL3World") -> None: for r in [range(1, 31), range(44, 51)]: for i in r: set_rule(world.multiworld.get_location(f"Cloudy Park 4 - Star {i}", world.player), - lambda state: can_reach_clean(state, world.player)) + lambda state: can_reach_coo(state, world.player)) for i in [18, *list(range(20, 25))]: set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player), lambda state: can_reach_ice(state, world.player)) diff --git a/worlds/kdl3/docs/en_Kirby's Dream Land 3.md b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md index c1e36fed546..008ee0fcc1e 100644 --- a/worlds/kdl3/docs/en_Kirby's Dream Land 3.md +++ b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md @@ -1,8 +1,8 @@ # Kirby's Dream Land 3 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -15,6 +15,7 @@ as Heart Stars, 1-Ups, and Invincibility Candy will be shuffled into the pool fo - Purifying a boss after acquiring a certain number of Heart Stars (indicated by their portrait flashing in the level select) - If enabled, 1-Ups and Maxim Tomatoes +- If enabled, every single Star Piece within a stage ## When the player receives an item, what happens? A sound effect will play, and Kirby will immediately receive the effects of that item, such as being able to receive Copy Abilities from enemies that diff --git a/worlds/kdl3/docs/setup_en.md b/worlds/kdl3/docs/setup_en.md index a13a0f1a74c..a73d248d4d1 100644 --- a/worlds/kdl3/docs/setup_en.md +++ b/worlds/kdl3/docs/setup_en.md @@ -43,8 +43,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page on the website allows you to configure -your personal settings and export a config file from them. +The [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page on the website allows you to configure +your personal options and export a config file from them. ### Verifying your config file @@ -53,7 +53,7 @@ If you would like to validate your config file to make sure it works, you may do ## Generating a Single-Player Game -1. Navigate to the [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page, configure your options, +1. Navigate to the [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page, configure your options, and click the "Generate Game" button. 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 543f0d83926..433b4534d1e 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -33,7 +33,8 @@ def test_simple_heart_stars(self): self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) self.run_location_test(LocationName.iceberg_samus, ["Ice"]) self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) - self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"]) + self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", + "Stone", "Ice"]) def run_location_test(self, location: str, itempool: typing.List[str]): items = itempool.copy()