From b0638b993d0e14c64a5bdffb26377c076a910084 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:57:38 -0500 Subject: [PATCH 01/78] FFMQ: Fix starting progressive gear (#2685) --- worlds/ffmq/Items.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index 3eab5dd532..d0898d7e81 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -223,11 +223,6 @@ for item, data in item_table.items(): def create_items(self) -> None: items = [] starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") - if self.multiworld.progressive_gear[self.player]: - for item_group in prog_map: - if starting_weapon in self.item_name_groups[item_group]: - starting_weapon = prog_map[item_group] - break self.multiworld.push_precollected(self.create_item(starting_weapon)) self.multiworld.push_precollected(self.create_item("Steel Armor")) if self.multiworld.sky_coin_mode[self.player] == "start_with": From 570ba28bee0482c61bb762e7c6fd113c76897539 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 10 Jan 2024 00:22:04 -0500 Subject: [PATCH 02/78] KH2: Fix Terra logic (#2676) --- worlds/kh2/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 7c5551dbd5..e40f38f3ae 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules): RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), RegionName.Ag: lambda state: self.ag_unlocked(state, 1), - RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state), RegionName.Bc: lambda state: self.bc_unlocked(state, 1), RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), @@ -417,7 +417,7 @@ class KH2FightRules(KH2Rules): RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state), RegionName.OldPete: lambda state: self.get_old_pete_rules(), RegionName.FuturePete: lambda state: self.get_future_pete_rules(state), - RegionName.Terra: lambda state: self.get_terra_rules(state), + RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player), RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state), RegionName.Barbosa: lambda state: self.get_barbosa_rules(state), RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(), From 82410fd554d18a26d470540e0c85b33be1ced48c Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 10 Jan 2024 08:52:43 -0800 Subject: [PATCH 03/78] Zillion: client win location check (#2682) --- worlds/zillion/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index ac73f6db50..b10507aaf8 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -16,7 +16,7 @@ from zilliandomizer.utils.loc_name_maps import id_to_loc from zilliandomizer.options import Chars from zilliandomizer.patch import RescueInfo -from .id_maps import make_id_to_others +from .id_maps import loc_name_to_id, make_id_to_others from .config import base_id, zillion_map @@ -323,6 +323,7 @@ class ZillionContext(CommonContext): elif isinstance(event_from_game, events.WinEventFromGame): if not self.finished_game: async_start(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]}, {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} ])) self.finished_game = True From e082c83dc765debf3181b1cbdd37390065159315 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:22:54 -0500 Subject: [PATCH 04/78] KH2: Fix plando breaking because of keyblades (#2678) --- worlds/kh2/Rules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index e40f38f3ae..111d12d0d6 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -1,7 +1,7 @@ from typing import Dict, Callable, TYPE_CHECKING from BaseClasses import CollectionState -from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table +from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks from .Names import LocationName, ItemName, RegionName from worlds.generic.Rules import add_rule, forbid_items, add_item_rule @@ -266,6 +266,8 @@ class KH2WorldRules(KH2Rules): add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) elif location.name in Donald_Checks: add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) + else: + add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys()) def set_kh2_goal(self): final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) From 4045c6a9cfbfdb1617df116737ce59e369b48f6c Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:56:43 -0500 Subject: [PATCH 05/78] KH2: Fix relative import (#2702) --- worlds/kh2/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 7ba7c0082d..b7caf74370 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool -from worlds.kh2 import default_itempool_option +from . import default_itempool_option class SoraEXP(Range): From 388413fcdd258190b3552859dd2886cd0ab80466 Mon Sep 17 00:00:00 2001 From: Flori <104439321+FlorianDeSmedt@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:10:25 +0100 Subject: [PATCH 06/78] Hollow Knight: Fix fragile/unbreakable charm variants counting as 2 distinct charms in logic (#2683) Deletes CHARM of the 3 unbreakable charms, adds 0.5 CHARM to Queen_fragment, King_Fragment and Void_heart --- worlds/hk/ExtractedData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/ExtractedData.py b/worlds/hk/ExtractedData.py index cf796050e4..0cbbc8bf85 100644 --- a/worlds/hk/ExtractedData.py +++ b/worlds/hk/ExtractedData.py @@ -3,7 +3,7 @@ connectors = {'Room_temple[left1]': 'Crossroads_02[door1]', 'Tutorial_01[right1]': 'Town[left1]', 'Tutorial_01[top1]': None, 'Tutorial_01[top2]': 'Cliffs_02[bot1]', 'Town[left1]': 'Tutorial_01[right1]', 'Town[bot1]': 'Crossroads_01[top1]', 'Town[right1]': 'Mines_10[left1]', 'Town[top1]': None, 'Town[door_station]': 'Room_Town_Stag_Station[left1]', 'Town[door_sly]': 'Room_shop[left1]', 'Town[door_mapper]': 'Room_mapper[left1]', 'Town[door_jiji]': 'Room_Ouiji[left1]', 'Town[door_bretta]': 'Room_Bretta[right1]', 'Town[room_divine]': 'Grimm_Divine[left1]', 'Town[room_grimm]': 'Grimm_Main_Tent[left1]', 'Room_shop[left1]': 'Town[door_sly]', 'Room_Town_Stag_Station[left1]': 'Town[door_station]', 'Room_mapper[left1]': 'Town[door_mapper]', 'Room_Bretta[right1]': 'Town[door_bretta]', 'Room_Ouiji[left1]': 'Town[door_jiji]', 'Grimm_Divine[left1]': 'Town[room_divine]', 'Grimm_Main_Tent[left1]': 'Town[room_grimm]', 'Crossroads_01[top1]': 'Town[bot1]', 'Crossroads_01[left1]': 'Crossroads_07[right1]', 'Crossroads_01[right1]': 'Crossroads_02[left1]', 'Crossroads_02[left1]': 'Crossroads_01[right1]', 'Crossroads_02[door1]': 'Room_temple[left1]', 'Crossroads_02[right1]': 'Crossroads_39[left1]', 'Crossroads_03[right1]': 'Crossroads_15[left1]', 'Crossroads_03[right2]': 'Mines_33[left1]', 'Crossroads_03[left1]': 'Crossroads_21[right1]', 'Crossroads_03[left2]': 'Crossroads_47[right1]', 'Crossroads_03[bot1]': 'Crossroads_19[top1]', 'Crossroads_03[top1]': 'Crossroads_16[bot1]', 'Crossroads_04[left1]': 'Crossroads_19[right1]', 'Crossroads_04[top1]': 'Crossroads_27[bot1]', 'Crossroads_04[door_Mender_House]': 'Room_Mender_House[left1]', 'Crossroads_04[door1]': 'Room_ruinhouse[left1]', 'Crossroads_04[door_charmshop]': 'Room_Charm_Shop[left1]', 'Crossroads_04[right1]': 'Crossroads_50[left1]', 'Crossroads_05[left1]': 'Crossroads_07[right2]', 'Crossroads_05[right1]': 'Crossroads_40[left1]', 'Crossroads_06[left1]': 'Crossroads_33[right1]', 'Crossroads_06[door1]': 'Crossroads_ShamanTemple[left1]', 'Crossroads_06[right1]': 'Crossroads_10[left1]', 'Crossroads_07[left1]': 'Crossroads_38[right1]', 'Crossroads_07[left2]': 'Crossroads_11_alt[right1]', 'Crossroads_07[left3]': 'Crossroads_25[right1]', 'Crossroads_07[right1]': 'Crossroads_01[left1]', 'Crossroads_07[right2]': 'Crossroads_05[left1]', 'Crossroads_07[bot1]': 'Crossroads_33[top1]', 'Crossroads_08[left1]': 'Crossroads_33[right2]', 'Crossroads_08[left2]': 'Crossroads_18[right1]', 'Crossroads_08[right1]': 'Crossroads_30[left1]', 'Crossroads_08[right2]': 'Crossroads_13[left1]', 'Crossroads_09[left1]': 'Crossroads_36[right2]', 'Crossroads_09[right1]': 'Crossroads_33[left1]', 'Crossroads_10[left1]': 'Crossroads_06[right1]', 'Crossroads_10[right1]': 'Crossroads_21[left1]', 'Crossroads_11_alt[left1]': 'Fungus1_01[right1]', 'Crossroads_11_alt[right1]': 'Crossroads_07[left2]', 'Crossroads_12[left1]': 'Crossroads_35[right1]', 'Crossroads_12[right1]': 'Crossroads_33[left2]', 'Crossroads_13[left1]': 'Crossroads_08[right2]', 'Crossroads_13[right1]': 'Crossroads_42[left1]', 'Crossroads_14[left1]': 'Crossroads_39[right1]', 'Crossroads_14[left2]': 'Crossroads_16[right1]', 'Crossroads_14[right1]': 'Crossroads_48[left1]', 'Crossroads_14[right2]': 'Crossroads_45[left1]', 'Crossroads_15[left1]': 'Crossroads_03[right1]', 'Crossroads_15[right1]': 'Crossroads_27[left1]', 'Crossroads_16[left1]': 'Crossroads_40[right1]', 'Crossroads_16[right1]': 'Crossroads_14[left2]', 'Crossroads_16[bot1]': 'Crossroads_03[top1]', 'Crossroads_18[right1]': 'Crossroads_08[left2]', 'Crossroads_18[right2]': 'Crossroads_52[left1]', 'Crossroads_18[bot1]': 'Fungus2_06[top1]', 'Crossroads_19[right1]': 'Crossroads_04[left1]', 'Crossroads_19[top1]': 'Crossroads_03[bot1]', 'Crossroads_19[left1]': 'Crossroads_42[right1]', 'Crossroads_19[left2]': 'Crossroads_43[right1]', 'Crossroads_21[left1]': 'Crossroads_10[right1]', 'Crossroads_21[right1]': 'Crossroads_03[left1]', 'Crossroads_21[top1]': 'Crossroads_22[bot1]', 'Crossroads_22[bot1]': 'Crossroads_21[top1]', 'Crossroads_25[right1]': 'Crossroads_07[left3]', 'Crossroads_25[left1]': 'Crossroads_36[right1]', 'Crossroads_27[right1]': 'Crossroads_46[left1]', 'Crossroads_27[bot1]': 'Crossroads_04[top1]', 'Crossroads_27[left1]': 'Crossroads_15[right1]', 'Crossroads_27[left2]': 'Crossroads_31[right1]', 'Crossroads_30[left1]': 'Crossroads_08[right1]', 'Crossroads_31[right1]': 'Crossroads_27[left2]', 'Crossroads_33[top1]': 'Crossroads_07[bot1]', 'Crossroads_33[left1]': 'Crossroads_09[right1]', 'Crossroads_33[left2]': 'Crossroads_12[right1]', 'Crossroads_33[right1]': 'Crossroads_06[left1]', 'Crossroads_33[right2]': 'Crossroads_08[left1]', 'Crossroads_35[bot1]': 'Fungus3_26[top1]', 'Crossroads_35[right1]': 'Crossroads_12[left1]', 'Crossroads_36[right1]': 'Crossroads_25[left1]', 'Crossroads_36[right2]': 'Crossroads_09[left1]', 'Crossroads_37[right1]': 'Crossroads_49[left1]', 'Crossroads_38[right1]': 'Crossroads_07[left1]', 'Crossroads_39[right1]': 'Crossroads_14[left1]', 'Crossroads_39[left1]': 'Crossroads_02[right1]', 'Crossroads_40[right1]': 'Crossroads_16[left1]', 'Crossroads_40[left1]': 'Crossroads_05[right1]', 'Crossroads_42[left1]': 'Crossroads_13[right1]', 'Crossroads_42[right1]': 'Crossroads_19[left1]', 'Crossroads_43[left1]': 'Crossroads_49[right1]', 'Crossroads_43[right1]': 'Crossroads_19[left2]', 'Crossroads_45[right1]': 'Mines_01[left1]', 'Crossroads_45[left1]': 'Crossroads_14[right2]', 'Crossroads_46[left1]': 'Crossroads_27[right1]', 'Crossroads_46b[right1]': 'RestingGrounds_02[left1]', 'Crossroads_ShamanTemple[left1]': 'Crossroads_06[door1]', 'Crossroads_47[right1]': 'Crossroads_03[left2]', 'Crossroads_48[left1]': 'Crossroads_14[right1]', 'Crossroads_49[right1]': 'Crossroads_43[left1]', 'Crossroads_49[left1]': 'Crossroads_37[right1]', 'Crossroads_49b[right1]': 'Ruins1_28[left1]', 'Crossroads_50[right1]': 'RestingGrounds_06[left1]', 'Crossroads_50[left1]': 'Crossroads_04[right1]', 'Crossroads_52[left1]': 'Crossroads_18[right2]', 'Room_ruinhouse[left1]': 'Crossroads_04[door1]', 'Room_Charm_Shop[left1]': 'Crossroads_04[door_charmshop]', 'Room_Mender_House[left1]': 'Crossroads_04[door_Mender_House]', 'Fungus1_01[left1]': 'Fungus1_01b[right1]', 'Fungus1_01[right1]': 'Crossroads_11_alt[left1]', 'Fungus1_01b[left1]': 'Fungus1_02[right1]', 'Fungus1_01b[right1]': 'Fungus1_01[left1]', 'Fungus1_02[left1]': 'Fungus1_17[right1]', 'Fungus1_02[right1]': 'Fungus1_01b[left1]', 'Fungus1_02[right2]': 'Fungus1_06[left1]', 'Fungus1_03[left1]': 'Fungus1_31[right1]', 'Fungus1_03[right1]': 'Fungus1_17[left1]', 'Fungus1_03[bot1]': 'Fungus1_05[top1]', 'Fungus1_04[left1]': 'Fungus1_25[right1]', 'Fungus1_04[right1]': 'Fungus1_21[left1]', 'Fungus1_05[right1]': 'Fungus1_14[left1]', 'Fungus1_05[bot1]': 'Fungus1_10[top1]', 'Fungus1_05[top1]': 'Fungus1_03[bot1]', 'Fungus1_06[left1]': 'Fungus1_02[right2]', 'Fungus1_06[bot1]': 'Fungus1_07[top1]', 'Fungus1_07[top1]': 'Fungus1_06[bot1]', 'Fungus1_07[left1]': 'Fungus1_19[right1]', 'Fungus1_07[right1]': 'Fungus1_08[left1]', 'Fungus1_08[left1]': 'Fungus1_07[right1]', 'Fungus1_09[left1]': 'Fungus1_15[right1]', 'Fungus1_09[right1]': 'Fungus1_30[left1]', 'Fungus1_10[left1]': 'Fungus1_30[right1]', 'Fungus1_10[right1]': 'Fungus1_19[left1]', 'Fungus1_10[top1]': 'Fungus1_05[bot1]', 'Fungus1_11[top1]': 'Fungus1_19[bot1]', 'Fungus1_11[right1]': 'Fungus1_34[left1]', 'Fungus1_11[right2]': 'Fungus1_37[left1]', 'Fungus1_11[left1]': 'Fungus1_29[right1]', 'Fungus1_11[bot1]': 'Fungus3_01[top1]', 'Fungus1_12[left1]': 'Fungus1_13[right1]', 'Fungus1_12[right1]': 'Fungus1_29[left1]', 'Fungus1_13[right1]': 'Fungus1_12[left1]', 'Fungus1_13[left1]': 'Fungus3_22[right1]', 'Fungus1_14[left1]': 'Fungus1_05[right1]', 'Fungus1_15[door1]': 'Room_nailmaster_02[left1]', 'Fungus1_15[right1]': 'Fungus1_09[left1]', 'Fungus1_16_alt[right1]': 'Fungus1_22[left1]', 'Fungus1_17[left1]': 'Fungus1_03[right1]', 'Fungus1_17[right1]': 'Fungus1_02[left1]', 'Fungus1_19[left1]': 'Fungus1_10[right1]', 'Fungus1_19[right1]': 'Fungus1_07[left1]', 'Fungus1_19[bot1]': 'Fungus1_11[top1]', 'Fungus1_20_v02[bot1]': 'Fungus1_21[top1]', 'Fungus1_20_v02[bot2]': 'Fungus1_32[top1]', 'Fungus1_20_v02[right1]': 'Fungus1_28[left2]', 'Fungus1_21[bot1]': 'Fungus1_22[top1]', 'Fungus1_21[top1]': 'Fungus1_20_v02[bot1]', 'Fungus1_21[left1]': 'Fungus1_04[right1]', 'Fungus1_21[right1]': 'Fungus1_32[left1]', 'Fungus1_22[bot1]': 'Fungus1_30[top1]', 'Fungus1_22[top1]': 'Fungus1_21[bot1]', 'Fungus1_22[left1]': 'Fungus1_16_alt[right1]', 'Fungus1_23[left1]': 'Fungus3_48[right2]', 'Fungus1_23[right1]': 'Fungus3_13[left1]', 'Fungus1_24[left1]': 'Fungus3_05[right1]', 'Fungus1_25[right1]': 'Fungus1_04[left1]', 'Fungus1_25[left1]': 'Fungus1_26[right1]', 'Fungus1_26[right1]': 'Fungus1_25[left1]', 'Fungus1_26[left1]': 'Fungus1_Slug[right1]', 'Fungus1_26[door_SlugShrine]': 'Room_Slug_Shrine[left1]', 'Fungus1_28[left1]': 'Cliffs_01[right3]', 'Fungus1_28[left2]': 'Fungus1_20_v02[right1]', 'Fungus1_29[left1]': 'Fungus1_12[right1]', 'Fungus1_29[right1]': 'Fungus1_11[left1]', 'Fungus1_30[top1]': 'Fungus1_22[bot1]', 'Fungus1_30[top3]': 'Fungus1_31[bot1]', 'Fungus1_30[left1]': 'Fungus1_09[right1]', 'Fungus1_30[right1]': 'Fungus1_10[left1]', 'Fungus1_31[top1]': 'Fungus1_32[bot1]', 'Fungus1_31[bot1]': 'Fungus1_30[top3]', 'Fungus1_31[right1]': 'Fungus1_03[left1]', 'Fungus1_32[bot1]': 'Fungus1_31[top1]', 'Fungus1_32[top1]': 'Fungus1_20_v02[bot2]', 'Fungus1_32[left1]': 'Fungus1_21[right1]', 'Fungus1_34[door1]': 'Fungus1_35[left1]', 'Fungus1_34[left1]': 'Fungus1_11[right1]', 'Fungus1_35[left1]': 'Fungus1_34[door1]', 'Fungus1_35[right1]': 'Fungus1_36[left1]', 'Fungus1_36[left1]': 'Fungus1_35[right1]', 'Fungus1_37[left1]': 'Fungus1_11[right2]', 'Fungus1_Slug[right1]': 'Fungus1_26[left1]', 'Room_Slug_Shrine[left1]': 'Fungus1_26[door_SlugShrine]', 'Room_nailmaster_02[left1]': 'Fungus1_15[door1]', 'Fungus3_01[top1]': 'Fungus1_11[bot1]', 'Fungus3_01[right1]': 'Fungus3_25[left1]', 'Fungus3_01[left1]': 'Fungus3_24[right1]', 'Fungus3_01[right2]': 'Fungus3_02[left1]', 'Fungus3_02[left1]': 'Fungus3_01[right2]', 'Fungus3_02[left2]': 'Fungus3_03[right1]', 'Fungus3_02[left3]': 'Fungus3_35[right1]', 'Fungus3_02[right1]': 'Fungus3_47[left1]', 'Fungus3_02[right2]': 'Fungus2_01[left1]', 'Fungus3_03[right1]': 'Fungus3_02[left2]', 'Fungus3_03[left1]': 'Fungus3_34[right1]', 'Fungus3_24[right1]': 'Fungus3_01[left1]', 'Fungus3_24[left1]': 'Fungus3_44[right1]', 'Fungus3_24[top1]': 'Fungus3_30[bot1]', 'Fungus3_25[right1]': 'Fungus3_25b[left1]', 'Fungus3_25[left1]': 'Fungus3_01[right1]', 'Fungus3_25b[right1]': 'Fungus3_26[left2]', 'Fungus3_25b[left1]': 'Fungus3_25[right1]', 'Fungus3_26[top1]': 'Crossroads_35[bot1]', 'Fungus3_26[left1]': 'Fungus3_28[right1]', 'Fungus3_26[left2]': 'Fungus3_25b[right1]', 'Fungus3_26[left3]': 'Fungus3_27[right1]', 'Fungus3_26[right1]': 'Fungus2_33[left1]', 'Fungus3_27[left1]': 'Fungus3_47[right1]', 'Fungus3_27[right1]': 'Fungus3_26[left3]', 'Fungus3_28[right1]': 'Fungus3_26[left1]', 'Fungus3_30[bot1]': 'Fungus3_24[top1]', 'Fungus3_35[right1]': 'Fungus3_02[left3]', 'Fungus3_44[bot1]': 'Fungus3_34[top1]', 'Fungus3_44[door1]': 'Room_Fungus_Shaman[left1]', 'Fungus3_44[right1]': 'Fungus3_24[left1]', 'Fungus3_47[left1]': 'Fungus3_02[right1]', 'Fungus3_47[right1]': 'Fungus3_27[left1]', 'Fungus3_47[door1]': 'Fungus3_archive[left1]', 'Room_Fungus_Shaman[left1]': 'Fungus3_44[door1]', 'Fungus3_archive[left1]': 'Fungus3_47[door1]', 'Fungus3_archive[bot1]': 'Fungus3_archive_02[top1]', 'Fungus3_archive_02[top1]': 'Fungus3_archive[bot1]', 'Fungus2_01[left1]': 'Fungus3_02[right2]', 'Fungus2_01[left2]': 'Fungus2_02[right1]', 'Fungus2_01[left3]': 'Fungus2_34[right1]', 'Fungus2_01[right1]': 'Fungus2_03[left1]', 'Fungus2_02[right1]': 'Fungus2_01[left2]', 'Fungus2_34[right1]': 'Fungus2_01[left3]', 'Fungus2_03[left1]': 'Fungus2_01[right1]', 'Fungus2_03[bot1]': 'Fungus2_18[top1]', 'Fungus2_03[right1]': 'Fungus2_04[left1]', 'Fungus2_04[top1]': 'Fungus2_05[bot1]', 'Fungus2_04[right1]': 'Fungus2_28[left1]', 'Fungus2_04[left1]': 'Fungus2_03[right1]', 'Fungus2_04[right2]': 'Fungus2_28[left2]', 'Fungus2_05[bot1]': 'Fungus2_04[top1]', 'Fungus2_05[right1]': 'Fungus2_06[left1]', 'Fungus2_06[top1]': 'Crossroads_18[bot1]', 'Fungus2_06[left1]': 'Fungus2_05[right1]', 'Fungus2_06[left2]': 'Fungus2_33[right1]', 'Fungus2_06[right1]': 'Fungus2_26[left1]', 'Fungus2_06[right2]': 'Fungus2_07[left1]', 'Fungus2_07[left1]': 'Fungus2_06[right2]', 'Fungus2_07[right1]': 'Fungus2_08[left1]', 'Fungus2_08[left1]': 'Fungus2_07[right1]', 'Fungus2_08[left2]': 'Fungus2_09[right1]', 'Fungus2_08[right1]': 'Fungus2_32[left1]', 'Fungus2_09[left1]': 'Fungus2_10[right1]', 'Fungus2_09[right1]': 'Fungus2_08[left2]', 'Fungus2_10[right1]': 'Fungus2_09[left1]', 'Fungus2_10[right2]': 'Fungus2_21[left1]', 'Fungus2_10[bot1]': 'Fungus2_11[top1]', 'Fungus2_11[top1]': 'Fungus2_10[bot1]', 'Fungus2_11[left1]': 'Fungus2_18[right1]', 'Fungus2_11[left2]': 'Fungus2_17[right1]', 'Fungus2_11[right1]': 'Fungus2_12[left1]', 'Fungus2_12[left1]': 'Fungus2_11[right1]', 'Fungus2_12[bot1]': 'Fungus2_13[top1]', 'Fungus2_13[top1]': 'Fungus2_12[bot1]', 'Fungus2_13[left2]': 'Fungus2_14[right1]', 'Fungus2_13[left3]': 'Fungus2_23[right1]', 'Fungus2_14[top1]': 'Fungus2_17[bot1]', 'Fungus2_14[right1]': 'Fungus2_13[left2]', 'Fungus2_14[bot3]': 'Fungus2_15[top3]', 'Fungus2_15[top3]': 'Fungus2_14[bot3]', 'Fungus2_15[right1]': 'Fungus2_31[left1]', 'Fungus2_15[left1]': 'Fungus2_25[right1]', 'Fungus2_17[left1]': 'Fungus2_29[right1]', 'Fungus2_17[right1]': 'Fungus2_11[left2]', 'Fungus2_17[bot1]': 'Fungus2_14[top1]', 'Fungus2_18[right1]': 'Fungus2_11[left1]', 'Fungus2_18[bot1]': 'Fungus2_19[top1]', 'Fungus2_18[top1]': 'Fungus2_03[bot1]', 'Fungus2_19[top1]': 'Fungus2_18[bot1]', 'Fungus2_19[left1]': 'Fungus2_20[right1]', 'Fungus2_20[right1]': 'Fungus2_19[left1]', 'Fungus2_20[left1]': 'Deepnest_01[right1]', 'Fungus2_21[right1]': 'Ruins1_01[left1]', 'Fungus2_21[left1]': 'Fungus2_10[right2]', 'Fungus2_23[right1]': 'Fungus2_13[left3]', 'Fungus2_23[right2]': 'Waterways_09[left1]', 'Fungus2_26[left1]': 'Fungus2_06[right1]', 'Fungus2_28[left1]': 'Fungus2_04[right1]', 'Fungus2_28[left2]': 'Fungus2_04[right2]', 'Fungus2_29[right1]': 'Fungus2_17[left1]', 'Fungus2_29[bot1]': 'Fungus2_30[top1]', 'Fungus2_30[bot1]': 'Fungus2_25[top2]', 'Fungus2_30[top1]': 'Fungus2_29[bot1]', 'Fungus2_31[left1]': 'Fungus2_15[right1]', 'Fungus2_32[left1]': 'Fungus2_08[right1]', 'Fungus2_33[right1]': 'Fungus2_06[left2]', 'Fungus2_33[left1]': 'Fungus3_26[right1]', 'Deepnest_01[right1]': 'Fungus2_20[left1]', 'Deepnest_01[bot1]': 'Deepnest_01b[top1]', 'Deepnest_01[bot2]': 'Deepnest_01b[top2]', 'Deepnest_01[left1]': 'Fungus3_39[right1]', 'Deepnest_01b[top1]': 'Deepnest_01[bot1]', 'Deepnest_01b[top2]': None, 'Deepnest_01b[right1]': 'Deepnest_02[left1]', 'Deepnest_01b[right2]': 'Deepnest_02[left2]', 'Deepnest_01b[bot1]': 'Deepnest_17[top1]', 'Deepnest_02[left1]': 'Deepnest_01b[right1]', 'Deepnest_02[left2]': 'Deepnest_01b[right2]', 'Deepnest_02[right1]': 'Deepnest_36[left1]', 'Deepnest_03[right1]': 'Deepnest_30[left1]', 'Deepnest_03[left1]': 'Deepnest_34[right1]', 'Deepnest_03[top1]': 'Deepnest_33[bot1]', 'Deepnest_03[left2]': 'Deepnest_31[right1]', 'Deepnest_09[left1]': 'Deepnest_10[right1]', 'Deepnest_10[right1]': 'Deepnest_09[left1]', 'Deepnest_10[right2]': 'Deepnest_41[left1]', 'Deepnest_10[right3]': 'Deepnest_41[left2]', 'Deepnest_10[door1]': 'Deepnest_Spider_Town[left1]', 'Deepnest_10[door2]': 'Room_spider_small[left1]', 'Room_spider_small[left1]': 'Deepnest_10[door2]', 'Deepnest_Spider_Town[left1]': 'Deepnest_10[door1]', 'Deepnest_14[right1]': 'Deepnest_17[left1]', 'Deepnest_14[left1]': 'Deepnest_26[right1]', 'Deepnest_14[bot1]': 'Deepnest_33[top1]', 'Deepnest_14[bot2]': 'Deepnest_33[top2]', 'Deepnest_16[left1]': 'Deepnest_17[right1]', 'Deepnest_16[bot1]': 'Fungus2_25[top1]', 'Deepnest_17[left1]': 'Deepnest_14[right1]', 'Deepnest_17[right1]': 'Deepnest_16[left1]', 'Deepnest_17[top1]': 'Deepnest_01b[bot1]', 'Deepnest_17[bot1]': 'Deepnest_30[top1]', 'Fungus2_25[top1]': 'Deepnest_16[bot1]', 'Fungus2_25[top2]': None, 'Fungus2_25[right1]': 'Fungus2_15[left1]', 'Deepnest_26[left1]': 'Deepnest_26b[right1]', 'Deepnest_26[left2]': 'Deepnest_26b[right2]', 'Deepnest_26[right1]': 'Deepnest_14[left1]', 'Deepnest_26[bot1]': 'Deepnest_35[top1]', 'Deepnest_26b[right2]': 'Deepnest_26[left2]', 'Deepnest_26b[right1]': 'Deepnest_26[left1]', 'Deepnest_30[left1]': 'Deepnest_03[right1]', 'Deepnest_30[top1]': 'Deepnest_17[bot1]', 'Deepnest_30[right1]': 'Deepnest_37[left1]', 'Deepnest_31[right1]': 'Deepnest_03[left2]', 'Deepnest_31[right2]': 'Deepnest_32[left1]', 'Deepnest_32[left1]': 'Deepnest_31[right2]', 'Deepnest_33[top1]': 'Deepnest_14[bot1]', 'Deepnest_33[top2]': 'Deepnest_14[bot2]', 'Deepnest_33[bot1]': 'Deepnest_03[top1]', 'Deepnest_34[left1]': 'Deepnest_39[right1]', 'Deepnest_34[right1]': 'Deepnest_03[left1]', 'Deepnest_34[top1]': 'Deepnest_35[bot1]', 'Deepnest_35[left1]': 'Deepnest_40[right1]', 'Deepnest_35[top1]': 'Deepnest_26[bot1]', 'Deepnest_35[bot1]': 'Deepnest_34[top1]', 'Deepnest_36[left1]': 'Deepnest_02[right1]', 'Deepnest_37[left1]': 'Deepnest_30[right1]', 'Deepnest_37[right1]': 'Abyss_03_b[left1]', 'Deepnest_37[top1]': 'Deepnest_38[bot1]', 'Deepnest_37[bot1]': 'Deepnest_44[top1]', 'Deepnest_38[bot1]': 'Deepnest_37[top1]', 'Deepnest_39[left1]': 'Deepnest_41[right1]', 'Deepnest_39[top1]': 'Deepnest_42[bot1]', 'Deepnest_39[door1]': 'Deepnest_45_v02[left1]', 'Deepnest_39[right1]': 'Deepnest_34[left1]', 'Deepnest_40[right1]': 'Deepnest_35[left1]', 'Deepnest_41[right1]': 'Deepnest_39[left1]', 'Deepnest_41[left1]': 'Deepnest_10[right2]', 'Deepnest_41[left2]': 'Deepnest_10[right3]', 'Deepnest_42[bot1]': 'Deepnest_39[top1]', 'Deepnest_42[left1]': 'Room_Mask_Maker[right1]', 'Deepnest_42[top1]': 'Deepnest_43[bot1]', 'Deepnest_43[bot1]': 'Deepnest_42[top1]', 'Deepnest_43[left1]': 'Fungus3_50[right1]', 'Deepnest_43[right1]': 'Fungus3_08[left1]', 'Deepnest_44[top1]': 'Deepnest_37[bot1]', 'Deepnest_45_v02[left1]': 'Deepnest_39[door1]', 'Room_Mask_Maker[right1]': 'Deepnest_42[left1]', 'Deepnest_East_01[bot1]': 'Abyss_03_c[top1]', 'Deepnest_East_01[right1]': 'Hive_03_c[left1]', 'Deepnest_East_01[top1]': 'Deepnest_East_02[bot1]', 'Deepnest_East_02[bot1]': 'Deepnest_East_01[top1]', 'Deepnest_East_02[bot2]': 'Hive_03[top1]', 'Deepnest_East_02[top1]': 'Waterways_14[bot2]', 'Deepnest_East_02[right1]': 'Deepnest_East_03[left2]', 'Deepnest_East_03[left1]': 'Ruins2_07[right1]', 'Deepnest_East_03[left2]': 'Deepnest_East_02[right1]', 'Deepnest_East_03[top1]': 'Deepnest_East_07[bot1]', 'Deepnest_East_03[top2]': None, 'Deepnest_East_03[right1]': 'Deepnest_East_04[left1]', 'Deepnest_East_03[right2]': 'Deepnest_East_06[left1]', 'Deepnest_East_04[left1]': 'Deepnest_East_03[right1]', 'Deepnest_East_04[left2]': 'Deepnest_East_07[right1]', 'Deepnest_East_04[right2]': 'Deepnest_East_15[left1]', 'Deepnest_East_04[right1]': 'Deepnest_East_11[left1]', 'Deepnest_East_06[top1]': 'Deepnest_East_18[bot1]', 'Deepnest_East_06[left1]': 'Deepnest_East_03[right2]', 'Deepnest_East_06[bot1]': 'Deepnest_East_14b[top1]', 'Deepnest_East_06[door1]': 'Room_nailmaster_03[left1]', 'Deepnest_East_06[right1]': 'Deepnest_East_16[left1]', 'Deepnest_East_07[bot1]': 'Deepnest_East_03[top1]', 'Deepnest_East_07[bot2]': 'Deepnest_East_03[top2]', 'Deepnest_East_07[left1]': 'Deepnest_East_08[right1]', 'Deepnest_East_07[left2]': 'Ruins2_11_b[right1]', 'Deepnest_East_07[right1]': 'Deepnest_East_04[left2]', 'Deepnest_East_08[right1]': 'Deepnest_East_07[left1]', 'Deepnest_East_08[top1]': 'Deepnest_East_09[bot1]', 'Deepnest_East_09[right1]': 'Room_Colosseum_01[left1]', 'Deepnest_East_09[left1]': 'Ruins2_10b[right1]', 'Deepnest_East_09[bot1]': 'Deepnest_East_08[top1]', 'Deepnest_East_10[left1]': 'Deepnest_East_18[right2]', 'Deepnest_East_11[right1]': 'Deepnest_East_12[left1]', 'Deepnest_East_11[left1]': 'Deepnest_East_04[right1]', 'Deepnest_East_11[top1]': 'Deepnest_East_13[bot1]', 'Deepnest_East_11[bot1]': 'Deepnest_East_18[top1]', 'Deepnest_East_12[right1]': 'Deepnest_East_Hornet[left1]', 'Deepnest_East_12[left1]': 'Deepnest_East_11[right1]', 'Deepnest_East_13[bot1]': 'Deepnest_East_11[top1]', 'Deepnest_East_14[top2]': 'Deepnest_East_16[bot1]', 'Deepnest_East_14[left1]': 'Deepnest_East_14b[right1]', 'Deepnest_East_14[door1]': 'Deepnest_East_17[left1]', 'Deepnest_East_14b[right1]': 'Deepnest_East_14[left1]', 'Deepnest_East_14b[top1]': 'Deepnest_East_06[bot1]', 'Deepnest_East_15[left1]': 'Deepnest_East_04[right2]', 'Deepnest_East_16[left1]': 'Deepnest_East_06[right1]', 'Deepnest_East_16[bot1]': 'Deepnest_East_14[top2]', 'Deepnest_East_17[left1]': 'Deepnest_East_14[door1]', 'Deepnest_East_18[top1]': 'Deepnest_East_11[bot1]', 'Deepnest_East_18[bot1]': 'Deepnest_East_06[top1]', 'Deepnest_East_18[right2]': 'Deepnest_East_10[left1]', 'Room_nailmaster_03[left1]': 'Deepnest_East_06[door1]', 'Deepnest_East_Hornet[left1]': 'Deepnest_East_12[right1]', 'Deepnest_East_Hornet[left2]': 'Room_Wyrm[right1]', 'Room_Wyrm[right1]': 'Deepnest_East_Hornet[left2]', 'GG_Lurker[left1]': 'Room_Colosseum_Spectate[right1]', 'Hive_01[left1]': 'Abyss_03_c[right1]', 'Hive_01[right1]': 'Hive_02[left2]', 'Hive_01[right2]': 'Hive_02[left3]', 'Hive_02[left1]': 'Hive_03_c[right3]', 'Hive_02[left2]': 'Hive_01[right1]', 'Hive_02[left3]': 'Hive_01[right2]', 'Hive_03_c[left1]': 'Deepnest_East_01[right1]', 'Hive_03_c[right2]': 'Hive_04[left2]', 'Hive_03_c[right3]': 'Hive_02[left1]', 'Hive_03_c[top1]': 'Hive_03[bot1]', 'Hive_03[bot1]': 'Hive_03_c[top1]', 'Hive_03[right1]': 'Hive_04[left1]', 'Hive_03[top1]': 'Deepnest_East_02[bot2]', 'Hive_04[left1]': 'Hive_03[right1]', 'Hive_04[left2]': 'Hive_03_c[right2]', 'Hive_04[right1]': 'Hive_05[left1]', 'Hive_05[left1]': 'Hive_04[right1]', 'Room_Colosseum_01[left1]': 'Deepnest_East_09[right1]', 'Room_Colosseum_01[bot1]': 'Room_Colosseum_02[top1]', 'Room_Colosseum_02[top1]': 'Room_Colosseum_01[bot1]', 'Room_Colosseum_02[top2]': 'Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[bot1]': 'Room_Colosseum_02[top2]', 'Room_Colosseum_Spectate[right1]': 'GG_Lurker[left1]', 'Abyss_01[left1]': 'Waterways_05[right1]', 'Abyss_01[left2]': 'Waterways_06[right1]', 'Abyss_01[left3]': 'Abyss_02[right1]', 'Abyss_01[right1]': 'Ruins2_04[left2]', 'Abyss_01[right2]': 'Waterways_07[left1]', 'Abyss_02[right1]': 'Abyss_01[left3]', 'Abyss_02[bot1]': 'Abyss_03[top1]', 'Abyss_03[bot1]': 'Abyss_17[top1]', 'Abyss_03[bot2]': 'Abyss_04[top1]', 'Abyss_03[top1]': 'Abyss_02[bot1]', 'Abyss_03_b[left1]': 'Deepnest_37[right1]', 'Abyss_03_c[right1]': 'Hive_01[left1]', 'Abyss_03_c[top1]': 'Deepnest_East_01[bot1]', 'Abyss_04[top1]': 'Abyss_03[bot2]', 'Abyss_04[left1]': 'Abyss_18[right1]', 'Abyss_04[bot1]': 'Abyss_06_Core[top1]', 'Abyss_04[right1]': 'Abyss_05[left1]', 'Abyss_05[left1]': 'Abyss_04[right1]', 'Abyss_05[right1]': 'Abyss_22[left1]', 'Abyss_06_Core[top1]': 'Abyss_04[bot1]', 'Abyss_06_Core[left1]': 'Abyss_08[right1]', 'Abyss_06_Core[left3]': 'Abyss_12[right1]', 'Abyss_06_Core[right2]': 'Abyss_16[left1]', 'Abyss_06_Core[bot1]': 'Abyss_15[top1]', 'Abyss_08[right1]': 'Abyss_06_Core[left1]', 'Abyss_09[right1]': 'Abyss_10[left1]', 'Abyss_09[right2]': 'Abyss_Lighthouse_room[left1]', 'Abyss_09[right3]': 'Abyss_10[left2]', 'Abyss_09[left1]': 'Abyss_16[right1]', 'Abyss_10[left1]': 'Abyss_09[right1]', 'Abyss_10[left2]': 'Abyss_09[right3]', 'Abyss_12[right1]': 'Abyss_06_Core[left3]', 'Abyss_15[top1]': 'Abyss_06_Core[bot1]', 'Abyss_16[left1]': 'Abyss_06_Core[right2]', 'Abyss_16[right1]': 'Abyss_09[left1]', 'Abyss_17[top1]': 'Abyss_03[bot1]', 'Abyss_18[left1]': 'Abyss_19[right1]', 'Abyss_18[right1]': 'Abyss_04[left1]', 'Abyss_19[left1]': 'Abyss_21[right1]', 'Abyss_19[right1]': 'Abyss_18[left1]', 'Abyss_19[bot1]': 'Abyss_20[top1]', 'Abyss_19[bot2]': 'Abyss_20[top2]', 'Abyss_20[top1]': 'Abyss_19[bot1]', 'Abyss_20[top2]': 'Abyss_19[bot2]', 'Abyss_21[right1]': 'Abyss_19[left1]', 'Abyss_22[left1]': 'Abyss_05[right1]', 'Abyss_Lighthouse_room[left1]': 'Abyss_09[right2]', 'Waterways_01[top1]': 'Ruins1_05b[bot1]', 'Waterways_01[left1]': 'Waterways_04[right1]', 'Waterways_01[right1]': 'Waterways_03[left1]', 'Waterways_01[bot1]': 'Waterways_02[top1]', 'Waterways_02[top1]': 'Waterways_01[bot1]', 'Waterways_02[top2]': 'Waterways_05[bot1]', 'Waterways_02[top3]': 'Waterways_04[bot1]', 'Waterways_02[bot1]': 'Waterways_08[top1]', 'Waterways_02[bot2]': 'Waterways_06[top1]', 'Waterways_03[left1]': 'Waterways_01[right1]', 'Waterways_04[bot1]': 'Waterways_02[top3]', 'Waterways_04[right1]': 'Waterways_01[left1]', 'Waterways_04[left1]': 'Waterways_04b[right1]', 'Waterways_04[left2]': 'Waterways_04b[right2]', 'Waterways_04b[right1]': 'Waterways_04[left1]', 'Waterways_04b[right2]': 'Waterways_04[left2]', 'Waterways_04b[left1]': 'Waterways_09[right1]', 'Waterways_05[right1]': 'Abyss_01[left1]', 'Waterways_05[bot1]': 'Waterways_02[top2]', 'Waterways_05[bot2]': 'Waterways_15[top1]', 'Waterways_06[right1]': 'Abyss_01[left2]', 'Waterways_06[top1]': 'Waterways_02[bot2]', 'Waterways_07[right1]': 'Waterways_13[left1]', 'Waterways_07[right2]': 'Waterways_13[left2]', 'Waterways_07[left1]': 'Abyss_01[right2]', 'Waterways_07[door1]': 'Ruins_House_03[left2]', 'Waterways_07[top1]': 'Waterways_14[bot1]', 'Waterways_08[top1]': 'Waterways_02[bot1]', 'Waterways_08[left1]': 'Waterways_12[right1]', 'Waterways_08[left2]': 'GG_Pipeway[right1]', 'Waterways_09[right1]': 'Waterways_04b[left1]', 'Waterways_09[left1]': 'Fungus2_23[right2]', 'Waterways_12[right1]': 'Waterways_08[left1]', 'Waterways_13[left1]': 'Waterways_07[right1]', 'Waterways_13[left2]': 'Waterways_07[right2]', 'Waterways_14[bot1]': 'Waterways_07[top1]', 'Waterways_14[bot2]': 'Deepnest_East_02[top1]', 'Waterways_15[top1]': 'Waterways_05[bot2]', 'GG_Pipeway[right1]': 'Waterways_08[left2]', 'GG_Pipeway[left1]': 'GG_Waterways[right1]', 'GG_Waterways[right1]': 'GG_Pipeway[left1]', 'GG_Waterways[door1]': 'Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[left1]': 'GG_Waterways[door1]', 'Room_GG_Shortcut[top1]': 'Ruins1_04[bot1]', 'Ruins1_01[left1]': 'Fungus2_21[right1]', 'Ruins1_01[top1]': 'Ruins1_17[bot1]', 'Ruins1_01[bot1]': 'Ruins1_02[top1]', 'Ruins1_02[top1]': 'Ruins1_01[bot1]', 'Ruins1_02[bot1]': 'Ruins1_03[top1]', 'Ruins1_03[top1]': 'Ruins1_02[bot1]', 'Ruins1_03[left1]': 'Ruins1_04[right1]', 'Ruins1_03[right1]': 'Ruins1_05c[left2]', 'Ruins1_03[right2]': 'Ruins1_05b[left1]', 'Ruins1_04[right1]': 'Ruins1_03[left1]', 'Ruins1_04[door1]': 'Room_nailsmith[left1]', 'Ruins1_04[bot1]': 'Room_GG_Shortcut[top1]', 'Ruins1_05b[left1]': 'Ruins1_03[right2]', 'Ruins1_05b[top1]': 'Ruins1_05c[bot1]', 'Ruins1_05b[bot1]': 'Waterways_01[top1]', 'Ruins1_05b[right1]': 'Ruins1_27[left1]', 'Ruins1_05c[left2]': 'Ruins1_03[right1]', 'Ruins1_05c[bot1]': 'Ruins1_05b[top1]', 'Ruins1_05c[top1]': 'Ruins1_05[bot1]', 'Ruins1_05c[top2]': 'Ruins1_05[bot2]', 'Ruins1_05c[top3]': 'Ruins1_05[bot3]', 'Ruins1_05[bot1]': 'Ruins1_05c[top1]', 'Ruins1_05[bot2]': 'Ruins1_05c[top2]', 'Ruins1_05[bot3]': 'Ruins1_05c[top3]', 'Ruins1_05[right1]': 'Ruins1_09[left1]', 'Ruins1_05[right2]': 'Ruins1_18[left1]', 'Ruins1_05[top1]': 'Ruins1_31[bot1]', 'Ruins1_06[left1]': 'Ruins1_17[right1]', 'Ruins1_06[right1]': 'Ruins1_31[left1]', 'Ruins1_09[top1]': 'Ruins1_23[bot1]', 'Ruins1_09[left1]': 'Ruins1_05[right1]', 'Ruins1_17[top1]': 'Ruins1_28[bot1]', 'Ruins1_17[right1]': 'Ruins1_06[left1]', 'Ruins1_17[bot1]': 'Ruins1_01[top1]', 'Ruins1_18[left1]': 'Ruins1_05[right2]', 'Ruins1_18[right1]': 'Ruins2_03b[left1]', 'Ruins1_18[right2]': 'Ruins2_01[left2]', 'Ruins1_23[top1]': 'Ruins1_30[bot1]', 'Ruins1_23[right1]': 'Ruins1_25[left2]', 'Ruins1_23[right2]': 'Ruins1_25[left3]', 'Ruins1_23[bot1]': 'Ruins1_09[top1]', 'Ruins1_23[left1]': 'Ruins1_31[right1]', 'Ruins1_24[left1]': 'Ruins1_32[right1]', 'Ruins1_24[right1]': 'Ruins1_30[left1]', 'Ruins1_24[left2]': 'Ruins1_32[right2]', 'Ruins1_24[right2]': 'Ruins1_30[left2]', 'Ruins1_25[left1]': 'Ruins1_30[right1]', 'Ruins1_25[left2]': 'Ruins1_23[right1]', 'Ruins1_25[left3]': 'Ruins1_23[right2]', 'Ruins1_27[left1]': 'Ruins1_05b[right1]', 'Ruins1_27[right1]': 'Ruins2_01_b[left1]', 'Ruins1_28[left1]': 'Crossroads_49b[right1]', 'Ruins1_28[right1]': 'Ruins1_29[left1]', 'Ruins1_28[bot1]': 'Ruins1_17[top1]', 'Ruins1_29[left1]': 'Ruins1_28[right1]', 'Ruins1_30[left1]': 'Ruins1_24[right1]', 'Ruins1_30[left2]': 'Ruins1_24[right2]', 'Ruins1_30[bot1]': 'Ruins1_23[top1]', 'Ruins1_30[right1]': 'Ruins1_25[left1]', 'Ruins1_31[bot1]': 'Ruins1_05[top1]', 'Ruins1_31[left1]': 'Ruins1_06[right1]', 'Ruins1_31[left2]': 'Ruins1_31b[right1]', 'Ruins1_31[left3]': 'Ruins1_31b[right2]', 'Ruins1_31[right1]': 'Ruins1_23[left1]', 'Ruins1_31b[right1]': 'Ruins1_31[left2]', 'Ruins1_31b[right2]': 'Ruins1_31[left3]', 'Ruins1_32[right1]': 'Ruins1_24[left1]', 'Ruins1_32[right2]': 'Ruins1_24[left2]', 'Room_nailsmith[left1]': 'Ruins1_04[door1]', 'Ruins2_01[top1]': 'Ruins2_03b[bot1]', 'Ruins2_01[bot1]': 'Ruins2_01_b[top1]', 'Ruins2_01[left2]': 'Ruins1_18[right2]', 'Ruins2_01_b[top1]': 'Ruins2_01[bot1]', 'Ruins2_01_b[left1]': 'Ruins1_27[right1]', 'Ruins2_01_b[right1]': 'Ruins2_04[left1]', 'Ruins2_03b[top1]': 'Ruins2_03[bot1]', 'Ruins2_03b[top2]': 'Ruins2_03[bot2]', 'Ruins2_03b[left1]': 'Ruins1_18[right1]', 'Ruins2_03b[bot1]': 'Ruins2_01[top1]', 'Ruins2_03[top1]': 'Ruins2_Watcher_Room[bot1]', 'Ruins2_03[bot1]': 'Ruins2_03b[top1]', 'Ruins2_03[bot2]': 'Ruins2_03b[top2]', 'Ruins2_04[left1]': 'Ruins2_01_b[right1]', 'Ruins2_04[left2]': 'Abyss_01[right1]', 'Ruins2_04[right1]': 'Ruins2_06[left1]', 'Ruins2_04[right2]': 'Ruins2_06[left2]', 'Ruins2_04[door_Ruin_House_01]': 'Ruins_House_01[left1]', 'Ruins2_04[door_Ruin_House_02]': 'Ruins_House_02[left1]', 'Ruins2_04[door_Ruin_House_03]': 'Ruins_House_03[left1]', 'Ruins2_04[door_Ruin_Elevator]': 'Ruins_Elevator[left1]', 'Ruins2_05[left1]': 'Ruins2_10b[right2]', 'Ruins2_05[top1]': 'Ruins2_09[bot1]', 'Ruins2_05[bot1]': 'Ruins2_06[top1]', 'Ruins2_06[left1]': 'Ruins2_04[right1]', 'Ruins2_06[left2]': 'Ruins2_04[right2]', 'Ruins2_06[right1]': 'Ruins2_08[left1]', 'Ruins2_06[right2]': 'Ruins2_07[left1]', 'Ruins2_06[top1]': 'Ruins2_05[bot1]', 'Ruins2_07[right1]': 'Deepnest_East_03[left1]', 'Ruins2_07[left1]': 'Ruins2_06[right2]', 'Ruins2_07[top1]': 'Ruins2_11_b[bot1]', 'Ruins2_08[left1]': 'Ruins2_06[right1]', 'Ruins2_09[bot1]': 'Ruins2_05[top1]', 'Ruins2_10[right1]': 'RestingGrounds_10[left1]', 'Ruins2_10[left1]': 'RestingGrounds_06[right1]', 'Ruins2_10b[right1]': 'Deepnest_East_09[left1]', 'Ruins2_10b[right2]': 'Ruins2_05[left1]', 'Ruins2_10b[left1]': 'Ruins_Bathhouse[right1]', 'Ruins2_11_b[right1]': 'Deepnest_East_07[left2]', 'Ruins2_11_b[left1]': 'Ruins2_11[right1]', 'Ruins2_11_b[bot1]': 'Ruins2_07[top1]', 'Ruins2_11[right1]': 'Ruins2_11_b[left1]', 'Ruins2_Watcher_Room[bot1]': 'Ruins2_03[top1]', 'Ruins_House_01[left1]': 'Ruins2_04[door_Ruin_House_01]', 'Ruins_House_02[left1]': 'Ruins2_04[door_Ruin_House_02]', 'Ruins_House_03[left1]': 'Ruins2_04[door_Ruin_House_03]', 'Ruins_House_03[left2]': 'Waterways_07[door1]', 'Ruins_Elevator[left1]': 'Ruins2_04[door_Ruin_Elevator]', 'Ruins_Elevator[left2]': 'Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[door1]': 'Ruins_Elevator[left2]', 'Ruins_Bathhouse[right1]': 'Ruins2_10b[left1]', 'RestingGrounds_02[right1]': 'RestingGrounds_04[left1]', 'RestingGrounds_02[left1]': 'Crossroads_46b[right1]', 'RestingGrounds_02[bot1]': 'RestingGrounds_06[top1]', 'RestingGrounds_02[top1]': None, 'RestingGrounds_04[left1]': 'RestingGrounds_02[right1]', 'RestingGrounds_04[right1]': 'RestingGrounds_05[left1]', 'RestingGrounds_05[left1]': 'RestingGrounds_04[right1]', 'RestingGrounds_05[left2]': 'RestingGrounds_07[right1]', 'RestingGrounds_05[left3]': 'RestingGrounds_17[right1]', 'RestingGrounds_05[right1]': 'RestingGrounds_08[left1]', 'RestingGrounds_05[right2]': 'RestingGrounds_09[left1]', 'RestingGrounds_05[bot1]': 'RestingGrounds_10[top1]', 'RestingGrounds_06[left1]': 'Crossroads_50[right1]', 'RestingGrounds_06[right1]': 'Ruins2_10[left1]', 'RestingGrounds_06[top1]': 'RestingGrounds_02[bot1]', 'RestingGrounds_07[right1]': 'RestingGrounds_05[left2]', 'RestingGrounds_08[left1]': 'RestingGrounds_05[right1]', 'RestingGrounds_09[left1]': 'RestingGrounds_05[right2]', 'RestingGrounds_10[left1]': 'Ruins2_10[right1]', 'RestingGrounds_10[top1]': 'RestingGrounds_05[bot1]', 'RestingGrounds_10[top2]': 'RestingGrounds_12[bot1]', 'RestingGrounds_12[bot1]': 'RestingGrounds_10[top2]', 'RestingGrounds_12[door_Mansion]': 'Room_Mansion[left1]', 'RestingGrounds_17[right1]': 'RestingGrounds_05[left3]', 'Room_Mansion[left1]': 'RestingGrounds_12[door_Mansion]', 'Mines_01[bot1]': 'Mines_02[top1]', 'Mines_01[left1]': 'Crossroads_45[right1]', 'Mines_02[top1]': 'Mines_01[bot1]', 'Mines_02[top2]': 'Mines_03[bot1]', 'Mines_02[left1]': 'Mines_33[right1]', 'Mines_02[right1]': 'Mines_29[left1]', 'Mines_03[right1]': 'Mines_17[left1]', 'Mines_03[bot1]': 'Mines_02[top2]', 'Mines_03[top1]': 'Mines_05[bot1]', 'Mines_04[right1]': 'Mines_07[left1]', 'Mines_04[top1]': 'Mines_37[bot1]', 'Mines_04[left1]': 'Mines_17[right1]', 'Mines_04[left2]': 'Mines_29[right1]', 'Mines_04[left3]': 'Mines_29[right2]', 'Mines_05[right1]': 'Mines_19[left1]', 'Mines_05[top1]': 'Mines_11[bot1]', 'Mines_05[bot1]': 'Mines_03[top1]', 'Mines_05[left1]': 'Mines_30[right1]', 'Mines_05[left2]': 'Mines_06[right1]', 'Mines_06[right1]': 'Mines_05[left2]', 'Mines_06[left1]': 'Mines_36[right1]', 'Mines_07[right1]': 'Mines_28[left1]', 'Mines_07[left1]': 'Mines_04[right1]', 'Mines_10[right1]': 'Mines_30[left1]', 'Mines_10[left1]': 'Town[right1]', 'Mines_10[bot1]': 'Mines_16[top1]', 'Mines_11[right1]': 'Mines_18[left1]', 'Mines_11[top1]': 'Mines_13[bot1]', 'Mines_11[bot1]': 'Mines_05[top1]', 'Mines_13[right1]': 'Mines_20[left1]', 'Mines_13[top1]': None, 'Mines_13[bot1]': 'Mines_11[top1]', 'Mines_16[top1]': 'Mines_10[bot1]', 'Mines_17[right1]': 'Mines_04[left1]', 'Mines_17[left1]': 'Mines_03[right1]', 'Mines_18[top1]': 'Mines_32[bot1]', 'Mines_18[left1]': 'Mines_11[right1]', 'Mines_18[right1]': 'Mines_20[left2]', 'Mines_19[left1]': 'Mines_05[right1]', 'Mines_19[right1]': 'Mines_20[left3]', 'Mines_20[left1]': 'Mines_13[right1]', 'Mines_20[left2]': 'Mines_18[right1]', 'Mines_20[left3]': 'Mines_19[right1]', 'Mines_20[bot1]': 'Mines_37[top1]', 'Mines_20[right1]': 'Mines_23[left1]', 'Mines_20[right2]': 'Mines_31[left1]', 'Mines_23[left1]': 'Mines_20[right1]', 'Mines_23[right1]': 'Mines_25[left1]', 'Mines_23[right2]': 'Mines_24[left1]', 'Mines_23[top1]': None, 'Mines_24[left1]': 'Mines_23[right2]', 'Mines_25[left1]': 'Mines_23[right1]', 'Mines_25[top1]': 'Mines_34[bot1]', 'Mines_28[left1]': 'Mines_07[right1]', 'Mines_28[bot1]': 'RestingGrounds_02[top1]', 'Mines_28[door1]': 'Mines_35[left1]', 'Mines_29[left1]': 'Mines_02[right1]', 'Mines_29[right1]': 'Mines_04[left2]', 'Mines_29[right2]': 'Mines_04[left3]', 'Mines_30[left1]': 'Mines_10[right1]', 'Mines_30[right1]': 'Mines_05[left1]', 'Mines_31[left1]': 'Mines_20[right2]', 'Mines_32[bot1]': 'Mines_18[top1]', 'Mines_33[right1]': 'Mines_02[left1]', 'Mines_33[left1]': 'Crossroads_03[right2]', 'Mines_34[bot1]': 'Mines_25[top1]', 'Mines_34[bot2]': 'Mines_23[top1]', 'Mines_34[left1]': 'Mines_13[top1]', 'Mines_35[left1]': 'Mines_28[door1]', 'Mines_36[right1]': 'Mines_06[left1]', 'Mines_37[bot1]': 'Mines_04[top1]', 'Mines_37[top1]': 'Mines_20[bot1]', 'Fungus3_04[left1]': 'Fungus3_21[right1]', 'Fungus3_04[left2]': 'Fungus3_13[right1]', 'Fungus3_04[right1]': 'Fungus3_34[left1]', 'Fungus3_04[right2]': 'Fungus3_05[left1]', 'Fungus3_05[left1]': 'Fungus3_04[right2]', 'Fungus3_05[right1]': 'Fungus1_24[left1]', 'Fungus3_05[right2]': 'Fungus3_11[left1]', 'Fungus3_08[left1]': 'Deepnest_43[right1]', 'Fungus3_08[right1]': 'Fungus3_11[left2]', 'Fungus3_08[top1]': 'Fungus3_10[bot1]', 'Fungus3_10[top1]': 'Fungus3_13[bot1]', 'Fungus3_10[bot1]': 'Fungus3_08[top1]', 'Fungus3_11[left1]': 'Fungus3_05[right2]', 'Fungus3_11[left2]': 'Fungus3_08[right1]', 'Fungus3_11[right1]': 'Fungus3_39[left1]', 'Fungus3_13[left1]': 'Fungus1_23[right1]', 'Fungus3_13[left2]': 'Fungus3_40[right1]', 'Fungus3_13[left3]': 'Fungus3_49[right1]', 'Fungus3_13[bot1]': 'Fungus3_10[top1]', 'Fungus3_13[right1]': 'Fungus3_04[left2]', 'Fungus3_21[right1]': 'Fungus3_04[left1]', 'Fungus3_21[top1]': 'Fungus3_22[bot1]', 'Fungus3_22[right1]': 'Fungus1_13[left1]', 'Fungus3_22[left1]': 'Fungus3_23[right1]', 'Fungus3_22[bot1]': 'Fungus3_21[top1]', 'Fungus3_23[right1]': 'Fungus3_22[left1]', 'Fungus3_23[left1]': 'Fungus3_48[right1]', 'Fungus3_34[right1]': 'Fungus3_03[left1]', 'Fungus3_34[left1]': 'Fungus3_04[right1]', 'Fungus3_34[top1]': 'Fungus3_44[bot1]', 'Fungus3_39[right1]': 'Deepnest_01[left1]', 'Fungus3_39[left1]': 'Fungus3_11[right1]', 'Fungus3_40[right1]': 'Fungus3_13[left2]', 'Fungus3_40[top1]': 'Fungus3_48[bot1]', 'Fungus3_48[right1]': 'Fungus3_23[left1]', 'Fungus3_48[right2]': 'Fungus1_23[left1]', 'Fungus3_48[door1]': 'Room_Queen[left1]', 'Fungus3_48[bot1]': 'Fungus3_40[top1]', 'Fungus3_49[right1]': 'Fungus3_13[left3]', 'Fungus3_50[right1]': 'Deepnest_43[left1]', 'Room_Queen[left1]': 'Fungus3_48[door1]', 'Cliffs_01[right1]': 'Cliffs_02[left1]', 'Cliffs_01[right2]': 'Cliffs_04[left1]', 'Cliffs_01[right3]': 'Fungus1_28[left1]', 'Cliffs_01[right4]': 'Cliffs_06[left1]', 'Cliffs_02[right1]': 'Town[top1]', 'Cliffs_02[bot1]': 'Tutorial_01[top2]', 'Cliffs_02[bot2]': 'Tutorial_01[top1]', 'Cliffs_02[door1]': 'Room_nailmaster[left1]', 'Cliffs_02[left1]': 'Cliffs_01[right1]', 'Cliffs_02[left2]': 'Cliffs_03[right1]', 'Cliffs_03[right1]': 'Cliffs_02[left2]', 'Cliffs_04[right1]': 'Cliffs_05[left1]', 'Cliffs_04[left1]': 'Cliffs_01[right2]', 'Cliffs_05[left1]': 'Cliffs_04[right1]', 'Cliffs_06[left1]': 'Cliffs_01[right4]', 'Room_nailmaster[left1]': 'Cliffs_02[door1]', 'White_Palace_01[left1]': 'White_Palace_11[door2]', 'White_Palace_01[right1]': 'White_Palace_02[left1]', 'White_Palace_01[top1]': 'White_Palace_03_hub[bot1]', 'White_Palace_02[left1]': 'White_Palace_01[right1]', 'White_Palace_03_hub[left1]': 'White_Palace_14[right1]', 'White_Palace_03_hub[left2]': 'White_Palace_04[right2]', 'White_Palace_03_hub[right1]': 'White_Palace_15[left1]', 'White_Palace_03_hub[top1]': 'White_Palace_06[bot1]', 'White_Palace_03_hub[bot1]': 'White_Palace_01[top1]', 'White_Palace_04[top1]': 'White_Palace_14[bot1]', 'White_Palace_04[right2]': 'White_Palace_03_hub[left2]', 'White_Palace_05[left1]': 'White_Palace_15[right1]', 'White_Palace_05[left2]': 'White_Palace_15[right2]', 'White_Palace_05[right1]': 'White_Palace_16[left1]', 'White_Palace_05[right2]': 'White_Palace_16[left2]', 'White_Palace_06[left1]': 'White_Palace_18[right1]', 'White_Palace_06[top1]': 'White_Palace_07[bot1]', 'White_Palace_06[bot1]': 'White_Palace_03_hub[top1]', 'White_Palace_07[top1]': 'White_Palace_12[bot1]', 'White_Palace_07[bot1]': 'White_Palace_06[top1]', 'White_Palace_08[left1]': 'White_Palace_13[right1]', 'White_Palace_08[right1]': 'White_Palace_13[left3]', 'White_Palace_09[right1]': 'White_Palace_13[left1]', 'White_Palace_11[door2]': 'White_Palace_01[left1]', 'White_Palace_12[right1]': 'White_Palace_13[left2]', 'White_Palace_12[bot1]': 'White_Palace_07[top1]', 'White_Palace_13[right1]': 'White_Palace_08[left1]', 'White_Palace_13[left1]': 'White_Palace_09[right1]', 'White_Palace_13[left2]': 'White_Palace_12[right1]', 'White_Palace_13[left3]': 'White_Palace_08[right1]', 'White_Palace_14[bot1]': 'White_Palace_04[top1]', 'White_Palace_14[right1]': 'White_Palace_03_hub[left1]', 'White_Palace_15[left1]': 'White_Palace_03_hub[right1]', 'White_Palace_15[right1]': 'White_Palace_05[left1]', 'White_Palace_15[right2]': 'White_Palace_05[left2]', 'White_Palace_16[left1]': 'White_Palace_05[right1]', 'White_Palace_16[left2]': 'White_Palace_05[right2]', 'White_Palace_17[right1]': 'White_Palace_19[left1]', 'White_Palace_17[bot1]': 'White_Palace_18[top1]', 'White_Palace_18[top1]': 'White_Palace_17[bot1]', 'White_Palace_18[right1]': 'White_Palace_06[left1]', 'White_Palace_19[top1]': 'White_Palace_20[bot1]', 'White_Palace_19[left1]': 'White_Palace_17[right1]', 'White_Palace_20[bot1]': 'White_Palace_19[top1]'} event_names = {'Abyss_01', 'Abyss_03', 'Abyss_03_b', 'Abyss_03_c', 'Abyss_04', 'Abyss_05', 'Abyss_06_Core', 'Abyss_09', 'Abyss_19', 'Broke_Sanctum_Glass_Floor', 'Can_Bench', 'Can_Repair_Fragile_Charms', 'Can_Replenish_Geo', 'Can_Replenish_Geo-Crossroads', 'Can_Stag', 'Cliffs_01', 'Cliffs_02', 'Completed_Path_of_Pain', 'Crossroads_03', 'Crossroads_07', 'Crossroads_08', 'Crossroads_14', 'Crossroads_18', 'Crossroads_19', 'Crossroads_21', 'Crossroads_27', 'Crossroads_33', 'Deepnest_01', 'Deepnest_01b', 'Deepnest_02', 'Deepnest_03', 'Deepnest_10', 'Deepnest_14', 'Deepnest_17', 'Deepnest_26', 'Deepnest_34', 'Deepnest_35', 'Deepnest_37', 'Deepnest_39', 'Deepnest_41', 'Deepnest_42', 'Deepnest_East_02', 'Deepnest_East_03', 'Deepnest_East_04', 'Deepnest_East_07', 'Deepnest_East_11', 'Deepnest_East_18', 'Defeated_Broken_Vessel', 'Defeated_Brooding_Mawlek', 'Defeated_Collector', 'Defeated_Colosseum_1', 'Defeated_Colosseum_2', 'Defeated_Colosseum_Zote', 'Defeated_Crystal_Guardian', 'Defeated_Dung_Defender', 'Defeated_Elder_Hu', 'Defeated_Elegant_Warrior', 'Defeated_Enraged_Guardian', 'Defeated_Failed_Champion', 'Defeated_False_Knight', 'Defeated_Flukemarm', 'Defeated_Galien', 'Defeated_Gorb', 'Defeated_Grey_Prince_Zote', 'Defeated_Grimm', 'Defeated_Gruz_Mother', 'Defeated_Hive_Knight', 'Defeated_Hornet_1', 'Defeated_Hornet_2', "Defeated_King's_Station_Arena", 'Defeated_Lost_Kin', 'Defeated_Mantis_Lords', 'Defeated_Markoth', 'Defeated_Marmu', 'Defeated_No_Eyes', 'Defeated_Nosk', 'Defeated_Pale_Lurker', 'Defeated_Path_of_Pain_Arena', 'Defeated_Sanctum_Warrior', 'Defeated_Shrumal_Ogre_Arena', 'Defeated_Soul_Master', 'Defeated_Soul_Tyrant', 'Defeated_Traitor_Lord', 'Defeated_Uumuu', 'Defeated_Watcher_Knights', "Defeated_West_Queen's_Gardens_Arena", 'Defeated_White_Defender', 'Defeated_Xero', 'First_Grimmchild_Upgrade', 'Fungus1_11', 'Fungus1_21', 'Fungus1_30', 'Fungus2_01', 'Fungus2_03', 'Fungus2_04', 'Fungus2_06', 'Fungus2_11', 'Fungus2_13', 'Fungus2_14', 'Fungus2_17', 'Fungus2_20', 'Fungus2_23', 'Fungus3_01', 'Fungus3_02', 'Fungus3_04', 'Fungus3_11', 'Fungus3_13', 'Fungus3_22', 'Fungus3_26', 'Fungus3_34', 'Fungus3_40', 'Fungus3_44', 'Fungus3_47', 'Hive_03_c', 'Left_Elevator', 'Lever-Dung_Defender', 'Lever-Shade_Soul', 'Lit_Abyss_Lighthouse', 'Lower_Tram', 'Mines_02', 'Mines_03', 'Mines_04', 'Mines_05', 'Mines_10', 'Mines_11', 'Mines_18', 'Mines_20', 'Mines_23', 'Nightmare_Lantern_Lit', 'Opened_Archives_Exit_Wall', 'Opened_Black_Egg_Temple', 'Opened_Dung_Defender_Wall', 'Opened_Emilitia_Door', 'Opened_Gardens_Stag_Exit', 'Opened_Glade_Door', "Opened_Lower_Kingdom's_Edge_Wall", 'Opened_Mawlek_Wall', 'Opened_Pleasure_House_Wall', 'Opened_Resting_Grounds_Catacombs_Wall', 'Opened_Resting_Grounds_Floor', 'Opened_Shaman_Pillar', 'Opened_Tramway_Exit_Gate', 'Opened_Waterways_Exit', 'Opened_Waterways_Manhole', 'Palace_Atrium_Gates_Opened', 'Palace_Entrance_Lantern_Lit', 'Palace_Left_Lantern_Lit', 'Palace_Right_Lantern_Lit', 'Rescued_Bretta', 'Rescued_Deepnest_Zote', 'Rescued_Sly', 'RestingGrounds_02', 'RestingGrounds_05', 'RestingGrounds_10', 'Right_Elevator', 'Ruins1_03', 'Ruins1_05', 'Ruins1_05b', 'Ruins1_05c', 'Ruins1_23', 'Ruins1_28', 'Ruins1_30', 'Ruins1_31', 'Ruins2_01', 'Ruins2_01_b', 'Ruins2_03b', 'Ruins2_04', 'Ruins2_10', 'Second_Grimmchild_Upgrade', 'Town', 'Tutorial_01', 'Upper_Tram', 'Warp-Lifeblood_Core_to_Abyss', 'Warp-Palace_Grounds_to_White_Palace', 'Warp-Path_of_Pain_Complete', 'Warp-White_Palace_Atrium_to_Palace_Grounds', 'Warp-White_Palace_Entrance_to_Palace_Grounds', 'Waterways_01', 'Waterways_02', 'Waterways_04', 'Waterways_04b', 'Waterways_07', 'White_Palace_01', 'White_Palace_03_hub', 'White_Palace_13'} exits = {'Room_temple': ['Room_temple[left1]'], 'Tutorial_01': ['Tutorial_01[right1]', 'Tutorial_01[top1]', 'Tutorial_01[top2]'], 'Town': ['Town[left1]', 'Town[bot1]', 'Town[right1]', 'Town[top1]', 'Town[door_station]', 'Town[door_sly]', 'Town[door_mapper]', 'Town[door_jiji]', 'Town[door_bretta]', 'Town[room_divine]', 'Town[room_grimm]'], 'Room_shop': ['Room_shop[left1]'], 'Room_Town_Stag_Station': ['Room_Town_Stag_Station[left1]'], 'Room_mapper': ['Room_mapper[left1]'], 'Room_Bretta': ['Room_Bretta[right1]'], 'Room_Ouiji': ['Room_Ouiji[left1]'], 'Grimm_Divine': ['Grimm_Divine[left1]'], 'Grimm_Main_Tent': ['Grimm_Main_Tent[left1]'], 'Crossroads_01': ['Crossroads_01[top1]', 'Crossroads_01[left1]', 'Crossroads_01[right1]'], 'Crossroads_02': ['Crossroads_02[left1]', 'Crossroads_02[door1]', 'Crossroads_02[right1]'], 'Crossroads_03': ['Crossroads_03[right1]', 'Crossroads_03[right2]', 'Crossroads_03[left1]', 'Crossroads_03[left2]', 'Crossroads_03[bot1]', 'Crossroads_03[top1]'], 'Crossroads_04': ['Crossroads_04[left1]', 'Crossroads_04[top1]', 'Crossroads_04[door_Mender_House]', 'Crossroads_04[door1]', 'Crossroads_04[door_charmshop]', 'Crossroads_04[right1]'], 'Crossroads_05': ['Crossroads_05[left1]', 'Crossroads_05[right1]'], 'Crossroads_06': ['Crossroads_06[left1]', 'Crossroads_06[door1]', 'Crossroads_06[right1]'], 'Crossroads_07': ['Crossroads_07[left1]', 'Crossroads_07[left2]', 'Crossroads_07[left3]', 'Crossroads_07[right1]', 'Crossroads_07[right2]', 'Crossroads_07[bot1]'], 'Crossroads_08': ['Crossroads_08[left1]', 'Crossroads_08[left2]', 'Crossroads_08[right1]', 'Crossroads_08[right2]'], 'Crossroads_09': ['Crossroads_09[left1]', 'Crossroads_09[right1]'], 'Crossroads_10': ['Crossroads_10[left1]', 'Crossroads_10[right1]'], 'Crossroads_11_alt': ['Crossroads_11_alt[left1]', 'Crossroads_11_alt[right1]'], 'Crossroads_12': ['Crossroads_12[left1]', 'Crossroads_12[right1]'], 'Crossroads_13': ['Crossroads_13[left1]', 'Crossroads_13[right1]'], 'Crossroads_14': ['Crossroads_14[left1]', 'Crossroads_14[left2]', 'Crossroads_14[right1]', 'Crossroads_14[right2]'], 'Crossroads_15': ['Crossroads_15[left1]', 'Crossroads_15[right1]'], 'Crossroads_16': ['Crossroads_16[left1]', 'Crossroads_16[right1]', 'Crossroads_16[bot1]'], 'Crossroads_18': ['Crossroads_18[right1]', 'Crossroads_18[right2]', 'Crossroads_18[bot1]'], 'Crossroads_19': ['Crossroads_19[right1]', 'Crossroads_19[top1]', 'Crossroads_19[left1]', 'Crossroads_19[left2]'], 'Crossroads_21': ['Crossroads_21[left1]', 'Crossroads_21[right1]', 'Crossroads_21[top1]'], 'Crossroads_22': ['Crossroads_22[bot1]'], 'Crossroads_25': ['Crossroads_25[right1]', 'Crossroads_25[left1]'], 'Crossroads_27': ['Crossroads_27[right1]', 'Crossroads_27[bot1]', 'Crossroads_27[left1]', 'Crossroads_27[left2]'], 'Crossroads_30': ['Crossroads_30[left1]'], 'Crossroads_31': ['Crossroads_31[right1]'], 'Crossroads_33': ['Crossroads_33[top1]', 'Crossroads_33[left1]', 'Crossroads_33[left2]', 'Crossroads_33[right1]', 'Crossroads_33[right2]'], 'Crossroads_35': ['Crossroads_35[bot1]', 'Crossroads_35[right1]'], 'Crossroads_36': ['Crossroads_36[right1]', 'Crossroads_36[right2]'], 'Crossroads_37': ['Crossroads_37[right1]'], 'Crossroads_38': ['Crossroads_38[right1]'], 'Crossroads_39': ['Crossroads_39[right1]', 'Crossroads_39[left1]'], 'Crossroads_40': ['Crossroads_40[right1]', 'Crossroads_40[left1]'], 'Crossroads_42': ['Crossroads_42[left1]', 'Crossroads_42[right1]'], 'Crossroads_43': ['Crossroads_43[left1]', 'Crossroads_43[right1]'], 'Crossroads_45': ['Crossroads_45[right1]', 'Crossroads_45[left1]'], 'Crossroads_46': ['Crossroads_46[left1]'], 'Crossroads_46b': ['Crossroads_46b[right1]'], 'Crossroads_ShamanTemple': ['Crossroads_ShamanTemple[left1]'], 'Crossroads_47': ['Crossroads_47[right1]'], 'Crossroads_48': ['Crossroads_48[left1]'], 'Crossroads_49': ['Crossroads_49[right1]', 'Crossroads_49[left1]'], 'Crossroads_49b': ['Crossroads_49b[right1]'], 'Crossroads_50': ['Crossroads_50[right1]', 'Crossroads_50[left1]'], 'Crossroads_52': ['Crossroads_52[left1]'], 'Room_ruinhouse': ['Room_ruinhouse[left1]'], 'Room_Charm_Shop': ['Room_Charm_Shop[left1]'], 'Room_Mender_House': ['Room_Mender_House[left1]'], 'Fungus1_01': ['Fungus1_01[left1]', 'Fungus1_01[right1]'], 'Fungus1_01b': ['Fungus1_01b[left1]', 'Fungus1_01b[right1]'], 'Fungus1_02': ['Fungus1_02[left1]', 'Fungus1_02[right1]', 'Fungus1_02[right2]'], 'Fungus1_03': ['Fungus1_03[left1]', 'Fungus1_03[right1]', 'Fungus1_03[bot1]'], 'Fungus1_04': ['Fungus1_04[left1]', 'Fungus1_04[right1]'], 'Fungus1_05': ['Fungus1_05[right1]', 'Fungus1_05[bot1]', 'Fungus1_05[top1]'], 'Fungus1_06': ['Fungus1_06[left1]', 'Fungus1_06[bot1]'], 'Fungus1_07': ['Fungus1_07[top1]', 'Fungus1_07[left1]', 'Fungus1_07[right1]'], 'Fungus1_08': ['Fungus1_08[left1]'], 'Fungus1_09': ['Fungus1_09[left1]', 'Fungus1_09[right1]'], 'Fungus1_10': ['Fungus1_10[left1]', 'Fungus1_10[right1]', 'Fungus1_10[top1]'], 'Fungus1_11': ['Fungus1_11[top1]', 'Fungus1_11[right1]', 'Fungus1_11[right2]', 'Fungus1_11[left1]', 'Fungus1_11[bot1]'], 'Fungus1_12': ['Fungus1_12[left1]', 'Fungus1_12[right1]'], 'Fungus1_13': ['Fungus1_13[right1]', 'Fungus1_13[left1]'], 'Fungus1_14': ['Fungus1_14[left1]'], 'Fungus1_15': ['Fungus1_15[door1]', 'Fungus1_15[right1]'], 'Fungus1_16_alt': ['Fungus1_16_alt[right1]'], 'Fungus1_17': ['Fungus1_17[left1]', 'Fungus1_17[right1]'], 'Fungus1_19': ['Fungus1_19[left1]', 'Fungus1_19[right1]', 'Fungus1_19[bot1]'], 'Fungus1_20_v02': ['Fungus1_20_v02[bot1]', 'Fungus1_20_v02[bot2]', 'Fungus1_20_v02[right1]'], 'Fungus1_21': ['Fungus1_21[bot1]', 'Fungus1_21[top1]', 'Fungus1_21[left1]', 'Fungus1_21[right1]'], 'Fungus1_22': ['Fungus1_22[bot1]', 'Fungus1_22[top1]', 'Fungus1_22[left1]'], 'Fungus1_23': ['Fungus1_23[left1]', 'Fungus1_23[right1]'], 'Fungus1_24': ['Fungus1_24[left1]'], 'Fungus1_25': ['Fungus1_25[right1]', 'Fungus1_25[left1]'], 'Fungus1_26': ['Fungus1_26[right1]', 'Fungus1_26[left1]', 'Fungus1_26[door_SlugShrine]'], 'Fungus1_28': ['Fungus1_28[left1]', 'Fungus1_28[left2]'], 'Fungus1_29': ['Fungus1_29[left1]', 'Fungus1_29[right1]'], 'Fungus1_30': ['Fungus1_30[top1]', 'Fungus1_30[top3]', 'Fungus1_30[left1]', 'Fungus1_30[right1]'], 'Fungus1_31': ['Fungus1_31[top1]', 'Fungus1_31[bot1]', 'Fungus1_31[right1]'], 'Fungus1_32': ['Fungus1_32[bot1]', 'Fungus1_32[top1]', 'Fungus1_32[left1]'], 'Fungus1_34': ['Fungus1_34[door1]', 'Fungus1_34[left1]'], 'Fungus1_35': ['Fungus1_35[left1]', 'Fungus1_35[right1]'], 'Fungus1_36': ['Fungus1_36[left1]'], 'Fungus1_37': ['Fungus1_37[left1]'], 'Fungus1_Slug': ['Fungus1_Slug[right1]'], 'Room_Slug_Shrine': ['Room_Slug_Shrine[left1]'], 'Room_nailmaster_02': ['Room_nailmaster_02[left1]'], 'Fungus3_01': ['Fungus3_01[top1]', 'Fungus3_01[right1]', 'Fungus3_01[left1]', 'Fungus3_01[right2]'], 'Fungus3_02': ['Fungus3_02[left1]', 'Fungus3_02[left2]', 'Fungus3_02[left3]', 'Fungus3_02[right1]', 'Fungus3_02[right2]'], 'Fungus3_03': ['Fungus3_03[right1]', 'Fungus3_03[left1]'], 'Fungus3_24': ['Fungus3_24[right1]', 'Fungus3_24[left1]', 'Fungus3_24[top1]'], 'Fungus3_25': ['Fungus3_25[right1]', 'Fungus3_25[left1]'], 'Fungus3_25b': ['Fungus3_25b[right1]', 'Fungus3_25b[left1]'], 'Fungus3_26': ['Fungus3_26[top1]', 'Fungus3_26[left1]', 'Fungus3_26[left2]', 'Fungus3_26[left3]', 'Fungus3_26[right1]'], 'Fungus3_27': ['Fungus3_27[left1]', 'Fungus3_27[right1]'], 'Fungus3_28': ['Fungus3_28[right1]'], 'Fungus3_30': ['Fungus3_30[bot1]'], 'Fungus3_35': ['Fungus3_35[right1]'], 'Fungus3_44': ['Fungus3_44[bot1]', 'Fungus3_44[door1]', 'Fungus3_44[right1]'], 'Fungus3_47': ['Fungus3_47[left1]', 'Fungus3_47[right1]', 'Fungus3_47[door1]'], 'Room_Fungus_Shaman': ['Room_Fungus_Shaman[left1]'], 'Fungus3_archive': ['Fungus3_archive[left1]', 'Fungus3_archive[bot1]'], 'Fungus3_archive_02': ['Fungus3_archive_02[top1]'], 'Fungus2_01': ['Fungus2_01[left1]', 'Fungus2_01[left2]', 'Fungus2_01[left3]', 'Fungus2_01[right1]'], 'Fungus2_02': ['Fungus2_02[right1]'], 'Fungus2_34': ['Fungus2_34[right1]'], 'Fungus2_03': ['Fungus2_03[left1]', 'Fungus2_03[bot1]', 'Fungus2_03[right1]'], 'Fungus2_04': ['Fungus2_04[top1]', 'Fungus2_04[right1]', 'Fungus2_04[left1]', 'Fungus2_04[right2]'], 'Fungus2_05': ['Fungus2_05[bot1]', 'Fungus2_05[right1]'], 'Fungus2_06': ['Fungus2_06[top1]', 'Fungus2_06[left1]', 'Fungus2_06[left2]', 'Fungus2_06[right1]', 'Fungus2_06[right2]'], 'Fungus2_07': ['Fungus2_07[left1]', 'Fungus2_07[right1]'], 'Fungus2_08': ['Fungus2_08[left1]', 'Fungus2_08[left2]', 'Fungus2_08[right1]'], 'Fungus2_09': ['Fungus2_09[left1]', 'Fungus2_09[right1]'], 'Fungus2_10': ['Fungus2_10[right1]', 'Fungus2_10[right2]', 'Fungus2_10[bot1]'], 'Fungus2_11': ['Fungus2_11[top1]', 'Fungus2_11[left1]', 'Fungus2_11[left2]', 'Fungus2_11[right1]'], 'Fungus2_12': ['Fungus2_12[left1]', 'Fungus2_12[bot1]'], 'Fungus2_13': ['Fungus2_13[top1]', 'Fungus2_13[left2]', 'Fungus2_13[left3]'], 'Fungus2_14': ['Fungus2_14[top1]', 'Fungus2_14[right1]', 'Fungus2_14[bot3]'], 'Fungus2_15': ['Fungus2_15[top3]', 'Fungus2_15[right1]', 'Fungus2_15[left1]'], 'Fungus2_17': ['Fungus2_17[left1]', 'Fungus2_17[right1]', 'Fungus2_17[bot1]'], 'Fungus2_18': ['Fungus2_18[right1]', 'Fungus2_18[bot1]', 'Fungus2_18[top1]'], 'Fungus2_19': ['Fungus2_19[top1]', 'Fungus2_19[left1]'], 'Fungus2_20': ['Fungus2_20[right1]', 'Fungus2_20[left1]'], 'Fungus2_21': ['Fungus2_21[right1]', 'Fungus2_21[left1]'], 'Fungus2_23': ['Fungus2_23[right1]', 'Fungus2_23[right2]'], 'Fungus2_26': ['Fungus2_26[left1]'], 'Fungus2_28': ['Fungus2_28[left1]', 'Fungus2_28[left2]'], 'Fungus2_29': ['Fungus2_29[right1]', 'Fungus2_29[bot1]'], 'Fungus2_30': ['Fungus2_30[bot1]', 'Fungus2_30[top1]'], 'Fungus2_31': ['Fungus2_31[left1]'], 'Fungus2_32': ['Fungus2_32[left1]'], 'Fungus2_33': ['Fungus2_33[right1]', 'Fungus2_33[left1]'], 'Deepnest_01': ['Deepnest_01[right1]', 'Deepnest_01[bot1]', 'Deepnest_01[bot2]', 'Deepnest_01[left1]'], 'Deepnest_01b': ['Deepnest_01b[top1]', 'Deepnest_01b[top2]', 'Deepnest_01b[right1]', 'Deepnest_01b[right2]', 'Deepnest_01b[bot1]'], 'Deepnest_02': ['Deepnest_02[left1]', 'Deepnest_02[left2]', 'Deepnest_02[right1]'], 'Deepnest_03': ['Deepnest_03[right1]', 'Deepnest_03[left1]', 'Deepnest_03[top1]', 'Deepnest_03[left2]'], 'Deepnest_09': ['Deepnest_09[left1]'], 'Deepnest_10': ['Deepnest_10[right1]', 'Deepnest_10[right2]', 'Deepnest_10[right3]', 'Deepnest_10[door1]', 'Deepnest_10[door2]'], 'Room_spider_small': ['Room_spider_small[left1]'], 'Deepnest_Spider_Town': ['Deepnest_Spider_Town[left1]'], 'Deepnest_14': ['Deepnest_14[right1]', 'Deepnest_14[left1]', 'Deepnest_14[bot1]', 'Deepnest_14[bot2]'], 'Deepnest_16': ['Deepnest_16[left1]', 'Deepnest_16[bot1]'], 'Deepnest_17': ['Deepnest_17[left1]', 'Deepnest_17[right1]', 'Deepnest_17[top1]', 'Deepnest_17[bot1]'], 'Fungus2_25': ['Fungus2_25[top1]', 'Fungus2_25[top2]', 'Fungus2_25[right1]'], 'Deepnest_26': ['Deepnest_26[left1]', 'Deepnest_26[left2]', 'Deepnest_26[right1]', 'Deepnest_26[bot1]'], 'Deepnest_26b': ['Deepnest_26b[right2]', 'Deepnest_26b[right1]'], 'Deepnest_30': ['Deepnest_30[left1]', 'Deepnest_30[top1]', 'Deepnest_30[right1]'], 'Deepnest_31': ['Deepnest_31[right1]', 'Deepnest_31[right2]'], 'Deepnest_32': ['Deepnest_32[left1]'], 'Deepnest_33': ['Deepnest_33[top1]', 'Deepnest_33[top2]', 'Deepnest_33[bot1]'], 'Deepnest_34': ['Deepnest_34[left1]', 'Deepnest_34[right1]', 'Deepnest_34[top1]'], 'Deepnest_35': ['Deepnest_35[left1]', 'Deepnest_35[top1]', 'Deepnest_35[bot1]'], 'Deepnest_36': ['Deepnest_36[left1]'], 'Deepnest_37': ['Deepnest_37[left1]', 'Deepnest_37[right1]', 'Deepnest_37[top1]', 'Deepnest_37[bot1]'], 'Deepnest_38': ['Deepnest_38[bot1]'], 'Deepnest_39': ['Deepnest_39[left1]', 'Deepnest_39[top1]', 'Deepnest_39[door1]', 'Deepnest_39[right1]'], 'Deepnest_40': ['Deepnest_40[right1]'], 'Deepnest_41': ['Deepnest_41[right1]', 'Deepnest_41[left1]', 'Deepnest_41[left2]'], 'Deepnest_42': ['Deepnest_42[bot1]', 'Deepnest_42[left1]', 'Deepnest_42[top1]'], 'Deepnest_43': ['Deepnest_43[bot1]', 'Deepnest_43[left1]', 'Deepnest_43[right1]'], 'Deepnest_44': ['Deepnest_44[top1]'], 'Deepnest_45_v02': ['Deepnest_45_v02[left1]'], 'Room_Mask_Maker': ['Room_Mask_Maker[right1]'], 'Deepnest_East_01': ['Deepnest_East_01[bot1]', 'Deepnest_East_01[right1]', 'Deepnest_East_01[top1]'], 'Deepnest_East_02': ['Deepnest_East_02[bot1]', 'Deepnest_East_02[bot2]', 'Deepnest_East_02[top1]', 'Deepnest_East_02[right1]'], 'Deepnest_East_03': ['Deepnest_East_03[left1]', 'Deepnest_East_03[left2]', 'Deepnest_East_03[top1]', 'Deepnest_East_03[top2]', 'Deepnest_East_03[right1]', 'Deepnest_East_03[right2]'], 'Deepnest_East_04': ['Deepnest_East_04[left1]', 'Deepnest_East_04[left2]', 'Deepnest_East_04[right2]', 'Deepnest_East_04[right1]'], 'Deepnest_East_06': ['Deepnest_East_06[top1]', 'Deepnest_East_06[left1]', 'Deepnest_East_06[bot1]', 'Deepnest_East_06[door1]', 'Deepnest_East_06[right1]'], 'Deepnest_East_07': ['Deepnest_East_07[bot1]', 'Deepnest_East_07[bot2]', 'Deepnest_East_07[left1]', 'Deepnest_East_07[left2]', 'Deepnest_East_07[right1]'], 'Deepnest_East_08': ['Deepnest_East_08[right1]', 'Deepnest_East_08[top1]'], 'Deepnest_East_09': ['Deepnest_East_09[right1]', 'Deepnest_East_09[left1]', 'Deepnest_East_09[bot1]'], 'Deepnest_East_10': ['Deepnest_East_10[left1]'], 'Deepnest_East_11': ['Deepnest_East_11[right1]', 'Deepnest_East_11[left1]', 'Deepnest_East_11[top1]', 'Deepnest_East_11[bot1]'], 'Deepnest_East_12': ['Deepnest_East_12[right1]', 'Deepnest_East_12[left1]'], 'Deepnest_East_13': ['Deepnest_East_13[bot1]'], 'Deepnest_East_14': ['Deepnest_East_14[top2]', 'Deepnest_East_14[left1]', 'Deepnest_East_14[door1]'], 'Deepnest_East_14b': ['Deepnest_East_14b[right1]', 'Deepnest_East_14b[top1]'], 'Deepnest_East_15': ['Deepnest_East_15[left1]'], 'Deepnest_East_16': ['Deepnest_East_16[left1]', 'Deepnest_East_16[bot1]'], 'Deepnest_East_17': ['Deepnest_East_17[left1]'], 'Deepnest_East_18': ['Deepnest_East_18[top1]', 'Deepnest_East_18[bot1]', 'Deepnest_East_18[right2]'], 'Room_nailmaster_03': ['Room_nailmaster_03[left1]'], 'Deepnest_East_Hornet': ['Deepnest_East_Hornet[left1]', 'Deepnest_East_Hornet[left2]'], 'Room_Wyrm': ['Room_Wyrm[right1]'], 'GG_Lurker': ['GG_Lurker[left1]'], 'Hive_01': ['Hive_01[left1]', 'Hive_01[right1]', 'Hive_01[right2]'], 'Hive_02': ['Hive_02[left1]', 'Hive_02[left2]', 'Hive_02[left3]'], 'Hive_03_c': ['Hive_03_c[left1]', 'Hive_03_c[right2]', 'Hive_03_c[right3]', 'Hive_03_c[top1]'], 'Hive_03': ['Hive_03[bot1]', 'Hive_03[right1]', 'Hive_03[top1]'], 'Hive_04': ['Hive_04[left1]', 'Hive_04[left2]', 'Hive_04[right1]'], 'Hive_05': ['Hive_05[left1]'], 'Room_Colosseum_01': ['Room_Colosseum_01[left1]', 'Room_Colosseum_01[bot1]'], 'Room_Colosseum_02': ['Room_Colosseum_02[top1]', 'Room_Colosseum_02[top2]'], 'Room_Colosseum_Spectate': ['Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[right1]'], 'Abyss_01': ['Abyss_01[left1]', 'Abyss_01[left2]', 'Abyss_01[left3]', 'Abyss_01[right1]', 'Abyss_01[right2]'], 'Abyss_02': ['Abyss_02[right1]', 'Abyss_02[bot1]'], 'Abyss_03': ['Abyss_03[bot1]', 'Abyss_03[bot2]', 'Abyss_03[top1]'], 'Abyss_03_b': ['Abyss_03_b[left1]'], 'Abyss_03_c': ['Abyss_03_c[right1]', 'Abyss_03_c[top1]'], 'Abyss_04': ['Abyss_04[top1]', 'Abyss_04[left1]', 'Abyss_04[bot1]', 'Abyss_04[right1]'], 'Abyss_05': ['Abyss_05[left1]', 'Abyss_05[right1]'], 'Abyss_06_Core': ['Abyss_06_Core[top1]', 'Abyss_06_Core[left1]', 'Abyss_06_Core[left3]', 'Abyss_06_Core[right2]', 'Abyss_06_Core[bot1]'], 'Abyss_08': ['Abyss_08[right1]'], 'Abyss_09': ['Abyss_09[right1]', 'Abyss_09[right2]', 'Abyss_09[right3]', 'Abyss_09[left1]'], 'Abyss_10': ['Abyss_10[left1]', 'Abyss_10[left2]'], 'Abyss_12': ['Abyss_12[right1]'], 'Abyss_15': ['Abyss_15[top1]'], 'Abyss_16': ['Abyss_16[left1]', 'Abyss_16[right1]'], 'Abyss_17': ['Abyss_17[top1]'], 'Abyss_18': ['Abyss_18[left1]', 'Abyss_18[right1]'], 'Abyss_19': ['Abyss_19[left1]', 'Abyss_19[right1]', 'Abyss_19[bot1]', 'Abyss_19[bot2]'], 'Abyss_20': ['Abyss_20[top1]', 'Abyss_20[top2]'], 'Abyss_21': ['Abyss_21[right1]'], 'Abyss_22': ['Abyss_22[left1]'], 'Abyss_Lighthouse_room': ['Abyss_Lighthouse_room[left1]'], 'Waterways_01': ['Waterways_01[top1]', 'Waterways_01[left1]', 'Waterways_01[right1]', 'Waterways_01[bot1]'], 'Waterways_02': ['Waterways_02[top1]', 'Waterways_02[top2]', 'Waterways_02[top3]', 'Waterways_02[bot1]', 'Waterways_02[bot2]'], 'Waterways_03': ['Waterways_03[left1]'], 'Waterways_04': ['Waterways_04[bot1]', 'Waterways_04[right1]', 'Waterways_04[left1]', 'Waterways_04[left2]'], 'Waterways_04b': ['Waterways_04b[right1]', 'Waterways_04b[right2]', 'Waterways_04b[left1]'], 'Waterways_05': ['Waterways_05[right1]', 'Waterways_05[bot1]', 'Waterways_05[bot2]'], 'Waterways_06': ['Waterways_06[right1]', 'Waterways_06[top1]'], 'Waterways_07': ['Waterways_07[right1]', 'Waterways_07[right2]', 'Waterways_07[left1]', 'Waterways_07[door1]', 'Waterways_07[top1]'], 'Waterways_08': ['Waterways_08[top1]', 'Waterways_08[left1]', 'Waterways_08[left2]'], 'Waterways_09': ['Waterways_09[right1]', 'Waterways_09[left1]'], 'Waterways_12': ['Waterways_12[right1]'], 'Waterways_13': ['Waterways_13[left1]', 'Waterways_13[left2]'], 'Waterways_14': ['Waterways_14[bot1]', 'Waterways_14[bot2]'], 'Waterways_15': ['Waterways_15[top1]'], 'GG_Pipeway': ['GG_Pipeway[right1]', 'GG_Pipeway[left1]'], 'GG_Waterways': ['GG_Waterways[right1]', 'GG_Waterways[door1]'], 'Room_GG_Shortcut': ['Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[top1]'], 'Ruins1_01': ['Ruins1_01[left1]', 'Ruins1_01[top1]', 'Ruins1_01[bot1]'], 'Ruins1_02': ['Ruins1_02[top1]', 'Ruins1_02[bot1]'], 'Ruins1_03': ['Ruins1_03[top1]', 'Ruins1_03[left1]', 'Ruins1_03[right1]', 'Ruins1_03[right2]'], 'Ruins1_04': ['Ruins1_04[right1]', 'Ruins1_04[door1]', 'Ruins1_04[bot1]'], 'Ruins1_05b': ['Ruins1_05b[left1]', 'Ruins1_05b[top1]', 'Ruins1_05b[bot1]', 'Ruins1_05b[right1]'], 'Ruins1_05c': ['Ruins1_05c[left2]', 'Ruins1_05c[bot1]', 'Ruins1_05c[top1]', 'Ruins1_05c[top2]', 'Ruins1_05c[top3]'], 'Ruins1_05': ['Ruins1_05[bot1]', 'Ruins1_05[bot2]', 'Ruins1_05[bot3]', 'Ruins1_05[right1]', 'Ruins1_05[right2]', 'Ruins1_05[top1]'], 'Ruins1_06': ['Ruins1_06[left1]', 'Ruins1_06[right1]'], 'Ruins1_09': ['Ruins1_09[top1]', 'Ruins1_09[left1]'], 'Ruins1_17': ['Ruins1_17[top1]', 'Ruins1_17[right1]', 'Ruins1_17[bot1]'], 'Ruins1_18': ['Ruins1_18[left1]', 'Ruins1_18[right1]', 'Ruins1_18[right2]'], 'Ruins1_23': ['Ruins1_23[top1]', 'Ruins1_23[right1]', 'Ruins1_23[right2]', 'Ruins1_23[bot1]', 'Ruins1_23[left1]'], 'Ruins1_24': ['Ruins1_24[left1]', 'Ruins1_24[right1]', 'Ruins1_24[left2]', 'Ruins1_24[right2]'], 'Ruins1_25': ['Ruins1_25[left1]', 'Ruins1_25[left2]', 'Ruins1_25[left3]'], 'Ruins1_27': ['Ruins1_27[left1]', 'Ruins1_27[right1]'], 'Ruins1_28': ['Ruins1_28[left1]', 'Ruins1_28[right1]', 'Ruins1_28[bot1]'], 'Ruins1_29': ['Ruins1_29[left1]'], 'Ruins1_30': ['Ruins1_30[left1]', 'Ruins1_30[left2]', 'Ruins1_30[bot1]', 'Ruins1_30[right1]'], 'Ruins1_31': ['Ruins1_31[bot1]', 'Ruins1_31[left1]', 'Ruins1_31[left2]', 'Ruins1_31[left3]', 'Ruins1_31[right1]'], 'Ruins1_31b': ['Ruins1_31b[right1]', 'Ruins1_31b[right2]'], 'Ruins1_32': ['Ruins1_32[right1]', 'Ruins1_32[right2]'], 'Room_nailsmith': ['Room_nailsmith[left1]'], 'Ruins2_01': ['Ruins2_01[top1]', 'Ruins2_01[bot1]', 'Ruins2_01[left2]'], 'Ruins2_01_b': ['Ruins2_01_b[top1]', 'Ruins2_01_b[left1]', 'Ruins2_01_b[right1]'], 'Ruins2_03b': ['Ruins2_03b[top1]', 'Ruins2_03b[top2]', 'Ruins2_03b[left1]', 'Ruins2_03b[bot1]'], 'Ruins2_03': ['Ruins2_03[top1]', 'Ruins2_03[bot1]', 'Ruins2_03[bot2]'], 'Ruins2_04': ['Ruins2_04[left1]', 'Ruins2_04[left2]', 'Ruins2_04[right1]', 'Ruins2_04[right2]', 'Ruins2_04[door_Ruin_House_01]', 'Ruins2_04[door_Ruin_House_02]', 'Ruins2_04[door_Ruin_House_03]', 'Ruins2_04[door_Ruin_Elevator]'], 'Ruins2_05': ['Ruins2_05[left1]', 'Ruins2_05[top1]', 'Ruins2_05[bot1]'], 'Ruins2_06': ['Ruins2_06[left1]', 'Ruins2_06[left2]', 'Ruins2_06[right1]', 'Ruins2_06[right2]', 'Ruins2_06[top1]'], 'Ruins2_07': ['Ruins2_07[right1]', 'Ruins2_07[left1]', 'Ruins2_07[top1]'], 'Ruins2_08': ['Ruins2_08[left1]'], 'Ruins2_09': ['Ruins2_09[bot1]'], 'Ruins2_10': ['Ruins2_10[right1]', 'Ruins2_10[left1]'], 'Ruins2_10b': ['Ruins2_10b[right1]', 'Ruins2_10b[right2]', 'Ruins2_10b[left1]'], 'Ruins2_11_b': ['Ruins2_11_b[right1]', 'Ruins2_11_b[left1]', 'Ruins2_11_b[bot1]'], 'Ruins2_11': ['Ruins2_11[right1]'], 'Ruins2_Watcher_Room': ['Ruins2_Watcher_Room[bot1]'], 'Ruins_House_01': ['Ruins_House_01[left1]'], 'Ruins_House_02': ['Ruins_House_02[left1]'], 'Ruins_House_03': ['Ruins_House_03[left1]', 'Ruins_House_03[left2]'], 'Ruins_Elevator': ['Ruins_Elevator[left1]', 'Ruins_Elevator[left2]'], 'Ruins_Bathhouse': ['Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[right1]'], 'RestingGrounds_02': ['RestingGrounds_02[right1]', 'RestingGrounds_02[left1]', 'RestingGrounds_02[bot1]', 'RestingGrounds_02[top1]'], 'RestingGrounds_04': ['RestingGrounds_04[left1]', 'RestingGrounds_04[right1]'], 'RestingGrounds_05': ['RestingGrounds_05[left1]', 'RestingGrounds_05[left2]', 'RestingGrounds_05[left3]', 'RestingGrounds_05[right1]', 'RestingGrounds_05[right2]', 'RestingGrounds_05[bot1]'], 'RestingGrounds_06': ['RestingGrounds_06[left1]', 'RestingGrounds_06[right1]', 'RestingGrounds_06[top1]'], 'RestingGrounds_07': ['RestingGrounds_07[right1]'], 'RestingGrounds_08': ['RestingGrounds_08[left1]'], 'RestingGrounds_09': ['RestingGrounds_09[left1]'], 'RestingGrounds_10': ['RestingGrounds_10[left1]', 'RestingGrounds_10[top1]', 'RestingGrounds_10[top2]'], 'RestingGrounds_12': ['RestingGrounds_12[bot1]', 'RestingGrounds_12[door_Mansion]'], 'RestingGrounds_17': ['RestingGrounds_17[right1]'], 'Room_Mansion': ['Room_Mansion[left1]'], 'Mines_01': ['Mines_01[bot1]', 'Mines_01[left1]'], 'Mines_02': ['Mines_02[top1]', 'Mines_02[top2]', 'Mines_02[left1]', 'Mines_02[right1]'], 'Mines_03': ['Mines_03[right1]', 'Mines_03[bot1]', 'Mines_03[top1]'], 'Mines_04': ['Mines_04[right1]', 'Mines_04[top1]', 'Mines_04[left1]', 'Mines_04[left2]', 'Mines_04[left3]'], 'Mines_05': ['Mines_05[right1]', 'Mines_05[top1]', 'Mines_05[bot1]', 'Mines_05[left1]', 'Mines_05[left2]'], 'Mines_06': ['Mines_06[right1]', 'Mines_06[left1]'], 'Mines_07': ['Mines_07[right1]', 'Mines_07[left1]'], 'Mines_10': ['Mines_10[right1]', 'Mines_10[left1]', 'Mines_10[bot1]'], 'Mines_11': ['Mines_11[right1]', 'Mines_11[top1]', 'Mines_11[bot1]'], 'Mines_13': ['Mines_13[right1]', 'Mines_13[top1]', 'Mines_13[bot1]'], 'Mines_16': ['Mines_16[top1]'], 'Mines_17': ['Mines_17[right1]', 'Mines_17[left1]'], 'Mines_18': ['Mines_18[top1]', 'Mines_18[left1]', 'Mines_18[right1]'], 'Mines_19': ['Mines_19[left1]', 'Mines_19[right1]'], 'Mines_20': ['Mines_20[left1]', 'Mines_20[left2]', 'Mines_20[left3]', 'Mines_20[bot1]', 'Mines_20[right1]', 'Mines_20[right2]'], 'Mines_23': ['Mines_23[left1]', 'Mines_23[right1]', 'Mines_23[right2]', 'Mines_23[top1]'], 'Mines_24': ['Mines_24[left1]'], 'Mines_25': ['Mines_25[left1]', 'Mines_25[top1]'], 'Mines_28': ['Mines_28[left1]', 'Mines_28[bot1]', 'Mines_28[door1]'], 'Mines_29': ['Mines_29[left1]', 'Mines_29[right1]', 'Mines_29[right2]'], 'Mines_30': ['Mines_30[left1]', 'Mines_30[right1]'], 'Mines_31': ['Mines_31[left1]'], 'Mines_32': ['Mines_32[bot1]'], 'Mines_33': ['Mines_33[right1]', 'Mines_33[left1]'], 'Mines_34': ['Mines_34[bot1]', 'Mines_34[bot2]', 'Mines_34[left1]'], 'Mines_35': ['Mines_35[left1]'], 'Mines_36': ['Mines_36[right1]'], 'Mines_37': ['Mines_37[bot1]', 'Mines_37[top1]'], 'Fungus3_04': ['Fungus3_04[left1]', 'Fungus3_04[left2]', 'Fungus3_04[right1]', 'Fungus3_04[right2]'], 'Fungus3_05': ['Fungus3_05[left1]', 'Fungus3_05[right1]', 'Fungus3_05[right2]'], 'Fungus3_08': ['Fungus3_08[left1]', 'Fungus3_08[right1]', 'Fungus3_08[top1]'], 'Fungus3_10': ['Fungus3_10[top1]', 'Fungus3_10[bot1]'], 'Fungus3_11': ['Fungus3_11[left1]', 'Fungus3_11[left2]', 'Fungus3_11[right1]'], 'Fungus3_13': ['Fungus3_13[left1]', 'Fungus3_13[left2]', 'Fungus3_13[left3]', 'Fungus3_13[bot1]', 'Fungus3_13[right1]'], 'Fungus3_21': ['Fungus3_21[right1]', 'Fungus3_21[top1]'], 'Fungus3_22': ['Fungus3_22[right1]', 'Fungus3_22[left1]', 'Fungus3_22[bot1]'], 'Fungus3_23': ['Fungus3_23[right1]', 'Fungus3_23[left1]'], 'Fungus3_34': ['Fungus3_34[right1]', 'Fungus3_34[left1]', 'Fungus3_34[top1]'], 'Fungus3_39': ['Fungus3_39[right1]', 'Fungus3_39[left1]'], 'Fungus3_40': ['Fungus3_40[right1]', 'Fungus3_40[top1]'], 'Fungus3_48': ['Fungus3_48[right1]', 'Fungus3_48[right2]', 'Fungus3_48[door1]', 'Fungus3_48[bot1]'], 'Fungus3_49': ['Fungus3_49[right1]'], 'Fungus3_50': ['Fungus3_50[right1]'], 'Room_Queen': ['Room_Queen[left1]'], 'Cliffs_01': ['Cliffs_01[right1]', 'Cliffs_01[right2]', 'Cliffs_01[right3]', 'Cliffs_01[right4]'], 'Cliffs_02': ['Cliffs_02[right1]', 'Cliffs_02[bot1]', 'Cliffs_02[bot2]', 'Cliffs_02[door1]', 'Cliffs_02[left1]', 'Cliffs_02[left2]'], 'Cliffs_03': ['Cliffs_03[right1]'], 'Cliffs_04': ['Cliffs_04[right1]', 'Cliffs_04[left1]'], 'Cliffs_05': ['Cliffs_05[left1]'], 'Cliffs_06': ['Cliffs_06[left1]'], 'Room_nailmaster': ['Room_nailmaster[left1]'], 'White_Palace_01': ['White_Palace_01[left1]', 'White_Palace_01[right1]', 'White_Palace_01[top1]'], 'White_Palace_02': ['White_Palace_02[left1]'], 'White_Palace_03_hub': ['White_Palace_03_hub[left1]', 'White_Palace_03_hub[left2]', 'White_Palace_03_hub[right1]', 'White_Palace_03_hub[top1]', 'White_Palace_03_hub[bot1]'], 'White_Palace_04': ['White_Palace_04[top1]', 'White_Palace_04[right2]'], 'White_Palace_05': ['White_Palace_05[left1]', 'White_Palace_05[left2]', 'White_Palace_05[right1]', 'White_Palace_05[right2]'], 'White_Palace_06': ['White_Palace_06[left1]', 'White_Palace_06[top1]', 'White_Palace_06[bot1]'], 'White_Palace_07': ['White_Palace_07[top1]', 'White_Palace_07[bot1]'], 'White_Palace_08': ['White_Palace_08[left1]', 'White_Palace_08[right1]'], 'White_Palace_09': ['White_Palace_09[right1]'], 'White_Palace_11': ['White_Palace_11[door2]'], 'White_Palace_12': ['White_Palace_12[right1]', 'White_Palace_12[bot1]'], 'White_Palace_13': ['White_Palace_13[right1]', 'White_Palace_13[left1]', 'White_Palace_13[left2]', 'White_Palace_13[left3]'], 'White_Palace_14': ['White_Palace_14[bot1]', 'White_Palace_14[right1]'], 'White_Palace_15': ['White_Palace_15[left1]', 'White_Palace_15[right1]', 'White_Palace_15[right2]'], 'White_Palace_16': ['White_Palace_16[left1]', 'White_Palace_16[left2]'], 'White_Palace_17': ['White_Palace_17[right1]', 'White_Palace_17[bot1]'], 'White_Palace_18': ['White_Palace_18[top1]', 'White_Palace_18[right1]'], 'White_Palace_19': ['White_Palace_19[top1]', 'White_Palace_19[left1]'], 'White_Palace_20': ['White_Palace_20[bot1]']} -item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Unbreakable_Heart': {'Fragile_Heart': 1, 'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Unbreakable_Greed': {'Fragile_Greed': 1, 'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, 'Unbreakable_Strength': {'Fragile_Strength': 1, 'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'WHITEFRAGMENT': 1}, 'King_Fragment': {'WHITEFRAGMENT': 1}, 'Void_Heart': {'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}} +item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'King_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Void_Heart': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}} items = {'Lurien': 'Dreamer', 'Monomon': 'Dreamer', 'Herrah': 'Dreamer', 'World_Sense': 'Dreamer', 'Dreamer': 'Fake', 'Mothwing_Cloak': 'Skill', 'Mantis_Claw': 'Skill', 'Crystal_Heart': 'Skill', 'Monarch_Wings': 'Skill', 'Shade_Cloak': 'Skill', "Isma's_Tear": 'Skill', 'Dream_Nail': 'Skill', 'Dream_Gate': 'Skill', 'Awoken_Dream_Nail': 'Skill', 'Vengeful_Spirit': 'Skill', 'Shade_Soul': 'Skill', 'Desolate_Dive': 'Skill', 'Descending_Dark': 'Skill', 'Howling_Wraiths': 'Skill', 'Abyss_Shriek': 'Skill', 'Cyclone_Slash': 'Skill', 'Dash_Slash': 'Skill', 'Great_Slash': 'Skill', 'Focus': 'Focus', 'Swim': 'Swim', 'Gathering_Swarm': 'Charm', 'Wayward_Compass': 'Charm', 'Grubsong': 'Charm', 'Stalwart_Shell': 'Charm', 'Baldur_Shell': 'Charm', 'Fury_of_the_Fallen': 'Charm', 'Quick_Focus': 'Charm', 'Lifeblood_Heart': 'Charm', 'Lifeblood_Core': 'Charm', "Defender's_Crest": 'Charm', 'Flukenest': 'Charm', 'Thorns_of_Agony': 'Charm', 'Mark_of_Pride': 'Charm', 'Steady_Body': 'Charm', 'Heavy_Blow': 'Charm', 'Sharp_Shadow': 'Charm', 'Spore_Shroom': 'Charm', 'Longnail': 'Charm', 'Shaman_Stone': 'Charm', 'Soul_Catcher': 'Charm', 'Soul_Eater': 'Charm', 'Glowing_Womb': 'Charm', 'Fragile_Heart': 'Charm', 'Unbreakable_Heart': 'Charm', 'Fragile_Greed': 'Charm', 'Unbreakable_Greed': 'Charm', 'Fragile_Strength': 'Charm', 'Unbreakable_Strength': 'Charm', "Nailmaster's_Glory": 'Charm', "Joni's_Blessing": 'Charm', 'Shape_of_Unn': 'Charm', 'Hiveblood': 'Charm', 'Dream_Wielder': 'Charm', 'Dashmaster': 'Charm', 'Quick_Slash': 'Charm', 'Spell_Twister': 'Charm', 'Deep_Focus': 'Charm', "Grubberfly's_Elegy": 'Charm', 'Queen_Fragment': 'Charm', 'King_Fragment': 'Charm', 'Void_Heart': 'Charm', 'Sprintmaster': 'Charm', 'Dreamshield': 'Charm', 'Weaversong': 'Charm', 'Grimmchild1': 'Charm', 'Grimmchild2': 'Charm', 'City_Crest': 'Key', 'Lumafly_Lantern': 'Key', 'Tram_Pass': 'Key', 'Simple_Key': 'Key', "Shopkeeper's_Key": 'Key', 'Elegant_Key': 'Key', 'Love_Key': 'Key', "King's_Brand": 'Key', 'Godtuner': 'Key', "Collector's_Map": 'Key', 'Mask_Shard': 'Mask', 'Double_Mask_Shard': 'Mask', 'Full_Mask': 'Mask', 'Vessel_Fragment': 'Vessel', 'Double_Vessel_Fragment': 'Vessel', 'Full_Soul_Vessel': 'Vessel', 'Charm_Notch': 'Notch', "Salubra's_Blessing": 'Notch', 'Pale_Ore': 'Ore', 'Geo_Chest-False_Knight': 'Geo', 'Geo_Chest-Soul_Master': 'Geo', 'Geo_Chest-Watcher_Knights': 'Geo', 'Geo_Chest-Greenpath': 'Geo', 'Geo_Chest-Mantis_Lords': 'Geo', 'Geo_Chest-Resting_Grounds': 'Geo', 'Geo_Chest-Crystal_Peak': 'Geo', 'Geo_Chest-Weavers_Den': 'Geo', 'Geo_Chest-Junk_Pit_1': 'JunkPitChest', 'Geo_Chest-Junk_Pit_2': 'JunkPitChest', 'Geo_Chest-Junk_Pit_3': 'JunkPitChest', 'Geo_Chest-Junk_Pit_5': 'JunkPitChest', 'Lumafly_Escape': 'JunkPitChest', 'One_Geo': 'Fake', 'Rancid_Egg': 'Egg', "Wanderer's_Journal": 'Relic', 'Hallownest_Seal': 'Relic', "King's_Idol": 'Relic', 'Arcane_Egg': 'Relic', 'Whispering_Root-Crossroads': 'Root', 'Whispering_Root-Greenpath': 'Root', 'Whispering_Root-Leg_Eater': 'Root', 'Whispering_Root-Mantis_Village': 'Root', 'Whispering_Root-Deepnest': 'Root', 'Whispering_Root-Queens_Gardens': 'Root', 'Whispering_Root-Kingdoms_Edge': 'Root', 'Whispering_Root-Waterways': 'Root', 'Whispering_Root-City': 'Root', 'Whispering_Root-Resting_Grounds': 'Root', 'Whispering_Root-Spirits_Glade': 'Root', 'Whispering_Root-Crystal_Peak': 'Root', 'Whispering_Root-Howling_Cliffs': 'Root', 'Whispering_Root-Ancestral_Mound': 'Root', 'Whispering_Root-Hive': 'Root', 'Boss_Essence-Elder_Hu': 'DreamWarrior', 'Boss_Essence-Xero': 'DreamWarrior', 'Boss_Essence-Gorb': 'DreamWarrior', 'Boss_Essence-Marmu': 'DreamWarrior', 'Boss_Essence-No_Eyes': 'DreamWarrior', 'Boss_Essence-Galien': 'DreamWarrior', 'Boss_Essence-Markoth': 'DreamWarrior', 'Boss_Essence-Failed_Champion': 'DreamBoss', 'Boss_Essence-Soul_Tyrant': 'DreamBoss', 'Boss_Essence-Lost_Kin': 'DreamBoss', 'Boss_Essence-White_Defender': 'DreamBoss', 'Boss_Essence-Grey_Prince_Zote': 'DreamBoss', 'Grub': 'Grub', 'Mimic_Grub': 'Mimic', 'Quill': 'Map', 'Crossroads_Map': 'Map', 'Greenpath_Map': 'Map', 'Fog_Canyon_Map': 'Map', 'Fungal_Wastes_Map': 'Map', 'Deepnest_Map': 'Map', 'Ancient_Basin_Map': 'Map', "Kingdom's_Edge_Map": 'Map', 'City_of_Tears_Map': 'Map', 'Royal_Waterways_Map': 'Map', 'Howling_Cliffs_Map': 'Map', 'Crystal_Peak_Map': 'Map', "Queen's_Gardens_Map": 'Map', 'Resting_Grounds_Map': 'Map', 'Dirtmouth_Stag': 'Stag', 'Crossroads_Stag': 'Stag', 'Greenpath_Stag': 'Stag', "Queen's_Station_Stag": 'Stag', "Queen's_Gardens_Stag": 'Stag', 'City_Storerooms_Stag': 'Stag', "King's_Station_Stag": 'Stag', 'Resting_Grounds_Stag': 'Stag', 'Distant_Village_Stag': 'Stag', 'Hidden_Station_Stag': 'Stag', 'Stag_Nest_Stag': 'Stag', 'Lifeblood_Cocoon_Small': 'Cocoon', 'Lifeblood_Cocoon_Large': 'Cocoon', 'Grimmkin_Flame': 'Flame', "Hunter's_Journal": 'Journal', 'Journal_Entry-Void_Tendrils': 'Journal', 'Journal_Entry-Charged_Lumafly': 'Journal', 'Journal_Entry-Goam': 'Journal', 'Journal_Entry-Garpede': 'Journal', 'Journal_Entry-Seal_of_Binding': 'Journal', 'Elevator_Pass': 'ElevatorPass', 'Left_Mothwing_Cloak': 'SplitCloak', 'Right_Mothwing_Cloak': 'SplitCloak', 'Split_Shade_Cloak': 'SplitCloak', 'Left_Mantis_Claw': 'SplitClaw', 'Right_Mantis_Claw': 'SplitClaw', 'Leftslash': 'CursedNail', 'Rightslash': 'CursedNail', 'Upslash': 'CursedNail', 'Downslash': 'CursedNail', 'Left_Crystal_Heart': 'SplitSuperdash', 'Right_Crystal_Heart': 'SplitSuperdash', 'Geo_Rock-Default': 'Rock', 'Geo_Rock-Deepnest': 'Rock', 'Geo_Rock-Abyss': 'Rock', 'Geo_Rock-GreenPath01': 'Rock', 'Geo_Rock-Outskirts': 'Rock', 'Geo_Rock-Outskirts420': 'Rock', 'Geo_Rock-GreenPath02': 'Rock', 'Geo_Rock-Fung01': 'Rock', 'Geo_Rock-Fung02': 'Rock', 'Geo_Rock-City': 'Rock', 'Geo_Rock-Hive': 'Rock', 'Geo_Rock-Mine': 'Rock', 'Geo_Rock-Grave02': 'Rock', 'Geo_Rock-Grave01': 'Rock', 'Boss_Geo-Massive_Moss_Charger': 'Boss_Geo', 'Boss_Geo-Gorgeous_Husk': 'Boss_Geo', 'Boss_Geo-Sanctum_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Elegant_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Crystal_Guardian': 'Boss_Geo', 'Boss_Geo-Enraged_Guardian': 'Boss_Geo', 'Boss_Geo-Gruz_Mother': 'Boss_Geo', 'Boss_Geo-Vengefly_King': 'Boss_Geo', 'Soul_Refill': 'Soul', 'Soul_Totem-A': 'Soul', 'Soul_Totem-B': 'Soul', 'Soul_Totem-C': 'Soul', 'Soul_Totem-D': 'Soul', 'Soul_Totem-E': 'Soul', 'Soul_Totem-F': 'Soul', 'Soul_Totem-G': 'Soul', 'Soul_Totem-Palace': 'Soul', 'Soul_Totem-Path_of_Pain': 'Soul', 'Lore_Tablet-City_Entrance': 'Lore', 'Lore_Tablet-Pleasure_House': 'Lore', 'Lore_Tablet-Sanctum_Entrance': 'Lore', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Lore', "Lore_Tablet-Watcher's_Spire": 'Lore', 'Lore_Tablet-Archives_Upper': 'Lore', 'Lore_Tablet-Archives_Left': 'Lore', 'Lore_Tablet-Archives_Right': 'Lore', "Lore_Tablet-Pilgrim's_Way_1": 'Lore', "Lore_Tablet-Pilgrim's_Way_2": 'Lore', 'Lore_Tablet-Mantis_Outskirts': 'Lore', 'Lore_Tablet-Mantis_Village': 'Lore', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Lore', 'Lore_Tablet-Greenpath_Below_Toll': 'Lore', 'Lore_Tablet-Greenpath_Lifeblood': 'Lore', 'Lore_Tablet-Greenpath_Stag': 'Lore', 'Lore_Tablet-Greenpath_QG': 'Lore', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Lore', 'Lore_Tablet-Dung_Defender': 'Lore', 'Lore_Tablet-Spore_Shroom': 'Lore', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Lore', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Lore', 'Lore_Tablet-Fungal_Core': 'Lore', 'Lore_Tablet-Ancient_Basin': 'Lore', "Lore_Tablet-King's_Pass_Focus": 'Lore', "Lore_Tablet-King's_Pass_Fury": 'Lore', "Lore_Tablet-King's_Pass_Exit": 'Lore', 'Lore_Tablet-World_Sense': 'Lore', 'Lore_Tablet-Howling_Cliffs': 'Lore', "Lore_Tablet-Kingdom's_Edge": 'Lore', 'Lore_Tablet-Palace_Workshop': 'PalaceLore', 'Lore_Tablet-Palace_Throne': 'PalaceLore', 'Lore_Tablet-Path_of_Pain_Entrance': 'PalaceLore'} location_to_region_lookup = {'Sly_1': 'Room_shop', 'Sly_2': 'Room_shop', 'Sly_3': 'Room_shop', 'Sly_4': 'Room_shop', 'Sly_5': 'Room_shop', 'Sly_6': 'Room_shop', 'Sly_7': 'Room_shop', 'Sly_8': 'Room_shop', 'Sly_9': 'Room_shop', 'Sly_10': 'Room_shop', 'Sly_11': 'Room_shop', 'Sly_12': 'Room_shop', 'Sly_13': 'Room_shop', 'Sly_14': 'Room_shop', 'Sly_15': 'Room_shop', 'Sly_16': 'Room_shop', 'Sly_(Key)_1': 'Room_shop', 'Sly_(Key)_2': 'Room_shop', 'Sly_(Key)_3': 'Room_shop', 'Sly_(Key)_4': 'Room_shop', 'Sly_(Key)_5': 'Room_shop', 'Sly_(Key)_6': 'Room_shop', 'Sly_(Key)_7': 'Room_shop', 'Sly_(Key)_8': 'Room_shop', 'Sly_(Key)_9': 'Room_shop', 'Sly_(Key)_10': 'Room_shop', 'Sly_(Key)_11': 'Room_shop', 'Sly_(Key)_12': 'Room_shop', 'Sly_(Key)_13': 'Room_shop', 'Sly_(Key)_14': 'Room_shop', 'Sly_(Key)_15': 'Room_shop', 'Sly_(Key)_16': 'Room_shop', 'Iselda_1': 'Room_mapper', 'Iselda_2': 'Room_mapper', 'Iselda_3': 'Room_mapper', 'Iselda_4': 'Room_mapper', 'Iselda_5': 'Room_mapper', 'Iselda_6': 'Room_mapper', 'Iselda_7': 'Room_mapper', 'Iselda_8': 'Room_mapper', 'Iselda_9': 'Room_mapper', 'Iselda_10': 'Room_mapper', 'Iselda_11': 'Room_mapper', 'Iselda_12': 'Room_mapper', 'Iselda_13': 'Room_mapper', 'Iselda_14': 'Room_mapper', 'Iselda_15': 'Room_mapper', 'Iselda_16': 'Room_mapper', 'Salubra_1': 'Room_Charm_Shop', 'Salubra_2': 'Room_Charm_Shop', 'Salubra_3': 'Room_Charm_Shop', 'Salubra_4': 'Room_Charm_Shop', 'Salubra_5': 'Room_Charm_Shop', 'Salubra_6': 'Room_Charm_Shop', 'Salubra_7': 'Room_Charm_Shop', 'Salubra_8': 'Room_Charm_Shop', 'Salubra_9': 'Room_Charm_Shop', 'Salubra_10': 'Room_Charm_Shop', 'Salubra_11': 'Room_Charm_Shop', 'Salubra_12': 'Room_Charm_Shop', 'Salubra_13': 'Room_Charm_Shop', 'Salubra_14': 'Room_Charm_Shop', 'Salubra_15': 'Room_Charm_Shop', 'Salubra_16': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_1': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_2': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_3': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_4': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_5': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_6': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_7': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_8': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_9': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_10': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_11': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_12': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_13': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_14': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_15': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_16': 'Room_Charm_Shop', 'Leg_Eater_1': 'Fungus2_26', 'Leg_Eater_2': 'Fungus2_26', 'Leg_Eater_3': 'Fungus2_26', 'Leg_Eater_4': 'Fungus2_26', 'Leg_Eater_5': 'Fungus2_26', 'Leg_Eater_6': 'Fungus2_26', 'Leg_Eater_7': 'Fungus2_26', 'Leg_Eater_8': 'Fungus2_26', 'Leg_Eater_9': 'Fungus2_26', 'Leg_Eater_10': 'Fungus2_26', 'Leg_Eater_11': 'Fungus2_26', 'Leg_Eater_12': 'Fungus2_26', 'Leg_Eater_13': 'Fungus2_26', 'Leg_Eater_14': 'Fungus2_26', 'Leg_Eater_15': 'Fungus2_26', 'Leg_Eater_16': 'Fungus2_26', 'Grubfather_1': 'Crossroads_38', 'Grubfather_2': 'Crossroads_38', 'Grubfather_3': 'Crossroads_38', 'Grubfather_4': 'Crossroads_38', 'Grubfather_5': 'Crossroads_38', 'Grubfather_6': 'Crossroads_38', 'Grubfather_7': 'Crossroads_38', 'Grubfather_8': 'Crossroads_38', 'Grubfather_9': 'Crossroads_38', 'Grubfather_10': 'Crossroads_38', 'Grubfather_11': 'Crossroads_38', 'Grubfather_12': 'Crossroads_38', 'Grubfather_13': 'Crossroads_38', 'Grubfather_14': 'Crossroads_38', 'Grubfather_15': 'Crossroads_38', 'Grubfather_16': 'Crossroads_38', 'Seer_1': 'RestingGrounds_07', 'Seer_2': 'RestingGrounds_07', 'Seer_3': 'RestingGrounds_07', 'Seer_4': 'RestingGrounds_07', 'Seer_5': 'RestingGrounds_07', 'Seer_6': 'RestingGrounds_07', 'Seer_7': 'RestingGrounds_07', 'Seer_8': 'RestingGrounds_07', 'Seer_9': 'RestingGrounds_07', 'Seer_10': 'RestingGrounds_07', 'Seer_11': 'RestingGrounds_07', 'Seer_12': 'RestingGrounds_07', 'Seer_13': 'RestingGrounds_07', 'Seer_14': 'RestingGrounds_07', 'Seer_15': 'RestingGrounds_07', 'Seer_16': 'RestingGrounds_07', 'Egg_Shop_1': 'Room_Ouiji', 'Egg_Shop_2': 'Room_Ouiji', 'Egg_Shop_3': 'Room_Ouiji', 'Egg_Shop_4': 'Room_Ouiji', 'Egg_Shop_5': 'Room_Ouiji', 'Egg_Shop_6': 'Room_Ouiji', 'Egg_Shop_7': 'Room_Ouiji', 'Egg_Shop_8': 'Room_Ouiji', 'Egg_Shop_9': 'Room_Ouiji', 'Egg_Shop_10': 'Room_Ouiji', 'Egg_Shop_11': 'Room_Ouiji', 'Egg_Shop_12': 'Room_Ouiji', 'Egg_Shop_13': 'Room_Ouiji', 'Egg_Shop_14': 'Room_Ouiji', 'Egg_Shop_15': 'Room_Ouiji', 'Egg_Shop_16': 'Room_Ouiji', 'Lurien': 'Ruins2_Watcher_Room', 'Monomon': 'Fungus3_archive_02', 'Herrah': 'Deepnest_Spider_Town', 'World_Sense': 'Room_temple', 'Mothwing_Cloak': 'Fungus1_04', 'Mantis_Claw': 'Fungus2_14', 'Crystal_Heart': 'Mines_31', 'Monarch_Wings': 'Abyss_21', 'Shade_Cloak': 'Abyss_10', "Isma's_Tear": 'Waterways_13', 'Dream_Nail': 'RestingGrounds_04', 'Vengeful_Spirit': 'Crossroads_ShamanTemple', 'Shade_Soul': 'Ruins1_31b', 'Desolate_Dive': 'Ruins1_24', 'Descending_Dark': 'Mines_35', 'Howling_Wraiths': 'Room_Fungus_Shaman', 'Abyss_Shriek': 'Abyss_12', 'Cyclone_Slash': 'Room_nailmaster', 'Dash_Slash': 'Room_nailmaster_03', 'Great_Slash': 'Room_nailmaster_02', 'Focus': 'Tutorial_01', 'Baldur_Shell': 'Fungus1_28', 'Fury_of_the_Fallen': 'Tutorial_01', 'Lifeblood_Core': 'Abyss_08', "Defender's_Crest": 'Waterways_05', 'Flukenest': 'Waterways_12', 'Thorns_of_Agony': 'Fungus1_14', 'Mark_of_Pride': 'Fungus2_31', 'Sharp_Shadow': 'Deepnest_44', 'Spore_Shroom': 'Fungus2_20', 'Soul_Catcher': 'Crossroads_ShamanTemple', 'Soul_Eater': 'RestingGrounds_10', 'Glowing_Womb': 'Crossroads_22', "Nailmaster's_Glory": 'Room_shop', "Joni's_Blessing": 'Cliffs_05', 'Shape_of_Unn': 'Fungus1_Slug', 'Hiveblood': 'Hive_05', 'Dashmaster': 'Fungus2_23', 'Quick_Slash': 'Deepnest_East_14b', 'Spell_Twister': 'Ruins1_30', 'Deep_Focus': 'Mines_36', 'Queen_Fragment': 'Room_Queen', 'King_Fragment': 'White_Palace_09', 'Void_Heart': 'Abyss_15', 'Dreamshield': 'RestingGrounds_17', 'Weaversong': 'Deepnest_45_v02', 'Grimmchild': 'Grimm_Main_Tent', 'Unbreakable_Heart': 'Grimm_Divine', 'Unbreakable_Greed': 'Grimm_Divine', 'Unbreakable_Strength': 'Grimm_Divine', 'City_Crest': 'Crossroads_10', 'Tram_Pass': 'Deepnest_26b', 'Simple_Key-Basin': 'Abyss_20', 'Simple_Key-City': 'Ruins1_17', 'Simple_Key-Lurker': 'GG_Lurker', "Shopkeeper's_Key": 'Mines_11', 'Love_Key': 'Fungus3_39', "King's_Brand": 'Room_Wyrm', 'Godtuner': 'GG_Waterways', "Collector's_Map": 'Ruins2_11', 'Mask_Shard-Brooding_Mawlek': 'Crossroads_09', 'Mask_Shard-Crossroads_Goam': 'Crossroads_13', 'Mask_Shard-Stone_Sanctuary': 'Fungus1_36', "Mask_Shard-Queen's_Station": 'Fungus2_01', 'Mask_Shard-Deepnest': 'Fungus2_25', 'Mask_Shard-Waterways': 'Waterways_04b', 'Mask_Shard-Enraged_Guardian': 'Mines_32', 'Mask_Shard-Hive': 'Hive_04', 'Mask_Shard-Grey_Mourner': 'Room_Mansion', 'Mask_Shard-Bretta': 'Room_Bretta', 'Vessel_Fragment-Greenpath': 'Fungus1_13', 'Vessel_Fragment-City': 'Ruins2_09', 'Vessel_Fragment-Crossroads': 'Crossroads_37', 'Vessel_Fragment-Basin': 'Abyss_04', 'Vessel_Fragment-Deepnest': 'Deepnest_38', 'Vessel_Fragment-Stag_Nest': 'Cliffs_03', 'Charm_Notch-Shrumal_Ogres': 'Fungus2_05', 'Charm_Notch-Fog_Canyon': 'Fungus3_28', 'Charm_Notch-Colosseum': 'Room_Colosseum_01', 'Charm_Notch-Grimm': 'Grimm_Main_Tent', 'Pale_Ore-Basin': 'Abyss_17', 'Pale_Ore-Crystal_Peak': 'Mines_34', 'Pale_Ore-Nosk': 'Deepnest_32', 'Pale_Ore-Colosseum': 'Room_Colosseum_01', 'Geo_Chest-False_Knight': 'Crossroads_10', 'Geo_Chest-Soul_Master': 'Ruins1_32', 'Geo_Chest-Watcher_Knights': 'Ruins2_03', 'Geo_Chest-Greenpath': 'Fungus1_13', 'Geo_Chest-Mantis_Lords': 'Fungus2_31', 'Geo_Chest-Resting_Grounds': 'RestingGrounds_10', 'Geo_Chest-Crystal_Peak': 'Mines_37', 'Geo_Chest-Weavers_Den': 'Deepnest_45_v02', 'Geo_Chest-Junk_Pit_1': 'GG_Waterways', 'Geo_Chest-Junk_Pit_2': 'GG_Waterways', 'Geo_Chest-Junk_Pit_3': 'GG_Waterways', 'Geo_Chest-Junk_Pit_5': 'GG_Waterways', 'Lumafly_Escape-Junk_Pit_Chest_4': 'GG_Waterways', 'Rancid_Egg-Sheo': 'Fungus1_15', 'Rancid_Egg-Fungal_Core': 'Fungus2_29', "Rancid_Egg-Queen's_Gardens": 'Fungus3_34', 'Rancid_Egg-Blue_Lake': 'Crossroads_50', 'Rancid_Egg-Crystal_Peak_Dive_Entrance': 'Mines_01', 'Rancid_Egg-Crystal_Peak_Dark_Room': 'Mines_29', 'Rancid_Egg-Crystal_Peak_Tall_Room': 'Mines_20', 'Rancid_Egg-City_of_Tears_Left': 'Ruins1_05c', 'Rancid_Egg-City_of_Tears_Pleasure_House': 'Ruins_Elevator', "Rancid_Egg-Beast's_Den": 'Deepnest_Spider_Town', 'Rancid_Egg-Dark_Deepnest': 'Deepnest_39', "Rancid_Egg-Weaver's_Den": 'Deepnest_45_v02', 'Rancid_Egg-Near_Quick_Slash': 'Deepnest_East_14', "Rancid_Egg-Upper_Kingdom's_Edge": 'Deepnest_East_07', 'Rancid_Egg-Waterways_East': 'Waterways_07', 'Rancid_Egg-Waterways_Main': 'Waterways_02', 'Rancid_Egg-Waterways_West_Bluggsac': 'Waterways_04', 'Rancid_Egg-Waterways_West_Pickup': 'Waterways_04b', "Rancid_Egg-Tuk_Defender's_Crest": 'Waterways_03', "Wanderer's_Journal-Cliffs": 'Cliffs_01', "Wanderer's_Journal-Greenpath_Stag": 'Fungus1_22', "Wanderer's_Journal-Greenpath_Lower": 'Fungus1_11', "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet": 'Fungus2_04', "Wanderer's_Journal-Above_Mantis_Village": 'Fungus2_17', "Wanderer's_Journal-Crystal_Peak_Crawlers": 'Mines_20', "Wanderer's_Journal-Resting_Grounds_Catacombs": 'RestingGrounds_10', "Wanderer's_Journal-King's_Station": 'Ruins2_05', "Wanderer's_Journal-Pleasure_House": 'Ruins_Elevator', "Wanderer's_Journal-City_Storerooms": 'Ruins1_28', "Wanderer's_Journal-Ancient_Basin": 'Abyss_02', "Wanderer's_Journal-Kingdom's_Edge_Entrance": 'Deepnest_East_07', "Wanderer's_Journal-Kingdom's_Edge_Camp": 'Deepnest_East_13', "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive": 'Deepnest_East_18', 'Hallownest_Seal-Crossroads_Well': 'Crossroads_01', 'Hallownest_Seal-Greenpath': 'Fungus1_10', 'Hallownest_Seal-Fog_Canyon_West': 'Fungus3_30', 'Hallownest_Seal-Fog_Canyon_East': 'Fungus3_26', "Hallownest_Seal-Queen's_Station": 'Fungus2_34', 'Hallownest_Seal-Fungal_Wastes_Sporgs': 'Fungus2_03', 'Hallownest_Seal-Mantis_Lords': 'Fungus2_31', 'Hallownest_Seal-Resting_Grounds_Catacombs': 'RestingGrounds_10', "Hallownest_Seal-King's_Station": 'Ruins2_08', 'Hallownest_Seal-City_Rafters': 'Ruins1_03', 'Hallownest_Seal-Soul_Sanctum': 'Ruins1_32', 'Hallownest_Seal-Watcher_Knight': 'Ruins2_03', 'Hallownest_Seal-Deepnest_By_Mantis_Lords': 'Deepnest_16', "Hallownest_Seal-Beast's_Den": 'Deepnest_Spider_Town', "Hallownest_Seal-Queen's_Gardens": 'Fungus3_48', "King's_Idol-Cliffs": 'Cliffs_01', "King's_Idol-Crystal_Peak": 'Mines_30', "King's_Idol-Glade_of_Hope": 'RestingGrounds_08', "King's_Idol-Dung_Defender": 'Waterways_15', "King's_Idol-Great_Hopper": 'Deepnest_East_08', "King's_Idol-Pale_Lurker": 'GG_Lurker', "King's_Idol-Deepnest": 'Deepnest_33', 'Arcane_Egg-Lifeblood_Core': 'Abyss_08', 'Arcane_Egg-Shade_Cloak': 'Abyss_10', 'Arcane_Egg-Birthplace': 'Abyss_15', 'Whispering_Root-Crossroads': 'Crossroads_07', 'Whispering_Root-Greenpath': 'Fungus1_13', 'Whispering_Root-Leg_Eater': 'Fungus2_33', 'Whispering_Root-Mantis_Village': 'Fungus2_17', 'Whispering_Root-Deepnest': 'Deepnest_39', 'Whispering_Root-Queens_Gardens': 'Fungus3_11', 'Whispering_Root-Kingdoms_Edge': 'Deepnest_East_07', 'Whispering_Root-Waterways': 'Abyss_01', 'Whispering_Root-City': 'Ruins1_17', 'Whispering_Root-Resting_Grounds': 'RestingGrounds_05', 'Whispering_Root-Spirits_Glade': 'RestingGrounds_08', 'Whispering_Root-Crystal_Peak': 'Mines_23', 'Whispering_Root-Howling_Cliffs': 'Cliffs_01', 'Whispering_Root-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Whispering_Root-Hive': 'Hive_02', 'Boss_Essence-Elder_Hu': 'Fungus2_32', 'Boss_Essence-Xero': 'RestingGrounds_02', 'Boss_Essence-Gorb': 'Cliffs_02', 'Boss_Essence-Marmu': 'Fungus3_40', 'Boss_Essence-No_Eyes': 'Fungus1_35', 'Boss_Essence-Galien': 'Deepnest_40', 'Boss_Essence-Markoth': 'Deepnest_East_10', 'Boss_Essence-Failed_Champion': 'Crossroads_10', 'Boss_Essence-Soul_Tyrant': 'Ruins1_24', 'Boss_Essence-Lost_Kin': 'Abyss_19', 'Boss_Essence-White_Defender': 'Waterways_15', 'Boss_Essence-Grey_Prince_Zote': 'Room_Bretta', 'Grub-Crossroads_Acid': 'Crossroads_35', 'Grub-Crossroads_Center': 'Crossroads_05', 'Grub-Crossroads_Stag': 'Crossroads_03', 'Grub-Crossroads_Spike': 'Crossroads_31', 'Grub-Crossroads_Guarded': 'Crossroads_48', 'Grub-Greenpath_Cornifer': 'Fungus1_06', 'Grub-Greenpath_Journal': 'Fungus1_07', 'Grub-Greenpath_MMC': 'Fungus1_13', 'Grub-Greenpath_Stag': 'Fungus1_21', 'Grub-Fog_Canyon': 'Fungus3_47', 'Grub-Fungal_Bouncy': 'Fungus2_18', 'Grub-Fungal_Spore_Shroom': 'Fungus2_20', 'Grub-Deepnest_Mimic': 'Deepnest_36', 'Grub-Deepnest_Nosk': 'Deepnest_31', 'Grub-Deepnest_Spike': 'Deepnest_03', 'Grub-Dark_Deepnest': 'Deepnest_39', "Grub-Beast's_Den": 'Deepnest_Spider_Town', "Grub-Kingdom's_Edge_Oro": 'Deepnest_East_14', "Grub-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Grub-Hive_External': 'Hive_03', 'Grub-Hive_Internal': 'Hive_04', 'Grub-Basin_Requires_Wings': 'Abyss_19', 'Grub-Basin_Requires_Dive': 'Abyss_17', 'Grub-Waterways_Main': 'Waterways_04', "Grub-Isma's_Grove": 'Waterways_13', 'Grub-Waterways_Requires_Tram': 'Waterways_14', 'Grub-City_of_Tears_Left': 'Ruins1_05', 'Grub-Soul_Sanctum': 'Ruins1_32', "Grub-Watcher's_Spire": 'Ruins2_03', 'Grub-City_of_Tears_Guarded': 'Ruins_House_01', "Grub-King's_Station": 'Ruins2_07', 'Grub-Resting_Grounds': 'RestingGrounds_10', 'Grub-Crystal_Peak_Below_Chest': 'Mines_04', 'Grub-Crystallized_Mound': 'Mines_35', 'Grub-Crystal_Peak_Spike': 'Mines_03', 'Grub-Crystal_Peak_Mimic': 'Mines_16', 'Grub-Crystal_Peak_Crushers': 'Mines_19', 'Grub-Crystal_Heart': 'Mines_31', 'Grub-Hallownest_Crown': 'Mines_24', 'Grub-Howling_Cliffs': 'Fungus1_28', "Grub-Queen's_Gardens_Stag": 'Fungus3_10', "Grub-Queen's_Gardens_Marmu": 'Fungus3_48', "Grub-Queen's_Gardens_Top": 'Fungus3_22', 'Grub-Collector_1': 'Ruins2_11', 'Grub-Collector_2': 'Ruins2_11', 'Grub-Collector_3': 'Ruins2_11', 'Mimic_Grub-Deepnest_1': 'Deepnest_36', 'Mimic_Grub-Deepnest_2': 'Deepnest_36', 'Mimic_Grub-Deepnest_3': 'Deepnest_36', 'Mimic_Grub-Crystal_Peak': 'Mines_16', 'Crossroads_Map': 'Crossroads_33', 'Greenpath_Map': 'Fungus1_06', 'Fog_Canyon_Map': 'Fungus3_25', 'Fungal_Wastes_Map': 'Fungus2_18', 'Deepnest_Map-Upper': 'Deepnest_01b', 'Deepnest_Map-Right': 'Fungus2_25', 'Ancient_Basin_Map': 'Abyss_04', "Kingdom's_Edge_Map": 'Deepnest_East_03', 'City_of_Tears_Map': 'Ruins1_31', 'Royal_Waterways_Map': 'Waterways_09', 'Howling_Cliffs_Map': 'Cliffs_01', 'Crystal_Peak_Map': 'Mines_30', "Queen's_Gardens_Map": 'Fungus1_24', 'Resting_Grounds_Map': 'RestingGrounds_09', 'Dirtmouth_Stag': 'Room_Town_Stag_Station', 'Crossroads_Stag': 'Crossroads_47', 'Greenpath_Stag': 'Fungus1_16_alt', "Queen's_Station_Stag": 'Fungus2_02', "Queen's_Gardens_Stag": 'Fungus3_40', 'City_Storerooms_Stag': 'Ruins1_29', "King's_Station_Stag": 'Ruins2_08', 'Resting_Grounds_Stag': 'RestingGrounds_09', 'Distant_Village_Stag': 'Deepnest_09', 'Hidden_Station_Stag': 'Abyss_22', 'Stag_Nest_Stag': 'Cliffs_03', "Lifeblood_Cocoon-King's_Pass": 'Tutorial_01', 'Lifeblood_Cocoon-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Lifeblood_Cocoon-Greenpath': 'Fungus1_32', 'Lifeblood_Cocoon-Fog_Canyon_West': 'Fungus3_30', 'Lifeblood_Cocoon-Mantis_Village': 'Fungus2_15', 'Lifeblood_Cocoon-Failed_Tramway': 'Deepnest_26', 'Lifeblood_Cocoon-Galien': 'Deepnest_40', "Lifeblood_Cocoon-Kingdom's_Edge": 'Deepnest_East_15', 'Grimmkin_Flame-City_Storerooms': 'Ruins1_28', 'Grimmkin_Flame-Greenpath': 'Fungus1_10', 'Grimmkin_Flame-Crystal_Peak': 'Mines_10', "Grimmkin_Flame-King's_Pass": 'Tutorial_01', 'Grimmkin_Flame-Resting_Grounds': 'RestingGrounds_06', "Grimmkin_Flame-Kingdom's_Edge": 'Deepnest_East_03', 'Grimmkin_Flame-Fungal_Core': 'Fungus2_30', 'Grimmkin_Flame-Ancient_Basin': 'Abyss_02', 'Grimmkin_Flame-Hive': 'Hive_03', 'Grimmkin_Flame-Brumm': 'Room_spider_small', "Hunter's_Journal": 'Fungus1_08', 'Journal_Entry-Void_Tendrils': 'Abyss_09', 'Journal_Entry-Charged_Lumafly': 'Fungus3_archive_02', 'Journal_Entry-Goam': 'Crossroads_52', 'Journal_Entry-Garpede': 'Deepnest_44', 'Journal_Entry-Seal_of_Binding': 'White_Palace_20', 'Elevator_Pass': 'Crossroads_49b', 'Split_Mothwing_Cloak': 'Fungus1_04', 'Left_Mantis_Claw': 'Fungus2_14', 'Right_Mantis_Claw': 'Fungus2_14', 'Leftslash': 'Tutorial_01', 'Rightslash': 'Tutorial_01', 'Upslash': 'Tutorial_01', 'Split_Crystal_Heart': 'Mines_31', 'Geo_Rock-Broken_Elevator_1': 'Abyss_01', 'Geo_Rock-Broken_Elevator_2': 'Abyss_01', 'Geo_Rock-Broken_Elevator_3': 'Abyss_01', 'Geo_Rock-Broken_Bridge_Upper': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower_Dupe': 'Abyss_02', 'Geo_Rock-Abyss_1': 'Abyss_06_Core', 'Geo_Rock-Abyss_2': 'Abyss_06_Core', 'Geo_Rock-Abyss_3': 'Abyss_06_Core', 'Geo_Rock-Basin_Tunnel': 'Abyss_18', 'Geo_Rock-Basin_Grub': 'Abyss_19', 'Geo_Rock-Basin_Before_Broken_Vessel': 'Abyss_19', 'Geo_Rock-Cliffs_Main_1': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_2': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_3': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_4': 'Cliffs_01', 'Geo_Rock-Below_Gorb_Dupe': 'Cliffs_02', 'Geo_Rock-Below_Gorb': 'Cliffs_02', 'Geo_Rock-Crossroads_Well': 'Crossroads_01', 'Geo_Rock-Crossroads_Center_Grub': 'Crossroads_05', 'Geo_Rock-Crossroads_Root': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_1': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_2': 'Crossroads_07', 'Geo_Rock-Crossroads_Aspid_Arena': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden': 'Crossroads_08', 'Geo_Rock-Crossroads_Above_False_Knight': 'Crossroads_10', 'Geo_Rock-Crossroads_Before_Acid_Grub': 'Crossroads_12', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_Above_Lever': 'Crossroads_16', 'Geo_Rock-Crossroads_Before_Fungal': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Shops': 'Crossroads_19', 'Geo_Rock-Crossroads_Before_Glowing_Womb': 'Crossroads_21', 'Geo_Rock-Crossroads_Above_Tram': 'Crossroads_27', 'Geo_Rock-Crossroads_Above_Mawlek': 'Crossroads_36', 'Geo_Rock-Crossroads_Vessel_Fragment': 'Crossroads_37', 'Geo_Rock-Crossroads_Goam_Alcove': 'Crossroads_42', 'Geo_Rock-Crossroads_Goam_Damage_Boost': 'Crossroads_42', 'Geo_Rock-Crossroads_Tram': 'Crossroads_46', 'Geo_Rock-Crossroads_Goam_Journal': 'Crossroads_52', 'Geo_Rock-Crossroads_Goam_Journal_Dupe': 'Crossroads_52', 'Geo_Rock-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Moss_Prophet': 'Deepnest_01', 'Geo_Rock-Moss_Prophet_Dupe': 'Deepnest_01', 'Geo_Rock-Deepnest_Below_Mimics': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Mimics_Dupe': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Spike_Grub': 'Deepnest_03', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe': 'Deepnest_03', 'Geo_Rock-Deepnest_Spike_Grub_Right': 'Deepnest_03', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3': 'Deepnest_16', 'Geo_Rock-Deepnest_Nosk_1': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_2': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_3': 'Deepnest_31', 'Geo_Rock-Deepnest_Above_Galien': 'Deepnest_35', 'Geo_Rock-Deepnest_Galien_Spike': 'Deepnest_35', 'Geo_Rock-Deepnest_Garpede_1': 'Deepnest_37', 'Geo_Rock-Deepnest_Garpede_2': 'Deepnest_37', 'Geo_Rock-Dark_Deepnest_Above_Grub_1': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Above_Grub_2': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Bottom_Left': 'Deepnest_39', 'Geo_Rock-Above_Mask_Maker_1': 'Deepnest_43', 'Geo_Rock-Above_Mask_Maker_2': 'Deepnest_43', "Geo_Rock-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_2": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_3": 'Deepnest_East_02', "Geo_Rock-Lower_Kingdom's_Edge_Dive": 'Deepnest_East_02', "Geo_Rock-Kingdom's_Edge_Below_Bardoon": 'Deepnest_East_04', "Geo_Rock-Kingdom's_Edge_Oro_Far_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Above_Root": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Above_Tower": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Below_Colosseum": 'Deepnest_East_08', "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Kingdom's_Edge_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Beast's_Den_Above_Trilobite": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Herrah": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_After_Herrah": 'Deepnest_Spider_Town', 'Geo_Rock-Greenpath_Entrance': 'Fungus1_01', 'Geo_Rock-Greenpath_Waterfall': 'Fungus1_01b', 'Geo_Rock-Greenpath_Below_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Lower': 'Fungus1_03', 'Geo_Rock-Greenpath_Below_Hornet': 'Fungus1_04', 'Geo_Rock-Greenpath_Above_Thorns': 'Fungus1_05', "Geo_Rock-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Geo_Rock-Greenpath_Acid_Bridge': 'Fungus1_10', 'Geo_Rock-Greenpath_After_MMC_Hidden': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC_Dupe': 'Fungus1_12', 'Geo_Rock-Greenpath_Obbles_Fool_Eater': 'Fungus1_19', 'Geo_Rock-Greenpath_Moss_Knights': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2': 'Fungus1_21', 'Geo_Rock-Greenpath_Below_Stag': 'Fungus1_22', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater': 'Fungus1_22', 'Geo_Rock-Baldur_Shell_Top_Left': 'Fungus1_28', 'Geo_Rock-Baldur_Shell_Alcove': 'Fungus1_28', 'Geo_Rock-Greenpath_MMC': 'Fungus1_29', 'Geo_Rock-Greenpath_Below_Toll': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe': 'Fungus1_31', 'Geo_Rock-Fungal_Below_Shrumal_Ogres': 'Fungus2_04', 'Geo_Rock-Fungal_Above_Cloth': 'Fungus2_08', 'Geo_Rock-Fungal_After_Cloth': 'Fungus2_10', "Geo_Rock-Fungal_Below_Pilgrim's_Way": 'Fungus2_11', "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe": 'Fungus2_11', 'Geo_Rock-Mantis_Outskirts_Guarded': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Alcove': 'Fungus2_13', 'Geo_Rock-Mantis_Village_After_Lever': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Below_Lore': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Lever': 'Fungus2_14', 'Geo_Rock-Above_Mantis_Lords_1': 'Fungus2_15', 'Geo_Rock-Above_Mantis_Lords_2': 'Fungus2_15', 'Geo_Rock-Fungal_After_Bouncy_Grub': 'Fungus2_18', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe': 'Fungus2_18', 'Geo_Rock-Fungal_Bouncy_Grub_Lever': 'Fungus2_18', 'Geo_Rock-Fungal_After_Cornifer': 'Fungus2_18', 'Geo_Rock-Fungal_Above_City_Entrance': 'Fungus2_21', 'Geo_Rock-Deepnest_By_Mantis_Lords_1': 'Fungus2_25', 'Geo_Rock-Deepnest_By_Mantis_Lords_2': 'Fungus2_25', 'Geo_Rock-Deepnest_Lower_Cornifer': 'Fungus2_25', 'Geo_Rock-Fungal_Core_Entrance': 'Fungus2_29', 'Geo_Rock-Fungal_Core_Hidden': 'Fungus2_30', 'Geo_Rock-Fungal_Core_Above_Elder': 'Fungus2_30', "Geo_Rock-Queen's_Gardens_Acid_Entrance": 'Fungus3_03', "Geo_Rock-Queen's_Gardens_Below_Stag": 'Fungus3_10', 'Geo_Rock-Fog_Canyon_East': 'Fungus3_26', 'Geo_Rock-Love_Key': 'Fungus3_39', 'Geo_Rock-Love_Key_Dupe': 'Fungus3_39', "Geo_Rock-Queen's_Gardens_Above_Marmu": 'Fungus3_48', 'Geo_Rock-Pale_Lurker': 'GG_Lurker', 'Geo_Rock-Godhome_Pipeway': 'GG_Pipeway', 'Geo_Rock-Hive_Entrance': 'Hive_01', 'Geo_Rock-Hive_Outside_Bench': 'Hive_02', 'Geo_Rock-Hive_Below_Root': 'Hive_02', 'Geo_Rock-Hive_After_Root': 'Hive_02', 'Geo_Rock-Hive_Below_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash_Dupe': 'Hive_03', 'Geo_Rock-Hive_Below_Grub': 'Hive_04', 'Geo_Rock-Hive_Above_Mask': 'Hive_04', 'Geo_Rock-Crystal_Peak_Lower_Middle': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2': 'Mines_02', 'Geo_Rock-Crystal_Peak_Before_Dark_Room': 'Mines_04', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe': 'Mines_04', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub': 'Mines_05', 'Geo_Rock-Crystal_Peak_Mimic_Grub': 'Mines_16', 'Geo_Rock-Crystal_Peak_Dive_Egg': 'Mines_20', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe': 'Mines_20', 'Geo_Rock-Crystal_Peak_Conga_Line': 'Mines_20', 'Geo_Rock-Hallownest_Crown_Dive': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Dive_Dupe': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2': 'Mines_25', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart': 'Mines_31', 'Geo_Rock-Crystal_Peak_Entrance': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2': 'Mines_33', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower': 'Mines_37', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher': 'Mines_37', 'Geo_Rock-Resting_Grounds_Catacombs_Grub': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left': 'RestingGrounds_10', 'Geo_Rock-Overgrown_Mound': 'Room_Fungus_Shaman', 'Geo_Rock-Fluke_Hermit_Dupe': 'Room_GG_Shortcut', 'Geo_Rock-Fluke_Hermit': 'Room_GG_Shortcut', 'Geo_Rock-Pleasure_House': 'Ruins_Elevator', 'Geo_Rock-City_of_Tears_Quirrel': 'Ruins1_03', 'Geo_Rock-City_of_Tears_Lemm': 'Ruins1_05b', 'Geo_Rock-City_of_Tears_Above_Lemm': 'Ruins1_05c', 'Geo_Rock-Soul_Sanctum': 'Ruins1_32', "Geo_Rock-Watcher's_Spire": 'Ruins2_01', "Geo_Rock-Above_King's_Station": 'Ruins2_05', "Geo_Rock-King's_Station": 'Ruins2_06', "Geo_Rock-King's_Pass_Left": 'Tutorial_01', "Geo_Rock-King's_Pass_Below_Fury": 'Tutorial_01', "Geo_Rock-King's_Pass_Hidden": 'Tutorial_01', "Geo_Rock-King's_Pass_Collapse": 'Tutorial_01', "Geo_Rock-King's_Pass_Above_Fury": 'Tutorial_01', 'Geo_Rock-Waterways_Tuk': 'Waterways_01', 'Geo_Rock-Waterways_Tuk_Alcove': 'Waterways_01', 'Geo_Rock-Waterways_Left': 'Waterways_04b', 'Geo_Rock-Waterways_East': 'Waterways_07', 'Geo_Rock-Waterways_Flukemarm': 'Waterways_08', 'Boss_Geo-Massive_Moss_Charger': 'Fungus1_29', 'Boss_Geo-Gorgeous_Husk': 'Ruins_House_02', 'Boss_Geo-Sanctum_Soul_Warrior': 'Ruins1_23', 'Boss_Geo-Elegant_Soul_Warrior': 'Ruins1_31b', 'Boss_Geo-Crystal_Guardian': 'Mines_18', 'Boss_Geo-Enraged_Guardian': 'Mines_32', 'Boss_Geo-Gruz_Mother': 'Crossroads_04', 'Boss_Geo-Vengefly_King': 'Fungus1_20_v02', 'Soul_Totem-Basin': 'Abyss_04', 'Soul_Totem-Cliffs_Main': 'Cliffs_01', 'Soul_Totem-Cliffs_Gorb': 'Cliffs_02', "Soul_Totem-Cliffs_Joni's": 'Cliffs_04', 'Soul_Totem-Crossroads_Goam_Journal': 'Crossroads_18', 'Soul_Totem-Crossroads_Shops': 'Crossroads_19', 'Soul_Totem-Crossroads_Mawlek_Upper': 'Crossroads_25', 'Soul_Totem-Crossroads_Acid': 'Crossroads_35', 'Soul_Totem-Crossroads_Mawlek_Lower': 'Crossroads_36', 'Soul_Totem-Crossroads_Myla': 'Crossroads_45', 'Soul_Totem-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Soul_Totem-Distant_Village': 'Deepnest_10', 'Soul_Totem-Deepnest_Vessel': 'Deepnest_38', 'Soul_Totem-Mask_Maker': 'Deepnest_42', "Soul_Totem-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Soul_Totem-Lower_Kingdom's_Edge_2": 'Deepnest_East_02', "Soul_Totem-Upper_Kingdom's_Edge": 'Deepnest_East_07', "Soul_Totem-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Soul_Totem-Oro_Dive_2': 'Deepnest_East_14', 'Soul_Totem-Oro_Dive_1': 'Deepnest_East_14', 'Soul_Totem-Oro': 'Deepnest_East_16', 'Soul_Totem-420_Geo_Rock': 'Deepnest_East_17', "Soul_Totem-Beast's_Den": 'Deepnest_Spider_Town', "Soul_Totem-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Soul_Totem-Greenpath_MMC': 'Fungus1_29', 'Soul_Totem-Greenpath_Below_Toll': 'Fungus1_30', "Soul_Totem-Before_Pilgrim's_Way": 'Fungus2_10', "Soul_Totem-Pilgrim's_Way": 'Fungus2_21', 'Soul_Totem-Fungal_Core': 'Fungus2_29', "Soul_Totem-Top_Left_Queen's_Gardens": 'Fungus3_21', 'Soul_Totem-Below_Marmu': 'Fungus3_40', 'Soul_Totem-Upper_Crystal_Peak': 'Mines_20', 'Soul_Totem-Hallownest_Crown': 'Mines_25', 'Soul_Totem-Outside_Crystallized_Mound': 'Mines_28', 'Soul_Totem-Crystal_Heart_1': 'Mines_31', 'Soul_Totem-Crystal_Heart_2': 'Mines_31', 'Soul_Totem-Crystallized_Mound': 'Mines_35', 'Soul_Totem-Resting_Grounds': 'RestingGrounds_05', 'Soul_Totem-Below_Xero': 'RestingGrounds_06', 'Soul_Totem-Sanctum_Below_Soul_Master': 'Ruins1_24', 'Soul_Totem-Sanctum_Below_Chest': 'Ruins1_32', 'Soul_Totem-Sanctum_Above_Grub': 'Ruins1_32', 'Soul_Totem-Waterways_Entrance': 'Waterways_01', 'Soul_Totem-Top_Left_Waterways': 'Waterways_04b', 'Soul_Totem-Waterways_East': 'Waterways_07', 'Soul_Totem-Waterways_Flukemarm': 'Waterways_08', 'Soul_Totem-White_Palace_Entrance': 'White_Palace_02', 'Soul_Totem-White_Palace_Hub': 'White_Palace_03_hub', 'Soul_Totem-White_Palace_Left': 'White_Palace_04', 'Soul_Totem-White_Palace_Final': 'White_Palace_09', 'Soul_Totem-White_Palace_Right': 'White_Palace_15', 'Soul_Totem-Path_of_Pain_Below_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Left_of_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Entrance': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Second': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Hidden': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Below_Thornskip': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Final': 'White_Palace_20', 'Soul_Totem-Pale_Lurker': 'GG_Lurker', 'Lore_Tablet-City_Entrance': 'Ruins1_02', 'Lore_Tablet-Pleasure_House': 'Ruins_Elevator', 'Lore_Tablet-Sanctum_Entrance': 'Ruins1_23', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Ruins1_32', "Lore_Tablet-Watcher's_Spire": 'Ruins2_Watcher_Room', 'Lore_Tablet-Archives_Upper': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Left': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Right': 'Fungus3_archive_02', "Lore_Tablet-Pilgrim's_Way_1": 'Crossroads_11_alt', "Lore_Tablet-Pilgrim's_Way_2": 'Fungus2_21', 'Lore_Tablet-Mantis_Outskirts': 'Fungus2_12', 'Lore_Tablet-Mantis_Village': 'Fungus2_14', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Fungus1_17', 'Lore_Tablet-Greenpath_Below_Toll': 'Fungus1_30', 'Lore_Tablet-Greenpath_Lifeblood': 'Fungus1_32', 'Lore_Tablet-Greenpath_Stag': 'Fungus1_21', 'Lore_Tablet-Greenpath_QG': 'Fungus1_13', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Fungus1_19', 'Lore_Tablet-Dung_Defender': 'Waterways_07', 'Lore_Tablet-Spore_Shroom': 'Fungus2_20', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Fungus2_07', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Fungus2_04', 'Lore_Tablet-Fungal_Core': 'Fungus2_30', 'Lore_Tablet-Ancient_Basin': 'Abyss_06_Core', "Lore_Tablet-King's_Pass_Focus": 'Tutorial_01', "Lore_Tablet-King's_Pass_Fury": 'Tutorial_01', "Lore_Tablet-King's_Pass_Exit": 'Tutorial_01', 'Lore_Tablet-World_Sense': 'Room_temple', 'Lore_Tablet-Howling_Cliffs': 'Cliffs_01', "Lore_Tablet-Kingdom's_Edge": 'Deepnest_East_17', 'Lore_Tablet-Palace_Workshop': 'White_Palace_08', 'Lore_Tablet-Palace_Throne': 'White_Palace_09', 'Lore_Tablet-Path_of_Pain_Entrance': 'White_Palace_18'} locations = ['Sly_1', 'Sly_2', 'Sly_3', 'Sly_4', 'Sly_5', 'Sly_6', 'Sly_7', 'Sly_8', 'Sly_9', 'Sly_10', 'Sly_11', 'Sly_12', 'Sly_13', 'Sly_14', 'Sly_15', 'Sly_16', 'Sly_(Key)_1', 'Sly_(Key)_2', 'Sly_(Key)_3', 'Sly_(Key)_4', 'Sly_(Key)_5', 'Sly_(Key)_6', 'Sly_(Key)_7', 'Sly_(Key)_8', 'Sly_(Key)_9', 'Sly_(Key)_10', 'Sly_(Key)_11', 'Sly_(Key)_12', 'Sly_(Key)_13', 'Sly_(Key)_14', 'Sly_(Key)_15', 'Sly_(Key)_16', 'Iselda_1', 'Iselda_2', 'Iselda_3', 'Iselda_4', 'Iselda_5', 'Iselda_6', 'Iselda_7', 'Iselda_8', 'Iselda_9', 'Iselda_10', 'Iselda_11', 'Iselda_12', 'Iselda_13', 'Iselda_14', 'Iselda_15', 'Iselda_16', 'Salubra_1', 'Salubra_2', 'Salubra_3', 'Salubra_4', 'Salubra_5', 'Salubra_6', 'Salubra_7', 'Salubra_8', 'Salubra_9', 'Salubra_10', 'Salubra_11', 'Salubra_12', 'Salubra_13', 'Salubra_14', 'Salubra_15', 'Salubra_16', 'Salubra_(Requires_Charms)_1', 'Salubra_(Requires_Charms)_2', 'Salubra_(Requires_Charms)_3', 'Salubra_(Requires_Charms)_4', 'Salubra_(Requires_Charms)_5', 'Salubra_(Requires_Charms)_6', 'Salubra_(Requires_Charms)_7', 'Salubra_(Requires_Charms)_8', 'Salubra_(Requires_Charms)_9', 'Salubra_(Requires_Charms)_10', 'Salubra_(Requires_Charms)_11', 'Salubra_(Requires_Charms)_12', 'Salubra_(Requires_Charms)_13', 'Salubra_(Requires_Charms)_14', 'Salubra_(Requires_Charms)_15', 'Salubra_(Requires_Charms)_16', 'Leg_Eater_1', 'Leg_Eater_2', 'Leg_Eater_3', 'Leg_Eater_4', 'Leg_Eater_5', 'Leg_Eater_6', 'Leg_Eater_7', 'Leg_Eater_8', 'Leg_Eater_9', 'Leg_Eater_10', 'Leg_Eater_11', 'Leg_Eater_12', 'Leg_Eater_13', 'Leg_Eater_14', 'Leg_Eater_15', 'Leg_Eater_16', 'Grubfather_1', 'Grubfather_2', 'Grubfather_3', 'Grubfather_4', 'Grubfather_5', 'Grubfather_6', 'Grubfather_7', 'Grubfather_8', 'Grubfather_9', 'Grubfather_10', 'Grubfather_11', 'Grubfather_12', 'Grubfather_13', 'Grubfather_14', 'Grubfather_15', 'Grubfather_16', 'Seer_1', 'Seer_2', 'Seer_3', 'Seer_4', 'Seer_5', 'Seer_6', 'Seer_7', 'Seer_8', 'Seer_9', 'Seer_10', 'Seer_11', 'Seer_12', 'Seer_13', 'Seer_14', 'Seer_15', 'Seer_16', 'Egg_Shop_1', 'Egg_Shop_2', 'Egg_Shop_3', 'Egg_Shop_4', 'Egg_Shop_5', 'Egg_Shop_6', 'Egg_Shop_7', 'Egg_Shop_8', 'Egg_Shop_9', 'Egg_Shop_10', 'Egg_Shop_11', 'Egg_Shop_12', 'Egg_Shop_13', 'Egg_Shop_14', 'Egg_Shop_15', 'Egg_Shop_16', 'Lurien', 'Monomon', 'Herrah', 'World_Sense', 'Mothwing_Cloak', 'Mantis_Claw', 'Crystal_Heart', 'Monarch_Wings', 'Shade_Cloak', "Isma's_Tear", 'Dream_Nail', 'Vengeful_Spirit', 'Shade_Soul', 'Desolate_Dive', 'Descending_Dark', 'Howling_Wraiths', 'Abyss_Shriek', 'Cyclone_Slash', 'Dash_Slash', 'Great_Slash', 'Focus', 'Baldur_Shell', 'Fury_of_the_Fallen', 'Lifeblood_Core', "Defender's_Crest", 'Flukenest', 'Thorns_of_Agony', 'Mark_of_Pride', 'Sharp_Shadow', 'Spore_Shroom', 'Soul_Catcher', 'Soul_Eater', 'Glowing_Womb', "Nailmaster's_Glory", "Joni's_Blessing", 'Shape_of_Unn', 'Hiveblood', 'Dashmaster', 'Quick_Slash', 'Spell_Twister', 'Deep_Focus', 'Queen_Fragment', 'King_Fragment', 'Void_Heart', 'Dreamshield', 'Weaversong', 'Grimmchild', 'Unbreakable_Heart', 'Unbreakable_Greed', 'Unbreakable_Strength', 'City_Crest', 'Tram_Pass', 'Simple_Key-Basin', 'Simple_Key-City', 'Simple_Key-Lurker', "Shopkeeper's_Key", 'Love_Key', "King's_Brand", 'Godtuner', "Collector's_Map", 'Mask_Shard-Brooding_Mawlek', 'Mask_Shard-Crossroads_Goam', 'Mask_Shard-Stone_Sanctuary', "Mask_Shard-Queen's_Station", 'Mask_Shard-Deepnest', 'Mask_Shard-Waterways', 'Mask_Shard-Enraged_Guardian', 'Mask_Shard-Hive', 'Mask_Shard-Grey_Mourner', 'Mask_Shard-Bretta', 'Vessel_Fragment-Greenpath', 'Vessel_Fragment-City', 'Vessel_Fragment-Crossroads', 'Vessel_Fragment-Basin', 'Vessel_Fragment-Deepnest', 'Vessel_Fragment-Stag_Nest', 'Charm_Notch-Shrumal_Ogres', 'Charm_Notch-Fog_Canyon', 'Charm_Notch-Colosseum', 'Charm_Notch-Grimm', 'Pale_Ore-Basin', 'Pale_Ore-Crystal_Peak', 'Pale_Ore-Nosk', 'Pale_Ore-Colosseum', 'Geo_Chest-False_Knight', 'Geo_Chest-Soul_Master', 'Geo_Chest-Watcher_Knights', 'Geo_Chest-Greenpath', 'Geo_Chest-Mantis_Lords', 'Geo_Chest-Resting_Grounds', 'Geo_Chest-Crystal_Peak', 'Geo_Chest-Weavers_Den', 'Geo_Chest-Junk_Pit_1', 'Geo_Chest-Junk_Pit_2', 'Geo_Chest-Junk_Pit_3', 'Geo_Chest-Junk_Pit_5', 'Lumafly_Escape-Junk_Pit_Chest_4', 'Rancid_Egg-Sheo', 'Rancid_Egg-Fungal_Core', "Rancid_Egg-Queen's_Gardens", 'Rancid_Egg-Blue_Lake', 'Rancid_Egg-Crystal_Peak_Dive_Entrance', 'Rancid_Egg-Crystal_Peak_Dark_Room', 'Rancid_Egg-Crystal_Peak_Tall_Room', 'Rancid_Egg-City_of_Tears_Left', 'Rancid_Egg-City_of_Tears_Pleasure_House', "Rancid_Egg-Beast's_Den", 'Rancid_Egg-Dark_Deepnest', "Rancid_Egg-Weaver's_Den", 'Rancid_Egg-Near_Quick_Slash', "Rancid_Egg-Upper_Kingdom's_Edge", 'Rancid_Egg-Waterways_East', 'Rancid_Egg-Waterways_Main', 'Rancid_Egg-Waterways_West_Bluggsac', 'Rancid_Egg-Waterways_West_Pickup', "Rancid_Egg-Tuk_Defender's_Crest", "Wanderer's_Journal-Cliffs", "Wanderer's_Journal-Greenpath_Stag", "Wanderer's_Journal-Greenpath_Lower", "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet", "Wanderer's_Journal-Above_Mantis_Village", "Wanderer's_Journal-Crystal_Peak_Crawlers", "Wanderer's_Journal-Resting_Grounds_Catacombs", "Wanderer's_Journal-King's_Station", "Wanderer's_Journal-Pleasure_House", "Wanderer's_Journal-City_Storerooms", "Wanderer's_Journal-Ancient_Basin", "Wanderer's_Journal-Kingdom's_Edge_Entrance", "Wanderer's_Journal-Kingdom's_Edge_Camp", "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive", 'Hallownest_Seal-Crossroads_Well', 'Hallownest_Seal-Greenpath', 'Hallownest_Seal-Fog_Canyon_West', 'Hallownest_Seal-Fog_Canyon_East', "Hallownest_Seal-Queen's_Station", 'Hallownest_Seal-Fungal_Wastes_Sporgs', 'Hallownest_Seal-Mantis_Lords', 'Hallownest_Seal-Resting_Grounds_Catacombs', "Hallownest_Seal-King's_Station", 'Hallownest_Seal-City_Rafters', 'Hallownest_Seal-Soul_Sanctum', 'Hallownest_Seal-Watcher_Knight', 'Hallownest_Seal-Deepnest_By_Mantis_Lords', "Hallownest_Seal-Beast's_Den", "Hallownest_Seal-Queen's_Gardens", "King's_Idol-Cliffs", "King's_Idol-Crystal_Peak", "King's_Idol-Glade_of_Hope", "King's_Idol-Dung_Defender", "King's_Idol-Great_Hopper", "King's_Idol-Pale_Lurker", "King's_Idol-Deepnest", 'Arcane_Egg-Lifeblood_Core', 'Arcane_Egg-Shade_Cloak', 'Arcane_Egg-Birthplace', 'Whispering_Root-Crossroads', 'Whispering_Root-Greenpath', 'Whispering_Root-Leg_Eater', 'Whispering_Root-Mantis_Village', 'Whispering_Root-Deepnest', 'Whispering_Root-Queens_Gardens', 'Whispering_Root-Kingdoms_Edge', 'Whispering_Root-Waterways', 'Whispering_Root-City', 'Whispering_Root-Resting_Grounds', 'Whispering_Root-Spirits_Glade', 'Whispering_Root-Crystal_Peak', 'Whispering_Root-Howling_Cliffs', 'Whispering_Root-Ancestral_Mound', 'Whispering_Root-Hive', 'Boss_Essence-Elder_Hu', 'Boss_Essence-Xero', 'Boss_Essence-Gorb', 'Boss_Essence-Marmu', 'Boss_Essence-No_Eyes', 'Boss_Essence-Galien', 'Boss_Essence-Markoth', 'Boss_Essence-Failed_Champion', 'Boss_Essence-Soul_Tyrant', 'Boss_Essence-Lost_Kin', 'Boss_Essence-White_Defender', 'Boss_Essence-Grey_Prince_Zote', 'Grub-Crossroads_Acid', 'Grub-Crossroads_Center', 'Grub-Crossroads_Stag', 'Grub-Crossroads_Spike', 'Grub-Crossroads_Guarded', 'Grub-Greenpath_Cornifer', 'Grub-Greenpath_Journal', 'Grub-Greenpath_MMC', 'Grub-Greenpath_Stag', 'Grub-Fog_Canyon', 'Grub-Fungal_Bouncy', 'Grub-Fungal_Spore_Shroom', 'Grub-Deepnest_Mimic', 'Grub-Deepnest_Nosk', 'Grub-Deepnest_Spike', 'Grub-Dark_Deepnest', "Grub-Beast's_Den", "Grub-Kingdom's_Edge_Oro", "Grub-Kingdom's_Edge_Camp", 'Grub-Hive_External', 'Grub-Hive_Internal', 'Grub-Basin_Requires_Wings', 'Grub-Basin_Requires_Dive', 'Grub-Waterways_Main', "Grub-Isma's_Grove", 'Grub-Waterways_Requires_Tram', 'Grub-City_of_Tears_Left', 'Grub-Soul_Sanctum', "Grub-Watcher's_Spire", 'Grub-City_of_Tears_Guarded', "Grub-King's_Station", 'Grub-Resting_Grounds', 'Grub-Crystal_Peak_Below_Chest', 'Grub-Crystallized_Mound', 'Grub-Crystal_Peak_Spike', 'Grub-Crystal_Peak_Mimic', 'Grub-Crystal_Peak_Crushers', 'Grub-Crystal_Heart', 'Grub-Hallownest_Crown', 'Grub-Howling_Cliffs', "Grub-Queen's_Gardens_Stag", "Grub-Queen's_Gardens_Marmu", "Grub-Queen's_Gardens_Top", 'Grub-Collector_1', 'Grub-Collector_2', 'Grub-Collector_3', 'Mimic_Grub-Deepnest_1', 'Mimic_Grub-Deepnest_2', 'Mimic_Grub-Deepnest_3', 'Mimic_Grub-Crystal_Peak', 'Crossroads_Map', 'Greenpath_Map', 'Fog_Canyon_Map', 'Fungal_Wastes_Map', 'Deepnest_Map-Upper', 'Deepnest_Map-Right', 'Ancient_Basin_Map', "Kingdom's_Edge_Map", 'City_of_Tears_Map', 'Royal_Waterways_Map', 'Howling_Cliffs_Map', 'Crystal_Peak_Map', "Queen's_Gardens_Map", 'Resting_Grounds_Map', 'Dirtmouth_Stag', 'Crossroads_Stag', 'Greenpath_Stag', "Queen's_Station_Stag", "Queen's_Gardens_Stag", 'City_Storerooms_Stag', "King's_Station_Stag", 'Resting_Grounds_Stag', 'Distant_Village_Stag', 'Hidden_Station_Stag', 'Stag_Nest_Stag', "Lifeblood_Cocoon-King's_Pass", 'Lifeblood_Cocoon-Ancestral_Mound', 'Lifeblood_Cocoon-Greenpath', 'Lifeblood_Cocoon-Fog_Canyon_West', 'Lifeblood_Cocoon-Mantis_Village', 'Lifeblood_Cocoon-Failed_Tramway', 'Lifeblood_Cocoon-Galien', "Lifeblood_Cocoon-Kingdom's_Edge", 'Grimmkin_Flame-City_Storerooms', 'Grimmkin_Flame-Greenpath', 'Grimmkin_Flame-Crystal_Peak', "Grimmkin_Flame-King's_Pass", 'Grimmkin_Flame-Resting_Grounds', "Grimmkin_Flame-Kingdom's_Edge", 'Grimmkin_Flame-Fungal_Core', 'Grimmkin_Flame-Ancient_Basin', 'Grimmkin_Flame-Hive', 'Grimmkin_Flame-Brumm', "Hunter's_Journal", 'Journal_Entry-Void_Tendrils', 'Journal_Entry-Charged_Lumafly', 'Journal_Entry-Goam', 'Journal_Entry-Garpede', 'Journal_Entry-Seal_of_Binding', 'Elevator_Pass', 'Split_Mothwing_Cloak', 'Left_Mantis_Claw', 'Right_Mantis_Claw', 'Leftslash', 'Rightslash', 'Upslash', 'Split_Crystal_Heart', 'Geo_Rock-Broken_Elevator_1', 'Geo_Rock-Broken_Elevator_2', 'Geo_Rock-Broken_Elevator_3', 'Geo_Rock-Broken_Bridge_Upper', 'Geo_Rock-Broken_Bridge_Lower', 'Geo_Rock-Broken_Bridge_Lower_Dupe', 'Geo_Rock-Abyss_1', 'Geo_Rock-Abyss_2', 'Geo_Rock-Abyss_3', 'Geo_Rock-Basin_Tunnel', 'Geo_Rock-Basin_Grub', 'Geo_Rock-Basin_Before_Broken_Vessel', 'Geo_Rock-Cliffs_Main_1', 'Geo_Rock-Cliffs_Main_2', 'Geo_Rock-Cliffs_Main_3', 'Geo_Rock-Cliffs_Main_4', 'Geo_Rock-Below_Gorb_Dupe', 'Geo_Rock-Below_Gorb', 'Geo_Rock-Crossroads_Well', 'Geo_Rock-Crossroads_Center_Grub', 'Geo_Rock-Crossroads_Root', 'Geo_Rock-Crossroads_Root_Dupe_1', 'Geo_Rock-Crossroads_Root_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden', 'Geo_Rock-Crossroads_Above_False_Knight', 'Geo_Rock-Crossroads_Before_Acid_Grub', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard', 'Geo_Rock-Crossroads_Above_Lever', 'Geo_Rock-Crossroads_Before_Fungal', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2', 'Geo_Rock-Crossroads_Before_Shops', 'Geo_Rock-Crossroads_Before_Glowing_Womb', 'Geo_Rock-Crossroads_Above_Tram', 'Geo_Rock-Crossroads_Above_Mawlek', 'Geo_Rock-Crossroads_Vessel_Fragment', 'Geo_Rock-Crossroads_Goam_Alcove', 'Geo_Rock-Crossroads_Goam_Damage_Boost', 'Geo_Rock-Crossroads_Tram', 'Geo_Rock-Crossroads_Goam_Journal', 'Geo_Rock-Crossroads_Goam_Journal_Dupe', 'Geo_Rock-Ancestral_Mound', 'Geo_Rock-Ancestral_Mound_Dupe', 'Geo_Rock-Ancestral_Mound_Tree', 'Geo_Rock-Ancestral_Mound_Tree_Dupe', 'Geo_Rock-Moss_Prophet', 'Geo_Rock-Moss_Prophet_Dupe', 'Geo_Rock-Deepnest_Below_Mimics', 'Geo_Rock-Deepnest_Below_Mimics_Dupe', 'Geo_Rock-Deepnest_Below_Spike_Grub', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe', 'Geo_Rock-Deepnest_Spike_Grub_Right', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3', 'Geo_Rock-Deepnest_Nosk_1', 'Geo_Rock-Deepnest_Nosk_2', 'Geo_Rock-Deepnest_Nosk_3', 'Geo_Rock-Deepnest_Above_Galien', 'Geo_Rock-Deepnest_Galien_Spike', 'Geo_Rock-Deepnest_Garpede_1', 'Geo_Rock-Deepnest_Garpede_2', 'Geo_Rock-Dark_Deepnest_Above_Grub_1', 'Geo_Rock-Dark_Deepnest_Above_Grub_2', 'Geo_Rock-Dark_Deepnest_Bottom_Left', 'Geo_Rock-Above_Mask_Maker_1', 'Geo_Rock-Above_Mask_Maker_2', "Geo_Rock-Lower_Kingdom's_Edge_1", "Geo_Rock-Lower_Kingdom's_Edge_2", "Geo_Rock-Lower_Kingdom's_Edge_3", "Geo_Rock-Lower_Kingdom's_Edge_Dive", "Geo_Rock-Kingdom's_Edge_Below_Bardoon", "Geo_Rock-Kingdom's_Edge_Oro_Far_Left", "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left", "Geo_Rock-Kingdom's_Edge_Above_Root", "Geo_Rock-Kingdom's_Edge_Above_Tower", "Geo_Rock-Kingdom's_Edge_Below_Colosseum", "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock", "Geo_Rock-Kingdom's_Edge_420_Geo_Rock", "Geo_Rock-Beast's_Den_Above_Trilobite", "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe", "Geo_Rock-Beast's_Den_Below_Herrah", "Geo_Rock-Beast's_Den_Below_Egg", "Geo_Rock-Beast's_Den_Below_Egg_Dupe", "Geo_Rock-Beast's_Den_Bottom", "Geo_Rock-Beast's_Den_Bottom_Dupe", "Geo_Rock-Beast's_Den_After_Herrah", 'Geo_Rock-Greenpath_Entrance', 'Geo_Rock-Greenpath_Waterfall', 'Geo_Rock-Greenpath_Below_Skip_Squit', 'Geo_Rock-Greenpath_Skip_Squit', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe', 'Geo_Rock-Greenpath_Second_Skip_Lower', 'Geo_Rock-Greenpath_Below_Hornet', 'Geo_Rock-Greenpath_Above_Thorns', "Geo_Rock-Greenpath_Hunter's_Journal", 'Geo_Rock-Greenpath_Acid_Bridge', 'Geo_Rock-Greenpath_After_MMC_Hidden', 'Geo_Rock-Greenpath_After_MMC', 'Geo_Rock-Greenpath_After_MMC_Dupe', 'Geo_Rock-Greenpath_Obbles_Fool_Eater', 'Geo_Rock-Greenpath_Moss_Knights', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2', 'Geo_Rock-Greenpath_Below_Stag', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater', 'Geo_Rock-Baldur_Shell_Top_Left', 'Geo_Rock-Baldur_Shell_Alcove', 'Geo_Rock-Greenpath_MMC', 'Geo_Rock-Greenpath_Below_Toll', 'Geo_Rock-Greenpath_Toll_Hidden', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe', 'Geo_Rock-Fungal_Below_Shrumal_Ogres', 'Geo_Rock-Fungal_Above_Cloth', 'Geo_Rock-Fungal_After_Cloth', "Geo_Rock-Fungal_Below_Pilgrim's_Way", "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe", 'Geo_Rock-Mantis_Outskirts_Guarded', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe', 'Geo_Rock-Mantis_Outskirts_Alcove', 'Geo_Rock-Mantis_Village_After_Lever', 'Geo_Rock-Mantis_Village_Above_Claw', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe', 'Geo_Rock-Mantis_Village_Below_Lore', 'Geo_Rock-Mantis_Village_Above_Lever', 'Geo_Rock-Above_Mantis_Lords_1', 'Geo_Rock-Above_Mantis_Lords_2', 'Geo_Rock-Fungal_After_Bouncy_Grub', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe', 'Geo_Rock-Fungal_Bouncy_Grub_Lever', 'Geo_Rock-Fungal_After_Cornifer', 'Geo_Rock-Fungal_Above_City_Entrance', 'Geo_Rock-Deepnest_By_Mantis_Lords_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_2', 'Geo_Rock-Deepnest_Lower_Cornifer', 'Geo_Rock-Fungal_Core_Entrance', 'Geo_Rock-Fungal_Core_Hidden', 'Geo_Rock-Fungal_Core_Above_Elder', "Geo_Rock-Queen's_Gardens_Acid_Entrance", "Geo_Rock-Queen's_Gardens_Below_Stag", 'Geo_Rock-Fog_Canyon_East', 'Geo_Rock-Love_Key', 'Geo_Rock-Love_Key_Dupe', "Geo_Rock-Queen's_Gardens_Above_Marmu", 'Geo_Rock-Pale_Lurker', 'Geo_Rock-Godhome_Pipeway', 'Geo_Rock-Hive_Entrance', 'Geo_Rock-Hive_Outside_Bench', 'Geo_Rock-Hive_Below_Root', 'Geo_Rock-Hive_After_Root', 'Geo_Rock-Hive_Below_Stash', 'Geo_Rock-Hive_Stash', 'Geo_Rock-Hive_Stash_Dupe', 'Geo_Rock-Hive_Below_Grub', 'Geo_Rock-Hive_Above_Mask', 'Geo_Rock-Crystal_Peak_Lower_Middle', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2', 'Geo_Rock-Crystal_Peak_Before_Dark_Room', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub', 'Geo_Rock-Crystal_Peak_Mimic_Grub', 'Geo_Rock-Crystal_Peak_Dive_Egg', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe', 'Geo_Rock-Crystal_Peak_Conga_Line', 'Geo_Rock-Hallownest_Crown_Dive', 'Geo_Rock-Hallownest_Crown_Dive_Dupe', 'Geo_Rock-Hallownest_Crown_Hidden', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart', 'Geo_Rock-Crystal_Peak_Entrance', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher', 'Geo_Rock-Resting_Grounds_Catacombs_Grub', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe', 'Geo_Rock-Resting_Grounds_Catacombs_Left', 'Geo_Rock-Overgrown_Mound', 'Geo_Rock-Fluke_Hermit_Dupe', 'Geo_Rock-Fluke_Hermit', 'Geo_Rock-Pleasure_House', 'Geo_Rock-City_of_Tears_Quirrel', 'Geo_Rock-City_of_Tears_Lemm', 'Geo_Rock-City_of_Tears_Above_Lemm', 'Geo_Rock-Soul_Sanctum', "Geo_Rock-Watcher's_Spire", "Geo_Rock-Above_King's_Station", "Geo_Rock-King's_Station", "Geo_Rock-King's_Pass_Left", "Geo_Rock-King's_Pass_Below_Fury", "Geo_Rock-King's_Pass_Hidden", "Geo_Rock-King's_Pass_Collapse", "Geo_Rock-King's_Pass_Above_Fury", 'Geo_Rock-Waterways_Tuk', 'Geo_Rock-Waterways_Tuk_Alcove', 'Geo_Rock-Waterways_Left', 'Geo_Rock-Waterways_East', 'Geo_Rock-Waterways_Flukemarm', 'Boss_Geo-Massive_Moss_Charger', 'Boss_Geo-Gorgeous_Husk', 'Boss_Geo-Sanctum_Soul_Warrior', 'Boss_Geo-Elegant_Soul_Warrior', 'Boss_Geo-Crystal_Guardian', 'Boss_Geo-Enraged_Guardian', 'Boss_Geo-Gruz_Mother', 'Boss_Geo-Vengefly_King', 'Soul_Totem-Basin', 'Soul_Totem-Cliffs_Main', 'Soul_Totem-Cliffs_Gorb', "Soul_Totem-Cliffs_Joni's", 'Soul_Totem-Crossroads_Goam_Journal', 'Soul_Totem-Crossroads_Shops', 'Soul_Totem-Crossroads_Mawlek_Upper', 'Soul_Totem-Crossroads_Acid', 'Soul_Totem-Crossroads_Mawlek_Lower', 'Soul_Totem-Crossroads_Myla', 'Soul_Totem-Ancestral_Mound', 'Soul_Totem-Distant_Village', 'Soul_Totem-Deepnest_Vessel', 'Soul_Totem-Mask_Maker', "Soul_Totem-Lower_Kingdom's_Edge_1", "Soul_Totem-Lower_Kingdom's_Edge_2", "Soul_Totem-Upper_Kingdom's_Edge", "Soul_Totem-Kingdom's_Edge_Camp", 'Soul_Totem-Oro_Dive_2', 'Soul_Totem-Oro_Dive_1', 'Soul_Totem-Oro', 'Soul_Totem-420_Geo_Rock', "Soul_Totem-Beast's_Den", "Soul_Totem-Greenpath_Hunter's_Journal", 'Soul_Totem-Greenpath_MMC', 'Soul_Totem-Greenpath_Below_Toll', "Soul_Totem-Before_Pilgrim's_Way", "Soul_Totem-Pilgrim's_Way", 'Soul_Totem-Fungal_Core', "Soul_Totem-Top_Left_Queen's_Gardens", 'Soul_Totem-Below_Marmu', 'Soul_Totem-Upper_Crystal_Peak', 'Soul_Totem-Hallownest_Crown', 'Soul_Totem-Outside_Crystallized_Mound', 'Soul_Totem-Crystal_Heart_1', 'Soul_Totem-Crystal_Heart_2', 'Soul_Totem-Crystallized_Mound', 'Soul_Totem-Resting_Grounds', 'Soul_Totem-Below_Xero', 'Soul_Totem-Sanctum_Below_Soul_Master', 'Soul_Totem-Sanctum_Below_Chest', 'Soul_Totem-Sanctum_Above_Grub', 'Soul_Totem-Waterways_Entrance', 'Soul_Totem-Top_Left_Waterways', 'Soul_Totem-Waterways_East', 'Soul_Totem-Waterways_Flukemarm', 'Soul_Totem-White_Palace_Entrance', 'Soul_Totem-White_Palace_Hub', 'Soul_Totem-White_Palace_Left', 'Soul_Totem-White_Palace_Final', 'Soul_Totem-White_Palace_Right', 'Soul_Totem-Path_of_Pain_Below_Lever', 'Soul_Totem-Path_of_Pain_Left_of_Lever', 'Soul_Totem-Path_of_Pain_Entrance', 'Soul_Totem-Path_of_Pain_Second', 'Soul_Totem-Path_of_Pain_Hidden', 'Soul_Totem-Path_of_Pain_Below_Thornskip', 'Soul_Totem-Path_of_Pain_Final', 'Soul_Totem-Pale_Lurker', 'Lore_Tablet-City_Entrance', 'Lore_Tablet-Pleasure_House', 'Lore_Tablet-Sanctum_Entrance', 'Lore_Tablet-Sanctum_Past_Soul_Master', "Lore_Tablet-Watcher's_Spire", 'Lore_Tablet-Archives_Upper', 'Lore_Tablet-Archives_Left', 'Lore_Tablet-Archives_Right', "Lore_Tablet-Pilgrim's_Way_1", "Lore_Tablet-Pilgrim's_Way_2", 'Lore_Tablet-Mantis_Outskirts', 'Lore_Tablet-Mantis_Village', 'Lore_Tablet-Greenpath_Upper_Hidden', 'Lore_Tablet-Greenpath_Below_Toll', 'Lore_Tablet-Greenpath_Lifeblood', 'Lore_Tablet-Greenpath_Stag', 'Lore_Tablet-Greenpath_QG', 'Lore_Tablet-Greenpath_Lower_Hidden', 'Lore_Tablet-Dung_Defender', 'Lore_Tablet-Spore_Shroom', 'Lore_Tablet-Fungal_Wastes_Hidden', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres', 'Lore_Tablet-Fungal_Core', 'Lore_Tablet-Ancient_Basin', "Lore_Tablet-King's_Pass_Focus", "Lore_Tablet-King's_Pass_Fury", "Lore_Tablet-King's_Pass_Exit", 'Lore_Tablet-World_Sense', 'Lore_Tablet-Howling_Cliffs', "Lore_Tablet-Kingdom's_Edge", 'Lore_Tablet-Palace_Workshop', 'Lore_Tablet-Palace_Throne', 'Lore_Tablet-Path_of_Pain_Entrance'] From b6f3ccb8c52b65833d1218ebc37fc55ad18f3e4c Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 12 Jan 2024 08:13:39 +1000 Subject: [PATCH 07/78] Touhou Mugakudan 3 song update. (#2659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds all the songs from the Touhou Mugakudan -Ⅲ- update. - Increases the upper limit of additional songs to 508 due to there being 512 songs now. - Finally fixes umpopoff. As it was the only song that had less than 3 difficulties but also didn't have proper difficulty values --- worlds/musedash/MuseDashCollection.py | 22 +++++++++++++++----- worlds/musedash/MuseDashData.txt | 12 +++++++++-- worlds/musedash/Options.py | 2 +- worlds/musedash/__init__.py | 3 ++- worlds/musedash/test/TestDifficultyRanges.py | 8 ++++++- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 55523542d7..6cd27c696c 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -34,6 +34,7 @@ class MuseDashCollections: "Rush-Hour", "Find this Month's Featured Playlist", "PeroPero in the Universe", + "umpopoff" ] album_items: Dict[str, AlbumData] = {} @@ -81,11 +82,22 @@ class MuseDashCollections: steamer_mode = sections[3] == "True" if song_name in self.DIFF_OVERRIDES: - # Note: These difficulties may not actually be representative of these songs. - # The game does not provide these difficulties so they have to be filled in. - diff_of_easy = 4 - diff_of_hard = 7 - diff_of_master = 10 + # These songs use non-standard difficulty values. Which are being overriden with standard values. + # But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value. + if sections[4] != '0': + diff_of_easy = 4 + else: + diff_of_easy = None + + if sections[5] != '0': + diff_of_hard = 7 + else: + diff_of_hard = None + + if sections[6] != '0': + diff_of_master = 10 + else: + diff_of_master = None else: diff_of_easy = self.parse_song_difficulty(sections[4]) diff_of_hard = self.parse_song_difficulty(sections[5]) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 54a0124474..fe3574f31b 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11| Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11| Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7| Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8| -Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7| +Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11 Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8| Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10| Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9| @@ -501,4 +501,12 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9| Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11 Swing Edge|68-3|Gambler's Tricks|True|4|8|10| Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11 -Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| \ No newline at end of file +Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| +Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8| +YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9| +Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9| +INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10| +Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9| +HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10| +Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11 +Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8| \ No newline at end of file diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 3fe28187fa..d5ce313f8f 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -36,7 +36,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 500 # Note will probably not reach this high if any other settings are done. + range_end = 508 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index a68fd2853d..af2d4cc207 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -328,5 +328,6 @@ class MuseDashWorld(World): "victoryLocation": self.victory_song_name, "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), - "gradeNeeded": self.options.grade_needed.value + "gradeNeeded": self.options.grade_needed.value, + "hasFiller": True, } diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index 01420347af..af3469aa08 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -66,5 +66,11 @@ class DifficultyRanges(MuseDashTestBase): for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: song = muse_dash_world.md_collection.song_items[song_name] - self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, + # umpopoff is a one time weird song. Its currently the only song in the game + # with non-standard difficulties and also doesn't have 3 or more difficulties. + if song_name == 'umpopoff': + self.assertTrue(song.easy is None and song.hard is not None and song.master is None, + f"Song '{song_name}' difficulty not set when it should be.") + else: + self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, f"Song '{song_name}' difficulty not set when it should be.") From f530895c33af75e68de547ca35fa9063dfe1d3fb Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Thu, 11 Jan 2024 14:44:12 -0800 Subject: [PATCH 08/78] WebHost: Fix /api/generate (#2693) --- WebHostLib/api/generate.py | 4 ++-- test/webhost/test_api_generate.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index 61e9164e26..5a66d1e693 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -20,8 +20,8 @@ def generate_api(): race = False meta_options_source = {} if 'file' in request.files: - file = request.files['file'] - options = get_yaml_data(file) + files = request.files.getlist('file') + options = get_yaml_data(files) if isinstance(options, Markup): return {"text": options.striptags()}, 400 if isinstance(options, str): diff --git a/test/webhost/test_api_generate.py b/test/webhost/test_api_generate.py index b8bdcb38c7..bd78edd9c7 100644 --- a/test/webhost/test_api_generate.py +++ b/test/webhost/test_api_generate.py @@ -1,5 +1,7 @@ +import io import unittest import json +import yaml class TestDocs(unittest.TestCase): @@ -23,7 +25,7 @@ class TestDocs(unittest.TestCase): response = self.client.post("/api/generate") self.assertIn("No options found. Expected file attachment or json weights.", response.text) - def test_generation_queued(self): + def test_generation_queued_weights(self): options = { "Tester1": { @@ -40,3 +42,19 @@ class TestDocs(unittest.TestCase): json_data = response.get_json() self.assertTrue(json_data["text"].startswith("Generation of seed ")) self.assertTrue(json_data["text"].endswith(" started successfully.")) + + def test_generation_queued_file(self): + options = { + "game": "Archipelago", + "name": "Tester", + "Archipelago": {} + } + response = self.client.post( + "/api/generate", + data={ + 'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml") + }, + ) + json_data = response.get_json() + self.assertTrue(json_data["text"].startswith("Generation of seed ")) + self.assertTrue(json_data["text"].endswith(" started successfully.")) From 2760deb5b6ad429d7c75d91eb0aab7fc819c7b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dana=C3=ABl=20V?= <104455676+ReverM@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:18:11 -0500 Subject: [PATCH 09/78] Docs: Fix broken link in Landstalker setup Guide (#2680) * Cleaning up (#4) Cleanup * Update landstalker_setup_en.md Fixed Redirect --- worlds/landstalker/docs/landstalker_setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md index 9f453c146d..32e46a4b33 100644 --- a/worlds/landstalker/docs/landstalker_setup_en.md +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings -and export a config file from them. +The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows +you to easily configure your personal settings ## How-to-play From ac2387e17cf4badcae9b543b7fdef4a78991fe8a Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Thu, 11 Jan 2024 15:22:04 -0800 Subject: [PATCH 10/78] Tests: remove deprecated option access from `WorldTestBase` (#2671) * remove deprecated option access from `WorldTestBase` * one in test_reachability --- test/bases.py | 2 +- test/general/test_reachability.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bases.py b/test/bases.py index d6a43c598f..7ce12cc7b7 100644 --- a/test/bases.py +++ b/test/bases.py @@ -285,7 +285,7 @@ class WorldTestBase(unittest.TestCase): if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): - excluded = self.multiworld.exclude_locations[1].value + excluded = self.multiworld.worlds[1].options.exclude_locations.value state = self.multiworld.get_all_state(False) for location in self.multiworld.get_locations(): if location.name not in excluded: diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index 828912ee35..cfd83c9404 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -37,7 +37,7 @@ class TestBase(unittest.TestCase): unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set()) with self.subTest("Game", game=game_name): world = setup_solo_multiworld(world_type) - excluded = world.exclude_locations[1].value + excluded = world.worlds[1].options.exclude_locations.value state = world.get_all_state(False) for location in world.get_locations(): if location.name not in excluded: From 44de140addbf1e293113f6ea76d27027b8536f68 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 12 Jan 2024 00:40:33 +0100 Subject: [PATCH 11/78] SC2: run download_data via concurrent.futures (#2704) --- worlds/sc2wol/Client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py index 3dbd2047de..83b7b62d29 100644 --- a/worlds/sc2wol/Client.py +++ b/worlds/sc2wol/Client.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import copy import ctypes -import json import logging import multiprocessing import os.path @@ -15,6 +14,7 @@ import queue import zipfile import io import random +import concurrent.futures from pathlib import Path # CommonClient import first to trigger ModuleUpdater @@ -42,6 +42,7 @@ import colorama from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart from MultiServer import mark_raw +pool = concurrent.futures.ThreadPoolExecutor(1) loop = asyncio.get_event_loop_policy().new_event_loop() nest_asyncio.apply(loop) max_bonus: int = 13 @@ -210,6 +211,11 @@ class StarcraftClientProcessor(ClientCommandProcessor): def _cmd_download_data(self) -> bool: """Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files.""" + pool.submit(self._download_data) + return True + + @staticmethod + def _download_data() -> bool: if "SC2PATH" not in os.environ: check_game_install_path() @@ -220,7 +226,7 @@ class StarcraftClientProcessor(ClientCommandProcessor): metadata = None tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, - metadata=metadata, force_download=True) + metadata=metadata, force_download=True) if tempzip != '': try: From a99c1e15adcd0cf2b4a94921b0cb453d0b3b2ec8 Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Thu, 11 Jan 2024 18:48:22 -0500 Subject: [PATCH 12/78] Shivers: Fixes issue with office elevator rule logic. (#2690) Office elevator logic was written as can reach Underground Tunnels OR can reach Office AND have Key for Office Elevator Meaning that key for office elevator was not required if Underground Tunnels could be reached when it should be. Changed to (can reach Underground Tunnels OR can reach Office) AND have Key for Office Elevator --- worlds/shivers/Rules.py | 8 ++++---- worlds/shivers/data/excluded_locations.json | 2 +- worlds/shivers/data/locations.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 4e1058fecf..62f4cd6a07 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -154,11 +154,11 @@ def get_rules_lookup(player: int): "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) }, "elevators": { - "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) - and state.has("Key for Office Elevator", player))), + "Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)) + and state.has("Key for Office Elevator", player)), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) - and state.has("Key for Three Floor Elevator", player))) + "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) + and state.has("Key for Three Floor Elevator", player)) }, "lightning": { "Ixupi Captured Lightning": lambda state: lightning_capturable(state, player) diff --git a/worlds/shivers/data/excluded_locations.json b/worlds/shivers/data/excluded_locations.json index 6ed625077a..a37285eb1d 100644 --- a/worlds/shivers/data/excluded_locations.json +++ b/worlds/shivers/data/excluded_locations.json @@ -42,7 +42,7 @@ "Information Plaque: Aliens (UFO)" ], "elevators": [ - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Bedroom Elevator", "Puzzle Solved Three Floor Elevator" ], diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json index 7d031b886b..fdf8ed69d1 100644 --- a/worlds/shivers/data/locations.json +++ b/worlds/shivers/data/locations.json @@ -110,7 +110,7 @@ "Information Plaque: Astronomical Construction (UFO)", "Information Plaque: Guillotine (Torture)", "Information Plaque: Aliens (UFO)", - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Bedroom Elevator", "Puzzle Solved Three Floor Elevator", "Ixupi Captured Lightning" @@ -129,7 +129,7 @@ "Ixupi Captured Sand", "Ixupi Captured Metal", "Ixupi Captured Lightning", - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Three Floor Elevator", "Puzzle Hint Found: Combo Lock in Mailbox", "Puzzle Hint Found: Orange Symbol", From 4ce8a7ec4d2d59331aa748074e2a160f4b6a2604 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 12 Jan 2024 00:49:14 +0100 Subject: [PATCH 13/78] PyCharm: ship a working unittest run config (#2694) --- .run/Archipelago Unittests.run.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .run/Archipelago Unittests.run.xml diff --git a/.run/Archipelago Unittests.run.xml b/.run/Archipelago Unittests.run.xml new file mode 100644 index 0000000000..24fea0f73f --- /dev/null +++ b/.run/Archipelago Unittests.run.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file From 47dd36456e6393b436319852626858ddc3324c28 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:49:54 -0500 Subject: [PATCH 14/78] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20move=20interv?= =?UTF-8?q?ention=20(#2687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 5a94a8b5ff..169ff1d59f 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -353,7 +353,9 @@ class PokemonRedBlueWorld(World): location.show_in_spoiler = False def intervene(move, test_state): - if self.multiworld.randomize_wild_pokemon[self.player]: + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] + if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons: accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if loc.type == "Wild Encounter"] @@ -363,8 +365,6 @@ class PokemonRedBlueWorld(World): zones.add(loc.name.split(" - ")[0]) return len(zones) - move_bit = pow(2, poke_data.hm_moves.index(move) + 2) - viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] placed_mons = [slot.item.name for slot in accessible_slots] if self.multiworld.area_1_to_1_mapping[self.player]: From e00b5a7d17715c28e3d811434a9478df6bbaaabd Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 12 Jan 2024 01:07:40 +0100 Subject: [PATCH 15/78] SoE: use new AP API and naming and make APworld (#2701) * SoE: new file naming also fixes test base deprecation * SoE: use options_dataclass * SoE: moar typing * SoE: no more multiworld.random * SoE: replace LogicMixin by SoEPlayerLogic object * SoE: add test that rocket parts always exist * SoE: Even moar typing * SoE: can haz apworld now * SoE: pep up test naming * SoE: use self.options for trap chances * SoE: remove unused import with outdated comment * SoE: move flag and trap extraction to dataclass as suggested by beauxq * SoE: test trap option parsing and item generation --- setup.py | 1 - worlds/soe/Logic.py | 70 ---------- worlds/soe/__init__.py | 166 +++++++++++------------- worlds/soe/logic.py | 85 ++++++++++++ worlds/soe/{Options.py => options.py} | 97 ++++++++------ worlds/soe/{Patch.py => patch.py} | 6 +- worlds/soe/test/__init__.py | 13 +- worlds/soe/test/test_access.py | 4 +- worlds/soe/test/test_goal.py | 12 +- worlds/soe/test/test_oob.py | 4 +- worlds/soe/test/test_sequence_breaks.py | 4 +- worlds/soe/test/test_traps.py | 55 ++++++++ 12 files changed, 298 insertions(+), 219 deletions(-) delete mode 100644 worlds/soe/Logic.py create mode 100644 worlds/soe/logic.py rename worlds/soe/{Options.py => options.py} (71%) rename worlds/soe/{Patch.py => patch.py} (86%) create mode 100644 worlds/soe/test/test_traps.py diff --git a/setup.py b/setup.py index c864a8cc9d..39a93e9385 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,6 @@ non_apworlds: set = { "Ocarina of Time", "Overcooked! 2", "Raft", - "Secret of Evermore", "Slay the Spire", "Sudoku", "Super Mario 64", diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py deleted file mode 100644 index fe5339c955..0000000000 --- a/worlds/soe/Logic.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Protocol, Set - -from BaseClasses import MultiWorld -from worlds.AutoWorld import LogicMixin -from . import pyevermizer -from .Options import EnergyCore, OutOfBounds, SequenceBreaks - -# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? - -# TODO: resolve/flatten/expand rules to get rid of recursion below where possible -# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) -rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] -# Logic.items are all items and extra items excluding non-progression items and duplicates -item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) - if item.name not in item_names and not item_names.add(item.name)] - - -class LogicProtocol(Protocol): - def has(self, name: str, player: int) -> bool: ... - def count(self, name: str, player: int) -> int: ... - def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... - def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... - - -# when this module is loaded, this mixin will extend BaseClasses.CollectionState -class SecretOfEvermoreLogic(LogicMixin): - def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: - """ - Returns reached count of one of evermizer's progress steps based on collected items. - i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP - """ - n = 0 - for item in items: - for pvd in item.provides: - if pvd[1] == progress: - if self.has(item.name, player): - n += self.count(item.name, player) * pvd[0] - if n >= max_count > 0: - return n - for rule in rules: - for pvd in rule.provides: - if pvd[1] == progress and pvd[0] > 0: - has = True - for req in rule.requires: - if not self.soe_has(req[1], world, player, req[0]): - has = False - break - if has: - n += pvd[0] - if n >= max_count > 0: - return n - return n - - def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: - """ - Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE - """ - if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call - w = world.worlds[player] - if w.energy_core == EnergyCore.option_fragments: - progress = pyevermizer.P_CORE_FRAGMENT - count = w.required_fragments - elif progress == pyevermizer.P_ALLOW_OOB: - if world.worlds[player].out_of_bounds == OutOfBounds.option_logic: - return True - elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: - if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic: - return True - return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index d02a8d02ee..b431e471e2 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -4,18 +4,20 @@ import os.path import threading import typing +# from . import pyevermizer # as part of the source tree +import pyevermizer # from package + import settings +from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial +from Utils import output_path from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule -from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial -from Utils import output_path +from .logic import SoEPlayerLogic +from .options import AvailableFragments, Difficulty, EnergyCore, RequiredFragments, SoEOptions, TrapChance +from .patch import SoEDeltaPatch, get_base_rom_path -import pyevermizer # from package -# from . import pyevermizer # as part of the source tree - -from . import Logic # load logic mixin -from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments -from .Patch import SoEDeltaPatch, get_base_rom_path +if typing.TYPE_CHECKING: + from BaseClasses import MultiWorld, CollectionState """ In evermizer: @@ -24,17 +26,17 @@ Items are uniquely defined by a pair of (type, id). For most items this is their vanilla location (i.e. CHECK_GOURD, number). Items have `provides`, which give the actual progression -instead of providing multiple events per item, we iterate through them in Logic.py +instead of providing multiple events per item, we iterate through them in logic.py e.g. Found any weapon Locations have `requires` and `provides`. Requirements have to be converted to (access) rules for AP e.g. Chest locked behind having a weapon -Provides could be events, but instead we iterate through the entire logic in Logic.py +Provides could be events, but instead we iterate through the entire logic in logic.py e.g. NPC available after fighting a Boss Rules are special locations that don't have a physical location -instead of implementing virtual locations and virtual items, we simply use them in Logic.py +instead of implementing virtual locations and virtual items, we simply use them in logic.py e.g. 2DEs+Wheel+Gauge = Rocket Rules and Locations live on the same logic tree returned by pyevermizer.get_logic() @@ -84,8 +86,8 @@ _other_items = ( ) -def _match_item_name(item, substr: str) -> bool: - sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name +def _match_item_name(item: pyevermizer.Item, substr: str) -> bool: + sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name return sub == substr or sub == substr+'s' @@ -158,8 +160,9 @@ class SoEWorld(World): Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a space station where the final boss must be defeated. """ - game: str = "Secret of Evermore" - option_definitions = soe_options + game: typing.ClassVar[str] = "Secret of Evermore" + options_dataclass = SoEOptions + options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False data_version = 4 @@ -170,31 +173,21 @@ class SoEWorld(World): location_name_to_id, location_id_to_raw = _get_location_mapping() item_name_groups = _get_item_grouping() - trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')] - + logic: SoEPlayerLogic evermizer_seed: int connect_name: str - energy_core: int - sequence_breaks: int - out_of_bounds: int - available_fragments: int - required_fragments: int _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] - def __init__(self, *args, **kwargs): + def __init__(self, multiworld: "MultiWorld", player: int): self.connect_name_available_event = threading.Event() - super(SoEWorld, self).__init__(*args, **kwargs) + super(SoEWorld, self).__init__(multiworld, player) def generate_early(self) -> None: - # store option values that change logic - self.energy_core = self.multiworld.energy_core[self.player].value - self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value - self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value - self.required_fragments = self.multiworld.required_fragments[self.player].value - if self.required_fragments > self.multiworld.available_fragments[self.player].value: - self.multiworld.available_fragments[self.player].value = self.required_fragments - self.available_fragments = self.multiworld.available_fragments[self.player].value + # create logic from options + if self.options.required_fragments.value > self.options.available_fragments.value: + self.options.available_fragments.value = self.options.required_fragments.value + self.logic = SoEPlayerLogic(self.player, self.options) def create_event(self, event: str) -> Item: return SoEItem(event, ItemClassification.progression, None, self.player) @@ -214,20 +207,20 @@ class SoEWorld(World): return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player) @classmethod - def stage_assert_generate(cls, multiworld): + def stage_assert_generate(cls, _: "MultiWorld") -> None: rom_file = get_base_rom_path() if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) - def create_regions(self): + def create_regions(self) -> None: # exclude 'hidden' on easy - max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256 + max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256 # TODO: generate *some* regions from locations' requirements? menu = Region('Menu', self.player, self.multiworld) self.multiworld.regions += [menu] - def get_sphere_index(evermizer_loc): + def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int: """Returns 0, 1 or 2 for locations in spheres 1, 2, 3+""" if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON: return 2 @@ -252,18 +245,18 @@ class SoEWorld(World): # mark some as excluded based on numbers above for trash_sphere, fills in trash_fills.items(): for typ, counts in fills.items(): - count = counts[self.multiworld.difficulty[self.player].value] - for location in self.multiworld.random.sample(spheres[trash_sphere][typ], count): + count = counts[self.options.difficulty.value] + for location in self.random.sample(spheres[trash_sphere][typ], count): assert location.name != "Energy Core #285", "Error in sphere generation" location.progress_type = LocationProgressType.EXCLUDED - def sphere1_blocked_items_rule(item): + def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool: if isinstance(item, SoEItem): # disable certain items in sphere 1 if item.name in {"Gauge", "Wheel"}: return False # and some more for non-easy, non-mystery - if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery): + if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery): if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}: return False return True @@ -273,13 +266,13 @@ class SoEWorld(World): add_item_rule(location, sphere1_blocked_items_rule) # make some logically late(r) bosses priority locations to increase complexity - if self.multiworld.difficulty[self.player] == Difficulty.option_mystery: - late_count = self.multiworld.random.randint(0, 2) + if self.options.difficulty == Difficulty.option_mystery: + late_count = self.random.randint(0, 2) else: - late_count = self.multiworld.difficulty[self.player].value + late_count = self.options.difficulty.value late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala", "Mungola", "Lightning Storm", "Magmar", "Volcano Viper") - late_locations = self.multiworld.random.sample(late_bosses, late_count) + late_locations = self.random.sample(late_bosses, late_count) # add locations to the world for sphere in spheres.values(): @@ -293,17 +286,17 @@ class SoEWorld(World): menu.connect(ingame, "New Game") self.multiworld.regions += [ingame] - def create_items(self): + def create_items(self) -> None: # add regular items to the pool exclusions: typing.List[str] = [] - if self.energy_core != EnergyCore.option_shuffle: + if self.options.energy_core != EnergyCore.option_shuffle: exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions))) # remove one pair of wings that will be placed in generate_basic items.remove(self.create_item("Wings")) - def is_ingredient(item): + def is_ingredient(item: pyevermizer.Item) -> bool: for ingredient in _ingredients: if _match_item_name(item, ingredient): return True @@ -311,84 +304,74 @@ class SoEWorld(World): # add energy core fragments to the pool ingredients = [n for n, item in enumerate(items) if is_ingredient(item)] - if self.energy_core == EnergyCore.option_fragments: + if self.options.energy_core == EnergyCore.option_fragments: items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core - for _ in range(self.available_fragments - 1): + for _ in range(self.options.available_fragments - 1): if len(ingredients) < 1: break # out of ingredients to replace - r = self.multiworld.random.choice(ingredients) + r = self.random.choice(ingredients) ingredients.remove(r) items[r] = self.create_item("Energy Core Fragment") # add traps to the pool - trap_count = self.multiworld.trap_count[self.player].value - trap_chances = {} - trap_names = {} + trap_count = self.options.trap_count.value + trap_names: typing.List[str] = [] + trap_weights: typing.List[int] = [] if trap_count > 0: - for trap_type in self.trap_types: - trap_option = getattr(self.multiworld, f'trap_chance_{trap_type}')[self.player] - trap_chances[trap_type] = trap_option.value - trap_names[trap_type] = trap_option.item_name - trap_chances_total = sum(trap_chances.values()) - if trap_chances_total == 0: - for trap_type in trap_chances: - trap_chances[trap_type] = 1 - trap_chances_total = len(trap_chances) + for trap_option in self.options.trap_chances: + trap_names.append(trap_option.item_name) + trap_weights.append(trap_option.value) + if sum(trap_weights) == 0: + trap_weights = [1 for _ in trap_weights] def create_trap() -> Item: - v = self.multiworld.random.randrange(trap_chances_total) - for t, c in trap_chances.items(): - if v < c: - return self.create_item(trap_names[t]) - v -= c - assert False, "Bug in create_trap" + return self.create_item(self.random.choices(trap_names, trap_weights)[0]) for _ in range(trap_count): if len(ingredients) < 1: break # out of ingredients to replace - r = self.multiworld.random.choice(ingredients) + r = self.random.choice(ingredients) ingredients.remove(r) items[r] = create_trap() self.multiworld.itempool += items - def set_rules(self): + def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player) # set Done from goal option once we have multiple goals set_rule(self.multiworld.get_location('Done', self.player), - lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player)) + lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS)) set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True) for loc in _locations: location = self.multiworld.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]: - def rule(state) -> bool: + def rule(state: "CollectionState") -> bool: for count, progress in requires: - if not state.soe_has(progress, self.multiworld, self.player, count): + if not self.logic.has(state, progress, count): return False return True return rule - def make_item_type_limit_rule(self, item_type: int): - return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type - - def generate_basic(self): + def generate_basic(self) -> None: # place Victory event self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory')) # place wings in halls NE to avoid softlock - wings_location = self.multiworld.random.choice(self._halls_ne_chest_names) + wings_location = self.random.choice(self._halls_ne_chest_names) wings_item = self.create_item('Wings') self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item) # place energy core at vanilla location for vanilla mode - if self.energy_core == EnergyCore.option_vanilla: + if self.options.energy_core == EnergyCore.option_vanilla: energy_core = self.create_item('Energy Core') self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core) # generate stuff for later - self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? + self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? + + def generate_output(self, output_directory: str) -> None: + from dataclasses import asdict - def generate_output(self, output_directory: str): player_name = self.multiworld.get_player_name(self.player) self.connect_name = player_name[:32] while len(self.connect_name.encode('utf-8')) > 32: @@ -397,24 +380,21 @@ class SoEWorld(World): placement_file = "" out_file = "" try: - money = self.multiworld.money_modifier[self.player].value - exp = self.multiworld.exp_modifier[self.player].value + money = self.options.money_modifier.value + exp = self.options.exp_modifier.value switches: typing.List[str] = [] - if self.multiworld.death_link[self.player].value: + if self.options.death_link.value: switches.append("--death-link") - if self.energy_core == EnergyCore.option_fragments: - switches.extend(('--available-fragments', str(self.available_fragments), - '--required-fragments', str(self.required_fragments))) + if self.options.energy_core == EnergyCore.option_fragments: + switches.extend(('--available-fragments', str(self.options.available_fragments.value), + '--required-fragments', str(self.options.required_fragments.value))) rom_file = get_base_rom_path() out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player)) out_file = out_base + '.sfc' placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' flags = 'l' # spoiler log - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] - if hasattr(option, 'to_flag'): - flags += option.to_flag() + flags += self.options.flags with open(placement_file, "wb") as f: # generate placement file for location in self.multiworld.get_locations(self.player): @@ -448,7 +428,7 @@ class SoEWorld(World): except FileNotFoundError: pass - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None: # wait for self.connect_name to be available. self.connect_name_available_event.wait() # we skip in case of error, so that the original error in the output thread is the one that gets raised @@ -457,7 +437,7 @@ class SoEWorld(World): multidata["connect_names"][self.connect_name] = payload def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"])) + return self.random.choice(list(self.item_name_groups["Ingredients"])) class SoEItem(Item): diff --git a/worlds/soe/logic.py b/worlds/soe/logic.py new file mode 100644 index 0000000000..ee81c76e58 --- /dev/null +++ b/worlds/soe/logic.py @@ -0,0 +1,85 @@ +import typing +from typing import Callable, Set + +from . import pyevermizer +from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions + +if typing.TYPE_CHECKING: + from BaseClasses import CollectionState + +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? + +# TODO: resolve/flatten/expand rules to get rid of recursion below where possible +# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) +rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +# Logic.items are all items and extra items excluding non-progression items and duplicates +item_names: Set[str] = set() +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) + if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value] + + +class SoEPlayerLogic: + __slots__ = "player", "out_of_bounds", "sequence_breaks", "has" + player: int + out_of_bounds: bool + sequence_breaks: bool + + has: Callable[..., bool] + """ + Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE + """ + + def __init__(self, player: int, options: "SoEOptions"): + self.player = player + self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic + self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic + + if options.energy_core == EnergyCore.option_fragments: + # override logic for energy core fragments + required_fragments = options.required_fragments.value + + def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool: + if progress == pyevermizer.P_ENERGY_CORE: + progress = pyevermizer.P_CORE_FRAGMENT + count = required_fragments + return self._has(state, progress, count) + + self.has = fragmented_has + else: + # default (energy core) logic + self.has = self._has + + def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int: + """ + Returns reached count of one of evermizer's progress steps based on collected items. + i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP + """ + n = 0 + for item in items: + for pvd in item.provides: + if pvd[1] == progress: + if state.has(item.name, self.player): + n += state.count(item.name, self.player) * pvd[0] + if n >= max_count > 0: + return n + for rule in rules: + for pvd in rule.provides: + if pvd[1] == progress and pvd[0] > 0: + has = True + for req in rule.requires: + if not self.has(state, req[1], req[0]): + has = False + break + if has: + n += pvd[0] + if n >= max_count > 0: + return n + return n + + def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool: + """Default implementation of has""" + if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB: + return True + if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: + return True + return self._count(state, progress, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/options.py similarity index 71% rename from worlds/soe/Options.py rename to worlds/soe/options.py index 3de2de34ac..0436b17618 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/options.py @@ -1,16 +1,18 @@ -import typing +from dataclasses import dataclass, fields +from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol -from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing +from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, ProgressionBalancing, \ + Range, Toggle # typing boilerplate -class FlagsProtocol(typing.Protocol): +class FlagsProtocol(Protocol): value: int default: int - flags: typing.List[str] + flags: List[str] -class FlagProtocol(typing.Protocol): +class FlagProtocol(Protocol): value: int default: int flag: str @@ -18,7 +20,7 @@ class FlagProtocol(typing.Protocol): # meta options class EvermizerFlags: - flags: typing.List[str] + flags: List[str] def to_flag(self: FlagsProtocol) -> str: return self.flags[self.value] @@ -200,13 +202,13 @@ class TrapCount(Range): # more meta options class ItemChanceMeta(AssembleOptions): - def __new__(mcs, name, bases, attrs): + def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta": if 'item_name' in attrs: attrs["display_name"] = f"{attrs['item_name']} Chance" attrs["range_start"] = 0 attrs["range_end"] = 100 - - return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + return cast(ItemChanceMeta, cls) class TrapChance(Range, metaclass=ItemChanceMeta): @@ -247,33 +249,50 @@ class SoEProgressionBalancing(ProgressionBalancing): special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} -soe_options: typing.Dict[str, AssembleOptions] = { - "difficulty": Difficulty, - "energy_core": EnergyCore, - "required_fragments": RequiredFragments, - "available_fragments": AvailableFragments, - "money_modifier": MoneyModifier, - "exp_modifier": ExpModifier, - "sequence_breaks": SequenceBreaks, - "out_of_bounds": OutOfBounds, - "fix_cheats": FixCheats, - "fix_infinite_ammo": FixInfiniteAmmo, - "fix_atlas_glitch": FixAtlasGlitch, - "fix_wings_glitch": FixWingsGlitch, - "shorter_dialogs": ShorterDialogs, - "short_boss_rush": ShortBossRush, - "ingredienizer": Ingredienizer, - "sniffamizer": Sniffamizer, - "callbeadamizer": Callbeadamizer, - "musicmizer": Musicmizer, - "doggomizer": Doggomizer, - "turdo_mode": TurdoMode, - "death_link": DeathLink, - "trap_count": TrapCount, - "trap_chance_quake": TrapChanceQuake, - "trap_chance_poison": TrapChancePoison, - "trap_chance_confound": TrapChanceConfound, - "trap_chance_hud": TrapChanceHUD, - "trap_chance_ohko": TrapChanceOHKO, - "progression_balancing": SoEProgressionBalancing, -} +# noinspection SpellCheckingInspection +@dataclass +class SoEOptions(PerGameCommonOptions): + difficulty: Difficulty + energy_core: EnergyCore + required_fragments: RequiredFragments + available_fragments: AvailableFragments + money_modifier: MoneyModifier + exp_modifier: ExpModifier + sequence_breaks: SequenceBreaks + out_of_bounds: OutOfBounds + fix_cheats: FixCheats + fix_infinite_ammo: FixInfiniteAmmo + fix_atlas_glitch: FixAtlasGlitch + fix_wings_glitch: FixWingsGlitch + shorter_dialogs: ShorterDialogs + short_boss_rush: ShortBossRush + ingredienizer: Ingredienizer + sniffamizer: Sniffamizer + callbeadamizer: Callbeadamizer + musicmizer: Musicmizer + doggomizer: Doggomizer + turdo_mode: TurdoMode + death_link: DeathLink + trap_count: TrapCount + trap_chance_quake: TrapChanceQuake + trap_chance_poison: TrapChancePoison + trap_chance_confound: TrapChanceConfound + trap_chance_hud: TrapChanceHUD + trap_chance_ohko: TrapChanceOHKO + progression_balancing: SoEProgressionBalancing + + @property + def trap_chances(self) -> Iterator[TrapChance]: + for field in fields(self): + option = getattr(self, field.name) + if isinstance(option, TrapChance): + yield option + + @property + def flags(self) -> str: + flags = '' + for field in fields(self): + option = getattr(self, field.name) + if isinstance(option, (EvermizerFlag, EvermizerFlags)): + flags += getattr(self, field.name).to_flag() + return flags diff --git a/worlds/soe/Patch.py b/worlds/soe/patch.py similarity index 86% rename from worlds/soe/Patch.py rename to worlds/soe/patch.py index f4de5d06ea..8270f2d86d 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/patch.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import BinaryIO, Optional import Utils from worlds.Files import APDeltaPatch @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: return file_name -def read_rom(stream, strip_header=True) -> bytes: +def read_rom(stream: BinaryIO, strip_header: bool=True) -> bytes: """Reads rom into bytearray and optionally strips off any smc header""" data = stream.read() if strip_header and len(data) % 0x400 == 0x200: @@ -40,5 +40,5 @@ def read_rom(stream, strip_header=True) -> bytes: if __name__ == '__main__': import sys - print('Please use ../../Patch.py', file=sys.stderr) + print('Please use ../../patch.py', file=sys.stderr) sys.exit(1) diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 27d38605aa..b3ba7018e4 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from typing import Iterable @@ -18,3 +18,14 @@ class SoETestBase(WorldTestBase): for location in unreachable: self.assertFalse(self.can_reach_location(location), f"{location} is reachable but shouldn't be") + + def testRocketPartsExist(self): + """Tests that rocket parts exist and are unique""" + self.assertEqual(len(self.get_items_by_name("Gauge")), 1) + self.assertEqual(len(self.get_items_by_name("Wheel")), 1) + diamond_eyes = self.get_items_by_name("Diamond Eye") + self.assertEqual(len(diamond_eyes), 3) + # verify diamond eyes are individual items + self.assertFalse(diamond_eyes[0] is diamond_eyes[1]) + self.assertFalse(diamond_eyes[0] is diamond_eyes[2]) + self.assertFalse(diamond_eyes[1] is diamond_eyes[2]) diff --git a/worlds/soe/test/test_access.py b/worlds/soe/test/test_access.py index c7da7b8896..81b8818eb5 100644 --- a/worlds/soe/test/test_access.py +++ b/worlds/soe/test/test_access.py @@ -7,7 +7,7 @@ class AccessTest(SoETestBase): def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] - def testBronzeAxe(self): + def test_bronze_axe(self): gourds = { "Pyramid bottom": (118, 121, 122, 123, 124, 125), "Pyramid top": (140,) @@ -16,7 +16,7 @@ class AccessTest(SoETestBase): items = [["Bronze Axe"]] self.assertAccessDependency(locations, items) - def testBronzeSpearPlus(self): + def test_bronze_spear_plus(self): locations = ["Megataur"] items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] self.assertAccessDependency(locations, items) diff --git a/worlds/soe/test/test_goal.py b/worlds/soe/test/test_goal.py index d127d38998..885c2a74ef 100644 --- a/worlds/soe/test/test_goal.py +++ b/worlds/soe/test/test_goal.py @@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase): "required_fragments": 20, } - def testFragments(self): + def test_fragments(self): self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) # 0 fragments fragments = self.get_items_by_name("Energy Core Fragment") @@ -24,11 +24,11 @@ class TestFragmentGoal(SoETestBase): self.assertEqual(self.count("Energy Core Fragment"), 21) self.assertBeatable(True) - def testNoWeapon(self): + def test_no_weapon(self): self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) self.assertBeatable(False) - def testNoRocket(self): + def test_no_rocket(self): self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) self.assertBeatable(False) @@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase): "energy_core": "shuffle", } - def testCore(self): + def test_core(self): self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) self.collect_by_name(["Energy Core"]) self.assertBeatable(True) - def testNoWeapon(self): + def test_no_weapon(self): self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) self.assertBeatable(False) - def testNoRocket(self): + def test_no_rocket(self): self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) self.assertBeatable(False) diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py index 27e00cd3e7..969e93d4f6 100644 --- a/worlds/soe/test/test_oob.py +++ b/worlds/soe/test/test_oob.py @@ -6,7 +6,7 @@ class OoBTest(SoETestBase): """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} - def testOoBAccess(self): + def test_oob_access(self): in_logic = self.options["out_of_bounds"] == "logic" # some locations that just need a weapon + OoB @@ -37,7 +37,7 @@ class OoBTest(SoETestBase): self.collect_by_name("Diamond Eye") self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) - def testOoBGoal(self): + def test_oob_goal(self): # still need Energy Core with OoB if sequence breaks are not in logic for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: self.collect_by_name(item) diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py index 4248f9b47d..8a7f9c64ed 100644 --- a/worlds/soe/test/test_sequence_breaks.py +++ b/worlds/soe/test/test_sequence_breaks.py @@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase): """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} - def testSequenceBreaksAccess(self): + def test_sequence_breaks_access(self): in_logic = self.options["sequence_breaks"] == "logic" # some locations that just need any weapon + sequence break @@ -30,7 +30,7 @@ class SequenceBreaksTest(SoETestBase): self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead self.assertEqual(self.can_reach_location("Escape"), in_logic) - def testSequenceBreaksGoal(self): + def test_sequence_breaks_goal(self): in_logic = self.options["sequence_breaks"] == "logic" # don't need Energy Core with sequence breaks in logic diff --git a/worlds/soe/test/test_traps.py b/worlds/soe/test/test_traps.py new file mode 100644 index 0000000000..f83a37be82 --- /dev/null +++ b/worlds/soe/test/test_traps.py @@ -0,0 +1,55 @@ +import typing +from dataclasses import fields + +from . import SoETestBase +from ..options import SoEOptions + +if typing.TYPE_CHECKING: + from .. import SoEWorld + + +class Bases: + # class in class to avoid running tests for TrapTest class + class TrapTestBase(SoETestBase): + """Test base for trap tests""" + option_name_to_item_name = { + # filtering by name here validates that there is no confusion between name and type + field.name: field.type.item_name for field in fields(SoEOptions) if field.name.startswith("trap_chance_") + } + + def test_dataclass(self) -> None: + """Test that the dataclass helper property returns the expected sequence""" + self.assertGreater(len(self.option_name_to_item_name), 0, "Expected more than 0 trap types") + world: "SoEWorld" = typing.cast("SoEWorld", self.multiworld.worlds[1]) + item_name_to_rolled_option = {option.item_name: option for option in world.options.trap_chances} + # compare that all fields are present - that is property in dataclass and selector code in test line up + self.assertEqual(sorted(self.option_name_to_item_name.values()), sorted(item_name_to_rolled_option), + "field names probably do not match field types") + # sanity check that chances are correctly set and returned by property + for option_name, item_name in self.option_name_to_item_name.items(): + self.assertEqual(item_name_to_rolled_option[item_name].value, + self.options.get(option_name, item_name_to_rolled_option[item_name].default)) + + def test_trap_count(self) -> None: + """Test that total trap count is correct""" + self.assertEqual(self.options["trap_count"], len(self.get_items_by_name(self.option_name_to_item_name.values()))) + + +class TestTrapAllZeroChance(Bases.TrapTestBase): + """Tests all zero chances still gives traps if trap_count is set.""" + options: typing.Dict[str, typing.Any] = { + "trap_count": 1, + **{name: 0 for name in Bases.TrapTestBase.option_name_to_item_name} + } + + +class TestTrapNoConfound(Bases.TrapTestBase): + """Tests that one zero chance does not give that trap.""" + options: typing.Dict[str, typing.Any] = { + "trap_count": 99, + "trap_chance_confound": 0, + } + + def test_no_confound_trap(self) -> None: + self.assertEqual(self.option_name_to_item_name["trap_chance_confound"], "Confound Trap") + self.assertEqual(len(self.get_items_by_name("Confound Trap")), 0) From b241644e54d23acd1fbe53c382d4c9851e390711 Mon Sep 17 00:00:00 2001 From: Ame Date: Fri, 12 Jan 2024 20:26:50 +0100 Subject: [PATCH 16/78] Docs: add FR guide for DLCQuest (#2699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: add Translate FR guide for DLCQuest * Add Translate * fix * Update worlds/dlcquest/docs/fr_DLCQuest.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Fix Translate * Fix translate * Update __init__.py * Update worlds/dlcquest/__init__.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/dlcquest/__init__.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/dlcquest/__init__.py | 13 +++++-- worlds/dlcquest/docs/fr_DLCQuest.md | 49 +++++++++++++++++++++++++ worlds/dlcquest/docs/setup_fr.md | 55 +++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 worlds/dlcquest/docs/fr_DLCQuest.md create mode 100644 worlds/dlcquest/docs/setup_fr.md diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index c22b7cd984..ca7a0157cb 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -13,14 +13,23 @@ client_version = 0 class DLCqwebworld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Tutorial", "A guide to setting up the Archipelago DLCQuest game on your computer.", "English", "setup_en.md", "setup/en", ["axe_y"] - )] + ) + setup_fr = Tutorial( + "Guide de configuration MultiWorld", + "Un guide pour configurer DLCQuest sur votre PC.", + "Français", + "setup_fr.md", + "setup/fr", + ["Deoxis"] + ) + tutorials = [setup_en, setup_fr] class DLCqworld(World): diff --git a/worlds/dlcquest/docs/fr_DLCQuest.md b/worlds/dlcquest/docs/fr_DLCQuest.md new file mode 100644 index 0000000000..95a8048dfe --- /dev/null +++ b/worlds/dlcquest/docs/fr_DLCQuest.md @@ -0,0 +1,49 @@ +# DLC Quest + +## Où se trouve la page des paramètres ? + +La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier. + + +## Quel est l'effet de la randomisation sur ce jeu ? + +Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest. + +## Quel est le but de DLC Quest ? + +DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie. +Il peut également choisir de faire les deux campagnes. + + +## Quels sont les emplacements dans DLC quest ? + +Les emplacements dans DLC Quest comprennent toujours +- les achats de DLC auprès du commerçant +- Les objectifs liés aux récompenses + - Tuer des moutons dans DLC Quest + - Objectifs spécifiques de l'attribution dans Live Freemium or Die + +Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection : +- Objets que votre personnage peut obtenir de différentes manières + - Swords + - Gun + - Box of Various Supplies + - Humble Indie Bindle + - Pickaxe +- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés + +## Quels objets peuvent se trouver dans le monde d'un autre joueur ? + +Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock + +Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille. +- Zombie Sheep +- Loading Screens +- Temporary Spikes + +## Que se passe-t-il lorsque le joueur reçoit un objet ? + +Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur. +Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception. + +Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion. \ No newline at end of file diff --git a/worlds/dlcquest/docs/setup_fr.md b/worlds/dlcquest/docs/setup_fr.md new file mode 100644 index 0000000000..78c69eb5a7 --- /dev/null +++ b/worlds/dlcquest/docs/setup_fr.md @@ -0,0 +1,55 @@ +# # Guide de configuration MultiWorld de DLCQuest + +## Logiciels requis + +- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/)) +- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases) +- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète) + +## Logiciels optionnels +- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases) + - (Uniquement pour le TextClient) + +## Créer un fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Où puis-je obtenir un fichier YAML ? + +Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings). + +## Rejoindre une partie multi-monde + +### Installer le mod + +- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous + + +- Extraire l'archive .zip à l'emplacement de votre choix + + +- Exécutez "DLCQuestipelagoInstaller.exe". + +![image](https://i.imgur.com/2sPhMgs.png) +- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire. + - Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut + - Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès. + - Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé. + +### Se connecter au MultiServer + +- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés. + +- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable. +![image](https://i.imgur.com/ZUiFrhf.png) + +- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes. +- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez. + +### Interagir avec le MultiWorld depuis le jeu + +Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte. +Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés. +Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes. \ No newline at end of file From 3933fd39293ada9b02abc9f5a6b5618fe0c61a8f Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:32:15 -0500 Subject: [PATCH 17/78] TUNIC: Implement New Game (#2172) --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/tunic/__init__.py | 279 +++++++++ worlds/tunic/docs/en_Tunic.md | 59 ++ worlds/tunic/docs/setup_en.md | 67 ++ worlds/tunic/er_data.py | 1001 ++++++++++++++++++++++++++++++ worlds/tunic/er_rules.py | 984 +++++++++++++++++++++++++++++ worlds/tunic/er_scripts.py | 453 ++++++++++++++ worlds/tunic/items.py | 214 +++++++ worlds/tunic/locations.py | 337 ++++++++++ worlds/tunic/options.py | 147 +++++ worlds/tunic/regions.py | 25 + worlds/tunic/rules.py | 345 ++++++++++ worlds/tunic/test/__init__.py | 6 + worlds/tunic/test/test_access.py | 70 +++ 15 files changed, 3991 insertions(+) create mode 100644 worlds/tunic/__init__.py create mode 100644 worlds/tunic/docs/en_Tunic.md create mode 100644 worlds/tunic/docs/setup_en.md create mode 100644 worlds/tunic/er_data.py create mode 100644 worlds/tunic/er_rules.py create mode 100644 worlds/tunic/er_scripts.py create mode 100644 worlds/tunic/items.py create mode 100644 worlds/tunic/locations.py create mode 100644 worlds/tunic/options.py create mode 100644 worlds/tunic/regions.py create mode 100644 worlds/tunic/rules.py create mode 100644 worlds/tunic/test/__init__.py create mode 100644 worlds/tunic/test/test_access.py diff --git a/README.md b/README.md index a1e03293d5..ce2130ce8e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Currently, the following games are supported: * Heretic * Landstalker: The Treasures of King Nole * Final Fantasy Mystic Quest +* TUNIC For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index e221371b24..95c0baea3a 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -164,6 +164,9 @@ # The Legend of Zelda (1) /worlds/tloz/ @Rosalie-A @t3hf1gm3nt +# TUNIC +/worlds/tunic/ @silent-destroyer + # Undertale /worlds/undertale/ @jonloveslegos diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py new file mode 100644 index 0000000000..b946ea8e30 --- /dev/null +++ b/worlds/tunic/__init__.py @@ -0,0 +1,279 @@ +from typing import Dict, List, Any + +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification +from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names +from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations +from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon +from .er_rules import set_er_location_rules +from .regions import tunic_regions +from .er_scripts import create_er_regions +from .options import TunicOptions +from worlds.AutoWorld import WebWorld, World +from decimal import Decimal, ROUND_HALF_UP + + +class TunicWeb(WebWorld): + tutorials = [ + Tutorial( + tutorial_name="Multiworld Setup Guide", + description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.", + language="English", + file_name="setup_en.md", + link="setup/en", + authors=["SilentDestroyer"] + ) + ] + theme = "grassFlowers" + game = "Tunic" + + +class TunicItem(Item): + game: str = "Tunic" + + +class TunicLocation(Location): + game: str = "Tunic" + + +class TunicWorld(World): + """ + Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game + about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will + confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox! + """ + game = "Tunic" + web = TunicWeb() + + data_version = 2 + options: TunicOptions + options_dataclass = TunicOptions + item_name_groups = item_name_groups + location_name_groups = location_name_groups + + item_name_to_id = item_name_to_id + location_name_to_id = location_name_to_id + + ability_unlocks: Dict[str, int] + slot_data_items: List[TunicItem] + tunic_portal_pairs: Dict[str, str] + er_portal_hints: Dict[int, str] + + def generate_early(self) -> None: + if self.options.start_with_sword and "Sword" not in self.options.start_inventory: + self.options.start_inventory.value["Sword"] = 1 + + def create_item(self, name: str) -> TunicItem: + item_data = item_table[name] + return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + + def create_items(self) -> None: + keys_behind_bosses = self.options.keys_behind_bosses + hexagon_quest = self.options.hexagon_quest + sword_progression = self.options.sword_progression + + tunic_items: List[TunicItem] = [] + self.slot_data_items = [] + + items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} + + for money_fool in fool_tiers[self.options.fool_traps]: + items_to_create["Fool Trap"] += items_to_create[money_fool] + items_to_create[money_fool] = 0 + + if sword_progression: + items_to_create["Stick"] = 0 + items_to_create["Sword"] = 0 + else: + items_to_create["Sword Upgrade"] = 0 + + if self.options.laurels_location: + laurels = self.create_item("Hero's Laurels") + if self.options.laurels_location == "6_coins": + self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) + elif self.options.laurels_location == "10_coins": + self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) + elif self.options.laurels_location == "10_fairies": + self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) + self.slot_data_items.append(laurels) + items_to_create["Hero's Laurels"] = 0 + + if keys_behind_bosses: + for rgb_hexagon, location in hexagon_locations.items(): + hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon) + self.multiworld.get_location(location, self.player).place_locked_item(hex_item) + self.slot_data_items.append(hex_item) + items_to_create[rgb_hexagon] = 0 + items_to_create[gold_hexagon] -= 3 + + if hexagon_quest: + # Calculate number of hexagons in item pool + hexagon_goal = self.options.hexagon_goal + extra_hexagons = self.options.extra_hexagon_percentage + items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) + + # Replace pages and normal hexagons with filler + for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): + items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item] + items_to_create[replaced_item] = 0 + + # Filler items that are still in the item pool to swap out + available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and + item_table[filler].classification == ItemClassification.filler] + + # Remove filler to make room for extra hexagons + for i in range(0, items_to_create[gold_hexagon]): + fill = self.random.choice(available_filler) + items_to_create[fill] -= 1 + if items_to_create[fill] == 0: + available_filler.remove(fill) + + if self.options.maskless: + mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) + tunic_items.append(mask_item) + items_to_create["Scavenger Mask"] = 0 + + if self.options.lanternless: + mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) + tunic_items.append(mask_item) + items_to_create["Lantern"] = 0 + + for item, quantity in items_to_create.items(): + for i in range(0, quantity): + tunic_item: TunicItem = self.create_item(item) + if item in slot_data_item_names: + self.slot_data_items.append(tunic_item) + tunic_items.append(tunic_item) + + self.multiworld.itempool += tunic_items + + def create_regions(self) -> None: + self.tunic_portal_pairs = {} + self.er_portal_hints = {} + self.ability_unlocks = randomize_ability_unlocks(self.random, self.options) + if self.options.entrance_rando: + portal_pairs, portal_hints = create_er_regions(self) + for portal1, portal2 in portal_pairs.items(): + self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() + self.er_portal_hints = portal_hints + + else: + for region_name in tunic_regions: + region = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(region) + + for region_name, exits in tunic_regions.items(): + region = self.multiworld.get_region(region_name, self.player) + region.add_exits(exits) + + for location_name, location_id in self.location_name_to_id.items(): + region = self.multiworld.get_region(location_table[location_name].region, self.player) + location = TunicLocation(self.player, location_name, location_id, region) + region.locations.append(location) + + victory_region = self.multiworld.get_region("Spirit Arena", self.player) + victory_location = TunicLocation(self.player, "The Heir", None, victory_region) + victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + victory_region.locations.append(victory_location) + + def set_rules(self) -> None: + if self.options.entrance_rando: + set_er_location_rules(self, self.ability_unlocks) + else: + set_region_rules(self, self.ability_unlocks) + set_location_rules(self, self.ability_unlocks) + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_items) + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.options.entrance_rando: + hint_data[self.player] = self.er_portal_hints + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "seed": self.random.randint(0, 2147483647), + "start_with_sword": self.options.start_with_sword.value, + "keys_behind_bosses": self.options.keys_behind_bosses.value, + "sword_progression": self.options.sword_progression.value, + "ability_shuffling": self.options.ability_shuffling.value, + "hexagon_quest": self.options.hexagon_quest.value, + "fool_traps": self.options.fool_traps.value, + "entrance_rando": self.options.entrance_rando.value, + "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], + "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], + "Hexagon Quest Ice Rod": self.ability_unlocks["Pages 52-53 (Ice Rod)"], + "Hexagon Quest Goal": self.options.hexagon_goal.value, + "Entrance Rando": self.tunic_portal_pairs + } + + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): + if tunic_item.name not in slot_data: + slot_data[tunic_item.name] = [] + if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: + continue + slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + + for start_item in self.options.start_inventory_from_pool: + if start_item in slot_data_item_names: + if start_item not in slot_data: + slot_data[start_item] = [] + for i in range(0, self.options.start_inventory_from_pool[start_item]): + slot_data[start_item].extend(["Your Pocket", self.player]) + + for plando_item in self.multiworld.plando_items[self.player]: + if plando_item["from_pool"]: + items_to_find = set() + for item_type in [key for key in ["item", "items"] if key in plando_item]: + for item in plando_item[item_type]: + items_to_find.add(item) + for item in items_to_find: + if item in slot_data_item_names: + slot_data[item] = [] + for item_location in self.multiworld.find_item_locations(item, self.player): + slot_data[item].extend([item_location.name, item_location.player]) + + return slot_data + + # for the universal tracker, doesn't get called in standard gen + def interpret_slot_data(self, slot_data: Dict[str, Any]) -> None: + # bypassing random yaml settings + self.options.start_with_sword.value = slot_data["start_with_sword"] + self.options.keys_behind_bosses.value = slot_data["keys_behind_bosses"] + self.options.sword_progression.value = slot_data["sword_progression"] + self.options.ability_shuffling.value = slot_data["ability_shuffling"] + self.options.hexagon_quest.value = slot_data["hexagon_quest"] + self.ability_unlocks["Pages 24-25 (Prayer)"] = slot_data["Hexagon Quest Prayer"] + self.ability_unlocks["Pages 42-43 (Holy Cross)"] = slot_data["Hexagon Quest Holy Cross"] + self.ability_unlocks["Pages 52-53 (Ice Rod)"] = slot_data["Hexagon Quest Ice Rod"] + + # swapping entrances around so the mapping matches what was generated + if slot_data["entrance_rando"]: + from BaseClasses import Entrance + from .er_data import portal_mapping + entrance_dict: Dict[str, Entrance] = {entrance.name: entrance + for region in self.multiworld.get_regions(self.player) + for entrance in region.entrances} + slot_portals: Dict[str, str] = slot_data["Entrance Rando"] + for portal1, portal2 in slot_portals.items(): + portal_name1: str = "" + portal_name2: str = "" + entrance1 = None + entrance2 = None + for portal in portal_mapping: + if portal.scene_destination() == portal1: + portal_name1 = portal.name + if portal.scene_destination() == portal2: + portal_name2 = portal.name + + for entrance_name, entrance in entrance_dict.items(): + if entrance_name.startswith(portal_name1): + entrance1 = entrance + if entrance_name.startswith(portal_name2): + entrance2 = entrance + if entrance1 is None: + raise Exception("entrance1 not found, portal1 is " + portal1) + if entrance2 is None: + raise Exception("entrance2 not found, portal2 is " + portal2) + entrance1.connected_region = entrance2.parent_region + entrance2.connected_region = entrance1.parent_region diff --git a/worlds/tunic/docs/en_Tunic.md b/worlds/tunic/docs/en_Tunic.md new file mode 100644 index 0000000000..82569195ca --- /dev/null +++ b/worlds/tunic/docs/en_Tunic.md @@ -0,0 +1,59 @@ +# TUNIC + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics, +and other unique items are shuffled.
+ +Ability shuffling is an option available from the settings page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), +preventing them from being used until they are unlocked.
+ +Enemy randomization and other options are also available and can be turned on in the client mod. + +## What is the goal of TUNIC when randomized? +The standard goal is the same as the vanilla game, which is to find the three hexagon keys, at which point you may either Take Your +Rightful Place or seek another path and Share Your Wisdom. + +Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal +being to find the required amount of them and then Share Your Wisdom. + +## What items from TUNIC can appear in another player's world? +Every item has a chance to appear in another player's world. + +## How many checks are in TUNIC? +There are 302 checks located across the world of TUNIC. + +## What do items from other worlds look like in TUNIC? +Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a +chest with the original chest texture for that item. + +Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with +a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression. + +## Is there a tracker pack? +There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty. + +There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon. + +## What should I know regarding logic? +- Nighttime is not considered in logic. Every check in the game is obtainable during the day. +- The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. +- The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. + +For Entrance Rando specifically: +- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. +- The West Garden fuse can be activated from below. +- You can pray at the tree at the exterior of the Library. +- The elevators in the Rooted Ziggurat only go down. +- The portal in the trophy room of the Old House is active from the start. +- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing. + +## What item groups are there? +Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, ice rod, and progressive sword. + +## What location groups are there? +Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group. diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md new file mode 100644 index 0000000000..087509240f --- /dev/null +++ b/worlds/tunic/docs/setup_en.md @@ -0,0 +1,67 @@ +# TUNIC Setup Guide + +## Installation + +### Required Software + +- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported) +- [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip) +- [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest) + +### Optional Software +- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest) (For use with EmoTracker/PopTracker) +- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest) + +### Find Your Relevant Game Directories + +Find your TUNIC game installation directory: + +- **Steam**: Right click TUNIC in your Steam Library, then *Manage → Browse local files*.
+ - **Steam Deck**: Hold down the power button, tap "Switch to Desktop", then launch Steam from Desktop Mode to access the above option. +- **PC Game Pass**: In the Xbox PC app, go to the TUNIC game page from your library, click the [...] button next to "Play", then +*Manage → Files → Browse...*
+- **Other platforms**: Follow a similar pattern of steps as above to locate your specific game directory. + +### Install BepInEx + +BepInEx is a general purpose framework for modding Unity games, and is used by the TUNIC Randomizer. + +Download [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip). + +If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html). + +Extract the contents of the BepInEx .zip file into your TUNIC game directory:
+- **Steam**: Steam\steamapps\common\TUNIC
+- **PC Game Pass**: XboxGames\Tunic\Content
+- **Other platforms**: Place into the same folder that the Tunic_Data/Secret Legend_Data folder is found. + +Launch the game once and close it to finish the BepInEx installation. + +### Install The TUNIC Randomizer Archipelago Client Mod + +Download the latest release of the [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest). + +The downloaded .zip will contain a folder called `Tunic Archipelago`. + +Copy the `Tunic Archipelago` folder into `BepInEx/plugins` in your TUNIC game installation directory. +The filepath to the mod should look like `BepInEx/plugins/Tunic Archipelago/TunicArchipelago.dll`
+ +Launch the game, and if everything was installed correctly you should see `Randomizer + Archipelago Mod Ver. x.y.z` in the top left corner of the title screen! + +## Configure Archipelago Settings + +### Configure Your YAML File + +Visit the [TUNIC settings page](/games/Tunic/player-settings) to generate a YAML with your selected settings. + +### Configure Your Mod Settings +Launch the game and click the button labeled `Open Settings File` on the Title Screen. +This will open the settings file in your default text editor, allowing you to edit your connection info. +At the top of the file, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room. +The rest of the settings that appear in this file can be changed in the `Randomizer Settings` submenu of the in-game options menu. + +Once your player settings have been saved, press `Connect`. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`. + +An error message will display if the game fails to connect to the server. + +Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization! diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py new file mode 100644 index 0000000000..2d3bcc025f --- /dev/null +++ b/worlds/tunic/er_data.py @@ -0,0 +1,1001 @@ +from typing import Dict, NamedTuple, List, Tuple +from enum import IntEnum + + +class Portal(NamedTuple): + name: str # human-readable name + region: str # AP region + destination: str # vanilla destination scene and tag + + def scene(self) -> str: # the actual scene name in Tunic + return tunic_er_regions[self.region].game_scene + + def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod + return self.scene() + ", " + self.destination + + +portal_mapping: List[Portal] = [ + Portal(name="Stick House Entrance", region="Overworld", + destination="Sword Cave_"), + Portal(name="Windmill Entrance", region="Overworld", + destination="Windmill_"), + Portal(name="Well Ladder Entrance", region="Overworld", + destination="Sewer_entrance"), + Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail", + destination="Sewer_west_aqueduct"), + Portal(name="Old House Door Entrance", region="Overworld Old House Door", + destination="Overworld Interiors_house"), + Portal(name="Old House Waterfall Entrance", region="Overworld", + destination="Overworld Interiors_under_checkpoint"), + Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail", + destination="Furnace_gyro_upper_north"), + Portal(name="Entrance to Furnace under Windmill", region="Overworld", + destination="Furnace_gyro_upper_east"), + Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace", + destination="Furnace_gyro_west"), + Portal(name="Entrance to Furnace from Beach", region="Overworld", + destination="Furnace_gyro_lower"), + Portal(name="Caustic Light Cave Entrance", region="Overworld", + destination="Overworld Cave_"), + Portal(name="Swamp Upper Entrance", region="Overworld Laurels", + destination="Swamp Redux 2_wall"), + Portal(name="Swamp Lower Entrance", region="Overworld", + destination="Swamp Redux 2_conduit"), + Portal(name="Ruined Passage Not-Door Entrance", region="Overworld", + destination="Ruins Passage_east"), + Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door", + destination="Ruins Passage_west"), + Portal(name="Atoll Upper Entrance", region="Overworld", + destination="Atoll Redux_upper"), + Portal(name="Atoll Lower Entrance", region="Overworld", + destination="Atoll Redux_lower"), + Portal(name="Special Shop Entrance", region="Overworld Laurels", + destination="ShopSpecial_"), + Portal(name="Maze Cave Entrance", region="Overworld", + destination="Maze Room_"), + Portal(name="West Garden Entrance near Belltower", region="Overworld Belltower", + destination="Archipelagos Redux_upper"), + Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", + destination="Archipelagos Redux_lower"), + Portal(name="West Garden Laurels Entrance", region="Overworld Laurels", + destination="Archipelagos Redux_lowest"), + Portal(name="Temple Door Entrance", region="Overworld Temple Door", + destination="Temple_main"), + Portal(name="Temple Rafters Entrance", region="Overworld", + destination="Temple_rafters"), + Portal(name="Ruined Shop Entrance", region="Overworld", + destination="Ruined Shop_"), + Portal(name="Patrol Cave Entrance", region="Overworld", + destination="PatrolCave_"), + Portal(name="Hourglass Cave Entrance", region="Overworld", + destination="Town Basement_beach"), + Portal(name="Changing Room Entrance", region="Overworld", + destination="Changing Room_"), + Portal(name="Cube Cave Entrance", region="Overworld", + destination="CubeRoom_"), + Portal(name="Stairs from Overworld to Mountain", region="Overworld", + destination="Mountain_"), + Portal(name="Overworld to Fortress", region="Overworld", + destination="Fortress Courtyard_"), + Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door", + destination="Town_FiligreeRoom_"), + Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door", + destination="EastFiligreeCache_"), + Portal(name="Overworld to Quarry Connector", region="Overworld", + destination="Darkwoods Tunnel_"), + Portal(name="Dark Tomb Main Entrance", region="Overworld", + destination="Crypt Redux_"), + Portal(name="Overworld to Forest Belltower", region="Overworld", + destination="Forest Belltower_"), + Portal(name="Town to Far Shore", region="Overworld Town Portal", + destination="Transit_teleporter_town"), + Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal", + destination="Transit_teleporter_starting island"), + Portal(name="Secret Gathering Place Entrance", region="Overworld", + destination="Waterfall_"), + + Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place", + destination="Overworld Redux_"), + + Portal(name="Windmill Exit", region="Windmill", + destination="Overworld Redux_"), + Portal(name="Windmill Shop", region="Windmill", + destination="Shop_"), + + Portal(name="Old House Door Exit", region="Old House Front", + destination="Overworld Redux_house"), + Portal(name="Old House to Glyph Tower", region="Old House Front", + destination="g_elements_"), + Portal(name="Old House Waterfall Exit", region="Old House Back", + destination="Overworld Redux_under_checkpoint"), + + Portal(name="Glyph Tower Exit", region="Relic Tower", + destination="Overworld Interiors_"), + + Portal(name="Changing Room Exit", region="Changing Room", + destination="Overworld Redux_"), + + Portal(name="Fountain HC Room Exit", region="Fountain Cross Room", + destination="Overworld Redux_"), + + Portal(name="Cube Cave Exit", region="Cube Cave", + destination="Overworld Redux_"), + + Portal(name="Guard Patrol Cave Exit", region="Patrol Cave", + destination="Overworld Redux_"), + + Portal(name="Ruined Shop Exit", region="Ruined Shop", + destination="Overworld Redux_"), + + Portal(name="Furnace Exit towards Well", region="Furnace Fuse", + destination="Overworld Redux_gyro_upper_north"), + Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path", + destination="Crypt Redux_"), + Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path", + destination="Overworld Redux_gyro_west"), + Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_lower"), + Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_upper_east"), + + Portal(name="Stick House Exit", region="Stick House", + destination="Overworld Redux_"), + + Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage", + destination="Overworld Redux_east"), + Portal(name="Ruined Passage Door Exit", region="Ruined Passage", + destination="Overworld Redux_west"), + + Portal(name="Southeast HC Room Exit", region="Southeast Cross Room", + destination="Overworld Redux_"), + + Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave", + destination="Overworld Redux_"), + + Portal(name="Maze Cave Exit", region="Maze Cave", + destination="Overworld Redux_"), + + Portal(name="Hourglass Cave Exit", region="Hourglass Cave", + destination="Overworld Redux_beach"), + + Portal(name="Special Shop Exit", region="Special Shop", + destination="Overworld Redux_"), + + Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters", + destination="Overworld Redux_rafters"), + Portal(name="Temple Door Exit", region="Sealed Temple", + destination="Overworld Redux_main"), + + Portal(name="Well Ladder Exit", region="Beneath the Well Front", + destination="Overworld Redux_entrance"), + Portal(name="Well to Well Boss", region="Beneath the Well Back", + destination="Sewer_Boss_"), + Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", + destination="Overworld Redux_west_aqueduct"), + + Portal(name="Well Boss to Well", region="Well Boss", + destination="Sewer_"), + Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", + destination="Crypt Redux_"), + + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", + destination="Overworld Redux_"), + Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", + destination="Furnace_"), + Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", + destination="Sewer_Boss_"), + + Portal(name="West Garden Exit near Hero's Grave", region="West Garden", + destination="Overworld Redux_lower"), + Portal(name="West Garden to Magic Dagger House", region="West Garden", + destination="archipelagos_house_"), + Portal(name="West Garden Exit after Boss", region="West Garden after Boss", + destination="Overworld Redux_upper"), + Portal(name="West Garden Shop", region="West Garden", + destination="Shop_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit", + destination="Overworld Redux_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="West Garden to Far Shore", region="West Garden Portal", + destination="Transit_teleporter_archipelagos_teleporter"), + + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", + destination="Archipelagos Redux_"), + + Portal(name="Atoll Upper Exit", region="Ruined Atoll", + destination="Overworld Redux_upper"), + Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", + destination="Overworld Redux_lower"), + Portal(name="Atoll Shop", region="Ruined Atoll", + destination="Shop_"), + Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", + destination="Transit_teleporter_atoll"), + Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Portal", + destination="Library Exterior_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll", + destination="Frog Stairs_eye"), + Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", + destination="Frog Stairs_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog's Domain Entry", + destination="Atoll Redux_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog's Domain Entry", + destination="Atoll Redux_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog's Domain Entry", + destination="frog cave main_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog's Domain Entry", + destination="frog cave main_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain", + destination="Frog Stairs_Entrance"), + Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", + destination="Frog Stairs_Exit"), + + Portal(name="Library Exterior Tree", region="Library Exterior Tree", + destination="Atoll Redux_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder", + destination="Library Hall_"), + + Portal(name="Library Hall Bookshelf Exit", region="Library Hall", + destination="Library Exterior_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall", + destination="Library Rotunda_"), + + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda", + destination="Library Hall_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda", + destination="Library Lab_"), + + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", + destination="Library Rotunda_"), + Portal(name="Library to Far Shore", region="Library Portal", + destination="Transit_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab", + destination="Library Arena_"), + + Portal(name="Librarian Arena Exit", region="Library Arena", + destination="Library Lab_"), + + Portal(name="Forest to Belltower", region="East Forest", + destination="Forest Belltower_"), + Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", + destination="East Forest Redux Laddercave_lower"), + Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", + destination="East Forest Redux Laddercave_gate"), + Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", + destination="East Forest Redux Laddercave_upper"), + Portal(name="Forest to Far Shore", region="East Forest Portal", + destination="Transit_teleporter_forest teleporter"), + Portal(name="Forest Guard House 2 Lower Entrance", region="East Forest", + destination="East Forest Redux Interior_lower"), + Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", + destination="East Forest Redux Interior_upper"), + Portal(name="Forest Grave Path Lower Entrance", region="East Forest", + destination="Sword Access_lower"), + Portal(name="Forest Grave Path Upper Entrance", region="East Forest", + destination="Sword Access_upper"), + + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", + destination="East Forest Redux_upper"), + Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", + destination="East Forest Redux_lower"), + Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", + destination="East Forest Redux_gate"), + Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", + destination="Forest Boss Room_"), + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Guard House 2 Lower Exit", region="Guard House 2", + destination="East Forest Redux_lower"), + Portal(name="Guard House 2 Upper Exit", region="Guard House 2", + destination="East Forest Redux_upper"), + + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", + destination="East Forest Redux Laddercave_"), + Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", + destination="Forest Belltower_"), + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + destination="Overworld Redux_"), + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room_"), + + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", + destination="Fortress Reliquary_Lower"), + Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", + destination="Fortress Reliquary_Upper"), + Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", + destination="Fortress Main_Big Door"), + Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", + destination="Fortress East_"), + Portal(name="Fortress Courtyard to Beneath the Earth", region="Fortress Exterior near cave", + destination="Fortress Basement_"), + Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", + destination="Forest Belltower_"), + Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", + destination="Overworld Redux_"), + Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", + destination="Shop_"), + + Portal(name="Beneath the Earth to Fortress Interior", region="Beneath the Vault Back", + destination="Fortress Main_"), + Portal(name="Beneath the Earth to Fortress Courtyard", region="Beneath the Vault Front", + destination="Fortress Courtyard_"), + + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", + destination="Fortress Courtyard_Big Door"), + Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", + destination="Fortress Basement_"), + Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", + destination="Fortress Arena_"), + Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", + destination="Shop_"), + Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", + destination="Fortress East_upper"), + Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", + destination="Fortress East_lower"), + + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", + destination="Fortress Main_lower"), + Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", + destination="Fortress Courtyard_"), + Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", + destination="Fortress Main_upper"), + + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", + destination="Fortress Courtyard_Lower"), + Portal(name="Fortress Hero's Grave", region="Fortress Grave Path", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", + destination="Fortress Courtyard_Upper"), + Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance", + destination="Dusty_"), + + Portal(name="Dusty Exit", region="Fortress Leaf Piles", + destination="Fortress Reliquary_"), + + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", + destination="Fortress Main_"), + Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", + destination="Transit_teleporter_spidertank"), + + Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", + destination="Mountaintop_"), + Portal(name="Mountain to Quarry", region="Lower Mountain", + destination="Quarry Redux_"), + Portal(name="Mountain to Overworld", region="Lower Mountain", + destination="Overworld Redux_"), + + Portal(name="Top of the Mountain Exit", region="Top of the Mountain", + destination="Mountain_"), + + Portal(name="Quarry Connector to Overworld", region="Quarry Connector", + destination="Overworld Redux_"), + Portal(name="Quarry Connector to Quarry", region="Quarry Connector", + destination="Quarry Redux_"), + + Portal(name="Quarry to Overworld Exit", region="Quarry Entry", + destination="Darkwoods Tunnel_"), + Portal(name="Quarry Shop", region="Quarry Entry", + destination="Shop_"), + Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry", + destination="Monastery_front"), + Portal(name="Quarry to Monastery Back", region="Monastery Rope", + destination="Monastery_back"), + Portal(name="Quarry to Mountain", region="Quarry Back", + destination="Mountain_"), + Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door", + destination="ziggurat2020_0_"), + Portal(name="Quarry to Far Shore", region="Quarry Portal", + destination="Transit_teleporter_quarry teleporter"), + + Portal(name="Monastery Rear Exit", region="Monastery Back", + destination="Quarry Redux_back"), + Portal(name="Monastery Front Exit", region="Monastery Front", + destination="Quarry Redux_front"), + Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry", + destination="Quarry Redux_"), + + Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry", + destination="ziggurat2020_0_"), + Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back", + destination="ziggurat2020_2_"), + + Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", + destination="ziggurat2020_3_"), + + Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front", + destination="ziggurat2020_2_"), + Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", + destination="ziggurat2020_FTRoom_"), + + Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", + destination="ziggurat2020_3_"), + Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal", + destination="Transit_teleporter_ziggurat teleporter"), + + Portal(name="Swamp Lower Exit", region="Swamp", + destination="Overworld Redux_conduit"), + Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance", + destination="Cathedral Redux_main"), + Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room", + destination="Cathedral Redux_secret"), + Portal(name="Swamp to Gauntlet", region="Back of Swamp", + destination="Cathedral Arena_"), + Portal(name="Swamp Shop", region="Swamp", + destination="Shop_"), + Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area", + destination="Overworld Redux_wall"), + Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Cathedral Main Exit", region="Cathedral", + destination="Swamp Redux 2_main"), + Portal(name="Cathedral Elevator", region="Cathedral", + destination="Cathedral Arena_"), + Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", + destination="Swamp Redux 2_secret"), + + Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit", + destination="Swamp Redux 2_"), + Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint", + destination="Cathedral Redux_"), + Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint", + destination="Shop_"), + + Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress", + destination="Fortress Reliquary_teleporter_relic plinth"), + Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry", + destination="Monastery_teleporter_relic plinth"), + Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden", + destination="Archipelagos Redux_teleporter_relic plinth"), + Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest", + destination="Sword Access_teleporter_relic plinth"), + Portal(name="Hero's Grave to Library", region="Hero Relic - Library", + destination="Library Hall_teleporter_relic plinth"), + Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp", + destination="Swamp Redux 2_teleporter_relic plinth"), + + Portal(name="Far Shore to West Garden", region="Far Shore to West Garden", + destination="Archipelagos Redux_teleporter_archipelagos_teleporter"), + Portal(name="Far Shore to Library", region="Far Shore to Library", + destination="Library Lab_teleporter_library teleporter"), + Portal(name="Far Shore to Quarry", region="Far Shore to Quarry", + destination="Quarry Redux_teleporter_quarry teleporter"), + Portal(name="Far Shore to East Forest", region="Far Shore to East Forest", + destination="East Forest Redux_teleporter_forest teleporter"), + Portal(name="Far Shore to Fortress", region="Far Shore to Fortress", + destination="Fortress Arena_teleporter_spidertank"), + Portal(name="Far Shore to Atoll", region="Far Shore", + destination="Atoll Redux_teleporter_atoll"), + Portal(name="Far Shore to Ziggurat", region="Far Shore", + destination="ziggurat2020_FTRoom_teleporter_ziggurat teleporter"), + Portal(name="Far Shore to Heir", region="Far Shore", + destination="Spirit Arena_teleporter_spirit arena"), + Portal(name="Far Shore to Town", region="Far Shore", + destination="Overworld Redux_teleporter_town"), + Portal(name="Far Shore to Spawn", region="Far Shore to Spawn", + destination="Overworld Redux_teleporter_starting island"), + + Portal(name="Heir Arena Exit", region="Spirit Arena", + destination="Transit_teleporter_spirit arena"), + + Portal(name="Purgatory Bottom Exit", region="Purgatory", + destination="Purgatory_bottom"), + Portal(name="Purgatory Top Exit", region="Purgatory", + destination="Purgatory_top"), +] + + +class RegionInfo(NamedTuple): + game_scene: str # the name of the scene in the actual game + dead_end: int = 0 # if a region has only one exit + hint: int = 0 # what kind of hint text you should have + + +class DeadEnd(IntEnum): + free = 0 # not a dead end + all_cats = 1 # dead end in every logic category + restricted = 2 # dead end only in restricted + # there's no dead ends that are only in unrestricted + + +class Hint(IntEnum): + none = 0 # big areas, empty hallways, etc. + region = 1 # at least one of the portals must not be a dead end + scene = 2 # multiple regions in the scene, so using region could mean no valid hints + special = 3 # for if there's a weird case of specific regions being viable + + +# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all +tunic_er_regions: Dict[str, RegionInfo] = { + "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld": RegionInfo("Overworld Redux"), + "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest + "Overworld Laurels": RegionInfo("Overworld Redux"), # all spots in Overworld that you need laurels to reach + "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region), + "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway + "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal + "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), + "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Town Portal": RegionInfo("Overworld Redux"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux"), + "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Windmill": RegionInfo("Windmill"), + "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door + "Old House Front": RegionInfo("Overworld Interiors"), # part with the bedroom + "Relic Tower": RegionInfo("g_elements", dead_end=DeadEnd.all_cats), + "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace + "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder + "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden + "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Passage": RegionInfo("Ruins Passage", hint=Hint.region), + "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Sealed Temple": RegionInfo("Temple", hint=Hint.scene), + "Sealed Temple Rafters": RegionInfo("Temple", hint=Hint.scene), + "Forest Belltower Upper": RegionInfo("Forest Belltower", hint=Hint.region), + "Forest Belltower Main": RegionInfo("Forest Belltower"), + "Forest Belltower Lower": RegionInfo("Forest Belltower"), + "East Forest": RegionInfo("East Forest Redux"), + "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), + "East Forest Portal": RegionInfo("East Forest Redux"), + "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), + "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), + "Guard House 2": RegionInfo("East Forest Redux Interior"), + "Forest Boss Room": RegionInfo("Forest Boss Room"), + "Forest Grave Path Main": RegionInfo("Sword Access"), + "Forest Grave Path Upper": RegionInfo("Sword Access"), + "Forest Grave Path by Grave": RegionInfo("Sword Access"), + "Forest Hero's Grave": RegionInfo("Sword Access"), + "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits + "Dark Tomb Main": RegionInfo("Crypt Redux"), + "Dark Tomb Dark Exit": RegionInfo("Crypt Redux"), + "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"), # can laurels backwards + "Well Boss": RegionInfo("Sewer_Boss"), # can walk through (with bombs at least) + "Beneath the Well Front": RegionInfo("Sewer"), + "Beneath the Well Main": RegionInfo("Sewer"), + "Beneath the Well Back": RegionInfo("Sewer"), + "West Garden": RegionInfo("Archipelagos Redux"), + "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats, hint=Hint.region), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, hint=Hint.special), + "West Garden Laurels Exit": RegionInfo("Archipelagos Redux"), + "West Garden after Boss": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave": RegionInfo("Archipelagos Redux"), + "Ruined Atoll": RegionInfo("Atoll Redux"), + "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), + "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux"), + "Frog's Domain Entry": RegionInfo("Frog Stairs"), + "Frog's Domain": RegionInfo("frog cave main", hint=Hint.region), + "Frog's Domain Back": RegionInfo("frog cave main", hint=Hint.scene), + "Library Exterior Tree": RegionInfo("Library Exterior"), + "Library Exterior Ladder": RegionInfo("Library Exterior"), + "Library Hall": RegionInfo("Library Hall"), + "Library Hero's Grave": RegionInfo("Library Hall"), + "Library Rotunda": RegionInfo("Library Rotunda"), + "Library Lab": RegionInfo("Library Lab"), + "Library Lab Lower": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab"), + "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), + "Fortress Exterior from Overworld": RegionInfo("Fortress Courtyard"), + "Fortress Exterior near cave": RegionInfo("Fortress Courtyard"), # where the shop and beneath the earth entry are + "Fortress Courtyard": RegionInfo("Fortress Courtyard"), + "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"), + "Beneath the Vault Front": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla entry point + "Beneath the Vault Back": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla exit point + "Eastern Vault Fortress": RegionInfo("Fortress Main"), + "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"), + "Fortress East Shortcut Upper": RegionInfo("Fortress East"), + "Fortress East Shortcut Lower": RegionInfo("Fortress East"), + "Fortress Grave Path": RegionInfo("Fortress Reliquary"), + "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted, hint=Hint.region), + "Fortress Grave Path Dusty Entrance": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave": RegionInfo("Fortress Reliquary"), + "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Arena": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Lower Mountain": RegionInfo("Mountain"), + "Lower Mountain Stairs": RegionInfo("Mountain"), + "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Quarry Connector": RegionInfo("Darkwoods Tunnel"), + "Quarry Entry": RegionInfo("Quarry Redux"), + "Quarry": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Back": RegionInfo("Quarry Redux"), + "Quarry Monastery Entry": RegionInfo("Quarry Redux"), + "Monastery Front": RegionInfo("Monastery"), + "Monastery Back": RegionInfo("Monastery"), + "Monastery Hero's Grave": RegionInfo("Monastery"), + "Monastery Rope": RegionInfo("Quarry Redux"), + "Lower Quarry": RegionInfo("Quarry Redux"), + "Lower Quarry Zig Door": RegionInfo("Quarry Redux"), + "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), + "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), + "Rooted Ziggurat Upper Front": RegionInfo("ziggurat2020_1"), + "Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator + "Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"), + "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), + "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side + "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side + "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side + "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), + "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), + "Swamp": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Main Entrance": RegionInfo("Swamp Redux 2"), + "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance + "Swamp Hero's Grave": RegionInfo("Swamp Redux 2"), + "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse + "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet Exit": RegionInfo("Cathedral Arena"), + "Far Shore": RegionInfo("Transit"), + "Far Shore to Spawn": RegionInfo("Transit"), + "Far Shore to East Forest": RegionInfo("Transit"), + "Far Shore to Quarry": RegionInfo("Transit"), + "Far Shore to Fortress": RegionInfo("Transit"), + "Far Shore to Library": RegionInfo("Transit"), + "Far Shore to West Garden": RegionInfo("Transit"), + "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Purgatory": RegionInfo("Purgatory"), + "Shop Entrance 1": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 2": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 3": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 4": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 5": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 6": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats) +} + + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways: Dict[str, str] = { + "Overworld Redux, Furnace_gyro_west": "Overworld Redux, Archipelagos Redux_lower", + "Overworld Redux, Furnace_gyro_upper_north": "Overworld Redux, Sewer_west_aqueduct", + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper: Dict[str, str] = {} +for p1, p2 in hallways.items(): + hallway_helper[p1] = p2 + hallway_helper[p2] = p1 + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways_nmg: Dict[str, str] = { + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper_nmg: Dict[str, str] = {} +for p1, p2 in hallways.items(): + hallway_helper[p1] = p2 + hallway_helper[p2] = p1 + + +# the key is the region you have, the value is the regions you get for having that region +# this is mostly so we don't have to do something overly complex to get this information +dependent_regions: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + ("Old House Front",): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Well Boss",): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"], + ("West Garden Portal", "West Garden Portal Item"): ["West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Courtyard Upper",): + ["Fortress Courtyard Upper", "Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper",): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress",): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front",): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Monastery Back", "Monastery Hero's Grave"): + ["Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave", "Swamp", + "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { + # can use ladder storage to get to the well rail + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Well to Furnace Rail"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + # can use laurels, ice grapple, or ladder storage to traverse + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + # can ice grapple from portal area to the rest, and vice versa + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + # can use ice grapple or ladder storage to get from any ladder to upper + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + # can ice grapple up + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + # can ice grapple down + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + # can use ladder storage at any of the Quarry ladders to get to Monastery Rope + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope", "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py new file mode 100644 index 0000000000..5d88022dc1 --- /dev/null +++ b/worlds/tunic/er_rules.py @@ -0,0 +1,984 @@ +from typing import Dict, TYPE_CHECKING +from worlds.generic.Rules import set_rule, forbid_item +from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage +from .er_data import Portal +from BaseClasses import Region + +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +ice_rod = "Pages 52-53 (Ice Rod)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], + portal_pairs: Dict[Portal, Portal]) -> None: + player = world.player + options = world.options + + regions["Menu"].connect( + connecting_region=regions["Overworld"]) + + # Overworld + regions["Overworld"].connect( + connecting_region=regions["Overworld Holy Cross"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Belltower"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Belltower"].connect( + connecting_region=regions["Overworld"]) + + # nmg: can laurels through the ruined passage door + regions["Overworld"].connect( + connecting_region=regions["Overworld Ruined Passage Door"], + rule=lambda state: state.has(key, player, 2) + or (state.has(laurels, player) and options.logic_rules)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Laurels"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Laurels"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + # nmg: can ice grapple through the door + regions["Overworld"].connect( + connecting_region=regions["Overworld Old House Door"], + rule=lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # not including ice grapple through this because it's very tedious to get an enemy here + regions["Overworld"].connect( + connecting_region=regions["Overworld Southeast Cross Door"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Overworld Southeast Cross Door"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # not including ice grapple through this because we're not including it on the other door + regions["Overworld"].connect( + connecting_region=regions["Overworld Fountain Cross Door"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Overworld Fountain Cross Door"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Town Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Overworld Town Portal"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Spawn Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Overworld Spawn Portal"].connect( + connecting_region=regions["Overworld"]) + + # nmg: ice grapple through temple door + regions["Overworld"].connect( + connecting_region=regions["Overworld Temple Door"], + name="Overworld Temple Door", + rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # Overworld side areas + regions["Old House Front"].connect( + connecting_region=regions["Old House Back"]) + # nmg: laurels through the gate + regions["Old House Back"].connect( + connecting_region=regions["Old House Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Sealed Temple"].connect( + connecting_region=regions["Sealed Temple Rafters"]) + regions["Sealed Temple Rafters"].connect( + connecting_region=regions["Sealed Temple"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + + # East Forest + regions["Forest Belltower Upper"].connect( + connecting_region=regions["Forest Belltower Main"]) + + regions["Forest Belltower Main"].connect( + connecting_region=regions["Forest Belltower Lower"]) + + # nmg: ice grapple up to dance fox spot, and vice versa + regions["East Forest"].connect( + connecting_region=regions["East Forest Dance Fox Spot"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["East Forest Dance Fox Spot"].connect( + connecting_region=regions["East Forest"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["East Forest"].connect( + connecting_region=regions["East Forest Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["East Forest Portal"].connect( + connecting_region=regions["East Forest"]) + + regions["Guard House 1 East"].connect( + connecting_region=regions["Guard House 1 West"]) + regions["Guard House 1 West"].connect( + connecting_region=regions["Guard House 1 East"], + rule=lambda state: state.has(laurels, player)) + + # nmg: ice grapple from upper grave path exit to the rest of it + regions["Forest Grave Path Upper"].connect( + connecting_region=regions["Forest Grave Path Main"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path Upper"], + rule=lambda state: state.has(laurels, player)) + + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + # nmg: ice grapple or laurels through the gate + regions["Forest Grave Path by Grave"].connect( + connecting_region=regions["Forest Grave Path Main"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + + regions["Forest Grave Path by Grave"].connect( + connecting_region=regions["Forest Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Forest Hero's Grave"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + + # Beneath the Well and Dark Tomb + regions["Beneath the Well Front"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + regions["Beneath the Well Main"].connect( + connecting_region=regions["Beneath the Well Front"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + + regions["Beneath the Well Back"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + regions["Beneath the Well Main"].connect( + connecting_region=regions["Beneath the Well Back"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + + regions["Well Boss"].connect( + connecting_region=regions["Dark Tomb Checkpoint"]) + # nmg: can laurels through the gate + regions["Dark Tomb Checkpoint"].connect( + connecting_region=regions["Well Boss"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Dark Tomb Entry Point"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Entry Point"], + rule=lambda state: has_lantern(state, player, options)) + + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Dark Exit"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Dark Exit"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + + # West Garden + regions["West Garden Laurels Exit"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden"].connect( + connecting_region=regions["West Garden Laurels Exit"], + rule=lambda state: state.has(laurels, player)) + + # todo: can you wake the boss, then grapple to it, then kill it? + regions["West Garden after Boss"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden"].connect( + connecting_region=regions["West Garden after Boss"], + rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + + regions["West Garden"].connect( + connecting_region=regions["West Garden Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["West Garden Hero's Grave"].connect( + connecting_region=regions["West Garden"]) + + regions["West Garden Portal"].connect( + connecting_region=regions["West Garden Portal Item"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden Portal Item"].connect( + connecting_region=regions["West Garden Portal"], + rule=lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + + # nmg: can ice grapple to and from the item behind the magic dagger house + regions["West Garden Portal Item"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["West Garden"].connect( + connecting_region=regions["West Garden Portal Item"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # Atoll and Frog's Domain + # nmg: ice grapple the bird below the portal + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Lower Entry Area"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Ruined Atoll Lower Entry Area"].connect( + connecting_region=regions["Ruined Atoll"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Frog Mouth"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Ruined Atoll Frog Mouth"].connect( + connecting_region=regions["Ruined Atoll"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Ruined Atoll Portal"].connect( + connecting_region=regions["Ruined Atoll"]) + + regions["Frog's Domain"].connect( + connecting_region=regions["Frog's Domain Back"], + rule=lambda state: state.has(grapple, player)) + + # Library + regions["Library Exterior Tree"].connect( + connecting_region=regions["Library Exterior Ladder"], + rule=lambda state: state.has(grapple, player) or state.has(laurels, player)) + regions["Library Exterior Ladder"].connect( + connecting_region=regions["Library Exterior Tree"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + and (state.has(grapple, player) or state.has(laurels, player))) + + regions["Library Hall"].connect( + connecting_region=regions["Library Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Hero's Grave"].connect( + connecting_region=regions["Library Hall"]) + + regions["Library Lab Lower"].connect( + connecting_region=regions["Library Lab"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Library Lab"].connect( + connecting_region=regions["Library Lab Lower"], + rule=lambda state: state.has(laurels, player)) + + regions["Library Lab"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab"]) + + # Eastern Vault Fortress + regions["Fortress Exterior from East Forest"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Exterior from East Forest"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player)) + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Exterior near cave"], + rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + + regions["Fortress Courtyard"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player)) + # nmg: can ice grapple an enemy in the courtyard + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Courtyard"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Courtyard Upper"].connect( + connecting_region=regions["Fortress Courtyard"]) + # nmg: can ice grapple to the upper ledge + regions["Fortress Courtyard"].connect( + connecting_region=regions["Fortress Courtyard Upper"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Courtyard Upper"].connect( + connecting_region=regions["Fortress Exterior from Overworld"]) + + regions["Beneath the Vault Front"].connect( + connecting_region=regions["Beneath the Vault Back"], + rule=lambda state: has_lantern(state, player, options)) + regions["Beneath the Vault Back"].connect( + connecting_region=regions["Beneath the Vault Front"]) + + regions["Fortress East Shortcut Upper"].connect( + connecting_region=regions["Fortress East Shortcut Lower"]) + # nmg: can ice grapple upwards + regions["Fortress East Shortcut Lower"].connect( + connecting_region=regions["Fortress East Shortcut Upper"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # nmg: ice grapple through the big gold door, can do it both ways + regions["Eastern Vault Fortress"].connect( + connecting_region=regions["Eastern Vault Fortress Gold Door"], + name="Fortress Gold Door", + rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", + "Activate Eastern Vault East Fuse"}, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Eastern Vault Fortress Gold Door"].connect( + connecting_region=regions["Eastern Vault Fortress"], + name="Fortress Gold Door", + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance"], + rule=lambda state: state.has(laurels, player)) + regions["Fortress Grave Path Dusty Entrance"].connect( + connecting_region=regions["Fortress Grave Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Fortress Hero's Grave"].connect( + connecting_region=regions["Fortress Grave Path"]) + + # nmg: ice grapple from upper grave path to lower + regions["Fortress Grave Path Upper"].connect( + connecting_region=regions["Fortress Grave Path"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Arena"].connect( + connecting_region=regions["Fortress Arena Portal"], + name="Fortress Arena to Fortress Portal", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Fortress Arena Portal"].connect( + connecting_region=regions["Fortress Arena"]) + + # Quarry + regions["Lower Mountain"].connect( + connecting_region=regions["Lower Mountain Stairs"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Lower Mountain Stairs"].connect( + connecting_region=regions["Lower Mountain"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + regions["Quarry Entry"].connect( + connecting_region=regions["Quarry Portal"], + name="Quarry to Quarry Portal", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Quarry Portal"].connect( + connecting_region=regions["Quarry Entry"]) + + regions["Quarry Entry"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Entry"]) + + regions["Quarry Back"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Back"]) + + regions["Quarry Monastery Entry"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Monastery Entry"]) + + regions["Monastery Rope"].connect( + connecting_region=regions["Quarry Back"]) + + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry"], + rule=lambda state: has_mask(state, player, options)) + + # nmg: bring a scav over, then ice grapple through the door + regions["Lower Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + name="Quarry to Zig Door", + rule=lambda state: state.has("Activate Quarry Fuse", player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Monastery Front"].connect( + connecting_region=regions["Monastery Back"]) + # nmg: can laurels through the gate + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Monastery Hero's Grave"].connect( + connecting_region=regions["Monastery Back"]) + + # Ziggurat + regions["Rooted Ziggurat Upper Entry"].connect( + connecting_region=regions["Rooted Ziggurat Upper Front"]) + + regions["Rooted Ziggurat Upper Front"].connect( + connecting_region=regions["Rooted Ziggurat Upper Back"], + rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + regions["Rooted Ziggurat Upper Back"].connect( + connecting_region=regions["Rooted Ziggurat Upper Front"], + rule=lambda state: state.has(laurels, player)) + + regions["Rooted Ziggurat Middle Top"].connect( + connecting_region=regions["Rooted Ziggurat Middle Bottom"]) + + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"], + rule=lambda state: state.has(laurels, player) + or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))) + # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"], + rule=lambda state: state.has(laurels, player) or can_ladder_storage(state, player, options)) + + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Rooted Ziggurat Portal Room Entrance"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"]) + + regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room Exit"], + name="Zig Portal Room Exit", + rule=lambda state: state.has("Activate Ziggurat Fuse", player)) + regions["Rooted Ziggurat Portal Room Exit"].connect( + connecting_region=regions["Rooted Ziggurat Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + + # Swamp and Cathedral + # nmg: ice grapple through cathedral door, can do it both ways + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Main Entrance"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Swamp to Cathedral Main Entrance"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Treasure Room"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Swamp to Cathedral Treasure Room"].connect( + connecting_region=regions["Swamp"]) + + regions["Back of Swamp"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: state.has(laurels, player)) + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Back of Swamp"], + rule=lambda state: state.has(laurels, player)) + + # nmg: can ice grapple down while you're on the pillars + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: state.has(laurels, player) + and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Back of Swamp"].connect( + connecting_region=regions["Swamp Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Swamp Hero's Grave"].connect( + connecting_region=regions["Back of Swamp"]) + + regions["Cathedral Gauntlet Checkpoint"].connect( + connecting_region=regions["Cathedral Gauntlet"]) + + regions["Cathedral Gauntlet"].connect( + connecting_region=regions["Cathedral Gauntlet Exit"], + rule=lambda state: state.has(laurels, player)) + regions["Cathedral Gauntlet Exit"].connect( + connecting_region=regions["Cathedral Gauntlet"], + rule=lambda state: state.has(laurels, player)) + + # Far Shore + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Spawn"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to Spawn"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to East Forest"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to East Forest"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to West Garden"], + name="Far Shore to West Garden", + rule=lambda state: state.has("Activate West Garden Fuse", player)) + regions["Far Shore to West Garden"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Quarry"], + name="Far Shore to Quarry", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Far Shore to Quarry"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Fortress"], + name="Far Shore to Fortress", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Far Shore to Fortress"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Library"], + name="Far Shore to Library", + rule=lambda state: state.has("Activate Library Fuse", player)) + regions["Far Shore to Library"].connect( + connecting_region=regions["Far Shore"]) + + # Misc + regions["Shop Entrance 1"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 2"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 3"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 4"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 5"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 6"].connect( + connecting_region=regions["Shop"]) + + regions["Spirit Arena"].connect( + connecting_region=regions["Spirit Arena Victory"], + rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if + world.options.hexagon_quest else + state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player))) + + # connecting the regions portals are in to other portals you can access via ladder storage + # using has_stick instead of can_ladder_storage since it's already checking the logic rules + if options.logic_rules == "unrestricted": + def get_paired_region(portal_sd: str) -> str: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal2.region + if portal2.scene_destination() == portal_sd: + return portal1.region + raise Exception("no matches found in get_paired_region") + + # The upper Swamp entrance + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Swamp Redux 2_wall")], + rule=lambda state: has_stick(state, player)) + # Western Furnace entrance, next to the sign that leads to West Garden + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_west")], + rule=lambda state: has_stick(state, player)) + # Upper West Garden entry, by the belltower + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden entry by the Furnace + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lower")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels entrance, by the beach + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lowest")], + rule=lambda state: has_stick(state, player)) + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Sewer_west_aqueduct")], + rule=lambda state: has_stick(state, player)) + # Well rail, east side. Need some height from the temple stairs + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + + # Furnace ladder to the fuse entrance + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to Dark Tomb + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Crypt Redux_")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to the West Garden connector + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_west")], + rule=lambda state: has_stick(state, player)) + + # West Garden exit after Garden Knight + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels exit + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_lowest")], + rule=lambda state: has_stick(state, player)) + + # Frog mouth entrance + regions["Ruined Atoll"].connect( + regions[get_paired_region("Atoll Redux, Frog Stairs_mouth")], + rule=lambda state: has_stick(state, player)) + + # Entrance by the dancing fox holy cross spot + regions["East Forest"].connect( + regions[get_paired_region("East Forest Redux, East Forest Redux Laddercave_upper")], + rule=lambda state: has_stick(state, player)) + + # From the west side of guard house 1 to the east side + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, East Forest Redux_gate")], + rule=lambda state: has_stick(state, player)) + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, Forest Boss Room_")], + rule=lambda state: has_stick(state, player)) + + # Upper exit from the Forest Grave Path, use ls at the ladder by the gate switch + regions["Forest Grave Path Main"].connect( + regions[get_paired_region("Sword Access, East Forest Redux_upper")], + rule=lambda state: has_stick(state, player)) + + # Fortress exterior shop, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the east side of the area + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the Beneath the Vault entrance ladder + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # ls at the ladder, need to gain a little height to get up the stairs + regions["Lower Mountain"].connect( + regions[get_paired_region("Mountain, Mountaintop_")], + rule=lambda state: has_stick(state, player)) + + # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword + regions["Quarry Monastery Entry"].connect( + regions[get_paired_region("Quarry Redux, Monastery_back")], + rule=lambda state: has_stick(state, player)) + + # Swamp to Gauntlet + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Arena_")], + rule=lambda state: has_stick(state, player)) + # Swamp to Overworld upper + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_wall")], + rule=lambda state: has_stick(state, player)) + # Ladder by the hero grave + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_conduit")], + rule=lambda state: has_stick(state, player)) + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Shop_")], + rule=lambda state: has_stick(state, player)) + # Need to put the cathedral HC code mid-flight + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Redux_secret")], + rule=lambda state: has_stick(state, player) + and has_ability(state, player, holy_cross, options, ability_unlocks)) + + +def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + player = world.player + multiworld = world.multiworld + options = world.options + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + + # Ability Shuffle Exclusive Rules + set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + lambda state: state.has("Activate Furnace Fuse", player)) + set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + lambda state: state.has(fairies, player, 20)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + lambda state: state.has_all({grapple, laurels}, player)) + set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( + state.has_all({grapple, ice_dagger, fire_wand}, player) and + has_ability(state, player, ice_rod, options, ability_unlocks))) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options, + ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + lambda state: state.has(laurels, player)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, player)) + + # Eastern Vault Fortress + set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + lambda state: state.has(vault_key, player)) + + # Beneath the Vault + set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + lambda state: state.has_group("melee weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), + lambda state: has_lantern(state, player, options)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + + # Ziggurat + set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + lambda state: has_sword(state, player) or state.has(fire_wand, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player), + lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)) + + # Bosses + set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + lambda state: state.has(laurels, player)) + # these two swamp checks really want you to kill the big skeleton first + set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + lambda state: has_sword(state, player)) + + # Hero's Grave and Far Shore + set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + lambda state: state.has(laurels, player)) + + # Events + set_rule(multiworld.get_location("Eastern Bell", player), + lambda state: (has_stick(state, player) or state.has(fire_wand, player))) + set_rule(multiworld.get_location("Western Bell", player), + lambda state: (has_stick(state, player) or state.has(fire_wand, player))) + set_rule(multiworld.get_location("Furnace Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Vault Fuse", player), + lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) + set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + lambda state: state.has("Activate Beneath the Vault Fuse", player)) + set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", + "Activate South and West Fortress Exterior Fuses"}, player)) + set_rule(multiworld.get_location("Quarry Connector Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player)) + set_rule(multiworld.get_location("Quarry Fuse", player), + lambda state: state.has("Activate Quarry Connector Fuse", player)) + set_rule(multiworld.get_location("Ziggurat Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py new file mode 100644 index 0000000000..84b97e13da --- /dev/null +++ b/worlds/tunic/er_scripts.py @@ -0,0 +1,453 @@ +from typing import Dict, List, Set, Tuple, TYPE_CHECKING +from BaseClasses import Region, ItemClassification, Item, Location +from .locations import location_table +from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_nmg, \ + dependent_regions, dependent_regions_nmg, dependent_regions_ur +from .er_rules import set_er_region_rules + +if TYPE_CHECKING: + from . import TunicWorld + + +class TunicERItem(Item): + game: str = "Tunic" + + +class TunicERLocation(Location): + game: str = "Tunic" + + +def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]: + regions: Dict[str, Region] = {} + portal_pairs: Dict[Portal, Portal] = pair_portals(world) + logic_rules = world.options.logic_rules + + # check if a portal leads to a hallway. if it does, update the hint text accordingly + def hint_helper(portal: Portal, hint_string: str = "") -> str: + # start by setting it as the name of the portal, for the case we're not using the hallway helper + if hint_string == "": + hint_string = portal.name + + if logic_rules: + hallways = hallway_helper_nmg + else: + hallways = hallway_helper + + if portal.scene_destination() in hallways: + # if we have a hallway, we want the region rather than the portal name + if hint_string == portal.name: + hint_string = portal.region + # library exterior is two regions, we just want to fix up the name + if hint_string in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_string = "Library Exterior" + + # search through the list for the other end of the hallway + for portala, portalb in portal_pairs.items(): + if portala.scene_destination() == hallways[portal.scene_destination()]: + # if we find that we have a chain of hallways, do recursion + if portalb.scene_destination() in hallways: + hint_region = portalb.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portalb, hint_string) + else: + # if we didn't find a chain, get the portal name for the end of the chain + hint_string = portalb.name + " then " + hint_string + return hint_string + # and then the same thing for the other portal, since we have to check each separately + if portalb.scene_destination() == hallways[portal.scene_destination()]: + if portala.scene_destination() in hallways: + hint_region = portala.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portala, hint_string) + else: + hint_string = portala.name + " then " + hint_string + return hint_string + return hint_string + + # create our regions, give them hint text if they're in a spot where it makes sense to + for region_name, region_data in tunic_er_regions.items(): + hint_text = "error" + if region_data.hint == 1: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == region_name: + hint_text = hint_helper(portal2) + break + if portal2.region == region_name: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 2: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal2) + break + if portal2.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 3: + # only the west garden portal item for now + if region_name == "West Garden Portal Item": + if world.options.logic_rules: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal2) + break + if portal2.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == "West Garden Portal": + hint_text = hint_helper(portal2) + break + if portal2.region == "West Garden Portal": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + regions[region_name] = Region(region_name, world.player, world.multiworld) + + set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) + + er_hint_data: Dict[int, str] = {} + for location_name, location_id in world.location_name_to_id.items(): + region = regions[location_table[location_name].er_region] + location = TunicERLocation(world.player, location_name, location_id, region) + region.locations.append(location) + if region.name == region.hint_text: + continue + er_hint_data[location.address] = region.hint_text + + create_randomized_entrances(portal_pairs, regions) + + for region in regions.values(): + world.multiworld.regions.append(region) + + place_event_items(world, regions) + + victory_region = regions["Spirit Arena Victory"] + victory_location = TunicERLocation(world.player, "The Heir", None, victory_region) + victory_location.place_locked_item(TunicERItem("Victory", ItemClassification.progression, None, world.player)) + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + victory_region.locations.append(victory_location) + + portals_and_hints = (portal_pairs, er_hint_data) + + return portals_and_hints + + +tunic_events: Dict[str, str] = { + "Eastern Bell": "Forest Belltower Upper", + "Western Bell": "Overworld Belltower", + "Furnace Fuse": "Furnace Fuse", + "South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld", + "Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper", + "Beneath the Vault Fuse": "Beneath the Vault Back", + "Eastern Vault West Fuses": "Eastern Vault Fortress", + "Eastern Vault East Fuse": "Eastern Vault Fortress", + "Quarry Connector Fuse": "Quarry Connector", + "Quarry Fuse": "Quarry", + "Ziggurat Fuse": "Rooted Ziggurat Lower Back", + "West Garden Fuse": "West Garden", + "Library Fuse": "Library Lab", +} + + +def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: + for event_name, region_name in tunic_events.items(): + region = regions[region_name] + location = TunicERLocation(world.player, event_name, None, region) + if event_name.endswith("Bell"): + location.place_locked_item( + TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player)) + else: + location.place_locked_item( + TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player)) + region.locations.append(location) + + +# pairing off portals, starting with dead ends +def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: + # separate the portals into dead ends and non-dead ends + portal_pairs: Dict[Portal, Portal] = {} + dead_ends: List[Portal] = [] + two_plus: List[Portal] = [] + fixed_shop = False + logic_rules = world.options.logic_rules.value + + # create separate lists for dead ends and non-dead ends + if logic_rules: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end == 1: + dead_ends.append(portal) + else: + two_plus.append(portal) + else: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end: + dead_ends.append(portal) + else: + two_plus.append(portal) + + connected_regions: Set[str] = set() + # make better start region stuff when/if implementing random start + start_region = "Overworld" + connected_regions.update(add_dependent_regions(start_region, logic_rules)) + + # need to plando fairy cave, or it could end up laurels locked + # fix this later to be random? probably not? + if world.options.laurels_location == "10_fairies": + portal1 = None + portal2 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Waterfall_": + portal1 = portal + break + for portal in dead_ends: + if portal.scene_destination() == "Waterfall, Overworld Redux_": + portal2 = portal + break + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + dead_ends.remove(portal2) + + if world.options.fixed_shop: + fixed_shop = True + portal1 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Windmill_": + portal1 = portal + break + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance 2", destination="Previous Region_") + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + + # we want to start by making sure every region is accessible + non_dead_end_regions = set() + for region_name, region_info in tunic_er_regions.items(): + if not region_info.dead_end: + non_dead_end_regions.add(region_name) + elif region_info.dead_end == 2 and logic_rules: + non_dead_end_regions.add(region_name) + + world.random.shuffle(two_plus) + check_success = 0 + portal1 = None + portal2 = None + while len(connected_regions) < len(non_dead_end_regions): + # find a portal in an inaccessible region + if check_success == 0: + for portal in two_plus: + if portal.region in connected_regions: + # if there's risk of self-locking, start over + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal1 = portal + two_plus.remove(portal) + check_success = 1 + break + + # then we find a portal in a connected region + if check_success == 1: + for portal in two_plus: + if portal.region not in connected_regions: + # if there's risk of self-locking, shuffle and try again + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal2 = portal + two_plus.remove(portal) + check_success = 2 + break + + # once we have both portals, connect them and add the new region(s) to connected_regions + if check_success == 2: + connected_regions.update(add_dependent_regions(portal2.region, logic_rules)) + portal_pairs[portal1] = portal2 + check_success = 0 + world.random.shuffle(two_plus) + + # add 6 shops, connect them to unique scenes + # this is due to a limitation in Tunic -- you wrong warp if there's multiple shops + shop_scenes: Set[str] = set() + shop_count = 6 + + if fixed_shop: + shop_count = 1 + shop_scenes.add("Overworld Redux") + + for i in range(shop_count): + portal1 = None + for portal in two_plus: + if portal.scene() not in shop_scenes: + shop_scenes.add(portal.scene()) + portal1 = portal + two_plus.remove(portal) + break + if portal1 is None: + raise Exception("Too many shops in the pool, or something else went wrong") + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {i + 1}", destination="Previous Region_") + portal_pairs[portal1] = portal2 + + # connect dead ends to random non-dead ends + # none of the key events are in dead ends, so we don't need to do gate_before_switch + while len(dead_ends) > 0: + portal1 = two_plus.pop() + portal2 = dead_ends.pop() + portal_pairs[portal1] = portal2 + + # then randomly connect the remaining portals to each other + # every region is accessible, so gate_before_switch is not necessary + while len(two_plus) > 1: + portal1 = two_plus.pop() + portal2 = two_plus.pop() + portal_pairs[portal1] = portal2 + + if len(two_plus) == 1: + raise Exception("two plus had an odd number of portals, investigate this") + + for portal1, portal2 in portal_pairs.items(): + world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + + return portal_pairs + + +# loop through our list of paired portals and make two-way connections +def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None: + for portal1, portal2 in portal_pairs.items(): + region1 = regions[portal1.region] + region2 = regions[portal2.region] + region1.connect(region2, f"{portal1.name} -> {portal2.name}") + # prevent the logic from thinking you can get to any shop-connected region from the shop + if portal2.name != "Shop": + region2.connect(region1, f"{portal2.name} -> {portal1.name}") + + +# loop through the static connections, return regions you can reach from this region +def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]: + region_set = set() + if not logic_rules: + regions_to_add = dependent_regions + elif logic_rules == 1: + regions_to_add = dependent_regions_nmg + else: + regions_to_add = dependent_regions_ur + for origin_regions, destination_regions in regions_to_add.items(): + if region_name in origin_regions: + # if you matched something in the first set, you get the regions in its paired set + region_set.update(destination_regions) + return region_set + # if you didn't match anything in the first sets, just gives you the region + region_set = {region_name} + return region_set + + +# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are +# doing this ensures the keys will not be locked behind the event-locked portal +def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: + # the western belltower cannot be locked since you can access it with laurels + # so we only need to make sure the forest belltower isn't locked + if check_portal.scene_destination() == "Overworld Redux, Temple_main": + i = 0 + for portal in two_plus: + if portal.region == "Forest Belltower Upper": + i += 1 + break + if i == 1: + return True + + # fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard + elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_": + i = j = k = 0 + for portal in two_plus: + if portal.region == "Fortress Courtyard Upper": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 2 or j == 2 or k == 5: + return True + + # fortress teleporter needs only the left fuses + elif check_portal.scene_destination() in ["Fortress Arena, Transit_teleporter_spidertank", + "Transit, Fortress Arena_teleporter_spidertank"]: + i = j = k = 0 + for portal in two_plus: + if portal.scene() == "Fortress Courtyard": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 8 or j == 2 or k == 5: + return True + + # Cathedral door needs Overworld and the front of Swamp + # Overworld is currently guaranteed, so no need to check it + elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main": + i = 0 + for portal in two_plus: + if portal.region == "Swamp": + i += 1 + if i == 4: + return True + + # Zig portal room exit needs Zig 3 to be accessible to hit the fuse + elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_": + i = 0 + for portal in two_plus: + if portal.scene() == "ziggurat2020_3": + i += 1 + if i == 2: + return True + + # Quarry teleporter needs you to hit the Darkwoods fuse + # Since it's physically in Quarry, we don't need to check for it + elif check_portal.scene_destination() in ["Quarry Redux, Transit_teleporter_quarry teleporter", + "Quarry Redux, ziggurat2020_0_"]: + i = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if i == 2: + return True + + # Same as above, but Quarry isn't guaranteed here + elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter": + i = j = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if portal.scene() == "Quarry Redux": + j += 1 + if i == 2 or j == 7: + return True + + # Need Library fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Library Lab": + i += 1 + if i == 3: + return True + + # Need West Garden fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Archipelagos Redux": + i += 1 + if i == 6: + return True + + # false means you're good to place the portal + return False diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py new file mode 100644 index 0000000000..16608620c6 --- /dev/null +++ b/worlds/tunic/items.py @@ -0,0 +1,214 @@ +from itertools import groupby +from typing import Dict, List, Set, NamedTuple +from BaseClasses import ItemClassification + + +class TunicItemData(NamedTuple): + classification: ItemClassification + quantity_in_item_pool: int + item_id_offset: int + item_group: str = "" + + +item_base_id = 509342400 + +item_table: Dict[str, TunicItemData] = { + "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "bombs"), + "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "bombs"), + "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "bombs"), + "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "bombs"), + "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "bombs"), + "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "bombs"), + "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "bombs"), + "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "bombs"), + "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "bombs"), + "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "bombs"), + "Lure": TunicItemData(ItemClassification.filler, 4, 10, "consumables"), + "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "consumables"), + "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "consumables"), + "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "consumables"), + "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "money"), + "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "consumables"), + "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "consumables"), + "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "consumables"), + "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "consumables"), + "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "consumables"), + "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "consumables"), + "Fairy": TunicItemData(ItemClassification.progression, 20, 21), + "Stick": TunicItemData(ItemClassification.progression, 1, 22, "weapons"), + "Sword": TunicItemData(ItemClassification.progression, 3, 23, "weapons"), + "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "weapons"), + "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "weapons"), + "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26), + "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27), + "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28), + "Lantern": TunicItemData(ItemClassification.progression, 1, 29), + "Gun": TunicItemData(ItemClassification.useful, 1, 30, "weapons"), + "Shield": TunicItemData(ItemClassification.useful, 1, 31), + "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32), + "Hourglass": TunicItemData(ItemClassification.useful, 1, 33), + "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "keys"), + "Key": TunicItemData(ItemClassification.progression, 2, 35, "keys"), + "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "keys"), + "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37, "potions"), + "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"), + "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39), + "Card Slot": TunicItemData(ItemClassification.useful, 4, 40), + "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "hexagons"), + "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "hexagons"), + "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "hexagons"), + "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "hexagons"), + "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "offerings"), + "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "offerings"), + "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "offerings"), + "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "offerings"), + "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "offerings"), + "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "offerings"), + "Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "hero relics"), + "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"), + "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"), + "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"), + "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"), + "Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "hero relics"), + "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "cards"), + "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "cards"), + "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "cards"), + "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "cards"), + "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "cards"), + "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "cards"), + "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "cards"), + "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "cards"), + "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "cards"), + "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "cards"), + "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "cards"), + "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "cards"), + "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "cards"), + "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "cards"), + "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "cards"), + "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "cards"), + "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "golden treasures"), + "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "golden treasures"), + "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "golden treasures"), + "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "golden treasures"), + "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "golden treasures"), + "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "golden treasures"), + "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "golden treasures"), + "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "golden treasures"), + "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "golden treasures"), + "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "golden treasures"), + "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "golden treasures"), + "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "golden treasures"), + "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85, "fool"), + "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "money"), + "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "money"), + "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "money"), + "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "money"), + "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "money"), + "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "money"), + "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "money"), + "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "money"), + "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "money"), + "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "money"), + "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "money"), + "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "money"), + "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "money"), + "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "money"), + "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "money"), + "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "money"), + "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "pages"), + "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "pages"), + "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "pages"), + "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "pages"), + "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "pages"), + "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "pages"), + "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "pages"), + "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "pages"), + "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "pages"), + "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "pages"), + "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "pages"), + "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "pages"), + "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "pages"), + "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "pages"), + "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "pages"), + "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "pages"), + "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "pages"), + "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "pages"), + "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "pages"), + "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "pages"), + "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "pages"), + "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "pages"), + "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "pages"), + "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "pages"), + "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "pages"), + "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"), + "Pages 52-53 (Ice Rod)": TunicItemData(ItemClassification.progression, 1, 128, "pages"), + "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"), +} + +fool_tiers: List[List[str]] = [ + [], + ["Money x1", "Money x10", "Money x15", "Money x16"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], +] + +slot_data_item_names = [ + "Stick", + "Sword", + "Sword Upgrade", + "Magic Dagger", + "Magic Wand", + "Magic Orb", + "Hero's Laurels", + "Lantern", + "Gun", + "Scavenger Mask", + "Shield", + "Dath Stone", + "Hourglass", + "Old House Key", + "Fortress Vault Key", + "Hero Relic - ATT", + "Hero Relic - DEF", + "Hero Relic - POTION", + "Hero Relic - HP", + "Hero Relic - SP", + "Hero Relic - MP", + "Pages 24-25 (Prayer)", + "Pages 42-43 (Holy Cross)", + "Pages 52-53 (Ice Rod)", + "Red Questagon", + "Green Questagon", + "Blue Questagon", + "Gold Questagon", +] + +item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} + +filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler] + + +def get_item_group(item_name: str) -> str: + return item_table[item_name].item_group + + +item_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != "" +} + +# extra groups for the purpose of aliasing items +extra_groups: Dict[str, Set[str]] = { + "laurels": {"Hero's Laurels"}, + "orb": {"Magic Orb"}, + "dagger": {"Magic Dagger"}, + "magic rod": {"Magic Wand"}, + "holy cross": {"Pages 42-43 (Holy Cross)"}, + "prayer": {"Pages 24-25 (Prayer)"}, + "ice rod": {"Pages 52-53 (Ice Rod)"}, + "melee weapons": {"Stick", "Sword", "Sword Upgrade"}, + "progressive sword": {"Sword Upgrade"}, + "abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Ice Rod)"}, + "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"} +} + +item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py new file mode 100644 index 0000000000..1501fb7da2 --- /dev/null +++ b/worlds/tunic/locations.py @@ -0,0 +1,337 @@ +from typing import Dict, NamedTuple, Set +from itertools import groupby + + +class TunicLocationData(NamedTuple): + region: str + er_region: str # entrance rando region + location_group: str = "region" + + +location_base_id = 509342400 + +location_table: Dict[str, TunicLocationData] = { + "Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Tentacle Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Entryway] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Save Room] Upper Floor Chest 1": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Save Room] Upper Floor Chest 2": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Second Room] Underwater Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Beneath the Well", "Dark Tomb Checkpoint"), + "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"), + "Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"), + "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), + "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", "holy cross"), + "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "East Forest", "holy cross"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), + "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), + "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), + "East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"), + "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"), + "East Forest - Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"), + "East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"), + "Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"), + "Forest Belltower - After Guard Captain": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", "holy cross"), + "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"), + "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"), + "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"), + "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"), + "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"), + "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"), + "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", "holy cross"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"), + "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), + "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), + "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), + "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"), + "Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"), + "Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena"), + "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", "holy cross"), + "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Behind Chalkboard by Fuse": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"), + "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"), + "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"), + "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"), + "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "Fortress Courtyard - Near Fuse": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Below Walkway": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Page Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "West Furnace - Lantern Pickup": TunicLocationData("Overworld", "Furnace Fuse"), + "Maze Cave - Maze Room Chest": TunicLocationData("Overworld", "Maze Cave"), + "Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"), + "Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"), + "Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"), + "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"), + "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"), + "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"), + "Shop - Potion 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Potion 2": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 2": TunicLocationData("Overworld", "Shop", "shop"), + "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"), + "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"), + "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"), + "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"), + "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"), + "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn"), + "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", "holy cross"), + "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", "holy cross"), + "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", "holy cross"), + "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", "holy cross"), + "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", "holy cross"), + "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", "holy cross"), + "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", "holy cross"), + "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave", "holy cross"), + "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", "holy cross"), + "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", "holy cross"), + "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", "holy cross"), + "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", "holy cross"), + "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", "holy cross"), + "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), + "Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), + "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"), + "Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"), + "Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"), + "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back"), + "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [East] Locked Room Upper Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"), + "Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"), + "Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"), + "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"), + "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"), + "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp"), + "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"), + "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), + "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), + "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("West Garden", "West Garden after Boss"), + "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), + "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), + "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"), + "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), + "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), +} + +hexagon_locations: Dict[str, str] = { + "Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup", + "Green Questagon": "Librarian - Hexagon Green", + "Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue", +} + +location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)} + + +def get_loc_group(location_name: str) -> str: + loc_group = location_table[location_name].location_group + if loc_group == "region": + # set loc_group as the region name. Typically, location groups are lowercase + loc_group = location_table[location_name].region.lower() + return loc_group + + +location_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(location_table, key=get_loc_group), get_loc_group) +} diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py new file mode 100644 index 0000000000..77fa2cdaf5 --- /dev/null +++ b/worlds/tunic/options.py @@ -0,0 +1,147 @@ +from dataclasses import dataclass + +from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, PerGameCommonOptions + + +class SwordProgression(DefaultOnToggle): + """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new + swords with increased range and attack power.""" + internal_name = "sword_progression" + display_name = "Sword Progression" + + +class StartWithSword(Toggle): + """Start with a sword in the player's inventory. Does not count towards Sword Progression.""" + internal_name = "start_with_sword" + display_name = "Start With Sword" + + +class KeysBehindBosses(Toggle): + """Places the three hexagon keys behind their respective boss fight in your world.""" + internal_name = "keys_behind_bosses" + display_name = "Keys Behind Bosses" + + +class AbilityShuffling(Toggle): + """Locks the usage of Prayer, Holy Cross*, and Ice Rod until the relevant pages of the manual have been found. + If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required + Hexagon goal amount. + *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other + player-facing codes. + """ + internal_name = "ability_shuffling" + display_name = "Ability Shuffling" + + +class LogicRules(Choice): + """Set which logic rules to use for your world. + Restricted: Standard logic, no glitches. + No Major Glitches: Ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. + Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. + *Special Shop is not in logic without the Hero's Laurels in Unrestricted due to soft lock potential. + *Using Ladder Storage to get to individual chests is not in logic to avoid tedium. + *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in + Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.""" + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + option_unrestricted = 2 + default = 0 + + +class Lanternless(Toggle): + """Choose whether you require the Lantern for dark areas. + When enabled, the Lantern is marked as Useful instead of Progression.""" + internal_name = "lanternless" + display_name = "Lanternless" + + +class Maskless(Toggle): + """Choose whether you require the Scavenger's Mask for Lower Quarry. + When enabled, the Scavenger's Mask is marked as Useful instead of Progression.""" + internal_name = "maskless" + display_name = "Maskless" + + +class FoolTraps(Choice): + """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative + effects to the player.""" + internal_name = "fool_traps" + display_name = "Fool Traps" + option_off = 0 + option_normal = 1 + option_double = 2 + option_onslaught = 3 + default = 1 + + +class HexagonQuest(Toggle): + """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed + after collecting the required number of them.""" + internal_name = "hexagon_quest" + display_name = "Hexagon Quest" + + +class HexagonGoal(Range): + """How many Gold Questagons are required to complete the game on Hexagon Quest.""" + internal_name = "hexagon_goal" + display_name = "Gold Hexagons Required" + range_start = 15 + range_end = 50 + default = 20 + + +class ExtraHexagonPercentage(Range): + """How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.""" + internal_name = "extra_hexagon_percentage" + display_name = "Percentage of Extra Gold Hexagons" + range_start = 0 + range_end = 100 + default = 50 + + +class EntranceRando(Toggle): + """Randomize the connections between scenes. + A small, very lost fox on a big adventure.""" + internal_name = "entrance_rando" + display_name = "Entrance Rando" + + +class FixedShop(Toggle): + """Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool. + Has no effect if Entrance Rando is not enabled.""" + internal_name = "fixed_shop" + display_name = "ER Fixed Shop" + + +class LaurelsLocation(Choice): + """Force the Hero's Laurels to be placed at a location in your world. + For if you want to avoid or specify early or late Laurels. + If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance.""" + internal_name = "laurels_location" + display_name = "Laurels Location" + option_anywhere = 0 + option_6_coins = 1 + option_10_coins = 2 + option_10_fairies = 3 + default = 0 + + +@dataclass +class TunicOptions(PerGameCommonOptions): + sword_progression: SwordProgression + start_with_sword: StartWithSword + keys_behind_bosses: KeysBehindBosses + ability_shuffling: AbilityShuffling + logic_rules: LogicRules + entrance_rando: EntranceRando + fixed_shop: FixedShop + fool_traps: FoolTraps + hexagon_quest: HexagonQuest + hexagon_goal: HexagonGoal + extra_hexagon_percentage: ExtraHexagonPercentage + lanternless: Lanternless + maskless: Maskless + laurels_location: LaurelsLocation + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py new file mode 100644 index 0000000000..5d5248f210 --- /dev/null +++ b/worlds/tunic/regions.py @@ -0,0 +1,25 @@ +from typing import Dict, Set + +tunic_regions: Dict[str, Set[str]] = { + "Menu": {"Overworld"}, + "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", + "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", + "Spirit Arena"}, + "Overworld Holy Cross": set(), + "East Forest": {"Eastern Vault Fortress"}, + "Dark Tomb": {"West Garden"}, + "Beneath the Well": {"Dark Tomb"}, + "West Garden": {"Overworld", "Dark Tomb"}, + "Ruined Atoll": {"Frog's Domain", "Library"}, + "Frog's Domain": set(), + "Library": set(), + "Eastern Vault Fortress": {"Beneath the Vault"}, + "Beneath the Vault": {"Eastern Vault Fortress"}, + "Quarry Back": {"Quarry"}, + "Quarry": {"Lower Quarry", "Rooted Ziggurat"}, + "Lower Quarry": {"Rooted Ziggurat"}, + "Rooted Ziggurat": set(), + "Swamp": {"Cathedral"}, + "Cathedral": set(), + "Spirit Arena": set() +} diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py new file mode 100644 index 0000000000..9906936a46 --- /dev/null +++ b/worlds/tunic/rules.py @@ -0,0 +1,345 @@ +from random import Random +from typing import Dict, TYPE_CHECKING + +from worlds.generic.Rules import set_rule, forbid_item +from BaseClasses import CollectionState +from .options import TunicOptions +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +ice_rod = "Pages 52-53 (Ice Rod)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: + ability_requirement = [1, 1, 1] + if options.hexagon_quest.value: + hexagon_goal = options.hexagon_goal.value + # Set ability unlocks to 25, 50, and 75% of goal amount + ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4] + abilities = [prayer, holy_cross, ice_rod] + random.shuffle(abilities) + return dict(zip(abilities, ability_requirement)) + + +def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, + ability_unlocks: Dict[str, int]) -> bool: + if not options.ability_shuffling: + return True + if options.hexagon_quest: + return state.has(gold_hexagon, player, ability_unlocks[ability]) + return state.has(ability, player) + + +# a check to see if you can whack things in melee at all +def has_stick(state: CollectionState, player: int) -> bool: + return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player) + + +def has_sword(state: CollectionState, player: int) -> bool: + return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) + + +def has_ice_grapple_logic(long_range: bool, state: CollectionState, player: int, options: TunicOptions, + ability_unlocks: Dict[str, int]) -> bool: + if not options.logic_rules: + return False + + if not long_range: + return state.has_all({ice_dagger, grapple}, player) + else: + return state.has_all({ice_dagger, fire_wand, grapple}, player) and \ + has_ability(state, player, ice_rod, options, ability_unlocks) + + +def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.logic_rules == "unrestricted" and has_stick(state, player): + return True + else: + return False + + +def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.maskless: + return True + else: + return state.has(mask, player) + + +def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.lanternless: + return True + else: + return state.has(lantern, player) + + +def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \ + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ + lambda state: has_stick(state, player) or state.has(fire_wand, player) + multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or can_ladder_storage(state, player, options) + multiworld.get_entrance("Beneath the Well -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("West Garden -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ + or can_ladder_storage(state, player, options) + multiworld.get_entrance("East Forest -> Eastern Vault Fortress", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ + or can_ladder_storage(state, player, options) + # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules + multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ + lambda state: has_lantern(state, player, options) and \ + has_ability(state, player, prayer, options, ability_unlocks) + multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ + lambda state: state.has_any({grapple, laurels}, player) and \ + has_ability(state, player, prayer, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ + lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ + and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options)) + multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ + lambda state: has_sword(state, player) or state.has(fire_wand, player) + multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ + lambda state: has_mask(state, player, options) + multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ + lambda state: (state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks)) \ + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Quarry -> Rooted Ziggurat", player).access_rule = \ + lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \ + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \ + lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value + else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)) and \ + has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) + + +def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + + # Ability Shuffle Exclusive Rules + set_rule(multiworld.get_location("Far Shore - Page Pickup", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) + or can_ladder_storage(state, player, options) + or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_lantern(state, player, options))) + set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) + or can_ladder_storage(state, player, options) + or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_lantern(state, player, options))) + set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Old House - Normal Chest", player), + lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and + (state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules))) + set_rule(multiworld.get_location("Old House - Shield Pickup", player), + lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and has_sword(state, player))) + set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), + lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and + (state.has(laurels, player) + or (has_lantern(state, player, options) and + (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + lambda state: state.has(fairies, player, 20)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + lambda state: state.has_all({grapple, laurels}, player)) + set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), + lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) + and has_ability(state, player, ice_rod, options, ability_unlocks)) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + lambda state: state.has(laurels, player) + and has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), + lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + lambda state: has_sword(state, player) or state.has(laurels, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or can_ladder_storage(state, player, options)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player) or options.logic_rules) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, player)) + + # Eastern Vault Fortress + set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + lambda state: has_sword(state, player) and + (has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + lambda state: state.has(vault_key, player) and + (has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + + # Beneath the Vault + set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) + set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), + lambda state: has_stick(state, player) and has_lantern(state, player, options)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(laurels, player) and state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + lambda state: has_sword(state, player)) + + # Hero's Grave + set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py new file mode 100644 index 0000000000..f8ab99d67d --- /dev/null +++ b/worlds/tunic/test/__init__.py @@ -0,0 +1,6 @@ +from test.bases import WorldTestBase + + +class TunicTestBase(WorldTestBase): + game = "Tunic" + player: int = 1 \ No newline at end of file diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py new file mode 100644 index 0000000000..d74858bd27 --- /dev/null +++ b/worlds/tunic/test/test_access.py @@ -0,0 +1,70 @@ +from . import TunicTestBase +from .. import options + + +class TestAccess(TunicTestBase): + # test whether you can get into the temple without laurels + def test_temple_access(self): + self.collect_all_but(["Hero's Laurels", "Lantern"]) + self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup")) + self.collect_by_name(["Lantern"]) + self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup")) + + # test that the wells function properly. Since fairies is written the same way, that should succeed too + def test_wells(self): + self.collect_all_but(["Golden Coin"]) + self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins")) + self.collect_by_name(["Golden Coin"]) + self.assertTrue(self.can_reach_location("Coins in the Well - 3 Coins")) + + +class TestStandardShuffle(TunicTestBase): + options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need to get holy cross to open the hc door in overworld + def test_hc_door(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Pages 42-43 (Holy Cross)") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need the gold questagons to open the hc door in overworld + def test_hc_door_hex_shuffle(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Gold Questagon") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestNoShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false} + + # test that you can get the item behind the overworld hc door with nothing and no ability shuffle + def test_hc_door_no_shuffle(self): + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestNormalGoal(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + # test that you need the three colored hexes to reach the Heir in standard + def test_normal_goal(self): + location = ["The Heir"] + items = [["Red Questagon", "Blue Questagon", "Green Questagon"]] + self.assertAccessDependency(location, items) + + +class TestER(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + def test_overworld_hc_chest(self): + # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld + self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) + self.collect_by_name(["Pages 42-43 (Holy Cross)"]) + self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) From 4a85f21c25970a8ebc7b86e7d3915eeb2c266012 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 13 Jan 2024 09:12:43 -0500 Subject: [PATCH 18/78] TUNIC: Update game page for blurb about playing vanilla first (#2712) * Update en_Tunic.md * Change emphasis a bit * Move the "haven't played before" section up * settings -> options * Update worlds/tunic/docs/en_Tunic.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update setup as well with settings -> options and some recent changes to the in-game settings --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/tunic/docs/en_Tunic.md | 11 ++++++++--- worlds/tunic/docs/setup_en.md | 12 +++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/worlds/tunic/docs/en_Tunic.md b/worlds/tunic/docs/en_Tunic.md index 82569195ca..e957f9eafa 100644 --- a/worlds/tunic/docs/en_Tunic.md +++ b/worlds/tunic/docs/en_Tunic.md @@ -1,15 +1,20 @@ # TUNIC -## 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 config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. + +## I haven't played TUNIC before. + +**Play vanilla first.** It is **_heavily discouraged_** to play this randomizer before playing the vanilla game. +It is recommended that you achieve both endings in the vanilla game before playing the randomizer. ## What does randomization do to this game? In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics, and other unique items are shuffled.
-Ability shuffling is an option available from the settings page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), +Ability shuffling is an option available from the options page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), preventing them from being used until they are unlocked.
Enemy randomization and other options are also available and can be turned on in the client mod. diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md index 087509240f..3c13331fe5 100644 --- a/worlds/tunic/docs/setup_en.md +++ b/worlds/tunic/docs/setup_en.md @@ -48,19 +48,17 @@ The filepath to the mod should look like `BepInEx/plugins/Tunic Archipelago/Tuni Launch the game, and if everything was installed correctly you should see `Randomizer + Archipelago Mod Ver. x.y.z` in the top left corner of the title screen! -## Configure Archipelago Settings +## Configure Archipelago Options ### Configure Your YAML File -Visit the [TUNIC settings page](/games/Tunic/player-settings) to generate a YAML with your selected settings. +Visit the [TUNIC options page](/games/Tunic/player-options) to generate a YAML with your selected options. ### Configure Your Mod Settings -Launch the game and click the button labeled `Open Settings File` on the Title Screen. -This will open the settings file in your default text editor, allowing you to edit your connection info. -At the top of the file, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room. -The rest of the settings that appear in this file can be changed in the `Randomizer Settings` submenu of the in-game options menu. +Launch the game and click the button labeled `Open AP Config` on the Title Screen. +In the menu that opens, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room. -Once your player settings have been saved, press `Connect`. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`. +Once you've input your information, click on Close. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`. An error message will display if the game fails to connect to the server. From 0c0adb0745fa206b3a8a3b6766f59d08054681ee Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 13 Jan 2024 18:01:36 +0100 Subject: [PATCH 19/78] Core: update kivy (#2718) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0db55a8035..f604556809 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PyYAML>=6.0.1 jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 -kivy>=2.2.1 +kivy>=2.3.0 bsdiff4>=1.2.4 platformdirs>=4.0.0 certifi>=2023.11.17 From 2725c0258f8f1f2745f1f928db82acf1422b3274 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 13 Jan 2024 12:23:14 -0600 Subject: [PATCH 20/78] Docs: specify that deathlink cause should contain the player name (#2557) * Docs: specify that the cause should contain the player name * accidental whitespace moment * fix table formatting --- docs/network protocol.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 274b6e3716..d10e6519a9 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -675,8 +675,8 @@ Tags are represented as a list of strings, the common Client tags follow: ### DeathLink A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data: -| Name | Type | Notes | -| ---- | ---- | ---- | -| time | float | Unix Time Stamp of time of death. | -| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." | -| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. | +| Name | Type | Notes | +|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| time | float | Unix Time Stamp of time of death. | +| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." | +| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. | From 01fb44c186e9d1157b0a1be932d99f21b39ba3bf Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:10:16 -0500 Subject: [PATCH 21/78] Docs: Added Disabled World information to README.md (#2705) * Add rationale for OriBF being disabled * Removed periods * Added warning to README.md * Apply suggestions from code review Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> * Added disable date Meant to provide context for any updates the world may need (For example, this world would need to change to the new options sstem in 0.4.4) * Moved rationale to local README Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds_disabled/README.md | 8 ++++++++ worlds_disabled/oribf/README.md | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 worlds_disabled/oribf/README.md diff --git a/worlds_disabled/README.md b/worlds_disabled/README.md index b891bc71d4..a7bffe222b 100644 --- a/worlds_disabled/README.md +++ b/worlds_disabled/README.md @@ -3,3 +3,11 @@ This folder is for already merged worlds that are unmaintained and currently broken. If you are interested in fixing and stepping up as maintainer for any of these worlds, please review the [world maintainer](/docs/world%20maintainer.md) documentation. + +## Information for Disabled Worlds + +For each disabled world, a README file can be found detailing when the world was disabled and the reasons that it +was disabled. In order to be considered for reactivation, these concerns should be handled at a bare minimum. However, +each world may have additional issues that also need to be handled, such as deprecated API calls or missing components. + + diff --git a/worlds_disabled/oribf/README.md b/worlds_disabled/oribf/README.md new file mode 100644 index 0000000000..0c78c23bea --- /dev/null +++ b/worlds_disabled/oribf/README.md @@ -0,0 +1,7 @@ +### Ori and the Blind Forest + +This world was disabled for the following reasons: + +* Missing client +* Unmaintained +* Outdated, fails tests as of Jun 29, 2023 From cfd758168cc46b7fd9981a12de868a9c80a010aa Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 13 Jan 2024 19:15:35 -0600 Subject: [PATCH 22/78] Tests: add a test for worlds to not modify the itempool after `create_items` (#1460) * Tests: add a test for worlds to only modify the itempool in `create_items` * extend test multiworld setup instead of a new function * cleanup the test a bit * put more strict wording in `create_items` docstring * list of shame * Don't call `set_rules` before testing * remove ChecksFinder from the list of shame --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Fabian Dill --- test/general/test_items.py | 22 ++++++++++++++++++++-- worlds/AutoWorld.py | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/general/test_items.py b/test/general/test_items.py index 2d8775d535..bd6c3fd853 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -1,5 +1,6 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister + +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -53,7 +54,7 @@ class TestBase(unittest.TestCase): f"{game_name} Item count MUST meet or exceed the number of locations", ) - def testItemsInDatapackage(self): + def test_items_in_datapackage(self): """Test that any created items in the itempool are in the datapackage""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): @@ -69,3 +70,20 @@ class TestBase(unittest.TestCase): with self.subTest("Name should be valid", game=game_name, item=name): self.assertIn(name, valid_names, "All item descriptions must match defined item names") + + def test_itempool_not_modified(self): + """Test that worlds don't modify the itempool after `create_items`""" + gen_steps = ("generate_early", "create_regions", "create_items") + additional_steps = ("set_rules", "generate_basic", "pre_fill") + excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3") + worlds_to_test = {game: world + for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games} + for game_name, world_type in worlds_to_test.items(): + with self.subTest("Game", game=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + created_items = multiworld.itempool.copy() + for step in additional_steps: + with self.subTest("step", step=step): + call_all(multiworld, step) + self.assertEqual(created_items, multiworld.itempool, + f"{game_name} modified the itempool during {step}") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f56c39f690..d4e463db54 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -328,7 +328,7 @@ class World(metaclass=AutoWorldRegister): def create_items(self) -> None: """ - Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted + Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`. """ pass From 73e41cb701f6c17385def548f9b96e5f4b74b1fd Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 13 Jan 2024 19:57:53 -0600 Subject: [PATCH 23/78] Core: migrate start_inventory_from_pool to new options API (#2666) * Core: migrate start_inventory_from_pool to new options API * get the other spot too * skip {} * oops Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Main.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Main.py b/Main.py index 8dac8f7d20..e49d8e781d 100644 --- a/Main.py +++ b/Main.py @@ -114,7 +114,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for _ in range(count): world.push_precollected(world.create_item(item_name, player)) - for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): + for item_name, count in getattr(world.worlds[player].options, + "start_inventory_from_pool", + StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) # remove from_pool items also from early items handling, as starting is plenty early. @@ -167,10 +169,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. - if any(world.start_inventory_from_pool[player].value for player in world.player_ids): + if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids): new_items: List[Item] = [] depletion_pool: Dict[int, Dict[str, int]] = { - player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids} + player: getattr(world.worlds[player].options, + "start_inventory_from_pool", + StartInventoryPool({})).value.copy() + for player in world.player_ids + } for player, items in depletion_pool.items(): player_world: AutoWorld.World = world.worlds[player] for count in items.values(): From 37b03807fd4afd95149e23507928c04ad3fc9f08 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 13 Jan 2024 20:04:12 -0600 Subject: [PATCH 24/78] Core: Log the worlds still using the old options API (#2707) --- worlds/AutoWorld.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d4e463db54..fdc50acc55 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -79,8 +79,8 @@ class AutoWorldRegister(type): if "options_dataclass" not in dct and "option_definitions" in dct: # TODO - switch to deprecate after a version if __debug__: - from warnings import warn - warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") + logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. " + "Please use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) From 962b9b28f0db12f9fa2c98b235a203f3f0570c79 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Jan 2024 03:09:03 +0100 Subject: [PATCH 25/78] Setup: don't install webhost dependencies (#2717) also makes ModuleUpdate detect changed requirements for update() --- ModuleUpdate.py | 21 ++++++++++++++++++--- setup.py | 2 -- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index c33e894e8b..c3dc8c8a87 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -4,14 +4,29 @@ import subprocess import multiprocessing import warnings -local_dir = os.path.dirname(__file__) -requirements_files = {os.path.join(local_dir, 'requirements.txt')} if sys.version_info < (3, 8, 6): raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) -update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process() +_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) +update_ran = _skip_update + + +class RequirementsSet(set): + def add(self, e): + global update_ran + update_ran &= _skip_update + super().add(e) + + def update(self, *s): + global update_ran + update_ran &= _skip_update + super().update(*s) + + +local_dir = os.path.dirname(__file__) +requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),)) if not update_ran: for entry in os.scandir(os.path.join(local_dir, "worlds")): diff --git a/setup.py b/setup.py index 39a93e9385..05e923ed3f 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,6 @@ if __name__ == "__main__": # TODO: move stuff to not require this import ModuleUpdate ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) - ModuleUpdate.update_ran = False # restore for later from worlds.LauncherComponents import components, icon_paths from Utils import version_tuple, is_windows, is_linux @@ -304,7 +303,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): print(f"Outputting to: {self.buildfolder}") os.makedirs(self.buildfolder, exist_ok=True) import ModuleUpdate - ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) ModuleUpdate.update(yes=self.yes) # auto-build cython modules From 6904bd5885f92f9eb0ca15fd3a59c8f440457a64 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 14 Jan 2024 06:31:13 -0800 Subject: [PATCH 26/78] Typing: improve kivy type stubs (#2681) --- .../{graphics.pyi => graphics/__init__.pyi} | 20 ++++--------------- typings/kivy/graphics/texture.pyi | 13 ++++++++++++ typings/kivy/uix/image.pyi | 9 +++++++++ 3 files changed, 26 insertions(+), 16 deletions(-) rename typings/kivy/{graphics.pyi => graphics/__init__.pyi} (54%) create mode 100644 typings/kivy/graphics/texture.pyi create mode 100644 typings/kivy/uix/image.pyi diff --git a/typings/kivy/graphics.pyi b/typings/kivy/graphics/__init__.pyi similarity index 54% rename from typings/kivy/graphics.pyi rename to typings/kivy/graphics/__init__.pyi index 1950910661..a1a5bc02f6 100644 --- a/typings/kivy/graphics.pyi +++ b/typings/kivy/graphics/__init__.pyi @@ -1,24 +1,12 @@ -""" FillType_* is not a real kivy type - just something to fill unknown typing. """ - -from typing import Sequence - -FillType_Vec = Sequence[int] - - -class FillType_Drawable: - def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... - - -class FillType_Texture(FillType_Drawable): - pass +from .texture import FillType_Drawable, FillType_Vec, Texture class FillType_Shape(FillType_Drawable): - texture: FillType_Texture + texture: Texture def __init__(self, *, - texture: FillType_Texture = ..., + texture: Texture = ..., pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... @@ -35,6 +23,6 @@ class Rectangle(FillType_Shape): def __init__(self, *, source: str = ..., - texture: FillType_Texture = ..., + texture: Texture = ..., pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... diff --git a/typings/kivy/graphics/texture.pyi b/typings/kivy/graphics/texture.pyi new file mode 100644 index 0000000000..19e03aad69 --- /dev/null +++ b/typings/kivy/graphics/texture.pyi @@ -0,0 +1,13 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Sequence + +FillType_Vec = Sequence[int] + + +class FillType_Drawable: + def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... + + +class Texture: + pass diff --git a/typings/kivy/uix/image.pyi b/typings/kivy/uix/image.pyi new file mode 100644 index 0000000000..fa014baec7 --- /dev/null +++ b/typings/kivy/uix/image.pyi @@ -0,0 +1,9 @@ +import io + +from kivy.graphics.texture import Texture + + +class CoreImage: + texture: Texture + + def __init__(self, data: io.BytesIO, ext: str) -> None: ... From ed6b7b26704965cd20db5a951fa9568c15480fa2 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 14 Jan 2024 06:48:30 -0800 Subject: [PATCH 27/78] Zillion: remove old option access from item link validation (#2673) * Zillion: remove old option access from item link validation and a little bit a cleaning in other stuff nearby * one option access missed --- worlds/zillion/__init__.py | 20 ++++++++++---------- worlds/zillion/logic.py | 13 ++++++++----- worlds/zillion/options.py | 30 +++++++++++++++--------------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 3f441d12ab..d30bef1444 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import functools import settings import threading import typing -from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast +from typing import Any, Dict, List, Set, Tuple, Optional, cast import os import logging @@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionOptions, ZillionStartChar, validate +from .options import ZillionOptions, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -225,7 +225,7 @@ class ZillionWorld(World): loc.access_rule = access_rule if not (limited_skill >= zz_loc.req): loc.progress_type = LocationProgressType.EXCLUDED - self.multiworld.exclude_locations[p].value.add(loc.name) + self.options.exclude_locations.value.add(loc.name) here.locations.append(loc) self.my_locations.append(loc) @@ -288,15 +288,15 @@ class ZillionWorld(World): if group["game"] == "Zillion": assert "item_pool" in group item_pool = group["item_pool"] - to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ" + to_stay: Chars = "JJ" if "JJ" in item_pool: assert "players" in group group_players = group["players"] - start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) - players_start_chars = [ - (player, start_chars[player].current_option_name) - for player in group_players - ] + players_start_chars: List[Tuple[int, Chars]] = [] + for player in group_players: + z_world = multiworld.worlds[player] + assert isinstance(z_world, ZillionWorld) + players_start_chars.append((player, z_world.options.start_char.get_char())) start_char_counts = Counter(sc for _, sc in players_start_chars) # majority rules if start_char_counts["Apple"] > start_char_counts["Champ"]: @@ -304,7 +304,7 @@ class ZillionWorld(World): elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ") + choices: Tuple[Chars, ...] = ("Apple", "Champ") to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 305546c78b..dcbc6131f1 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,9 +1,11 @@ -from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter +from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter + from BaseClasses import CollectionState + +from zilliandomizer.logic_components.items import Item, items from zilliandomizer.logic_components.locations import Location from zilliandomizer.randomizer import Randomizer -from zilliandomizer.logic_components.items import Item, items -from .region import ZillionLocation + from .item import ZillionItem from .id_maps import item_name_to_id @@ -18,11 +20,12 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: returns a hash of the player and of the set locations with their items """ + from . import ZillionWorld z_world = cs.multiworld.worlds[p] - my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations")) + assert isinstance(z_world, ZillionWorld) _hash = p - for z_loc in my_locations: + for z_loc in z_world.my_locations: zz_name = z_loc.zz_loc.name zz_item = z_loc.item.zz_item \ if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \ diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index cb861e9621..97f8b817f7 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,14 @@ from collections import Counter from dataclasses import dataclass -from typing import Dict, Tuple +from typing import ClassVar, Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice -from zilliandomizer.options import \ - Options as ZzOptions, char_to_gun, char_to_jump, ID, \ - VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts +from zilliandomizer.options import ( + Options as ZzOptions, char_to_gun, char_to_jump, ID, + VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts +) from zilliandomizer.options.parsing import validate as zz_validate @@ -107,6 +108,15 @@ class ZillionStartChar(Choice): display_name = "start character" default = "random" + _name_capitalization: ClassVar[Dict[int, Chars]] = { + option_jj: "JJ", + option_apple: "Apple", + option_champ: "Champ", + } + + def get_char(self) -> Chars: + return ZillionStartChar._name_capitalization[self.value] + class ZillionIDCardCount(Range): """ @@ -348,16 +358,6 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met - name_capitalization: Dict[str, Chars] = { - "jj": "JJ", - "apple": "Apple", - "champ": "Champ", - } - - start_char = options.start_char - start_char_name = name_capitalization[start_char.current_key] - assert start_char_name in chars - starting_cards = options.starting_cards room_gen = options.room_gen @@ -371,7 +371,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": max_level.value, False, # tutorial skill, - start_char_name, + options.start_char.get_char(), floppy_req.value, options.continues.value, bool(options.randomize_alarms.value), From 6ac3d5c6511182282637535bb853da6ec95c15f2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Jan 2024 21:24:34 +0100 Subject: [PATCH 28/78] Core: set consistent server defaults (#2566) --- WebHostLib/templates/generate.html | 10 +++++----- settings.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 33f8dbc09e..53d98dfae6 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -69,8 +69,8 @@ @@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld + +
+
- -
-
diff --git a/settings.py b/settings.py index acae86095c..c58eadf155 100644 --- a/settings.py +++ b/settings.py @@ -597,8 +597,8 @@ class ServerOptions(Group): disable_item_cheat: Union[DisableItemCheat, bool] = False location_check_points: LocationCheckPoints = LocationCheckPoints(1) hint_cost: HintCost = HintCost(10) - release_mode: ReleaseMode = ReleaseMode("goal") - collect_mode: CollectMode = CollectMode("goal") + release_mode: ReleaseMode = ReleaseMode("auto") + collect_mode: CollectMode = CollectMode("auto") remaining_mode: RemainingMode = RemainingMode("goal") auto_shutdown: AutoShutdown = AutoShutdown(0) compatibility: Compatibility = Compatibility(2) @@ -673,7 +673,7 @@ class GeneratorOptions(Group): spoiler: Spoiler = Spoiler(3) glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here? race: Race = Race(0) - plando_options: PlandoOptions = PlandoOptions("bosses") + plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") class SNIOptions(Group): From ad074490bcb07aeec17edf871d4f26ad0835aabe Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Jan 2024 21:30:00 +0100 Subject: [PATCH 29/78] Test: add location access rule benchmark (#2433) --- test/benchmark/__init__.py | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 test/benchmark/__init__.py diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py new file mode 100644 index 0000000000..5f890e8530 --- /dev/null +++ b/test/benchmark/__init__.py @@ -0,0 +1,127 @@ +import time + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") + + +if __name__ == "__main__": + import argparse + import logging + import gc + import collections + import typing + + # makes this module runnable from its folder. + import sys + import os + sys.path.remove(os.path.dirname(__file__)) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + + from Utils import init_logging, local_path + local_path.cached_path = new_home + from BaseClasses import MultiWorld, CollectionState, Location + from worlds import AutoWorld + from worlds.AutoWorld import call_all + + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + if sys.version_info >= (3, 9): + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + else: + @staticmethod + def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + for game in sorted(AutoWorld.AutoWorldRegister.world_types): + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + multiworld = MultiWorld(1) + multiworld.game[1] = game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed(0) + multiworld.state = CollectionState(multiworld) + args = argparse.Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(getattr(option, "default")) + }) + multiworld.set_options(args) + + gc.collect() + for step in self.gen_steps: + with TimeIt(f"{game} step {step}", logger): + call_all(multiworld, step) + gc.collect() + + locations = sorted(multiworld.get_unfilled_locations()) + if not locations: + continue + + all_state = multiworld.get_all_state(False) + for location in locations: + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state/len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state/len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() From 5b93db121f1e73387286cdebbe0993b931038215 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 15 Jan 2024 06:29:30 +0300 Subject: [PATCH 30/78] Stardew Valley: Added missing rule on the club card (#2722) --- worlds/stardew_valley/rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index f56dec39a1..88aa13f314 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp logic.received("Bus Repair").simplify()) MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player), logic.received(Wallet.skull_key).simplify()) + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player), + logic.received("Club Card").simplify()) for floor in range(25, 200 + 25, 25): MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player), logic.can_mine_to_skull_cavern_floor(floor).simplify()) From 6d393fe42b49d7d9ba8b6c513306a0f92209f2ba Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:47:32 -0500 Subject: [PATCH 31/78] TLOZ: update to new options API (#2714) --- worlds/tloz/ItemPool.py | 20 ++++++++++---------- worlds/tloz/Options.py | 14 +++++++------- worlds/tloz/Rules.py | 13 +++++++------ worlds/tloz/__init__.py | 11 ++++++----- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 456598edec..5b90e99722 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -94,17 +94,17 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() final_starting_weapons = [weapon for weapon in starting_weapons - if weapon not in world.multiworld.non_local_items[world.player]] + if weapon not in world.options.non_local_items] if not final_starting_weapons: final_starting_weapons = starting_weapons starting_weapon = random.choice(final_starting_weapons) - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: + if world.options.StartingPosition == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon - elif world.multiworld.StartingPosition[world.player] in \ + elif world.options.StartingPosition in \ [StartingPosition.option_unsafe, StartingPosition.option_dangerous]: - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous: + if world.options.StartingPosition == StartingPosition.option_dangerous: for location in dangerous_weapon_locations: - if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + if world.options.ExpandedPool or "Drop" not in location: start_weapon_locations.append(location) placed_items[random.choice(start_weapon_locations)] = starting_weapon else: @@ -115,7 +115,7 @@ def get_pool_core(world): # Triforce Fragments fragment = "Triforce Fragment" - if world.multiworld.ExpandedPool[world.player]: + if world.options.ExpandedPool: possible_level_locations = [location for location in all_level_locations if location not in level_locations[8]] else: @@ -125,15 +125,15 @@ def get_pool_core(world): if location in possible_level_locations: possible_level_locations.remove(location) for level in range(1, 9): - if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: + if world.options.TriforceLocations == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment - elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons: + elif world.options.TriforceLocations == TriforceLocations.option_dungeons: placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment else: pool.append(fragment) # Level 9 junk fill - if world.multiworld.ExpandedPool[world.player] > 0: + if world.options.ExpandedPool > 0: spots = random.sample(level_locations[8], len(level_locations[8]) // 2) for spot in spots: junk = random.choice(list(minor_items.keys())) @@ -142,7 +142,7 @@ def get_pool_core(world): # Finish Pool final_pool = basic_pool - if world.multiworld.ExpandedPool[world.player]: + if world.options.ExpandedPool: final_pool = { item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) for item in set(basic_pool) | set(minor_items) | set(take_any_items) diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py index 96bd3e296d..58a50ec359 100644 --- a/worlds/tloz/Options.py +++ b/worlds/tloz/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Option, DefaultOnToggle, Choice +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions class ExpandedPool(DefaultOnToggle): @@ -32,9 +33,8 @@ class StartingPosition(Choice): option_dangerous = 2 option_very_dangerous = 3 - -tloz_options: typing.Dict[str, type(Option)] = { - "ExpandedPool": ExpandedPool, - "TriforceLocations": TriforceLocations, - "StartingPosition": StartingPosition -} +@dataclass +class TlozOptions(PerGameCommonOptions): + ExpandedPool: ExpandedPool + TriforceLocations: TriforceLocations + StartingPosition: StartingPosition diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 12bf466bce..b94002f25d 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player world = tloz_world.multiworld + options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): @@ -23,7 +24,7 @@ def set_rules(tloz_world: "TLoZWorld"): # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons for i, level in enumerate(tloz_world.levels[1:10]): for location in level.locations: - if world.StartingPosition[player] < StartingPosition.option_dangerous \ + if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: add_rule(world.get_location(location.name, player), lambda state: state.has_group("weapons", player)) @@ -66,7 +67,7 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) add_rule(world.get_location("Level 7 Boss", player), lambda state: state.has("Recorder", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), lambda state: state.has("Recorder", player)) add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), @@ -75,13 +76,13 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) for location in food_locations: - if world.ExpandedPool[player] or "Drop" not in location: + if options.ExpandedPool or "Drop" not in location: add_rule(world.get_location(location, player), lambda state: state.has("Food", player)) add_rule(world.get_location("Level 8 Item (Magical Key)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) @@ -106,13 +107,13 @@ def set_rules(tloz_world: "TLoZWorld"): for location in stepladder_locations: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: for location in stepladder_locations_expanded: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Take Any Item Left", player), lambda state: state.has_group("candles", player) or state.has("Raft", player)) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 6e8927c4e7..f6aa715239 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -13,7 +13,7 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_loca from .Items import item_table, item_prices, item_game_ids from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations -from .Options import tloz_options +from .Options import TlozOptions from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late from .Rules import set_rules from worlds.AutoWorld import World, WebWorld @@ -63,7 +63,8 @@ class TLoZWorld(World): This randomizer shuffles all the items in the game around, leading to a new adventure every time. """ - option_definitions = tloz_options + options_dataclass = TlozOptions + options = TlozOptions settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False @@ -132,7 +133,7 @@ class TLoZWorld(World): for i, level in enumerate(level_locations): for location in level: - if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + if self.options.ExpandedPool or "Drop" not in location: self.levels[i + 1].locations.append( self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) @@ -144,7 +145,7 @@ class TLoZWorld(World): self.levels[level].locations.append(boss_event) for location in major_locations: - if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + if self.options.ExpandedPool or "Take Any" not in location: overworld.locations.append( self.create_location(location, self.location_name_to_id[location], overworld)) @@ -311,7 +312,7 @@ class TLoZWorld(World): return self.multiworld.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: - if self.multiworld.ExpandedPool[self.player]: + if self.options.ExpandedPool: take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item From d10f8f66c7288787553d3929157bf03b100f3d42 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:48:44 -0700 Subject: [PATCH 32/78] Shivers: Fix rule logic for location 'Final Riddle: Guillotine Dropped' (#2706) --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 62f4cd6a07..57488ff333 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -151,7 +151,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player), "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player), "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player), - "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) + "Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player)) }, "elevators": { "Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)) From 518b04c08eb45d6dad3db6d8ae5075e15c5c48fc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:17:46 +0100 Subject: [PATCH 33/78] SoE: minor typing and style fixes (#2724) * SoE: fix typing for tests * SoE: explicitly export pyevermizer To support loading the module from source (rather than module) we import pyevermizer from `__init__.py` in other files. This has been an implicit export and `mypy --strict` disables implicit exports, so we export it explicitly now. * SoE: fix style in patch.py * SoE: remove unused imports * SoE: fix format mistakes * SoE: cleaner typing in SoEOptions.flags as suggested by beauxq --- worlds/soe/__init__.py | 9 +++++---- worlds/soe/options.py | 8 +++++--- worlds/soe/patch.py | 2 +- worlds/soe/test/__init__.py | 4 ++-- worlds/soe/test/test_access.py | 6 +++--- worlds/soe/test/test_goal.py | 12 ++++++------ worlds/soe/test/test_oob.py | 4 ++-- worlds/soe/test/test_sequence_breaks.py | 4 ++-- worlds/soe/test/test_traps.py | 3 ++- 9 files changed, 28 insertions(+), 24 deletions(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index b431e471e2..74387fb1be 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -13,12 +13,15 @@ from Utils import output_path from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule from .logic import SoEPlayerLogic -from .options import AvailableFragments, Difficulty, EnergyCore, RequiredFragments, SoEOptions, TrapChance +from .options import Difficulty, EnergyCore, SoEOptions from .patch import SoEDeltaPatch, get_base_rom_path if typing.TYPE_CHECKING: from BaseClasses import MultiWorld, CollectionState +__all__ = ["pyevermizer", "SoEWorld"] + + """ In evermizer: @@ -158,7 +161,7 @@ class SoESettings(settings.Group): class SoEWorld(World): """ Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a - space station where the final boss must be defeated. + space station where the final boss must be defeated. """ game: typing.ClassVar[str] = "Secret of Evermore" options_dataclass = SoEOptions @@ -370,8 +373,6 @@ class SoEWorld(World): self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? def generate_output(self, output_directory: str) -> None: - from dataclasses import asdict - player_name = self.multiworld.get_player_name(self.player) self.connect_name = player_name[:32] while len(self.connect_name.encode('utf-8')) > 32: diff --git a/worlds/soe/options.py b/worlds/soe/options.py index 0436b17618..cb9e9bb6de 100644 --- a/worlds/soe/options.py +++ b/worlds/soe/options.py @@ -1,8 +1,8 @@ from dataclasses import dataclass, fields from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol -from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, ProgressionBalancing, \ - Range, Toggle +from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ + ProgressionBalancing, Range, Toggle # typing boilerplate @@ -294,5 +294,7 @@ class SoEOptions(PerGameCommonOptions): for field in fields(self): option = getattr(self, field.name) if isinstance(option, (EvermizerFlag, EvermizerFlags)): - flags += getattr(self, field.name).to_flag() + assert isinstance(option, Option) + # noinspection PyUnresolvedReferences + flags += option.to_flag() return flags diff --git a/worlds/soe/patch.py b/worlds/soe/patch.py index 8270f2d86d..a322de2af6 100644 --- a/worlds/soe/patch.py +++ b/worlds/soe/patch.py @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: return file_name -def read_rom(stream: BinaryIO, strip_header: bool=True) -> bytes: +def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes: """Reads rom into bytearray and optionally strips off any smc header""" data = stream.read() if strip_header and len(data) % 0x400 == 0x200: diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index b3ba7018e4..1ab8521630 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase): game = "Secret of Evermore" def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), - satisfied=True) -> None: + satisfied: bool = True) -> None: """ Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True. Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True @@ -19,7 +19,7 @@ class SoETestBase(WorldTestBase): self.assertFalse(self.can_reach_location(location), f"{location} is reachable but shouldn't be") - def testRocketPartsExist(self): + def testRocketPartsExist(self) -> None: """Tests that rocket parts exist and are unique""" self.assertEqual(len(self.get_items_by_name("Gauge")), 1) self.assertEqual(len(self.get_items_by_name("Wheel")), 1) diff --git a/worlds/soe/test/test_access.py b/worlds/soe/test/test_access.py index 81b8818eb5..f1d6ee993b 100644 --- a/worlds/soe/test/test_access.py +++ b/worlds/soe/test/test_access.py @@ -4,10 +4,10 @@ from . import SoETestBase class AccessTest(SoETestBase): @staticmethod - def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): + def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]: return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] - def test_bronze_axe(self): + def test_bronze_axe(self) -> None: gourds = { "Pyramid bottom": (118, 121, 122, 123, 124, 125), "Pyramid top": (140,) @@ -16,7 +16,7 @@ class AccessTest(SoETestBase): items = [["Bronze Axe"]] self.assertAccessDependency(locations, items) - def test_bronze_spear_plus(self): + def test_bronze_spear_plus(self) -> None: locations = ["Megataur"] items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] self.assertAccessDependency(locations, items) diff --git a/worlds/soe/test/test_goal.py b/worlds/soe/test/test_goal.py index 885c2a74ef..bb64b8eca7 100644 --- a/worlds/soe/test/test_goal.py +++ b/worlds/soe/test/test_goal.py @@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase): "required_fragments": 20, } - def test_fragments(self): + def test_fragments(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) # 0 fragments fragments = self.get_items_by_name("Energy Core Fragment") @@ -24,11 +24,11 @@ class TestFragmentGoal(SoETestBase): self.assertEqual(self.count("Energy Core Fragment"), 21) self.assertBeatable(True) - def test_no_weapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) self.assertBeatable(False) - def test_no_rocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) self.assertBeatable(False) @@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase): "energy_core": "shuffle", } - def test_core(self): + def test_core(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) self.collect_by_name(["Energy Core"]) self.assertBeatable(True) - def test_no_weapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) self.assertBeatable(False) - def test_no_rocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) self.assertBeatable(False) diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py index 969e93d4f6..3c1a2829de 100644 --- a/worlds/soe/test/test_oob.py +++ b/worlds/soe/test/test_oob.py @@ -6,7 +6,7 @@ class OoBTest(SoETestBase): """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} - def test_oob_access(self): + def test_oob_access(self) -> None: in_logic = self.options["out_of_bounds"] == "logic" # some locations that just need a weapon + OoB @@ -37,7 +37,7 @@ class OoBTest(SoETestBase): self.collect_by_name("Diamond Eye") self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) - def test_oob_goal(self): + def test_oob_goal(self) -> None: # still need Energy Core with OoB if sequence breaks are not in logic for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: self.collect_by_name(item) diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py index 8a7f9c64ed..2da8c9242c 100644 --- a/worlds/soe/test/test_sequence_breaks.py +++ b/worlds/soe/test/test_sequence_breaks.py @@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase): """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} - def test_sequence_breaks_access(self): + def test_sequence_breaks_access(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # some locations that just need any weapon + sequence break @@ -30,7 +30,7 @@ class SequenceBreaksTest(SoETestBase): self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead self.assertEqual(self.can_reach_location("Escape"), in_logic) - def test_sequence_breaks_goal(self): + def test_sequence_breaks_goal(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # don't need Energy Core with sequence breaks in logic diff --git a/worlds/soe/test/test_traps.py b/worlds/soe/test/test_traps.py index f83a37be82..7babd4522b 100644 --- a/worlds/soe/test/test_traps.py +++ b/worlds/soe/test/test_traps.py @@ -32,7 +32,8 @@ class Bases: def test_trap_count(self) -> None: """Test that total trap count is correct""" - self.assertEqual(self.options["trap_count"], len(self.get_items_by_name(self.option_name_to_item_name.values()))) + self.assertEqual(self.options["trap_count"], + len(self.get_items_by_name(self.option_name_to_item_name.values()))) class TestTrapAllZeroChance(Bases.TrapTestBase): From b4077a0717ecd990e9ef4f3f18743d319a59e075 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:19:18 -0500 Subject: [PATCH 34/78] TLOZ: properly assign options (#2726) whoops used a = instead of a : mad that im doing a literal one character change PR :/ --- worlds/tloz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index f6aa715239..259bfe2047 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -64,7 +64,7 @@ class TLoZWorld(World): every time. """ options_dataclass = TlozOptions - options = TlozOptions + options: TlozOptions settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False From 79e2f7e35727a1e5757b41fed1abce18b4c61260 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Jan 2024 20:50:16 +0100 Subject: [PATCH 35/78] Tests: test that World.options is not set on the class (#2725) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/general/test_options.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/general/test_options.py b/test/general/test_options.py index e1136f93c9..211704dfe6 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -10,3 +10,10 @@ class TestOptions(unittest.TestCase): for option_key, option in world_type.options_dataclass.type_hints.items(): with self.subTest(game=gamename, option=option_key): self.assertTrue(option.__doc__) + + def test_options_are_not_set_by_world(self): + """Test that options attribute is not already set""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertFalse(hasattr(world_type, "options"), + f"Unexpected assignment to {world_type.__name__}.options!") From 30ec080449c594d41ad8913476ddd29e58756f20 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:21:02 -0500 Subject: [PATCH 36/78] FFMQ: Reset protection (#2727) Bizhawk's "hard reset" option fills RAM with 0x55s. This causes game completion to be erroneously flagged, and likely many erroneous location checks with it. This fix checks for 0x55 and will not proceed to process anything if present. --- worlds/ffmq/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index c53f275017..7de486314c 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ class FFMQClient(SNIClient): received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 == b'\x00' or check_2 == b'\x00': + if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): return def get_range(data_range): From d756960a0b81c3e042ac690d0c03acbc89e09b4d Mon Sep 17 00:00:00 2001 From: Held_der_Zeit <76132257+ZeitHeld@users.noreply.github.com> Date: Tue, 16 Jan 2024 06:54:48 +0100 Subject: [PATCH 37/78] Worlds Docs: Translations German (Clique, BK Sudoku, OoT) (#2581) * Sudoku German * German OOT (+ Room Image) * German Clique * german translation * translation flexibility - ff1 * german setup - oot * Transaltion Flexibilty - SM64 * translation flexibilty - factorio * translation flexibilty - kh2 * translation flexibility - Super Metroid * translation flexibility - Stardew Valley * german translation added - clique * translation flexibility - terraria * translation flexibilty - checksfinder * Sudoku Setup - Grammar Fix * Sudoku Main - Fix Grammar * Revert "translation flexibility - ff1" This reverts commit 6df434c682ef31dbedb88a90137bdc5103b12062. * Revert "Transaltion Flexibilty - SM64" This reverts commit 754bf95d2f9fa75bb5681bb2f6ad37faf1393b14. * Revert "translation flexibilty - factorio" This reverts commit db1226a9dec901e3a5f107ffa53612fe5cf001f0. * Revert "translation flexibility - Super Metroid" This reverts commit ca5bd9a64aa81b70bfb7e35b4e4bd137d93b4f90. * Revert "translation flexibilty - kh2" This reverts commit 076534ee32573f61c64861e2d2f940da95696272. * Revert "translation flexibility - Stardew Valley" This reverts commit 4b137013942262f63e1fbafae6248883b7956f51. * Revert "translation flexibility - terraria" This reverts commit a0abfc8a038d0519dfc55af6155aa62a74399def. * Revert "translation flexibilty - checksfinder" This reverts commit a4de49961d799e0301694b1629d8942780f4a325. * Sugesstion - Fixes in Grammar (and Typos) One or two suggesstions need to be changed a bit further (such as an incomplete sentence) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update guide_de.md * Update setup_de.md * Update de_Sudoku.md * Update __init__.py * Update worlds/oot/docs/setup_de.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/bk_sudoku/__init__.py | 29 ++++--- worlds/bk_sudoku/docs/de_Sudoku.md | 21 +++++ worlds/bk_sudoku/docs/setup_de.md | 27 ++++++ worlds/clique/__init__.py | 30 ++++--- worlds/clique/docs/de_Clique.md | 18 ++++ worlds/clique/docs/guide_de.md | 25 ++++++ worlds/oot/__init__.py | 11 ++- worlds/oot/docs/MultiWorld-Room_oot.png | Bin 0 -> 56829 bytes worlds/oot/docs/de_Ocarina of Time.md | 41 +++++++++ worlds/oot/docs/setup_de.md | 108 ++++++++++++++++++++++++ 10 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 worlds/bk_sudoku/docs/de_Sudoku.md create mode 100644 worlds/bk_sudoku/docs/setup_de.md create mode 100644 worlds/clique/docs/de_Clique.md create mode 100644 worlds/clique/docs/guide_de.md create mode 100644 worlds/oot/docs/MultiWorld-Room_oot.png create mode 100644 worlds/oot/docs/de_Ocarina of Time.md create mode 100644 worlds/oot/docs/setup_de.md diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 36d863bb44..195339c380 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -7,16 +7,25 @@ from ..AutoWorld import WebWorld, World class Bk_SudokuWebWorld(WebWorld): options_page = "games/Sudoku/info/en" theme = 'partyTime' - tutorials = [ - Tutorial( - tutorial_name='Setup Guide', - description='A guide to playing BK Sudoku', - language='English', - file_name='setup_en.md', - link='setup/en', - authors=['Jarno'] - ) - ] + + setup_en = Tutorial( + tutorial_name='Setup Guide', + description='A guide to playing BK Sudoku', + language='English', + file_name='setup_en.md', + link='setup/en', + authors=['Jarno'] + ) + setup_de = Tutorial( + tutorial_name='Setup Anleitung', + description='Eine Anleitung um BK-Sudoku zu spielen', + language='Deutsch', + file_name='setup_de.md', + link='setup/de', + authors=['Held_der_Zeit'] + ) + + tutorials = [setup_en, setup_de] class Bk_SudokuWorld(World): diff --git a/worlds/bk_sudoku/docs/de_Sudoku.md b/worlds/bk_sudoku/docs/de_Sudoku.md new file mode 100644 index 0000000000..abb50c5498 --- /dev/null +++ b/worlds/bk_sudoku/docs/de_Sudoku.md @@ -0,0 +1,21 @@ +# BK-Sudoku + +## Was ist das für ein Spiel? + +BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder +beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis +für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein +weitere „Checks” zu erreichen. +(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu +spielen/generieren.) + +## Wie werden Hinweise freigeschalten? + +Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen +Gegenstand der noch nicht gefunden wurde. + +## Wo ist die Seite für die Einstellungen? + +Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen +kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der +Schwierigkeitsgrad des Sudoku ausgewählt werden. diff --git a/worlds/bk_sudoku/docs/setup_de.md b/worlds/bk_sudoku/docs/setup_de.md new file mode 100644 index 0000000000..71a8e5f624 --- /dev/null +++ b/worlds/bk_sudoku/docs/setup_de.md @@ -0,0 +1,27 @@ +# BK-Sudoku Setup Anleitung + +## Benötigte Software +- [Bk-Sudoku](https://github.com/Jarno458/sudoku) +- Windows 8 oder höher + +## Generelles Konzept + +Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku +spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten. + +Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig +eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt. + +## Installationsprozess + +Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases). +Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei. + +## Verbinden mit einer Multiworld + +1. Starte `Bk_Sudoku.exe` +2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest +3. Trage die Server-URL und den Port ein +4. Drücke auf Verbinden (connect) +5. Wähle deinen Schwierigkeitsgrad +6. Versuche das Sudoku zu Lösen diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 5838389047..30c0e47f81 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -11,16 +11,26 @@ from .Rules import get_button_rule class CliqueWebWorld(WebWorld): theme = "partyTime" - tutorials = [ - Tutorial( - tutorial_name="Start Guide", - description="A guide to playing Clique.", - language="English", - file_name="guide_en.md", - link="guide/en", - authors=["Phar"] - ) - ] + + setup_en = Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Clique.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["Phar"] + ) + + setup_de = Tutorial( + tutorial_name="Anleitung zum Anfangen", + description="Eine Anleitung um Clique zu spielen.", + language="Deutsch", + file_name="guide_de.md", + link="guide/de", + authors=["Held_der_Zeit"] + ) + + tutorials = [setup_en, setup_de] class CliqueWorld(World): diff --git a/worlds/clique/docs/de_Clique.md b/worlds/clique/docs/de_Clique.md new file mode 100644 index 0000000000..cde0a23cf6 --- /dev/null +++ b/worlds/clique/docs/de_Clique.md @@ -0,0 +1,18 @@ +# Clique + +## Was ist das für ein Spiel? + +~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~ +~~(rote) Knöpfe zu drücken.~~ + +Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach +es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten +Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand +anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann. + +Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden. + +## Wo ist die Seite für die Einstellungen? + +Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um +eine YAML-Datei zu konfigurieren und zu exportieren. diff --git a/worlds/clique/docs/guide_de.md b/worlds/clique/docs/guide_de.md new file mode 100644 index 0000000000..26e08dbbdd --- /dev/null +++ b/worlds/clique/docs/guide_de.md @@ -0,0 +1,25 @@ +# Clique Anleitung + +Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib +Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden). + +Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten. + +Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst. +Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf +deinem Handy starten und produktiv sein während du wartest! + +Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche +(mindestens) eins der Folgenden: + +- Dein Zimmer aufräumen. +- Die Wäsche machen. +- Etwas Essen von einem X-Belieben Fast Food Restaruant holen. +- Das tägliche Wordle machen. +- ~~Deine Seele an **Phar** verkaufen.~~ +- Deine Hausaufgaben erledigen. +- Deine Post abholen. + + +~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~ +~~Discord kontaktieren. *zwinker* *zwinker*~~ diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index e9c889d6f6..eb9c41f0b0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -118,7 +118,16 @@ class OOTWeb(WebWorld): ["TheLynk"] ) - tutorials = [setup, setup_es, setup_fr] + setup_de = Tutorial( + setup.tutorial_name, + setup.description, + "Deutsch", + "setup_de.md", + "setup/de", + ["Held_der_Zeit"] + ) + + tutorials = [setup, setup_es, setup_fr, setup_de] class OOTWorld(World): diff --git a/worlds/oot/docs/MultiWorld-Room_oot.png b/worlds/oot/docs/MultiWorld-Room_oot.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f224e5e1af13016d371fbdc87455b0110ee4ae GIT binary patch literal 56829 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

HgOEE#j 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 literal 0 HcmV?d00001 diff --git a/worlds/oot/docs/de_Ocarina of Time.md b/worlds/oot/docs/de_Ocarina of Time.md new file mode 100644 index 0000000000..4d9fd2ea14 --- /dev/null +++ b/worlds/oot/docs/de_Ocarina of Time.md @@ -0,0 +1,41 @@ +# The Legend of Zelda: Ocarina of Time + +## Wo ist die Seite für die Einstellungen? + +Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um +eine YAML-Datei zu konfigurieren und zu exportieren. + +## Was macht der Randomizer in diesem Spiel? + +Items, welche der Spieler für gewöhnlich im Verlauf des Spiels erhalten würde, wurden umhergemischt. Die Logik bleit +bestehen, damit ist das Spiel immer durchspielbar. Doch weil die Items durch das ganze Spiel gemischt wurden, müssen + manche Bereiche früher bescuht werden, als man es in Vanilla tun würde. +Eine Liste von implementierter Logik, die unoffensichtlich erscheinen kann, kann +[hier (Englisch)](https://wiki.ootrandomizer.com/index.php?title=Logic) gefunden werden. + +## Welche Items und Bereiche werden gemischt? + +Alle ausrüstbare und sammelbare Gegenstände, sowie Munition können gemischt werden. Und alle Bereiche, die einen +dieser Items enthalten könnten, haben (sehr wahrscheinlich) ihren Inhalt verändert. Goldene Skulltulas können ebenfalls +dazugezählt werden, je nach Wunsch des Spielers. + +## Welche Items können in sich in der Welt eines anderen Spielers befinden? + +Jedes dieser Items, die gemicht werden können, können in einer Multiworld auch in der Welt eines anderen Spielers +fallen. Es ist jedoch möglich ausgewählte Items auf deine eigene Welt zu beschränken. + +## Wie sieht ein Item einer anderen Welt in OoT aus? + +Items, die zu einer anderen Welt gehören, werden repräsentiert durch Zelda's Brief. + +## Was passiert, wenn der Spieler ein Item erhält? + +Sobald der Spieler ein Item erhält, wird Link das Item über seinen Kopf halten und der ganzen Welt präsentieren. +Gut für's Geschäft! + +## Einzigartige Lokale Befehle + +Die folgenden Befehle stehen nur im OoTClient, um mit Archipelago zu spielen, zur Verfügung: + +- `/n64` Überprüffe den Verbindungsstatus deiner N64 +- `/deathlink` Schalte den "Deathlink" des Clients um. Überschreibt die zuvor konfigurierten Einstellungen. diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md new file mode 100644 index 0000000000..92c3150a7d --- /dev/null +++ b/worlds/oot/docs/setup_de.md @@ -0,0 +1,108 @@ +# Setup Anleitung für Ocarina of Time: Archipelago Edition + +## WICHTIG + +Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux. + +## Benötigte Software + +- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen. + - Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden. + - Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über + den obrigen Link gefunden werden. +- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert + werden kann. +- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!) + +## Konfigurieren von BizHawk + +Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen: + +- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu + `"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren. + **ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und** + **wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die** + **Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.** +- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann + den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal + abstürzen. +- **(Optional)** Unter `Config > Customize` kannst du die Haken in den "Run in background" + (Laufe weiter im Hintergrund) und "Accept background input" (akzeptiere Tastendruck im Hintergrund) Kästchen setzen. + Dies erlaubt dir das Spiel im Hintergrund weiter zu spielen, selbst wenn ein anderes Fenster aktiv ist. (Nützlich bei + mehreren oder eher großen Bildschrimen/Monitoren.) +- Unter `Config > Hotkeys` sind viele Hotkeys, die mit oft genuten Tasten belegt worden sind. Es wird empfohlen die + meisten (oder alle) Hotkeys zu deaktivieren. Dies kann schnell mit `Esc` erledigt werden. +- Wird mit einem Kontroller gespielt, bei der Tastenbelegung (bei einem Laufendem Spiel, unter + `Config > Controllers...`), deaktiviere "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" und gehe stattdessen in + den Reiter `Analog Controls` um den Stick zu belegen, da sonst Probleme beim Zielen auftreten (mit dem Bogen oder + ähnliches). Y-Axis ist für Oben und Unten, und die X-Axis ist für Links und Rechts. +- Unter `N64` setze einen Haken bei "Use Expansion Slot" (Benutze Erweiterungs-Slot). Dies wird benötigt damit + savestates/schnellspeichern funktioniert. (Das N64-Menü taucht nur **nach** dem laden einer N64-ROM auf.) + +Es wird sehr empfohlen N64 Rom-Erweiterungen (\*.n64, \*.z64) mit dem Emuhawk - welcher zuvor installiert wurde - zu +verknüpfen. +Um dies zu tun, muss eine beliebige N64 Rom aufgefunden werden, welche in deinem Besitz ist, diese Rechtsklicken und +dann auf "Öffnen mit..." gehen. Gehe dann auf "Andere App auswählen" und suche nach deinen BizHawk-Ordner, in der +sich der Emulator befindet, und wähle dann `EmuHawk.exe` **(NICHT "DiscoHawk.exe"!)** aus. + +Eine Alternative BizHawk Setup Anleitung (auf Englisch), sowie weitere Hilfe bei Problemen kann +[hier](https://wiki.ootrandomizer.com/index.php?title=Bizhawk) gefunden werden. + +## Erstelle eine YAML-Datei + +### Was ist eine YAML-Datei und Warum brauch ich eine? + +Eine YAML-Datie enthält einen Satz an einstellbaren Optionen, die dem Generator mitteilen, wie +dein Spiel generiert werden soll. In einer Multiworld stellt jeder Spieler eine eigene YAML-Datei zur Verfügung. Dies +erlaubt jeden Spieler eine personalisierte Erfahrung nach derem Geschmack. Damit kann auch jeder Spieler in einer +Multiworld (des gleichen Spiels) völlig unterschiedliche Einstellungen haben. + +Für weitere Informationen, besuche die allgemeine Anleitung zum Erstellen einer +YAML-Datei: [Archipelago Setup Anleitung](/tutorial/Archipelago/setup/en) + +### Woher bekomme ich eine YAML-Datei? + +Die Seite für die Spielereinstellungen auf dieser Website erlaubt es dir deine persönlichen Einstellungen nach +vorlieben zu konfigurieren und eine YAML-Datei zu exportieren. +Seite für die Spielereinstellungen: +[Seite für die Spielereinstellungen von Ocarina of Time](/games/Ocarina%20of%20Time/player-options) + +### Überprüfen deiner YAML-Datei + +Wenn du deine YAML-Datei überprüfen möchtest, um sicher zu gehen, dass sie funktioniert, kannst du dies auf der +YAML-Überprüfungsseite tun. +YAML-Überprüfungsseite: [YAML-Überprüfungsseite](/check) + +## Beitreten einer Multiworld + +### Erhalte deinen OoT-Patch + +(Der folgende Prozess ist bei den meisten ROM-basierenden Spielen sehr ähnlich.) + +Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem Host abzugeben. Ist dies getan, +erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle +teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren +und klicke dann auf `Download APZ5 File...`. +![Screenshot of a Multiworld Room with an Ocarina of Time Player](/static/generated/docs/Ocarina%20of%20Time/MultiWorld-room_oot.png) + +Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen +deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch +(sofern das "Öffnen mit..." ausgewählt wurde). + +### Verbinde zum Multiserver + +Sind einmal der Client und der Emulator gestartet, müssen sie nur noch miteinander verbunden werden. Gehe dazu in +deinen Archipelago-Ordner, dann zu `data/lua`, und füge das `connector_oot.lua` Script per Drag&Drop (ziehen und +fallen lassen) auf das EmuHawk-Fenster. (Alternativ kannst du die Lua-Konsole manuell öffnen, gehe dazu auf +`Script > Open Script` und durchsuche die Ordner nach `data/lua/connector_oot.lua`.) + +Um den Client mit dem Multiserver zu verbinden, füge einfach `:` in das Textfeld ganz oben im +Client ein und drücke Enter oder "Connect" (verbinden). Wird ein Passwort benötigt, musst du es danach unten in das +Textfeld (für den Chat und Befehle) eingeben. +Alternativ kannst du auch in dem unterem Textfeld den folgenden Befehl schreiben: +`/connect : [Passwort]` (wie die Adresse und der Port lautet steht in dem Raum, oder wird von deinem +Host an dich weitergegeben.) +Beispiel: `/connect archipelago.gg:12345 Passw123` + +Du bist nun bereit für dein Zeitreise-Abenteuer in Hyrule. From adad7b532de9c94fe8d6e2217b1edf83ba183cb7 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 16 Jan 2024 07:09:54 -0500 Subject: [PATCH 38/78] Lingo: Turn The Colorful into a countdown achievement (#2710) The Colorful currently, in logic, does not expect you to solve the achievement panel until all of the doors are opened. This is not enforced by the client in complex door shuffle. It is also not typical of how achievements in Lingo usually work, and it ended up this way because of the fact that The Colorful is, uniquely, not a countdown panel. This change modifies logic so that solving each panel within The Colorful is required in order to access the achievement, rather than opening all of the doors. This will be accompanied by a change to the client that will turn the achievement panel into a countdown. --- worlds/lingo/data/LL1.yaml | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index ea5886fea0..cc46677990 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -2635,12 +2635,6 @@ panels: - OBSTACLE The Colorful: - # The set of required_doors in the achievement panel should prevent - # generation from asking you to solve The Colorful before opening all of the - # doors. Access from the roof is included so that the painting here could be - # an entrance. The client will have to be hardcoded to not open the door to - # the achievement until all of the doors are open, whether by solving the - # panels or through receiving items. entrances: The Colorful (Gray): room: The Colorful (Gray) @@ -2651,27 +2645,27 @@ id: Countdown Panels/Panel_colorful_colorful check: True tag: forbid - required_door: + required_panel: - room: The Colorful (White) - door: Progress Door + panel: BEGIN - room: The Colorful (Black) - door: Progress Door + panel: FOUND - room: The Colorful (Red) - door: Progress Door + panel: LOAF - room: The Colorful (Yellow) - door: Progress Door + panel: CREAM - room: The Colorful (Blue) - door: Progress Door + panel: SUN - room: The Colorful (Purple) - door: Progress Door + panel: SPOON - room: The Colorful (Orange) - door: Progress Door + panel: LETTERS - room: The Colorful (Green) - door: Progress Door + panel: WALLS - room: The Colorful (Brown) - door: Progress Door + panel: IRON - room: The Colorful (Gray) - door: Progress Door + panel: OBSTACLE achievement: The Colorful paintings: - id: arrows_painting_12 From c6896c6af9dd408b6827f031db06afcb7eeebaf7 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 16 Jan 2024 07:11:20 -0500 Subject: [PATCH 39/78] Lingo: Make The Colorful optionally progressive (#2711) --- worlds/lingo/data/LL1.yaml | 22 ++++++++++++++++++++++ worlds/lingo/data/ids.yaml | 1 + worlds/lingo/items.py | 6 ++++++ worlds/lingo/options.py | 8 ++++++++ worlds/lingo/player_logic.py | 3 ++- 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index cc46677990..d7d4630e86 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -2670,6 +2670,28 @@ paintings: - id: arrows_painting_12 orientation: north + progression: + Progressive Colorful: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door Welcome Back Area: entrances: Starting Room: diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 3239f21854..2b9e7f3d8c 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1452,3 +1452,4 @@ progression: Progressive Fearless: 444470 Progressive Orange Tower: 444482 Progressive Art Gallery: 444563 + Progressive Colorful: 444580 diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index af24570f27..7b1a650561 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -28,6 +28,10 @@ class ItemData(NamedTuple): # door shuffle is on and tower isn't progressive return world.options.shuffle_doors != ShuffleDoors.option_none \ and not world.options.progressive_orange_tower + elif self.mode == "the colorful": + # complex door shuffle is on and colorful isn't progressive + return world.options.shuffle_doors == ShuffleDoors.option_complex \ + and not world.options.progressive_colorful elif self.mode == "complex door": return world.options.shuffle_doors == ShuffleDoors.option_complex elif self.mode == "door group": @@ -70,6 +74,8 @@ def load_item_data(): if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]: if room_name == "Orange Tower": door_mode = "orange tower" + elif room_name == "The Colorful": + door_mode = "the colorful" else: door_mode = "special" diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index c00208621f..ec6158fab5 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -21,6 +21,13 @@ class ProgressiveOrangeTower(DefaultOnToggle): display_name = "Progressive Orange Tower" +class ProgressiveColorful(DefaultOnToggle): + """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. + If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them. + If on, there are ten progressive items, which open up the sequence from White forward.""" + display_name = "Progressive Colorful" + + class LocationChecks(Choice): """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. @@ -117,6 +124,7 @@ class DeathLink(Toggle): class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors progressive_orange_tower: ProgressiveOrangeTower + progressive_colorful: ProgressiveColorful location_checks: LocationChecks shuffle_colors: ShuffleColors shuffle_panels: ShufflePanels diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index fa497c59bd..57bcc4bfd5 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -83,7 +83,8 @@ class LingoPlayerLogic: def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: - if room_name == "Orange Tower" and not world.options.progressive_orange_tower: + if (room_name == "Orange Tower" and not world.options.progressive_orange_tower)\ + or (room_name == "The Colorful" and not world.options.progressive_colorful): self.set_door_item(room_name, door_data.name, door_data.item_name) else: progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name From 0efc13fc8a2b8b8a3bfd391a9f8ef5be804337ab Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:12:33 -0500 Subject: [PATCH 40/78] KH2: Location Groups and Subclasses (#2700) --- worlds/kh2/Client.py | 3 +- worlds/kh2/Items.py | 21 +----- worlds/kh2/Locations.py | 145 +++++++++++++++++++-------------------- worlds/kh2/Regions.py | 11 +-- worlds/kh2/Rules.py | 2 +- worlds/kh2/Subclasses.py | 29 ++++++++ worlds/kh2/__init__.py | 10 +-- 7 files changed, 118 insertions(+), 103 deletions(-) create mode 100644 worlds/kh2/Subclasses.py diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 544e710741..513d85257b 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -821,7 +821,8 @@ class KH2Context(CommonContext): def finishedGame(ctx: KH2Context, message): if ctx.kh2slotdata['FinalXemnas'] == 1: - if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked: + if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ + & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: ctx.final_xemnas = True # three proofs if ctx.kh2slotdata['Goal'] == 0: diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index 3e656b418b..cb3d7c8d85 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -2,22 +2,7 @@ import typing from BaseClasses import Item from .Names import ItemName - - -class KH2Item(Item): - game: str = "Kingdom Hearts 2" - - -class ItemData(typing.NamedTuple): - quantity: int = 0 - kh2id: int = 0 - # Save+ mem addr - memaddr: int = 0 - # some items have bitmasks. if bitmask>0 bitor to give item else - bitmask: int = 0 - # if ability then - ability: bool = False - +from .Subclasses import ItemData # 0x130000 Reports_Table = { @@ -209,7 +194,7 @@ Armor_Table = { ItemName.GrandRibbon: ItemData(1, 157, 0x35D4), } Usefull_Table = { - ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per + ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696), ItemName.UnknownDisk: ItemData(1, 462, 0x365F), @@ -349,7 +334,7 @@ GoofyAbility_Table = { Wincon_Table = { ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item - ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), + # ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14 # ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster } diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 9d7d948443..61fafe9094 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1,19 +1,9 @@ import typing from BaseClasses import Location -from .Names import LocationName, ItemName - - -class KH2Location(Location): - game: str = "Kingdom Hearts 2" - - -class LocationData(typing.NamedTuple): - locid: int - yml: str - charName: str = "Sora" - charNumber: int = 1 - +from .Names import LocationName, ItemName, RegionName +from .Subclasses import LocationData +from .Regions import KH2REGIONS # data's addrcheck sys3 addr obtained roomid bit index is eventid LoD_Checks = { @@ -541,7 +531,7 @@ TWTNW_Checks = { LocationName.Xemnas1: LocationData(26, "Double Get Bonus"), LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"), LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"), - LocationName.FinalXemnas: LocationData(71, "Get Bonus"), + # LocationName.FinalXemnas: LocationData(71, "Get Bonus"), LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"), } @@ -806,74 +796,75 @@ Atlantica_Checks = { } event_location_to_item = { - LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, - LocationName.McpEventLocation: ItemName.McpEvent, + LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, + LocationName.McpEventLocation: ItemName.McpEvent, # LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent, - LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, - LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, - LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, - LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, - LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, - LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, - LocationName.CerberusEventLocation: ItemName.CerberusEvent, - LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, - LocationName.HydraEventLocation: ItemName.HydraEvent, + LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, + LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, + LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, + LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, + LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, + LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, + LocationName.CerberusEventLocation: ItemName.CerberusEvent, + LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, + LocationName.HydraEventLocation: ItemName.HydraEvent, LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent, - LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, - LocationName.HadesEventLocation: ItemName.HadesEvent, + LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, + LocationName.HadesEventLocation: ItemName.HadesEvent, # LocationName.ASZexionEventLocation: ItemName.ASZexionEvent, - LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, - LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, - LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, + LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, + LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, + LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, # LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation, - LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, - LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, - LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, - LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, + LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, + LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, + LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, + LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, # LocationName.ASVexenEventLocation: ItemName.ASVexenEvent, - LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, - LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, - LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, - LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, - LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, - LocationName.RoxasEventLocation: ItemName.RoxasEvent, - LocationName.XigbarEventLocation: ItemName.XigbarEvent, - LocationName.LuxordEventLocation: ItemName.LuxordEvent, - LocationName.SaixEventLocation: ItemName.SaixEvent, - LocationName.XemnasEventLocation: ItemName.XemnasEvent, - LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, - LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, + LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, + LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, + LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, + LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, + LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, + LocationName.RoxasEventLocation: ItemName.RoxasEvent, + LocationName.XigbarEventLocation: ItemName.XigbarEvent, + LocationName.LuxordEventLocation: ItemName.LuxordEvent, + LocationName.SaixEventLocation: ItemName.SaixEvent, + LocationName.XemnasEventLocation: ItemName.XemnasEvent, + LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, + LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, # LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent, - LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, - LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, - LocationName.BeastEventLocation: ItemName.BeastEvent, - LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, - LocationName.XaldinEventLocation: ItemName.XaldinEvent, - LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, - LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, - LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, + LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, + LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, + LocationName.BeastEventLocation: ItemName.BeastEvent, + LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, + LocationName.XaldinEventLocation: ItemName.XaldinEvent, + LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, + LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, + LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, # LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent, - LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, - LocationName.ScarEventLocation: ItemName.ScarEvent, - LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, - LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, - LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, - LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, - LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, - LocationName.SephiEventLocation: ItemName.SephiEvent, - LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, - LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, - LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, - LocationName.TransportEventLocation: ItemName.TransportEvent, - LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, - LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, + LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, + LocationName.ScarEventLocation: ItemName.ScarEvent, + LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, + LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, + LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, + LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, + LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, + LocationName.SephiEventLocation: ItemName.SephiEvent, + LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, + LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, + LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, + LocationName.TransportEventLocation: ItemName.TransportEvent, + LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, + LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, # LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent, - LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, - LocationName.TerraEventLocation: ItemName.TerraEvent, - LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, - LocationName.Axel1EventLocation: ItemName.Axel1Event, - LocationName.Axel2EventLocation: ItemName.Axel2Event, - LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, + LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, + LocationName.TerraEventLocation: ItemName.TerraEvent, + LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, + LocationName.Axel1EventLocation: ItemName.Axel1Event, + LocationName.Axel2EventLocation: ItemName.Axel2Event, + LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, + LocationName.FinalXemnasEventLocation: ItemName.Victory, } all_weapon_slot = { LocationName.FAKESlot, @@ -1361,3 +1352,9 @@ exclusion_table = { location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest" } } + +location_groups: typing.Dict[str, list] +location_groups = { + Region_Name: [loc for loc in Region_Locs if "Event" not in loc] + for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs +} diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 6dd8313107..235500ec89 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1,9 +1,11 @@ import typing from BaseClasses import MultiWorld, Region +from . import Locations -from .Locations import KH2Location, event_location_to_item -from . import LocationName, RegionName, Events_Table +from .Subclasses import KH2Location +from .Names import LocationName, RegionName +from .Items import Events_Table KH2REGIONS: typing.Dict[str, typing.List[str]] = { "Menu": [], @@ -788,7 +790,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = { LocationName.ArmoredXemnas2EventLocation ], RegionName.FinalXemnas: [ - LocationName.FinalXemnas + LocationName.FinalXemnasEventLocation ], RegionName.DataXemnas: [ LocationName.XemnasDataPowerBoost, @@ -1020,7 +1022,8 @@ def create_regions(self): multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in KH2REGIONS.items()] # fill the event locations with events - for location, item in event_location_to_item.items(): + + for location, item in Locations.event_location_to_item.items(): multiworld.get_location(location, player).place_locked_item( multiworld.worlds[player].create_event_item(item)) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 111d12d0d6..65f690fdde 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -270,7 +270,7 @@ class KH2WorldRules(KH2Rules): add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) + final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) if self.multiworld.FinalXemnas[self.player]: diff --git a/worlds/kh2/Subclasses.py b/worlds/kh2/Subclasses.py new file mode 100644 index 0000000000..79f52c41c0 --- /dev/null +++ b/worlds/kh2/Subclasses.py @@ -0,0 +1,29 @@ +import typing + +from BaseClasses import Location, Item + + +class KH2Location(Location): + game: str = "Kingdom Hearts 2" + + +class LocationData(typing.NamedTuple): + locid: int + yml: str + charName: str = "Sora" + charNumber: int = 1 + + +class KH2Item(Item): + game: str = "Kingdom Hearts 2" + + +class ItemData(typing.NamedTuple): + quantity: int = 0 + kh2id: int = 0 + # Save+ mem addr + memaddr: int = 0 + # some items have bitmasks. if bitmask>0 bitor to give item else + bitmask: int = 0 + # if ability then + ability: bool = False diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 2bddbd5ec3..d02614d380 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -12,6 +12,7 @@ from .OpenKH import patch_kh2 from .Options import KingdomHearts2Options from .Regions import create_regions, connect_regions from .Rules import * +from .Subclasses import KH2Item def launch_client(): @@ -49,7 +50,9 @@ class KH2World(World): for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)} location_name_to_id = {item: location for location, item in enumerate(all_locations.keys(), 0x130000)} + item_name_groups = item_groups + location_name_groups = location_groups visitlocking_dict: Dict[str, int] plando_locations: Dict[str, str] @@ -253,11 +256,8 @@ class KH2World(World): self.goofy_gen_early() self.keyblade_gen_early() - if self.multiworld.FinalXemnas[self.player]: - self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory - else: - self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name - self.total_locations -= 1 + # final xemnas isn't a location anymore + # self.total_locations -= 1 if self.options.WeaponSlotStartHint: for location in all_weapon_slot: From d390d2eff801ebd8d2d920ff8536439cdf1bfbd6 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 16 Jan 2024 07:13:02 -0500 Subject: [PATCH 41/78] Lingo: Remove colors from Bearer SIXes (#2677) --- worlds/lingo/data/LL1.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index d7d4630e86..32a7659b82 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -4218,9 +4218,6 @@ SIX: id: Backside Room/Panel_six_six_5 tag: midwhite - colors: - - red - - yellow hunt: True required_door: room: Number Hunt @@ -4296,9 +4293,6 @@ SIX: id: Backside Room/Panel_six_six_6 tag: midwhite - colors: - - red - - yellow hunt: True required_door: room: Number Hunt From 7affb885ba533e4abfc74f276eaad3bbb0431e9e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:13:44 +0100 Subject: [PATCH 42/78] The Witness: Add "Town Desert Laser Redirect Control (Panel)" as an item (#2669) --- worlds/witness/Options.py | 10 +++++++--- worlds/witness/WitnessItems.txt | 4 +++- worlds/witness/WitnessLogic.txt | 6 +++--- worlds/witness/WitnessLogicExpert.txt | 6 +++--- worlds/witness/WitnessLogicVanilla.txt | 6 +++--- worlds/witness/player_logic.py | 3 ++- worlds/witness/rules.py | 19 +++++++++++++------ .../Complex_Additional_Panels.txt | 1 + .../settings/Door_Shuffle/Simple_Panels.txt | 2 +- worlds/witness/static_logic.py | 1 - 10 files changed, 36 insertions(+), 22 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 4c4b4f7626..84855bf867 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -114,9 +114,13 @@ class ShufflePostgame(Toggle): class VictoryCondition(Choice): - """Change the victory condition from the original game's ending (elevator) to beating the Challenge - or solving the mountaintop box, either using the short solution - (7 lasers or whatever you've changed it to) or the long solution (11 lasers or whatever you've changed it to).""" + """Set the victory condition for this world. + Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers). + Challenge: Beat the secret Challenge (requires Challenge Lasers). + Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers). + Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers). + It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser + to count, the laser locks on the Elevator and Challenge Timer panels do not.""" display_name = "Victory Condition" option_elevator = 0 option_challenge = 1 diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 750d6bd4eb..6457117909 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -69,6 +69,7 @@ Doors: 1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A 1169 - Town Windmill Entry (Panel) - 0x17F5F 1172 - Town Cargo Box Entry (Panel) - 0x0A0C8 +1173 - Town Desert Laser Redirect Control (Panel) - 0x09F98 1182 - Windmill Turn Control (Panel) - 0x17D02 1184 - Theater Entry (Panel) - 0x17F89 1185 - Theater Video Input (Panel) - 0x00815 @@ -234,7 +235,7 @@ Doors: 2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B 2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675 2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA -2015 - Town Control Panels - 0x2896A,0x334D8 +2015 - Town Control Panels - 0x2896A,0x334D8,0x09F98 2020 - Windmill & Theater Control Panels - 0x17D02,0x00815 2025 - Bunker Control Panels - 0x34BC5,0x34BC6,0x0A079 2030 - Swamp Control Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07 @@ -250,6 +251,7 @@ Doors: 2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8 2135 - Town Maze Panels - 0x2896A,0x28A79 +2137 - Town Dockside House Panels - 0x0A0C8,0x09F98 2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2 2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF 2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index acfbe8c14e..424f990c33 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index b1d9b8e30e..accb640e34 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 779ead6bde..4c5e52c5cb 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Black/White Squares -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index e1ef1ae431..e05199c2b3 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -103,7 +103,8 @@ class WitnessPlayerLogic: if option_entity in self.EVENT_NAMES_BY_HEX: new_items = frozenset({frozenset([option_entity])}) - elif option_entity in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: + elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", + "PP2 Weirdness", "Theater to Tunnels"}: new_items = frozenset({frozenset([option_entity])}) else: new_items = self.reduce_req_within_region(option_entity) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 75c662ac0f..5eded11ad4 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -29,8 +29,9 @@ laser_hexes = [ ] -def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[CollectionState], bool]: - if laser_hex == "0x012FB": +def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, + redirect_required: bool) -> Callable[[CollectionState], bool]: + if laser_hex == "0x012FB" and redirect_required: return lambda state: ( _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state) and state.has("Desert Laser Redirection", player) @@ -39,11 +40,11 @@ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[ return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat) -def _has_lasers(amount: int, world: "WitnessWorld") -> Callable[[CollectionState], bool]: +def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> Callable[[CollectionState], bool]: laser_lambdas = [] for laser_hex in laser_hexes: - has_laser_lambda = _has_laser(laser_hex, world, world.player) + has_laser_lambda = _has_laser(laser_hex, world, world.player, redirect_required) laser_lambdas.append(has_laser_lambda) @@ -155,10 +156,16 @@ def _has_item(item: str, world: "WitnessWorld", player: int, return lambda state: state.can_reach(item, "Region", player) if item == "7 Lasers": laser_req = world.options.mountain_lasers.value - return _has_lasers(laser_req, world) + return _has_lasers(laser_req, world, False) + if item == "7 Lasers + Redirect": + laser_req = world.options.mountain_lasers.value + return _has_lasers(laser_req, world, True) if item == "11 Lasers": laser_req = world.options.challenge_lasers.value - return _has_lasers(laser_req, world) + return _has_lasers(laser_req, world, False) + if item == "11 Lasers + Redirect": + laser_req = world.options.challenge_lasers.value + return _has_lasers(laser_req, world, True) elif item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) elif item == "Theater to Tunnels": diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt index 79bda7ea22..f7acbb2e55 100644 --- a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt +++ b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt @@ -10,6 +10,7 @@ Quarry Boathouse Hook Control (Panel) Monastery Shutters Control (Panel) Town Maze Rooftop Bridge (Panel) Town RGB Control (Panel) +Town Desert Laser Redirect Control (Panel) Windmill Turn Control (Panel) Theater Video Input (Panel) Bunker Drop-Down Door Controls (Panel) diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt index 79da154491..42258bca1a 100644 --- a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt +++ b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt @@ -10,7 +10,7 @@ Monastery Panels Town Church & RGB House Panels Town Maze Panels Windmill & Theater Panels -Town Cargo Box Entry (Panel) +Town Dockside House Panels Treehouse Panels Bunker Panels Swamp Panels diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 29c171d45c..0e8d649af6 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -109,7 +109,6 @@ class StaticWitnessLogicObj: "Laser", "Laser Hedges", "Laser Pressure Plates", - "Desert Laser Redirect" } is_vault_or_video = "Vault" in entity_name or "Video" in entity_name From fe3bc8d6be2d260f83910f8539f912b974775d4f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:14:38 +0100 Subject: [PATCH 43/78] The Witness: Add Obelisk Side locations to always and priority hints (#2665) --- worlds/witness/hints.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index d238aa4adf..68fc68946b 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -187,8 +187,8 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]: return always -def get_always_hint_locations(_: "WitnessWorld") -> List[str]: - return [ +def get_always_hint_locations(world: "WitnessWorld") -> List[str]: + always = [ "Challenge Vault Box", "Mountain Bottom Floor Discard", "Theater Eclipse EP", @@ -196,6 +196,16 @@ def get_always_hint_locations(_: "WitnessWorld") -> List[str]: "Mountainside Cloud Cycle EP", ] + # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side + if world.options.EP_difficulty == "eclipse": + always.append("Town Obelisk Side 6") # Eclipse EP + + if world.options.EP_difficulty != "normal": + always.append("Treehouse Obelisk Side 4") # Couch EP + always.append("River Obelisk Side 1") # Cloud Cycle EP. Needs to be changed to "Mountainside Obelisk" soon + + return always + def get_priority_hint_items(world: "WitnessWorld") -> List[str]: priority = { @@ -249,8 +259,8 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: return sorted(priority) -def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: - return [ +def get_priority_hint_locations(world: "WitnessWorld") -> List[str]: + priority = [ "Swamp Purple Underwater", "Shipwreck Vault Box", "Town RGB Room Left", @@ -265,6 +275,13 @@ def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", ] + + # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side + if world.options.EP_difficulty != "normal": + priority.append("Town Obelisk Side 6") # Theater Flowers EP + priority.append("Treehouse Obelisk Side 4") # Shipwreck Green EP + + return priority def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): From d000b52ae0cf346b4ff730c5c9d0eff3fc9b44c5 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Tue, 16 Jan 2024 13:38:19 +0100 Subject: [PATCH 44/78] V6: Use new options api (#2668) * v6: Use new options API * v6: Add display names for some options --- worlds/v6/Options.py | 21 ++++++++++++--------- worlds/v6/Regions.py | 11 ----------- worlds/v6/Rules.py | 28 +++++++++++++++------------- worlds/v6/__init__.py | 14 +++++++------- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/worlds/v6/Options.py b/worlds/v6/Options.py index 107fbab465..1950d1bcbd 100644 --- a/worlds/v6/Options.py +++ b/worlds/v6/Options.py @@ -1,8 +1,10 @@ import typing -from Options import Option, DeathLink, Range, Toggle +from dataclasses import dataclass +from Options import Option, DeathLink, Range, Toggle, PerGameCommonOptions class DoorCost(Range): """Amount of Trinkets required to enter Areas. Set to 0 to disable artificial locks.""" + display_name = "Door Cost" range_start = 0 range_end = 3 default = 3 @@ -13,6 +15,7 @@ class AreaCostRandomizer(Toggle): class DeathLinkAmnesty(Range): """Amount of Deaths to take before sending a DeathLink signal, for balancing difficulty""" + display_name = "Death Link Amnesty" range_start = 0 range_end = 30 default = 15 @@ -25,11 +28,11 @@ class MusicRandomizer(Toggle): """Randomize Music""" display_name = "Music Randomizer" -v6_options: typing.Dict[str,type(Option)] = { - "MusicRandomizer": MusicRandomizer, - "AreaRandomizer": AreaRandomizer, - "DoorCost": DoorCost, - "AreaCostRandomizer": AreaCostRandomizer, - "death_link": DeathLink, - "DeathLinkAmnesty": DeathLinkAmnesty -} \ No newline at end of file +@dataclass +class V6Options(PerGameCommonOptions): + music_rando: MusicRandomizer + area_rando: AreaRandomizer + door_cost: DoorCost + area_cost: AreaCostRandomizer + death_link: DeathLink + death_link_amnesty: DeathLinkAmnesty diff --git a/worlds/v6/Regions.py b/worlds/v6/Regions.py index 5a8f0315f4..f6e9ee7538 100644 --- a/worlds/v6/Regions.py +++ b/worlds/v6/Regions.py @@ -31,14 +31,3 @@ def create_regions(world: MultiWorld, player: int): locWrp_names = ["Edge Games"] regWrp.locations += [V6Location(player, loc_name, location_table[loc_name], regWrp) for loc_name in locWrp_names] world.regions.append(regWrp) - - -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule): - sourceRegion = world.get_region(source, player) - targetRegion = world.get_region(target, player) - - connection = Entrance(player,'', sourceRegion) - connection.access_rule = rule - - sourceRegion.exits.append(connection) - connection.connect(targetRegion) \ No newline at end of file diff --git a/worlds/v6/Rules.py b/worlds/v6/Rules.py index ecb34f2f32..bf0d60499e 100644 --- a/worlds/v6/Rules.py +++ b/worlds/v6/Rules.py @@ -1,6 +1,6 @@ import typing from ..generic.Rules import add_rule -from .Regions import connect_regions, v6areas +from .Regions import v6areas def _has_trinket_range(state, player, start, end) -> bool: @@ -10,34 +10,36 @@ def _has_trinket_range(state, player, start, end) -> bool: return True -def set_rules(world, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]): +def set_rules(multiworld, options, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]): areashuffle = list(range(len(v6areas))) - if world.AreaRandomizer[player].value: - world.random.shuffle(areashuffle) + if options.area_rando: + multiworld.random.shuffle(areashuffle) area_connections.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)}) area_connections.update({0: 0}) - if world.AreaCostRandomizer[player].value: - world.random.shuffle(areashuffle) + if options.area_cost: + multiworld.random.shuffle(areashuffle) area_cost_map.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)}) area_cost_map.update({0: 0}) + menu_region = multiworld.get_region("Menu", player) for i in range(1, 5): - connect_regions(world, player, "Menu", v6areas[area_connections[i] - 1], - lambda state, i=i: _has_trinket_range(state, player, - world.DoorCost[player].value * (area_cost_map[i] - 1), - world.DoorCost[player].value * area_cost_map[i])) + target_region = multiworld.get_region(v6areas[area_connections[i] - 1], player) + menu_region.connect(connecting_region=target_region, + rule=lambda state, i=i: _has_trinket_range(state, player, + options.door_cost * (area_cost_map[i] - 1), + options.door_cost * area_cost_map[i])) # Special Rule for V - add_rule(world.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and + add_rule(multiworld.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and state.can_reach("The Tower", 'Region', player) and state.can_reach("Space Station 2", 'Region', player) and state.can_reach("Warp Zone", 'Region', player)) # Special Rule for NPC Trinket - add_rule(world.get_location("NPC Trinket", player), + add_rule(multiworld.get_location("NPC Trinket", player), lambda state: state.can_reach("Laboratory", 'Region', player) or (state.can_reach("The Tower", 'Region', player) and state.can_reach("Space Station 2", 'Region', player) and state.can_reach("Warp Zone", 'Region', player))) - world.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player) + multiworld.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player) diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 6ff7fba60c..30a76f82cc 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -2,7 +2,7 @@ import typing import os, json from .Items import item_table, V6Item from .Locations import location_table, V6Location -from .Options import v6_options +from .Options import V6Options from .Rules import set_rules from .Regions import create_regions from BaseClasses import Item, ItemClassification, Tutorial @@ -41,7 +41,7 @@ class V6World(World): music_map: typing.Dict[int,int] - option_definitions = v6_options + options_dataclass = V6Options def create_regions(self): create_regions(self.multiworld, self.player) @@ -49,7 +49,7 @@ class V6World(World): def set_rules(self): self.area_connections = {} self.area_cost_map = {} - set_rules(self.multiworld, self.player, self.area_connections, self.area_cost_map) + set_rules(self.multiworld, self.options, self.player, self.area_connections, self.area_cost_map) def create_item(self, name: str) -> Item: return V6Item(name, ItemClassification.progression, item_table[name], self.player) @@ -61,7 +61,7 @@ class V6World(World): def generate_basic(self): musiclist_o = [1,2,3,4,9,12] musiclist_s = musiclist_o.copy() - if self.multiworld.MusicRandomizer[self.player].value: + if self.options.music_rando: self.multiworld.random.shuffle(musiclist_s) self.music_map = dict(zip(musiclist_o, musiclist_s)) @@ -69,10 +69,10 @@ class V6World(World): return { "MusicRando": self.music_map, "AreaRando": self.area_connections, - "DoorCost": self.multiworld.DoorCost[self.player].value, + "DoorCost": self.options.door_cost.value, "AreaCostRando": self.area_cost_map, - "DeathLink": self.multiworld.death_link[self.player].value, - "DeathLink_Amnesty": self.multiworld.DeathLinkAmnesty[self.player].value + "DeathLink": self.options.death_link.value, + "DeathLink_Amnesty": self.options.death_link_amnesty.value } def generate_output(self, output_directory: str): From 3a588099bd46833697a5407490b39348e1a89a00 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 16 Jan 2024 07:09:47 -0700 Subject: [PATCH 45/78] Pokemon Emerald: Automatically exclude locations based on goal (#2655) --- worlds/pokemon_emerald/__init__.py | 58 ++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index b7730fbdf7..5d50e0db96 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -7,7 +7,7 @@ import logging import os from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar -from BaseClasses import ItemClassification, MultiWorld, Tutorial +from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType from Fill import FillError, fill_restrictive from Options import Toggle import settings @@ -20,7 +20,7 @@ from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_m offset_item_value) from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_with_tags) -from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, +from .options import (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility, HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions) from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type @@ -146,6 +146,60 @@ class PokemonEmeraldWorld(World): self.multiworld.regions.extend(regions.values()) + # Exclude locations which are always locked behind the player's goal + def exclude_locations(location_names: List[str]): + for location_name in location_names: + try: + self.multiworld.get_location(location_name, + self.player).progress_type = LocationProgressType.EXCLUDED + except KeyError: + continue # Location not in multiworld + + if self.options.goal == Goal.option_champion: + # Always required to beat champion before receiving this + exclude_locations([ + "Littleroot Town - S.S. Ticket from Norman" + ]) + + # S.S. Ticket requires beating champion, so ferry is not accessible until after goal + if not self.options.enable_ferry: + exclude_locations([ + "SS Tidal - Hidden Item in Lower Deck Trash Can", + "SS Tidal - TM49 from Thief" + ]) + + # Construction workers don't move until champion is defeated + if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value: + exclude_locations([ + "Safari Zone NE - Hidden Item North", + "Safari Zone NE - Hidden Item East", + "Safari Zone NE - Item on Ledge", + "Safari Zone SE - Hidden Item in South Grass 1", + "Safari Zone SE - Hidden Item in South Grass 2", + "Safari Zone SE - Item in Grass" + ]) + elif self.options.goal == Goal.option_norman: + # If the player sets their options such that Surf or the Balance + # Badge is vanilla, a very large number of locations become + # "post-Norman". Similarly, access to the E4 may require you to + # defeat Norman as an event or to get his badge, making postgame + # locations inaccessible. Detecting these situations isn't trivial + # and excluding all locations requiring Surf would be a bad idea. + # So for now we just won't touch it and blame the user for + # constructing their options in this way. Players usually expect + # to only partially complete their world when playing this goal + # anyway. + + # Locations which are directly unlocked by defeating Norman. + exclude_locations([ + "Petalburg Gym - Balance Badge", + "Petalburg Gym - TM42 from Norman", + "Petalburg City - HM03 from Wally's Uncle", + "Dewford Town - TM36 from Sludge Bomb Man", + "Mauville City - Basement Key from Wattson", + "Mauville City - TM24 from Wattson" + ]) + def create_items(self) -> None: item_locations: List[PokemonEmeraldLocation] = [ location From 5df7a8f686251dd017d9d2088902ee40e535a96a Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 16 Jan 2024 09:10:59 -0500 Subject: [PATCH 46/78] Lingo: Disable forced good item when early color hallways is on (#2729) --- worlds/lingo/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 57bcc4bfd5..f3efc2914c 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -224,7 +224,7 @@ class LingoPlayerLogic: "kind of logic error.") if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ - and not early_color_hallways is False: + and not early_color_hallways: # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are From 1c2dcb7b01286cfea53f4c07b52ea37a495ac157 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:11:52 +0100 Subject: [PATCH 47/78] The Witness: Add Desert Control Panels (#2643) --- worlds/witness/WitnessItems.txt | 7 +++++-- worlds/witness/WitnessLogic.txt | 12 ++++++------ worlds/witness/WitnessLogicExpert.txt | 12 ++++++------ worlds/witness/WitnessLogicVanilla.txt | 12 ++++++------ worlds/witness/__init__.py | 3 ++- worlds/witness/locations.py | 4 ++-- .../Door_Shuffle/Complex_Additional_Panels.txt | 3 +++ 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 6457117909..758ed45465 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -41,10 +41,13 @@ Doors: 1102 - Tutorial Outpost Exit (Panel) - 0x04CA4 1105 - Symmetry Island Lower (Panel) - 0x000B0 1107 - Symmetry Island Upper (Panel) - 0x1C349 +1108 - Desert Surface 3 Control (Panel) - 0x09FA0 +1109 - Desert Surface 8 Control (Panel) - 0x09F86 1110 - Desert Light Room Entry (Panel) - 0x0C339 1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B 1112 - Desert Light Control (Panel) - 0x09FAA 1113 - Desert Flood Room Entry (Panel) - 0x0A249 +1114 - Desert Elevator Room Hexagonal Control (Panel) - 0x0A015 1115 - Quarry Elevator Control (Panel) - 0x17CC4 1117 - Quarry Entry 1 (Panel) - 0x09E57 1118 - Quarry Entry 2 (Panel) - 0x17C09 @@ -232,7 +235,7 @@ Doors: 1984 - Caves Shortcuts - 0x2D859,0x2D73F 1987 - Tunnels Doors - 0x27739,0x27263,0x09E87,0x0348A -2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0A015,0x09FA0,0x09F86 2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675 2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA 2015 - Town Control Panels - 0x2896A,0x334D8,0x09F98 @@ -243,7 +246,7 @@ Doors: 2100 - Symmetry Island Panels - 0x1C349,0x000B0 2101 - Tutorial Outpost Panels - 0x0A171,0x04CA4 -2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249 +2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249,0x0A015,0x09FA0,0x09F86 2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4 2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675 2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 424f990c33..ec0922bec6 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +158111 - 0x17C31 (Elevator Room Transparent) - True - True +158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True +158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Elevator Room Bent 1) - True - True +158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True +158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True 159035 - 0x037BB (Elevator EP) - 0x01317 - True Door - 0x01317 (Elevator) - 0x03608 diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index accb640e34..056ae145c4 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +158111 - 0x17C31 (Elevator Room Transparent) - True - True +158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True +158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Elevator Room Bent 1) - True - True +158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True +158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True 159035 - 0x037BB (Elevator EP) - 0x01317 - True Door - 0x01317 (Elevator) - 0x03608 diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 4c5e52c5cb..71af12f76d 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +158111 - 0x17C31 (Elevator Room Transparent) - True - True +158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True +158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Elevator Room Bent 1) - True - True +158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True +158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True 159035 - 0x037BB (Elevator EP) - 0x01317 - True Door - 0x01317 (Elevator) - 0x03608 diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 6360c33aef..aeee7009cc 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -176,7 +176,8 @@ class WitnessWorld(World): extra_checks = [ ("First Hallway Room", "First Hallway Bend"), ("First Hallway", "First Hallway Straight"), - ("Desert Outside", "Desert Surface 3"), + ("Desert Outside", "Desert Surface 1"), + ("Desert Outside", "Desert Surface 2"), ] for i in range(num_early_locs, needed_size): diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index d20be27940..026977701a 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -55,8 +55,8 @@ class StaticWitnessLocations: "Desert Light Room 3", "Desert Pond Room 5", "Desert Flood Room 6", - "Desert Final Hexagonal", - "Desert Final Bent 3", + "Desert Elevator Room Hexagonal", + "Desert Elevator Room Bent 3", "Desert Laser Panel", "Quarry Entry 1 Panel", diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt index f7acbb2e55..b843709085 100644 --- a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt +++ b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt @@ -1,4 +1,7 @@ Items: +Desert Surface 3 Control (Panel) +Desert Surface 8 Control (Panel) +Desert Elevator Room Hexagonal Control (Panel) Desert Flood Controls (Panel) Desert Light Control (Panel) Quarry Elevator Control (Panel) From e6f7ed50608f4939707db2dfdb86cf161b110c54 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:13:04 +0100 Subject: [PATCH 48/78] The Witness: Progressive Symmetry (#2644) --- worlds/witness/WitnessItems.txt | 1 + worlds/witness/hints.py | 3 +-- worlds/witness/settings/Symbol_Shuffle.txt | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 758ed45465..8e50e328a4 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -16,6 +16,7 @@ Symbols: 72 - Colored Squares 80 - Arrows 200 - Progressive Dots - Dots,Full Dots +210 - Progressive Symmetry - Symmetry,Colored Dots 260 - Progressive Stars - Stars,Stars + Same Colored Symbol Useful: diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 68fc68946b..c00827feee 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -227,9 +227,8 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: "Eraser", "Black/White Squares", "Colored Squares", - "Colored Dots", "Sound Dots", - "Symmetry" + "Progressive Symmetry" ] priority.update(world.random.sample(symbols, 5)) diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt index 3d0342f5e2..253fe98bad 100644 --- a/worlds/witness/settings/Symbol_Shuffle.txt +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -1,9 +1,8 @@ Items: Arrows Progressive Dots -Colored Dots Sound Dots -Symmetry +Progressive Symmetry Triangles Eraser Shapers From 5c7bae7940a8ce5d330360b155da1151507589e6 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:14:06 +0100 Subject: [PATCH 49/78] The Witness: Local Laser Shuffle + Option Presets (#2590) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/Options.py | 5 +- worlds/witness/__init__.py | 34 ++++++++++--- worlds/witness/presets.py | 101 +++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 worlds/witness/presets.py diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 84855bf867..a15485c485 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -28,11 +28,14 @@ class ShuffleSymbols(DefaultOnToggle): display_name = "Shuffle Symbols" -class ShuffleLasers(Toggle): +class ShuffleLasers(Choice): """If on, the 11 lasers are turned into items and will activate on their own upon receiving them. Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still be redirected as normal, for both applications of redirection.""" display_name = "Shuffle Lasers" + option_off = 0 + option_local = 1 + option_anywhere = 2 class ShuffleDoors(Choice): diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index aeee7009cc..b2890768c6 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -6,6 +6,7 @@ from typing import Dict, Optional from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState from Options import PerGameCommonOptions, Toggle +from .presets import witness_option_presets from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ get_priority_hint_items, make_hints, generate_joke_hints from worlds.AutoWorld import World, WebWorld @@ -31,6 +32,8 @@ class WitnessWebWorld(WebWorld): ["NewSoupVi", "Jarno"] )] + options_presets = witness_option_presets + class WitnessWorld(World): """ @@ -102,14 +105,29 @@ class WitnessWorld(World): self.log_ids_to_hints = dict() - if not (self.options.shuffle_symbols or self.options.shuffle_doors or self.options.shuffle_lasers): - if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" - f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't" - f" seem right.") - else: - raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any" - f" progression items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle.") + interacts_with_multiworld = ( + self.options.shuffle_symbols or + self.options.shuffle_doors or + self.options.shuffle_lasers == "anywhere" + ) + + has_progression = ( + interacts_with_multiworld + or self.options.shuffle_lasers == "local" + or self.options.shuffle_boat + or self.options.early_caves == "add_to_pool" + ) + + if not has_progression and self.multiworld.players == 1: + warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.") + elif not interacts_with_multiworld and self.multiworld.players > 1: + raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough" + f" progression items that can be placed in other players' worlds. Please turn on Symbol" + f" Shuffle, Door Shuffle or non-local Laser Shuffle.") + + if self.options.shuffle_lasers == "local": + self.options.local_items.value |= self.item_name_groups["Lasers"] def create_regions(self): self.regio.create_regions(self, self.player_logic) diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py new file mode 100644 index 0000000000..1fee1a7968 --- /dev/null +++ b/worlds/witness/presets.py @@ -0,0 +1,101 @@ +from typing import Any, Dict + +from .options import * + +witness_option_presets: Dict[str, Dict[str, Any]] = { + # Great for short syncs & scratching that "speedrun with light routing elements" itch. + "Short & Dense": { + "progression_balancing": 30, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": False, + "shuffle_doors": ShuffleDoors.option_panels, + "door_groupings": DoorGroupings.option_off, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_local, + + "disable_non_randomized_puzzles": True, + "shuffle_discarded_panels": False, + "shuffle_vault_boxes": False, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_off, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_mountain_box_short, + "mountain_lasers": 7, + "challenge_lasers": 11, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": False, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": PuzzleSkipAmount.default, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, + + # For relative beginners who want to move to the next step. + "Advanced, But Chill": { + "progression_balancing": 30, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": True, + "shuffle_doors": ShuffleDoors.option_doors, + "door_groupings": DoorGroupings.option_regional, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_off, + + "disable_non_randomized_puzzles": False, + "shuffle_discarded_panels": True, + "shuffle_vault_boxes": True, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_obelisk_sides, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_mountain_box_long, + "mountain_lasers": 6, + "challenge_lasers": 9, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": False, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": 15, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, + + # Allsanity but without the BS (no expert, no tedious EPs). + "Nice Allsanity": { + "progression_balancing": 50, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": True, + "shuffle_doors": ShuffleDoors.option_mixed, + "door_groupings": DoorGroupings.option_off, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_anywhere, + + "disable_non_randomized_puzzles": False, + "shuffle_discarded_panels": True, + "shuffle_vault_boxes": True, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_individual, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_challenge, + "mountain_lasers": 6, + "challenge_lasers": 9, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": True, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": 15, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, +} From e15873e8611f7fcf1b4918ed79faf23524ddda97 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:23:30 +0100 Subject: [PATCH 50/78] The Witness: Bonk trap support (#2645) --- worlds/witness/Options.py | 2 +- worlds/witness/WitnessItems.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index a15485c485..3912d1de11 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -189,7 +189,7 @@ class HintAmount(Range): class DeathLink(Toggle): """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies. - The effect of a "death" in The Witness is a Power Surge.""" + The effect of a "death" in The Witness is a Bonk Trap.""" display_name = "Death Link" diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 8e50e328a4..e17464a092 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -30,8 +30,9 @@ Filler: #503 - Energy Fill (Max) - 1 Traps: -600 - Slowness - 8 +600 - Slowness - 6 610 - Power Surge - 2 +615 - Bonk - 1 Jokes: 650 - Functioning Brain From 5dcaa6ca20991cfc1a1f6e7db559c564f4f83f1e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:24:10 +0100 Subject: [PATCH 51/78] The Witness: Death Link Amnesty (#2646) --- worlds/witness/Options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 3912d1de11..27aa8b9d95 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -193,6 +193,15 @@ class DeathLink(Toggle): display_name = "Death Link" +class DeathLinkAmnesty(Range): + """Number of panel fails to allow before sending a death through Death Link. + 0 means every panel fail will send a death, 1 means every other panel fail will send a death, etc.""" + display_name = "Death Link Amnesty" + range_start = 0 + range_end = 5 + default = 1 + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -216,3 +225,4 @@ class TheWitnessOptions(PerGameCommonOptions): puzzle_skip_amount: PuzzleSkipAmount hint_amount: HintAmount death_link: DeathLink + death_link_amnesty: DeathLinkAmnesty From 325a510ba73321ef25e9e2739412f1d69e65b4d8 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:26:18 -0500 Subject: [PATCH 52/78] KH2: Promise charm logic (#2635) --- worlds/kh2/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 65f690fdde..1124f8109c 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -83,6 +83,8 @@ class KH2Rules: return state.has(ItemName.TornPages, self.player, amount) def level_locking_unlock(self, state: CollectionState, amount): + if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player): + return True return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]]) def summon_levels_unlocked(self, state: CollectionState, amount) -> bool: From 71a3e2230d92dd59cc3c092addb7c8c62060de7a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:27:09 +0100 Subject: [PATCH 53/78] The Witness: Allow Mountain Lasers to go up to 11 instead of 7. (#2618) --- worlds/witness/Options.py | 7 +++++-- worlds/witness/player_logic.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 27aa8b9d95..ac1f2bc828 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -140,10 +140,13 @@ class PuzzleRandomization(Choice): class MountainLasers(Range): - """Sets the amount of beams required to enter the final area.""" + """Sets the amount of lasers required to enter the Mountain. + If set to a higher amount than 7, the mountaintop box will be slightly rotated to make it possible to solve without + the hatch being opened. + This change will also be applied logically to the long solution ("Challenge Lasers" setting).""" display_name = "Required Lasers for Mountain Entry" range_start = 1 - range_end = 7 + range_end = 11 default = 7 diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index e05199c2b3..5d538e62b7 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -323,7 +323,10 @@ class WitnessPlayerLogic: elif victory == 3: self.VICTORY_LOCATION = "0xFFF00" - if chal_lasers <= 7: + # Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less + # (challenge_lasers <= 7), you can now solve it without opening Mountain Entry first. + # Furthermore, if the user sets mountain_lasers > 7, the box is rotated to not require Mountain Entry either. + if chal_lasers <= 7 or mnt_lasers > 7: adjustment_linesets_in_order.append([ "Requirement Changes:", "0xFFF00 - 11 Lasers - True", From 4fdeec4f70f82bf439b335fb28d4c0af9d6f296c Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:33:34 +0100 Subject: [PATCH 54/78] The Witness: Cleanup - Options Access, data version, snake_case for file name (#2631) --- worlds/witness/__init__.py | 9 ++++----- worlds/witness/{Options.py => options.py} | 0 2 files changed, 4 insertions(+), 5 deletions(-) rename worlds/witness/{Options.py => options.py} (100%) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index b2890768c6..a645abc081 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -16,7 +16,7 @@ from .locations import WitnessPlayerLocations, StaticWitnessLocations from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData from .regions import WitnessRegions from .rules import set_rules -from .Options import TheWitnessOptions +from .options import TheWitnessOptions from .utils import get_audio_logs from logging import warning, error @@ -43,7 +43,6 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 14 StaticWitnessLogic() StaticWitnessLocations() @@ -91,10 +90,10 @@ class WitnessWorld(World): } def generate_early(self): - disabled_locations = self.multiworld.exclude_locations[self.player].value + disabled_locations = self.options.exclude_locations.value self.player_logic = WitnessPlayerLogic( - self, disabled_locations, self.multiworld.start_inventory[self.player].value + self, disabled_locations, self.options.start_inventory.value ) self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) @@ -272,7 +271,7 @@ class WitnessWorld(World): self.own_itempool += new_items self.multiworld.itempool += new_items if self.items.item_data[item_name].local_only: - self.multiworld.local_items[self.player].value.add(item_name) + self.options.local_items.value.add(item_name) def fill_slot_data(self) -> dict: hint_amount = self.options.hint_amount.value diff --git a/worlds/witness/Options.py b/worlds/witness/options.py similarity index 100% rename from worlds/witness/Options.py rename to worlds/witness/options.py From de8fe21d4a13cdc6500850550d19230808e14f88 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:10:19 +0100 Subject: [PATCH 55/78] Tests: create sane cov defaults (#2728) --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..17a60ad125 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: + if typing.TYPE_CHECKING: From 49ecd4b9c113e8d9b4f8e928326733deee589c1c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:10:58 +0100 Subject: [PATCH 56/78] CI: flake8: max-complexity=14 (#2731) The value of 10 does not really fit some of our world patterns and values up to 15 may be acceptable. Looking at some worlds, 14 seems to be achievable without too much work and reduces the noise in test output, making it more usable. --- .github/workflows/analyze-modified-files.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index ba2660809a..d01365745c 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -71,7 +71,7 @@ jobs: continue-on-error: true if: env.diff != '' && matrix.task == 'flake8' run: | - flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }} + flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }} - name: "mypy: Type check modified files" continue-on-error: true From 602c2966fc6574a5b455a59cce50b67dbaed8123 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Jan 2024 17:23:18 +0100 Subject: [PATCH 57/78] LttP: move _hint_text to SubClasses (#2532) --- BaseClasses.py | 3 --- worlds/alttp/SubClasses.py | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 855e69c5d4..38598d42d9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1056,9 +1056,6 @@ class Location: @property def hint_text(self) -> str: - hint_text = getattr(self, "_hint_text", None) - if hint_text: - return hint_text return "at " + self.name.replace("_", " ").replace("-", " ") diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 64e4adaec9..22eeebe181 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -26,6 +26,13 @@ class ALttPLocation(Location): self.player_address = player_address self._hint_text = hint_text + @property + def hint_text(self) -> str: + hint_text = getattr(self, "_hint_text", None) + if hint_text: + return hint_text + return "at " + self.name.replace("_", " ").replace("-", " ") + class ALttPItem(Item): game: str = "A Link to the Past" From 834b6e35b450a471e26f0e5a4c5c367faf760f7a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 18 Jan 2024 01:52:33 +0100 Subject: [PATCH 58/78] Setup: auto update vc redist (#2502) --- inno_setup.iss | 2 +- setup.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index be5de320a1..b122cdc00b 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -197,7 +197,7 @@ begin begin // Is the installed version at least the packaged one ? Log('VC Redist x64 Version : found ' + strVersion); - Result := (CompareStr(strVersion, 'v14.32.31332') < 0); + Result := (CompareStr(strVersion, 'v14.38.33130') < 0); end else begin diff --git a/setup.py b/setup.py index 05e923ed3f..9b28715ae9 100644 --- a/setup.py +++ b/setup.py @@ -349,6 +349,18 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): for folder in sdl2.dep_bins + glew.dep_bins: shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) print(f"copying {folder} -> {self.libfolder}") + # windows needs Visual Studio C++ Redistributable + # Installer works for x64 and arm64 + print("Downloading VC Redist") + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe", + context=context) as download: + vc_redist = download.read() + print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", ) + with open("VC_redist.x64.exe", "wb") as vc_file: + vc_file.write(vc_redist) for data in self.extra_data: self.installfile(Path(data)) From 4c901dcfc0554fd2d2774bb73499655d3ce1e99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dana=C3=ABl=20V?= <104455676+ReverM@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:56:34 -0500 Subject: [PATCH 59/78] TUNIC: Change Tunic to TUNIC (#2720) --- worlds/tunic/__init__.py | 8 ++++---- worlds/tunic/docs/{en_Tunic.md => en_TUNIC.md} | 0 worlds/tunic/er_scripts.py | 4 ++-- worlds/tunic/test/__init__.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename worlds/tunic/docs/{en_Tunic.md => en_TUNIC.md} (100%) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index b946ea8e30..d8311de856 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -24,15 +24,15 @@ class TunicWeb(WebWorld): ) ] theme = "grassFlowers" - game = "Tunic" + game = "TUNIC" class TunicItem(Item): - game: str = "Tunic" + game: str = "TUNIC" class TunicLocation(Location): - game: str = "Tunic" + game: str = "TUNIC" class TunicWorld(World): @@ -41,7 +41,7 @@ class TunicWorld(World): about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox! """ - game = "Tunic" + game = "TUNIC" web = TunicWeb() data_version = 2 diff --git a/worlds/tunic/docs/en_Tunic.md b/worlds/tunic/docs/en_TUNIC.md similarity index 100% rename from worlds/tunic/docs/en_Tunic.md rename to worlds/tunic/docs/en_TUNIC.md diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 84b97e13da..4d640b2fda 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -10,11 +10,11 @@ if TYPE_CHECKING: class TunicERItem(Item): - game: str = "Tunic" + game: str = "TUNIC" class TunicERLocation(Location): - game: str = "Tunic" + game: str = "TUNIC" def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]: diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py index f8ab99d67d..d7ae47f7d7 100644 --- a/worlds/tunic/test/__init__.py +++ b/worlds/tunic/test/__init__.py @@ -2,5 +2,5 @@ from test.bases import WorldTestBase class TunicTestBase(WorldTestBase): - game = "Tunic" + game = "TUNIC" player: int = 1 \ No newline at end of file From ec440b7785a6464d5f2e2caf16cafd0b48880219 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 17 Jan 2024 19:58:48 -0500 Subject: [PATCH 60/78] Lingo: NORTH requires hint panels (#2732) --- worlds/lingo/data/LL1.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 32a7659b82..da78a5123d 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1118,7 +1118,13 @@ id: Cross Room/Panel_north_missing colors: green tag: forbid - required_room: Outside The Bold + required_panel: + - room: Outside The Bold + panel: MOUTH + - room: Outside The Bold + panel: YEAST + - room: Outside The Bold + panel: WET DIAMONDS: id: Cross Room/Panel_diamonds_missing colors: green @@ -4414,9 +4420,14 @@ colors: blue tag: forbid required_panel: - room: The Bearer (West) - panel: SMILE - required_room: Outside The Bold + - room: The Bearer (West) + panel: SMILE + - room: Outside The Bold + panel: MOUTH + - room: Outside The Bold + panel: YEAST + - room: Outside The Bold + panel: WET Cross Tower (South): entrances: # No roof access The Bearer (North): From ac7b707e3ebe1bfcdd332157239b4ace74295ae5 Mon Sep 17 00:00:00 2001 From: Bicoloursnake <60069210+Bicoloursnake@users.noreply.github.com> Date: Wed, 17 Jan 2024 20:18:03 -0500 Subject: [PATCH 61/78] OOT: Adjust the Logic Trick Keys to be an ordered object (#2736) --- worlds/oot/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 120027e29d..2543cdc715 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1271,7 +1271,7 @@ class LogicTricks(OptionList): https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py """ display_name = "Logic Tricks" - valid_keys = frozenset(normalized_name_tricks) + valid_keys = tuple(normalized_name_tricks.keys()) valid_keys_casefold = True From 1307754f0291aa117ace540e54574923487e0a46 Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 19 Jan 2024 12:14:26 -0800 Subject: [PATCH 62/78] LADX: music shuffle (#2101) --- worlds/ladx/Options.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 691891c0b3..117242208b 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -399,6 +399,26 @@ class Palette(Choice): option_pink = 4 option_inverted = 5 +class Music(Choice, LADXROption): + """ + [Vanilla] Regular Music + [Shuffled] Shuffled Music + [Off] No music + """ + ladxr_name = "music" + option_vanilla = 0 + option_shuffled = 1 + option_off = 2 + + + def to_ladxr_option(self, all_options): + s = "" + if self.value == self.option_shuffled: + s = "random" + elif self.value == self.option_off: + s = "off" + return self.ladxr_name, s + class WarpImprovements(DefaultOffToggle): """ [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. @@ -444,6 +464,7 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'shuffle_maps': ShuffleMaps, 'shuffle_compasses': ShuffleCompasses, 'shuffle_stone_beaks': ShuffleStoneBeaks, + 'music': Music, 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, From 5f9ce2b7b60fe05a54ccd3498c67da7cac361a63 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 19 Jan 2024 15:31:45 -0500 Subject: [PATCH 63/78] Noita: Update to use new Options API (#2370) Reworking the options to make it work with the new options API. Also reworked stuff in several spots to use world: NoitaWorld instead of multiworld: MultiWorld --- worlds/noita/Events.py | 42 ----- worlds/noita/Rules.py | 166 ------------------- worlds/noita/__init__.py | 31 ++-- worlds/noita/events.py | 43 +++++ worlds/noita/{Items.py => items.py} | 44 ++--- worlds/noita/{Locations.py => locations.py} | 23 +-- worlds/noita/{Options.py => options.py} | 30 ++-- worlds/noita/{Regions.py => regions.py} | 66 ++++---- worlds/noita/rules.py | 172 ++++++++++++++++++++ 9 files changed, 315 insertions(+), 302 deletions(-) delete mode 100644 worlds/noita/Events.py delete mode 100644 worlds/noita/Rules.py create mode 100644 worlds/noita/events.py rename worlds/noita/{Items.py => items.py} (82%) rename worlds/noita/{Locations.py => locations.py} (94%) rename worlds/noita/{Options.py => options.py} (87%) rename worlds/noita/{Regions.py => regions.py} (61%) create mode 100644 worlds/noita/rules.py diff --git a/worlds/noita/Events.py b/worlds/noita/Events.py deleted file mode 100644 index e759d38c6c..0000000000 --- a/worlds/noita/Events.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Dict - -from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region -from . import Items, Locations - - -def create_event(player: int, name: str) -> Item: - return Items.NoitaItem(name, ItemClassification.progression, None, player) - - -def create_location(player: int, name: str, region: Region) -> Location: - return Locations.NoitaLocation(player, name, None, region) - - -def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location: - region = multiworld.get_region(region_name, player) - - new_location = create_location(player, item, region) - new_location.place_locked_item(create_event(player, item)) - - region.locations.append(new_location) - return new_location - - -def create_all_events(multiworld: MultiWorld, player: int) -> None: - for region, event in event_locks.items(): - create_locked_location_event(multiworld, player, region, event) - - multiworld.completion_condition[player] = lambda state: state.has("Victory", player) - - -# Maps region names to event names -event_locks: Dict[str, str] = { - "The Work": "Victory", - "Mines": "Portal to Holy Mountain 1", - "Coal Pits": "Portal to Holy Mountain 2", - "Snowy Depths": "Portal to Holy Mountain 3", - "Hiisi Base": "Portal to Holy Mountain 4", - "Underground Jungle": "Portal to Holy Mountain 5", - "The Vault": "Portal to Holy Mountain 6", - "Temple of the Art": "Portal to Holy Mountain 7", -} diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py deleted file mode 100644 index 8190b80dc7..0000000000 --- a/worlds/noita/Rules.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import List, NamedTuple, Set - -from BaseClasses import CollectionState, MultiWorld -from . import Items, Locations -from .Options import BossesAsChecks, VictoryCondition -from worlds.generic import Rules as GenericRules - - -class EntranceLock(NamedTuple): - source: str - destination: str - event: str - items_needed: int - - -entrance_locks: List[EntranceLock] = [ - EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1), - EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2), - EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3), - EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4), - EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5), - EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6), - EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7), -] - - -holy_mountain_regions: List[str] = [ - "Coal Pits Holy Mountain", - "Snowy Depths Holy Mountain", - "Hiisi Base Holy Mountain", - "Underground Jungle Holy Mountain", - "Vault Holy Mountain", - "Temple of the Art Holy Mountain", - "Laboratory Holy Mountain", -] - - -wand_tiers: List[str] = [ - "Wand (Tier 1)", # Coal Pits - "Wand (Tier 2)", # Snowy Depths - "Wand (Tier 3)", # Hiisi Base - "Wand (Tier 4)", # Underground Jungle - "Wand (Tier 5)", # The Vault - "Wand (Tier 6)", # Temple of the Art -] - -items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", - "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", - "Powder Pouch"] - -perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) - - -# ---------------- -# Helper Functions -# ---------------- - - -def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: - return sum(state.count(perk, player) for perk in perk_list) >= amount - - -def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: - return state.count("Orb", player) >= amount - - -def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int): - location = multiworld.get_location(location_name, player) - GenericRules.forbid_items_for_player(location, items, player) - - -# ---------------- -# Rule Functions -# ---------------- - - -# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them) -def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None: - for location_name in Locations.location_name_to_id.keys(): - if "Shop Item" in location_name: - forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player) - - -# Prevent high tier wands from appearing in early Holy Mountain shops -def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None: - for i, region_name in enumerate(holy_mountain_regions): - wands_to_forbid = wand_tiers[i+1:] - - locations_in_region = Locations.location_region_mapping[region_name].keys() - for location_name in locations_in_region: - forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) - - # Prevent high tier wands from appearing in the Secret shop - wands_to_forbid = wand_tiers[3:] - locations_in_region = Locations.location_region_mapping["Secret Shop"].keys() - for location_name in locations_in_region: - forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) - - -def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None: - for lock in entrance_locks: - location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player) - GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player)) - - -def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None: - victory_condition = multiworld.victory_condition[player].value - for lock in entrance_locks: - location = multiworld.get_location(lock.event, player) - - if victory_condition == VictoryCondition.option_greed_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) - ) - elif victory_condition == VictoryCondition.option_pure_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) and - has_orb_count(state, player, items_needed) - ) - elif victory_condition == VictoryCondition.option_peaceful_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) and - has_orb_count(state, player, items_needed * 3) - ) - - -def biome_unlock_conditions(multiworld: MultiWorld, player: int): - lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances - magical_entrances = multiworld.get_region("Magical Temple", player).entrances - wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances - for entrance in lukki_entrances: - entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\ - state.has("All-Seeing Eye Perk", player) - for entrance in magical_entrances: - entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player) - for entrance in wizard_entrances: - entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player) - - -def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: - victory_condition = multiworld.victory_condition[player].value - victory_location = multiworld.get_location("Victory", player) - - if victory_condition == VictoryCondition.option_pure_ending: - victory_location.access_rule = lambda state: has_orb_count(state, player, 11) - elif victory_condition == VictoryCondition.option_peaceful_ending: - victory_location.access_rule = lambda state: has_orb_count(state, player, 33) - - -# ---------------- -# Main Function -# ---------------- - - -def create_all_rules(multiworld: MultiWorld, player: int) -> None: - if multiworld.players > 1: - ban_items_from_shops(multiworld, player) - ban_early_high_tier_wands(multiworld, player) - lock_holy_mountains_into_spheres(multiworld, player) - holy_mountain_unlock_conditions(multiworld, player) - biome_unlock_conditions(multiworld, player) - victory_unlock_conditions(multiworld, player) - - # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) - if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses: - forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 792b90e3f5..b8f8e4ae83 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -1,6 +1,8 @@ from BaseClasses import Item, Tutorial from worlds.AutoWorld import WebWorld, World -from . import Events, Items, Locations, Options, Regions, Rules +from typing import Dict, Any +from . import events, items, locations, regions, rules +from .options import NoitaOptions class NoitaWeb(WebWorld): @@ -24,13 +26,14 @@ class NoitaWorld(World): """ game = "Noita" - option_definitions = Options.noita_options + options: NoitaOptions + options_dataclass = NoitaOptions - item_name_to_id = Items.item_name_to_id - location_name_to_id = Locations.location_name_to_id + item_name_to_id = items.item_name_to_id + location_name_to_id = locations.location_name_to_id - item_name_groups = Items.item_name_groups - location_name_groups = Locations.location_name_groups + item_name_groups = items.item_name_groups + location_name_groups = locations.location_name_groups data_version = 2 web = NoitaWeb() @@ -40,21 +43,21 @@ class NoitaWorld(World): raise Exception("Noita yaml's slot name has invalid character(s).") # Returned items will be sent over to the client - def fill_slot_data(self): - return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", + "pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price") def create_regions(self) -> None: - Regions.create_all_regions_and_connections(self.multiworld, self.player) - Events.create_all_events(self.multiworld, self.player) + regions.create_all_regions_and_connections(self) def create_item(self, name: str) -> Item: - return Items.create_item(self.player, name) + return items.create_item(self.player, name) def create_items(self) -> None: - Items.create_all_items(self.multiworld, self.player) + items.create_all_items(self) def set_rules(self) -> None: - Rules.create_all_rules(self.multiworld, self.player) + rules.create_all_rules(self) def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(Items.filler_items) + return self.random.choice(items.filler_items) diff --git a/worlds/noita/events.py b/worlds/noita/events.py new file mode 100644 index 0000000000..4ec04e98b4 --- /dev/null +++ b/worlds/noita/events.py @@ -0,0 +1,43 @@ +from typing import Dict, TYPE_CHECKING +from BaseClasses import Item, ItemClassification, Location, Region +from . import items, locations + +if TYPE_CHECKING: + from . import NoitaWorld + + +def create_event(player: int, name: str) -> Item: + return items.NoitaItem(name, ItemClassification.progression, None, player) + + +def create_location(player: int, name: str, region: Region) -> Location: + return locations.NoitaLocation(player, name, None, region) + + +def create_locked_location_event(player: int, region: Region, item: str) -> Location: + new_location = create_location(player, item, region) + new_location.place_locked_item(create_event(player, item)) + + region.locations.append(new_location) + return new_location + + +def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None: + for region_name, event in event_locks.items(): + region = created_regions[region_name] + create_locked_location_event(world.player, region, event) + + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + + +# Maps region names to event names +event_locks: Dict[str, str] = { + "The Work": "Victory", + "Mines": "Portal to Holy Mountain 1", + "Coal Pits": "Portal to Holy Mountain 2", + "Snowy Depths": "Portal to Holy Mountain 3", + "Hiisi Base": "Portal to Holy Mountain 4", + "Underground Jungle": "Portal to Holy Mountain 5", + "The Vault": "Portal to Holy Mountain 6", + "Temple of the Art": "Portal to Holy Mountain 7", +} diff --git a/worlds/noita/Items.py b/worlds/noita/items.py similarity index 82% rename from worlds/noita/Items.py rename to worlds/noita/items.py index c859a80394..6b662fbee6 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/items.py @@ -1,9 +1,14 @@ import itertools from collections import Counter -from typing import Dict, List, NamedTuple, Set +from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING -from BaseClasses import Item, ItemClassification, MultiWorld -from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs +from BaseClasses import Item, ItemClassification +from .options import BossesAsChecks, VictoryCondition, ExtraOrbs + +if TYPE_CHECKING: + from . import NoitaWorld +else: + NoitaWorld = object class ItemData(NamedTuple): @@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]: return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else [] -def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]: +def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]: filler_pool = weights.copy() - if multiworld.bad_effects[player].value == 0: + if not world.options.bad_effects: del filler_pool["Trap"] - return multiworld.random.choices(population=list(filler_pool.keys()), - weights=list(filler_pool.values()), - k=count) + return world.random.choices(population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=count) -def create_all_items(multiworld: MultiWorld, player: int) -> None: - locations_to_fill = len(multiworld.get_unfilled_locations(player)) +def create_all_items(world: NoitaWorld) -> None: + player = world.player + locations_to_fill = len(world.multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() - + create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player]) - + create_spatial_awareness_item(multiworld.bosses_as_checks[player]) - + create_kantele(multiworld.victory_condition[player]) + + create_orb_items(world.options.victory_condition, world.options.extra_orbs) + + create_spatial_awareness_item(world.options.bosses_as_checks) + + create_kantele(world.options.victory_condition) ) # if there's not enough shop-allowed items in the pool, we can encounter gen issues # 39 is the number of shop-valid items we need to guarantee if len(itempool) < 39: - itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool)) + itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool)) # this is so that it passes tests and gens if you have minimal locations and only one player - if multiworld.players == 1: - for location in multiworld.get_unfilled_locations(player): + if world.multiworld.players == 1: + for location in world.multiworld.get_unfilled_locations(player): if "Shop Item" in location.name: location.item = create_item(player, itempool.pop()) - locations_to_fill = len(multiworld.get_unfilled_locations(player)) + locations_to_fill = len(world.multiworld.get_unfilled_locations(player)) - itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) - multiworld.itempool += [create_item(player, name) for name in itempool] + itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool)) + world.multiworld.itempool += [create_item(player, name) for name in itempool] # 110000 - 110032 diff --git a/worlds/noita/Locations.py b/worlds/noita/locations.py similarity index 94% rename from worlds/noita/Locations.py rename to worlds/noita/locations.py index 7c27d699cc..afe16c54e4 100644 --- a/worlds/noita/Locations.py +++ b/worlds/noita/locations.py @@ -201,11 +201,10 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = { } -# Iterating the hidden chest and pedestal locations here to avoid clutter above -def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]: - if locinfo.ltype in ["chest", "pedestal"]: - return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)} - return {locname: locinfo.id} +def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]: + if amt == 1: + return {location_name: base_id} + return {f"{location_name} {i+1}": base_id + i for i in range(amt)} location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(), @@ -215,9 +214,11 @@ location_name_to_id: Dict[str, int] = {} for location_group in location_region_mapping.values(): for locname, locinfo in location_group.items(): - location_name_to_id.update(generate_location_entries(locname, locinfo)) - if locinfo.ltype in ["chest", "pedestal"]: - for i in range(20): - location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}") - else: - location_name_groups[locinfo.ltype].add(locname) + # Iterating the hidden chest and pedestal locations here to avoid clutter above + amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1 + entries = make_location_range(locname, locinfo.id, amount) + + location_name_to_id.update(entries) + location_name_groups[locinfo.ltype].update(entries.keys()) + +shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name} diff --git a/worlds/noita/Options.py b/worlds/noita/options.py similarity index 87% rename from worlds/noita/Options.py rename to worlds/noita/options.py index 0b54597f36..7d987571a5 100644 --- a/worlds/noita/Options.py +++ b/worlds/noita/options.py @@ -1,5 +1,5 @@ -from typing import Dict -from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool +from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions +from dataclasses import dataclass class PathOption(Choice): @@ -99,16 +99,16 @@ class ShopPrice(Choice): default = 100 -noita_options: Dict[str, AssembleOptions] = { - "start_inventory_from_pool": StartInventoryPool, - "death_link": DeathLink, - "bad_effects": Traps, - "victory_condition": VictoryCondition, - "path_option": PathOption, - "hidden_chests": HiddenChests, - "pedestal_checks": PedestalChecks, - "orbs_as_checks": OrbsAsChecks, - "bosses_as_checks": BossesAsChecks, - "extra_orbs": ExtraOrbs, - "shop_price": ShopPrice, -} +@dataclass +class NoitaOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + death_link: DeathLink + bad_effects: Traps + victory_condition: VictoryCondition + path_option: PathOption + hidden_chests: HiddenChests + pedestal_checks: PedestalChecks + orbs_as_checks: OrbsAsChecks + bosses_as_checks: BossesAsChecks + extra_orbs: ExtraOrbs + shop_price: ShopPrice diff --git a/worlds/noita/Regions.py b/worlds/noita/regions.py similarity index 61% rename from worlds/noita/Regions.py rename to worlds/noita/regions.py index 561d483b48..6a9c867723 100644 --- a/worlds/noita/Regions.py +++ b/worlds/noita/regions.py @@ -1,48 +1,43 @@ # Regions are areas in your game that you travel to. -from typing import Dict, Set, List +from typing import Dict, List, TYPE_CHECKING -from BaseClasses import Entrance, MultiWorld, Region -from . import Locations +from BaseClasses import Entrance, Region +from . import locations +from .events import create_all_events + +if TYPE_CHECKING: + from . import NoitaWorld -def add_location(player: int, loc_name: str, id: int, region: Region) -> None: - location = Locations.NoitaLocation(player, loc_name, id, region) - region.locations.append(location) - - -def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None: - locations = Locations.location_region_mapping.get(region.name, {}) - for location_name, location_data in locations.items(): +def create_locations(world: "NoitaWorld", region: Region) -> None: + locs = locations.location_region_mapping.get(region.name, {}) + for location_name, location_data in locs.items(): location_type = location_data.ltype flag = location_data.flag - opt_orbs = multiworld.orbs_as_checks[player].value - opt_bosses = multiworld.bosses_as_checks[player].value - opt_paths = multiworld.path_option[player].value - opt_num_chests = multiworld.hidden_chests[player].value - opt_num_pedestals = multiworld.pedestal_checks[player].value + is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks + is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks + amount = 0 + if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: + amount = 1 + elif location_type == "chest" and flag <= world.options.path_option: + amount = world.options.hidden_chests.value + elif location_type == "pedestal" and flag <= world.options.path_option: + amount = world.options.pedestal_checks.value - is_orb_allowed = location_type == "orb" and flag <= opt_orbs - is_boss_allowed = location_type == "boss" and flag <= opt_bosses - if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: - add_location(player, location_name, location_data.id, region) - elif location_type == "chest" and flag <= opt_paths: - for i in range(opt_num_chests): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) - elif location_type == "pedestal" and flag <= opt_paths: - for i in range(opt_num_pedestals): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) + region.add_locations(locations.make_location_range(location_name, location_data.id, amount), + locations.NoitaLocation) # Creates a new Region with the locations found in `location_region_mapping` and adds them to the world. -def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region: - new_region = Region(region_name, player, multiworld) - add_locations(multiworld, player, new_region) +def create_region(world: "NoitaWorld", region_name: str) -> Region: + new_region = Region(region_name, world.player, world.multiworld) + create_locations(world, new_region) return new_region -def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]: - return {name: create_region(multiworld, player, name) for name in noita_regions} +def create_regions(world: "NoitaWorld") -> Dict[str, Region]: + return {name: create_region(world, name) for name in noita_regions} # An "Entrance" is really just a connection between two regions @@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None: # Creates all regions and connections. Called from NoitaWorld. -def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None: - created_regions = create_regions(multiworld, player) - create_connections(player, created_regions) +def create_all_regions_and_connections(world: "NoitaWorld") -> None: + created_regions = create_regions(world) + create_connections(world.player, created_regions) + create_all_events(world, created_regions) - multiworld.regions += created_regions.values() + world.multiworld.regions += created_regions.values() # Oh, what a tangled web we weave diff --git a/worlds/noita/rules.py b/worlds/noita/rules.py new file mode 100644 index 0000000000..95039bee46 --- /dev/null +++ b/worlds/noita/rules.py @@ -0,0 +1,172 @@ +from typing import List, NamedTuple, Set, TYPE_CHECKING + +from BaseClasses import CollectionState +from . import items, locations +from .options import BossesAsChecks, VictoryCondition +from worlds.generic import Rules as GenericRules + +if TYPE_CHECKING: + from . import NoitaWorld + + +class EntranceLock(NamedTuple): + source: str + destination: str + event: str + items_needed: int + + +entrance_locks: List[EntranceLock] = [ + EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1), + EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2), + EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3), + EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4), + EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5), + EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6), + EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7), +] + + +holy_mountain_regions: List[str] = [ + "Coal Pits Holy Mountain", + "Snowy Depths Holy Mountain", + "Hiisi Base Holy Mountain", + "Underground Jungle Holy Mountain", + "Vault Holy Mountain", + "Temple of the Art Holy Mountain", + "Laboratory Holy Mountain", +] + + +wand_tiers: List[str] = [ + "Wand (Tier 1)", # Coal Pits + "Wand (Tier 2)", # Snowy Depths + "Wand (Tier 3)", # Hiisi Base + "Wand (Tier 4)", # Underground Jungle + "Wand (Tier 5)", # The Vault + "Wand (Tier 6)", # Temple of the Art +] + + +items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", + "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", + "Powder Pouch"} + +perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys())) + + +# ---------------- +# Helper Functions +# ---------------- + + +def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: + return sum(state.count(perk, player) for perk in perk_list) >= amount + + +def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: + return state.count("Orb", player) >= amount + + +def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]): + for shop_location in shop_locations: + location = world.multiworld.get_location(shop_location, world.player) + GenericRules.forbid_items_for_player(location, forbidden_items, world.player) + + +# ---------------- +# Rule Functions +# ---------------- + + +# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them) +# def ban_items_from_shops(world: "NoitaWorld") -> None: +# for location_name in Locations.location_name_to_id.keys(): +# if "Shop Item" in location_name: +# forbid_items_at_location(world, location_name, items_hidden_from_shops) +def ban_items_from_shops(world: "NoitaWorld") -> None: + forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops) + + +# Prevent high tier wands from appearing in early Holy Mountain shops +def ban_early_high_tier_wands(world: "NoitaWorld") -> None: + for i, region_name in enumerate(holy_mountain_regions): + wands_to_forbid = set(wand_tiers[i+1:]) + + locations_in_region = set(locations.location_region_mapping[region_name].keys()) + forbid_items_at_locations(world, locations_in_region, wands_to_forbid) + + # Prevent high tier wands from appearing in the Secret shop + wands_to_forbid = set(wand_tiers[3:]) + locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys()) + forbid_items_at_locations(world, locations_in_region, wands_to_forbid) + + +def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None: + for lock in entrance_locks: + location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player) + GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player)) + + +def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None: + victory_condition = world.options.victory_condition.value + for lock in entrance_locks: + location = world.multiworld.get_location(lock.event, world.player) + + if victory_condition == VictoryCondition.option_greed_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) + ) + elif victory_condition == VictoryCondition.option_pure_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) and + has_orb_count(state, world.player, items_needed) + ) + elif victory_condition == VictoryCondition.option_peaceful_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) and + has_orb_count(state, world.player, items_needed * 3) + ) + + +def biome_unlock_conditions(world: "NoitaWorld"): + lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances + magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances + wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances + for entrance in lukki_entrances: + entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\ + state.has("All-Seeing Eye Perk", world.player) + for entrance in magical_entrances: + entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player) + for entrance in wizard_entrances: + entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player) + + +def victory_unlock_conditions(world: "NoitaWorld") -> None: + victory_condition = world.options.victory_condition.value + victory_location = world.multiworld.get_location("Victory", world.player) + + if victory_condition == VictoryCondition.option_pure_ending: + victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11) + elif victory_condition == VictoryCondition.option_peaceful_ending: + victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33) + + +# ---------------- +# Main Function +# ---------------- + + +def create_all_rules(world: "NoitaWorld") -> None: + if world.multiworld.players > 1: + ban_items_from_shops(world) + ban_early_high_tier_wands(world) + lock_holy_mountains_into_spheres(world) + holy_mountain_unlock_conditions(world) + biome_unlock_conditions(world) + victory_unlock_conditions(world) + + # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) + if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses: + toveri = world.multiworld.get_location("Toveri", world.player) + GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player) From aa72f671bc7a0690200476b0630afd9e06279207 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 21 Jan 2024 19:34:24 +0100 Subject: [PATCH 64/78] SoE: fix naming of atlas medallion (#2747) In pyevermizer, it's called Atlas Medallion, not Amulet, leading to an empty group and to code not considering them as an alchemy ingredient when swapping out for a trap or an energy core fragment. Also adds a test. --- worlds/soe/__init__.py | 2 +- worlds/soe/test/test_item_mapping.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 worlds/soe/test/test_item_mapping.py diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 74387fb1be..bbe018da53 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -81,7 +81,7 @@ for _loc in _locations: # item helpers _ingredients = ( 'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron', - 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet', + 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion', 'Ash', 'Acorn' ) _other_items = ( diff --git a/worlds/soe/test/test_item_mapping.py b/worlds/soe/test/test_item_mapping.py new file mode 100644 index 0000000000..7df05837c7 --- /dev/null +++ b/worlds/soe/test/test_item_mapping.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from .. import SoEWorld + + +class TestMapping(TestCase): + def test_atlas_medallion_name_group(self) -> None: + """ + Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in item groups. + """ + self.assertIn("Any Atlas Medallion", SoEWorld.item_name_groups) + + def test_atlas_medallion_name_items(self) -> None: + """ + Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in items. + """ + found_medallion = False + for name in SoEWorld.item_name_to_id: + self.assertNotIn("Atlas Amulet", name, "Expected Atlas Medallion, not Amulet") + if "Atlas Medallion" in name: + found_medallion = True + self.assertTrue(found_medallion, "Did not find Atlas Medallion in items") From b4212d1c3eaef5980fae7329796636274d80df1a Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 28 Jan 2024 15:13:03 -0500 Subject: [PATCH 65/78] TUNIC: Fix for nmg logic bug (#2772) --- worlds/tunic/er_data.py | 51 ++++++++++++++++++++++---------------- worlds/tunic/er_rules.py | 19 +++++++++++--- worlds/tunic/er_scripts.py | 6 ++--- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 2d3bcc025f..95d33d4aff 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -37,7 +37,7 @@ portal_mapping: List[Portal] = [ destination="Furnace_gyro_lower"), Portal(name="Caustic Light Cave Entrance", region="Overworld", destination="Overworld Cave_"), - Portal(name="Swamp Upper Entrance", region="Overworld Laurels", + Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry", destination="Swamp Redux 2_wall"), Portal(name="Swamp Lower Entrance", region="Overworld", destination="Swamp Redux 2_conduit"), @@ -49,7 +49,7 @@ portal_mapping: List[Portal] = [ destination="Atoll Redux_upper"), Portal(name="Atoll Lower Entrance", region="Overworld", destination="Atoll Redux_lower"), - Portal(name="Special Shop Entrance", region="Overworld Laurels", + Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry", destination="ShopSpecial_"), Portal(name="Maze Cave Entrance", region="Overworld", destination="Maze Room_"), @@ -57,7 +57,7 @@ portal_mapping: List[Portal] = [ destination="Archipelagos Redux_upper"), Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", destination="Archipelagos Redux_lower"), - Portal(name="West Garden Laurels Entrance", region="Overworld Laurels", + Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry", destination="Archipelagos Redux_lowest"), Portal(name="Temple Door Entrance", region="Overworld Temple Door", destination="Temple_main"), @@ -533,7 +533,9 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Overworld": RegionInfo("Overworld Redux"), "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest - "Overworld Laurels": RegionInfo("Overworld Redux"), # all spots in Overworld that you need laurels to reach + "Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot + "Overworld Special Shop Entry": RegionInfo("Overworld Redux"), # special shop entry spot + "Overworld West Garden Laurels Entry": RegionInfo("Overworld Redux"), # west garden laurels entry "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region), "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal @@ -710,7 +712,7 @@ for p1, p2 in hallways.items(): hallway_helper[p2] = p1 # so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints -hallways_nmg: Dict[str, str] = { +hallways_ur: Dict[str, str] = { "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", @@ -720,20 +722,22 @@ hallways_nmg: Dict[str, str] = { "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", } -hallway_helper_nmg: Dict[str, str] = {} -for p1, p2 in hallways.items(): - hallway_helper[p1] = p2 - hallway_helper[p2] = p1 +hallway_helper_ur: Dict[str, str] = {} +for p1, p2 in hallways_ur.items(): + hallway_helper_ur[p1] = p2 + hallway_helper_ur[p2] = p1 # the key is the region you have, the value is the regions you get for having that region # this is mostly so we don't have to do something overly complex to get this information dependent_regions: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"): - ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", - "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal"], ("Old House Front",): ["Old House Front", "Old House Back"], ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): @@ -818,12 +822,14 @@ dependent_regions: Dict[Tuple[str, ...], List[str]] = { dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", "Overworld Ruined Passage Door"): - ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", - "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal"], # can laurels through the gate ("Old House Front", "Old House Back"): ["Old House Front", "Old House Back"], @@ -908,13 +914,14 @@ dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { # can use ladder storage to get to the well rail - ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", "Overworld Ruined Passage Door"): - ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", - "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Well to Furnace Rail"], + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal", "Overworld Well to Furnace Rail"], # can laurels through the gate ("Old House Front", "Old House Back"): ["Old House Front", "Old House Back"], diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 5d88022dc1..ab0cf02bd9 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -53,9 +53,23 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re or (state.has(laurels, player) and options.logic_rules)) regions["Overworld"].connect( - connecting_region=regions["Overworld Laurels"], + connecting_region=regions["Overworld Swamp Upper Entry"], rule=lambda state: state.has(laurels, player)) - regions["Overworld Laurels"].connect( + regions["Overworld Swamp Upper Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Special Shop Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Special Shop Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld West Garden Laurels Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld West Garden Laurels Entry"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has(laurels, player)) @@ -230,7 +244,6 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["West Garden Laurels Exit"], rule=lambda state: state.has(laurels, player)) - # todo: can you wake the boss, then grapple to it, then kill it? regions["West Garden after Boss"].connect( connecting_region=regions["West Garden"], rule=lambda state: state.has(laurels, player)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 4d640b2fda..4e28344b20 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,7 @@ from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_nmg, \ +from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_ur, \ dependent_regions, dependent_regions_nmg, dependent_regions_ur from .er_rules import set_er_region_rules @@ -28,8 +28,8 @@ def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[i if hint_string == "": hint_string = portal.name - if logic_rules: - hallways = hallway_helper_nmg + if logic_rules == "unrestricted": + hallways = hallway_helper_ur else: hallways = hallway_helper From 0bc9966d6f4d216948c7db345b54a04ce94f2d73 Mon Sep 17 00:00:00 2001 From: JusticePS <5125765+JusticePS@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:37:55 -0800 Subject: [PATCH 66/78] Adventure: Fix iterable copy error when freeincarnate_max is tuned low (#2774) --- worlds/adventure/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 105725bd05..9b9b0d77d8 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -271,7 +271,7 @@ class AdventureWorld(World): overworld_locations_copy = overworld.locations.copy() all_locations = self.multiworld.get_locations(self.player) - locations_copy = all_locations.copy() + locations_copy = list(all_locations) for loc in all_locations: if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT: locations_copy.remove(loc) From 69c80501c494be4edf8210949fa5fed7e9f73f7c Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:39:45 -0500 Subject: [PATCH 67/78] KH2: Fix empty location groups (#2757) Co-authored-by: Aaron Wagener --- worlds/kh2/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 61fafe9094..100e971e7d 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1356,5 +1356,5 @@ exclusion_table = { location_groups: typing.Dict[str, list] location_groups = { Region_Name: [loc for loc in Region_Locs if "Event" not in loc] - for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs + for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs and "Event" not in Region_Locs[0] } From 1b188bab3c3de90bcba374aec2e95f3fbe56999d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:21:23 +0100 Subject: [PATCH 68/78] Doc: add GM libs to network protocol.md (#2744) --- docs/network protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index d10e6519a9..338db55299 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -27,6 +27,8 @@ There are also a number of community-supported libraries available that implemen | Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | | Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | | | Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | | +| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older | +| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | | ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. From 5663c21f3990eea1e3f5f37f975d0a76ac032b00 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:34:54 +0100 Subject: [PATCH 69/78] Tests: test that item/location name groups are not empty (#2748) * Tests: test that item/location name groups are not empty * Tests: better name for test_groups TestCase --------- Co-authored-by: Fabian Dill --- test/general/test_groups.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/general/test_groups.py diff --git a/test/general/test_groups.py b/test/general/test_groups.py new file mode 100644 index 0000000000..486d3311fa --- /dev/null +++ b/test/general/test_groups.py @@ -0,0 +1,27 @@ +from unittest import TestCase + +from worlds.AutoWorld import AutoWorldRegister + + +class TestNameGroups(TestCase): + def test_item_name_groups_not_empty(self) -> None: + """ + Test that there are no empty item name groups, which is likely a bug. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + if not world_type.item_id_to_name: + continue # ignore worlds without items + with self.subTest(game=game_name): + for name, group in world_type.item_name_groups.items(): + self.assertTrue(group, f"Item name group \"{name}\" of \"{game_name}\" is empty") + + def test_location_name_groups_not_empty(self) -> None: + """ + Test that there are no empty location name groups, which is likely a bug. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + if not world_type.location_id_to_name: + continue # ignore worlds without locations + with self.subTest(game=game_name): + for name, group in world_type.location_name_groups.items(): + self.assertTrue(group, f"Location name group \"{name}\" of \"{game_name}\" is empty") From dc49d50c2ceb12258c06aaad6bd6f4a0f9267194 Mon Sep 17 00:00:00 2001 From: Benny D <78334662+benny-dreamly@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:58:31 -0700 Subject: [PATCH 70/78] Docs: fixed typo in Stardew Valley setup guide (#2770) * fix typo * fix another typo * Update worlds/stardew_valley/docs/en_Stardew Valley.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/stardew_valley/docs/en_Stardew Valley.md | 2 +- worlds/stardew_valley/docs/setup_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index a880a40b97..04ba9c15c3 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -124,6 +124,6 @@ List of supported mods: ## Multiplayer -You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. +You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 68c7fb9af6..d8f0e16b10 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -84,4 +84,4 @@ See the [Supported mods documentation](https://github.com/agilbert1412/StardewAr ### Multiplayer -You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-terms plans to support that feature. \ No newline at end of file +You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. From 144769a14183492a5b73ede4c6e0c0109038416e Mon Sep 17 00:00:00 2001 From: Ixrec Date: Tue, 30 Jan 2024 08:00:47 +0000 Subject: [PATCH 71/78] Tests: use strict equality in some tests # (#2778) * Tests: replace .assertLess/GreaterEqual() with .assertEqual() in two tests where strict equality seems more correct --- test/general/test_items.py | 8 ++++---- test/general/test_locations.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/general/test_items.py b/test/general/test_items.py index bd6c3fd853..1612937225 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -43,15 +43,15 @@ class TestBase(unittest.TestCase): with self.subTest(group_name, group_name=group_name): self.assertNotIn(group_name, world_type.item_name_to_id) - def test_item_count_greater_equal_locations(self): - """Test that by the pre_fill step under default settings, each game submits items >= locations""" + def test_item_count_equal_locations(self): + """Test that by the pre_fill step under default settings, each game submits items == locations""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): multiworld = setup_solo_multiworld(world_type) - self.assertGreaterEqual( + self.assertEqual( len(multiworld.itempool), len(multiworld.get_unfilled_locations()), - f"{game_name} Item count MUST meet or exceed the number of locations", + f"{game_name} Item count MUST match the number of locations", ) def test_items_in_datapackage(self): diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 725b48e62f..2ac059312c 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -11,14 +11,14 @@ class TestBase(unittest.TestCase): multiworld = setup_solo_multiworld(world_type) locations = Counter(location.name for location in multiworld.get_locations()) if locations: - self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location name {locations.most_common(1)}") + self.assertEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location name {locations.most_common(1)}") locations = Counter(location.address for location in multiworld.get_locations() if type(location.address) is int) if locations: - self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") + self.assertEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") def test_locations_in_datapackage(self): """Tests that created locations not filled before fill starts exist in the datapackage.""" From 697deb98b4df3bee4b8bd0c4bdf024c7276e770d Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 30 Jan 2024 03:06:10 -0500 Subject: [PATCH 72/78] =?UTF-8?q?=20Pok=C3=A9mon=20R/B:=20Fix=20Thunder=20?= =?UTF-8?q?Stone=20item=20groups=20#2740?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index b584869f41..24cad13252 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -42,7 +42,7 @@ item_table = { "Repel": ItemData(30, ItemClassification.filler, ["Consumables"]), "Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils", "Key Items"]), "Fire Stone": ItemData(32, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), - "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones" "Key Items"]), + "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), "Water Stone": ItemData(34, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), "HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]), "Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]), From 016c1e9bb4e539e80df260d114285a48f7fd6e76 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 30 Jan 2024 14:42:33 -0600 Subject: [PATCH 73/78] Docs: world api general cleanup/overhaul (#2598) * Docs: world api general cleanup/overhaul * add pep-0287 to style doc * some cleanup, reorganization, and grammar improvements * reorder item and region creation * address review comments * fix indent * linter grammar Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/options api.md | 31 +- docs/style.md | 7 +- docs/world api.md | 747 ++++++++++++++++++++------------------------ 3 files changed, 359 insertions(+), 426 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index 48a3f763fa..bfab0096bb 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -27,14 +27,15 @@ Choice, and defining `alias_true = option_full`. - All options support `random` as a generic option. `random` chooses from any of the available values for that option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. -As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's -create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass: +As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option +to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our +option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass: ```python # options.py from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions +from Options import Toggle, Range, Choice, PerGameCommonOptions class StartingSword(Toggle): @@ -42,13 +43,33 @@ class StartingSword(Toggle): display_name = "Start With Sword" +class Difficulty(Choice): + """Sets overall game difficulty.""" + display_name = "Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options + alias_expert = 2 # same as hard + default = 1 # default to normal + + +class FinalBossHP(Range): + """Sets the HP of the final boss""" + display_name = "Final Boss HP" + range_start = 100 + range_end = 10000 + default = 2000 + + @dataclass class ExampleGameOptions(PerGameCommonOptions): starting_sword: StartingSword + difficulty: Difficulty + final_boss_health: FinalBossHP ``` -This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it -to our world's `__init__.py`: +To then submit this to the multiworld, we add it to our world's `__init__.py`: ```python from worlds.AutoWorld import World diff --git a/docs/style.md b/docs/style.md index 4cc8111425..fbf681f28e 100644 --- a/docs/style.md +++ b/docs/style.md @@ -6,7 +6,6 @@ * 120 character per line for all source files. * Avoid white space errors like trailing spaces. - ## Python Code * We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences. @@ -18,9 +17,10 @@ * Use type annotations where possible for function signatures and class members. * Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls. +* New classes, attributes, and methods in core code should have docstrings that follow + [reST style](https://peps.python.org/pep-0287/). * Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier. - ## Markdown * We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html). @@ -30,20 +30,17 @@ * One space between bullet/number and text. * No lazy numbering. - ## HTML * Indent with 2 spaces for new code. * kebab-case for ids and classes. - ## CSS * Indent with 2 spaces for new code. * `{` on the same line as the selector. * No space between selector and `{`. - ## JS * Indent with 2 spaces. diff --git a/docs/world api.md b/docs/world api.md index 0ab06da656..72a67bca9d 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -1,95 +1,95 @@ # Archipelago API -This document tries to explain some internals required to implement a game for -Archipelago's generation and server. Once a seed is generated, a client or mod is -required to send and receive items between the game and server. +This document tries to explain some aspects of the Archipelago World API used when implementing the generation logic of +a game. -Client implementation is out of scope of this document. Please refer to an -existing game that provides a similar API to yours. -Refer to the following documents as well: -- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) -- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) +Client implementation is out of scope of this document. Please refer to an existing game that provides a similar API to +yours, and the following documents: + +* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) +* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) Archipelago will be abbreviated as "AP" from now on. - ## Language AP worlds are written in python3. -Clients that connect to the server to sync items can be in any language that -allows using WebSockets. - +Clients that connect to the server to sync items can be in any language that allows using WebSockets. ## Coding style -AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). -When in doubt use an IDE with coding style linter, for example PyCharm Community Edition. - +AP follows a [style guide](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). +When in doubt, use an IDE with a code-style linter, for example PyCharm Community Edition. ## Docstrings -Docstrings are strings attached to an object in Python that describe what the -object is supposed to be. Certain docstrings will be picked up and used by AP. -They are assigned by writing a string without any assignment right below a -definition. The string must be a triple-quoted string. +Docstrings are strings attached to an object in Python that describe what the object is supposed to be. Certain +docstrings will be picked up and used by AP. They are assigned by writing a string without any assignment right below a +definition. The string must be a triple-quoted string, and should +follow [reST style](https://peps.python.org/pep-0287/). + Example: + ```python from worlds.AutoWorld import World -class MyGameWorld(World): - """This is the description of My Game that will be displayed on the AP - website.""" -``` +class MyGameWorld(World): + """This is the description of My Game that will be displayed on the AP website.""" +``` + ## Definitions -This section will cover various classes and objects you can use for your world. -While some of the attributes and methods are mentioned here, not all of them are, -but you can find them in `BaseClasses.py`. +This section covers various classes and objects you can use for your world. While some of the attributes and methods +are mentioned here, not all of them are, but you can find them in +[`BaseClasses.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py). ### World Class -A `World` class is the class with all the specifics of a certain game to be -included. It will be instantiated for each player that rolls a seed for that -game. +A `World` is the class with all the specifics of a certain game that is to be included. A new instance will be created +for each player of the game for any given generated multiworld. ### WebWorld Class -A `WebWorld` class contains specific attributes and methods that can be modified -for your world specifically on the webhost: +A `WebWorld` class contains specific attributes and methods that can be modified for your world specifically on the +webhost: -`settings_page`, which can be changed to a link instead of an AP generated settings page. +* `options_page` can be changed to a link instead of an AP-generated options page. -`theme` to be used for your game specific AP pages. Available themes: +* `theme` to be used for your game-specific AP pages. Available themes: -| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | -|---|---|---|---|---|---|---|---| -| | | | | | | | | + | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | + |--------------------------------------------|---------------------------------------------|----------------------------------------------------|-------------------------------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------------|---------------------------------------------| + | | | | | | | | | -`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs. +* `bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be + placed by the site to help users report bugs. -`tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. +* `tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. -`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be -prefixed with the same string as defined here. Default already has 'en'. +* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The + documents must be prefixed with the same string as defined here. Default already has 'en'. -`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values -are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of -the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. +* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values + are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names + of the options and the values are the values to be set for that option. These presets will be available for users to + select from on the game's options page. Note: The values must be a non-aliased value for the option type and can only include the following option types: - - If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` - values. - - If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the +* If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + * If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the `special_range_names` keys. - - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. - - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. - - `random` is also a valid value for any of these option types. +* If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. +* If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. +* `random` is also a valid value for any of these option types. -`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time. +`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on +the webhost at this time. Here is an example of a defined preset: + ```python # presets.py options_presets = { @@ -114,6 +114,7 @@ options_presets = { } } + # __init__.py class RLWeb(WebWorld): options_presets = options_presets @@ -122,47 +123,55 @@ class RLWeb(WebWorld): ### MultiWorld Object -The `MultiWorld` object references the whole multiworld (all items and locations -for all players) and is accessible through `self.multiworld` inside a `World` object. +The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible +through `self.multiworld` from your `World` object. ### Player -The player is just an integer in AP and is accessible through `self.player` -inside a `World` object. +The player is just an `int` in AP and is accessible through `self.player` from your `World` object. ### Player Options -Players provide customized settings for their World in the form of yamls. -A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`. -(It must be a subclass of `PerGameCommonOptions`.) -Option results are automatically added to the `World` object for easy access. -Those are accessible through `self.options.`, and you can get a dictionary of the option values via -`self.options.as_dict()`, passing the desired options as strings. +Options are provided by the user as part of the generation process, intended to affect how their randomizer experience +should play out. These can control aspects such as what locations should be shuffled, what items are in the itempool, +etc. Players provide the customized options for their World in the form of yamls. + +By convention, options are defined in `options.py` and will be used when parsing the players' yaml files. Each option +has its own class, which inherits from a base option type, a docstring to describe it, and a `display_name` property +shown on the website and in spoiler logs. + +The available options are defined by creating a `dataclass`, which must be a subclass of `PerGameCommonOptions`. It has +defined fields for the option names used in the player yamls and used for options access, with their types matching the +appropriate Option class. By convention, the strings that define your option names should be in `snake_case`. The +`dataclass` is then assigned to your `World` by defining its `options_dataclass`. Option results are then automatically +added to the `World` object for easy access, between `World` creation and `generate_early`. These are accessible through +`self.options.`, and you can get a dictionary with option values +via `self.options.as_dict()`, +passing the desired option names as strings. + +Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, and `Range`. +For more information, see the [options api doc](options%20api.md). ### World Settings -Any AP installation can provide settings for a world, for example a ROM file, accessible through -`self.settings.` or `cls.settings.` (new API) -or `Utils.get_options()["_options"][""]` (deprecated). +Settings are set by the user outside the generation process. They can be used for those settings that may affect +generation or client behavior, but should remain static between generations, such as the path to a ROM file. +These settings are accessible through `self.settings.` or `cls.settings.`. -Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. +Users can set these in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. -Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) -for details. +Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) for details. ### Locations -Locations are places where items can be located in your game. This may be chests -or boss drops for RPG-like games but could also be progress in a research tree. +Locations are places where items can be located in your game. This may be chests or boss drops for RPG-like games, but +could also be progress in a research tree, or even something more abstract like a level up. -Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed -in a Region, has access rules and a classification. -The name needs to be unique in each game and must not be numeric (has to -contain least 1 letter or symbol). The ID needs to be unique across all games -and is best in the same range as the item IDs. -World-specific IDs are 1 to 253-1, IDs ≤ 0 are global and reserved. +Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, +and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 +letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs. -Special locations with ID `None` can hold events. +World-specific IDs must be in the range 1 to 253-1; IDs ≤ 0 are global and reserved. Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`. The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being @@ -170,22 +179,21 @@ required, and will prevent progression and useful items from being placed at exc #### Documenting Locations -Worlds can optionally provide a `location_descriptions` map which contains -human-friendly descriptions of locations or location groups. These descriptions -will show up in location-selection options in the Weighted Options page. Extra +Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and +location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra indentation and single newlines will be collapsed into spaces. ```python -# Locations.py +# locations.py location_descriptions = { "Red Potion #6": "In a secret destructible block under the second stairway", - "L2 Spaceship": """ - The group of all items in the spaceship in Level 2. + "L2 Spaceship": + """ + The group of all items in the spaceship in Level 2. - This doesn't include the item on the spaceship door, since it can be - accessed without the Spaeship Key. - """ + This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. + """ } ``` @@ -193,7 +201,7 @@ location_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Locations import location_descriptions +from .locations import location_descriptions class MyGameWorld(World): @@ -202,47 +210,45 @@ class MyGameWorld(World): ### Items -Items are all things that can "drop" for your game. This may be RPG items like -weapons, could as well be technologies you normally research in a research tree. +Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally +research in a research tree. -Each item has a `name`, an `id` (can be known as "code"), and a classification. -The most important classification is `progression` (formerly advancement). -Progression items are items which a player may require to progress in -their world. Progression items will be assigned to locations with higher -priority and moved around to meet defined rules and accomplish progression -balancing. +Each item has a `name`, a `code` (hereafter referred to as `id`), and a classification. +The most important classification is `progression`. Progression items are items which a player *may* require to progress +in their world. If an item can possibly be considered for logic (it's referenced in a location's rules) it *must* be +progression. Progression items will be assigned to locations with higher priority, and moved around to meet defined rules +and satisfy progression balancing. -The name needs to be unique in each game, meaning a duplicate item has the -same ID. Name must not be numeric (has to contain at least 1 letter or symbol). +The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they +will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). -Special items with ID `None` can mark events (read below). +Other classifications include: -Other classifications include * `filler`: a regular item or trash item -* `useful`: generally quite useful, but not required for anything logical +* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations * `trap`: negative impact on the player * `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be combined with `progression`; see below) * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that - will not be moved around by progression balancing; used, e.g., for currency or tokens + will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres #### Documenting Items -Worlds can optionally provide an `item_descriptions` map which contains -human-friendly descriptions of items or item groups. These descriptions will -show up in item-selection options in the Weighted Options page. Extra -indentation and single newlines will be collapsed into spaces. +Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item +groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and +single newlines will be collapsed into spaces. ```python -# Items.py +# items.py item_descriptions = { "Red Potion": "A standard health potion", - "Spaceship Key": """ - The key to the spaceship in Level 2. + "Spaceship Key": + """ + The key to the spaceship in Level 2. - This is necessary to get to the Star Realm. - """ + This is necessary to get to the Star Realm. + """ } ``` @@ -250,7 +256,7 @@ item_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Items import item_descriptions +from .items import item_descriptions class MyGameWorld(World): @@ -259,216 +265,129 @@ class MyGameWorld(World): ### Events -Events will mark some progress. You define an event location, an -event item, strap some rules to the location (i.e. hold certain -items) and manually place the event item at the event location. +An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to +track certain logic interactions, with the Event Item being required for access in other locations or regions, but not +being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is +never made aware of them and these locations can never be checked, nor can the items be received during play. +They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that +is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last +relevant Item. Events function just like any other Location, and can still have their own access rules, etc. +By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement. +They must not exist in the `name_to_id` lookups, as they have no ID. -Events can be used to either simplify the logic or to get better spoiler logs. -Events will show up in the spoiler playthrough but they do not represent actual -items or locations within the game. +The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created: -There is one special case for events: Victory. To get the win condition to show -up in the spoiler log, you create an event item and place it at an event -location with the `access_rules` for game completion. Once that's done, the -world's win condition can be as simple as checking for that item. +```python +from worlds.AutoWorld import World +from BaseClasses import ItemClassification +from .subclasses import MyGameLocation, MyGameItem -By convention the victory event is called `"Victory"`. It can be placed at one -or more event locations based on player options. + +class MyGameWorld(World): + victory_loc = MyGameLocation(self.player, "Victory", None) + victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player)) +``` ### Regions -Regions are logical groups of locations that share some common access rules. If -location logic is written from scratch, using regions greatly simplifies the -definition and allows to somewhat easily implement things like entrance -randomizer in logic. +Regions are logical containers that typically hold locations that share some common access rules. If location logic is +written from scratch, using regions greatly simplifies the requirements and can help with implementing things +like entrance randomization in logic. -Regions have a list called `exits`, which are `Entrance` objects representing -transitions to other regions. +Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions. -There has to be one special region "Menu" from which the logic unfolds. AP -assumes that a player will always be able to return to the "Menu" region by -resetting the game ("Save and quit"). +There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to +return to the "Menu" region by resetting the game ("Save and quit"). ### Entrances -An `Entrance` connects to a region, is assigned to region's exits and has rules -to define if it and thus the connected region is accessible. -They can be static (regular logic) or be defined/connected during generation -(entrance randomizer). +An `Entrance` has a `parent_region` and `connected_region`, where it is in the `exits` of its parent, and the +`entrances` of its connected region. The `Entrance` then has rules assigned to it to determine if it can be passed +through, making the connected region accessible. They can be static (regular logic) or be defined/connected during +generation (entrance randomization). ### Access Rules -An access rule is a function that returns `True` or `False` for a `Location` or -`Entrance` based on the current `state` (items that can be collected). +An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state` +(items that have been collected). ### Item Rules -An item rule is a function that returns `True` or `False` for a `Location` based -on a single item. It can be used to reject placement of an item there. - +An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to +reject the placement of an item there. ## Implementation ### Your World -All code for your world implementation should be placed in a python package in -the `/worlds` directory. The starting point for the package is `__init__.py`. -Conventionally, your world class is placed in that file. +All code for your world implementation should be placed in a python package in the `/worlds` directory. The starting +point for the package is `__init__.py`. Conventionally, your `World` class is placed in that file. -World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, -which can be imported as `from worlds.AutoWorld import World` from your package. +World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, which can be imported as +`from worlds.AutoWorld import World` from your package. AP will pick up your world automatically due to the `AutoWorld` implementation. ### Requirements -If your world needs specific python packages, they can be listed in -`worlds//requirements.txt`. ModuleUpdate.py will automatically -pick up and install them. +If your world needs specific python packages, they can be listed in `worlds//requirements.txt`. +ModuleUpdate.py will automatically pick up and install them. See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format). ### Relative Imports -AP will only import the `__init__.py`. Depending on code size it makes sense to -use multiple files and use relative imports to access them. +AP will only import the `__init__.py`. Depending on code size, it may make sense to use multiple files and use relative +imports to access them. -e.g. `from .options import MyGameOptions` from your `__init__.py` will load -`world/[world_name]/options.py` and make its `MyGameOptions` accessible. +e.g. `from .options import MyGameOptions` from your `__init__.py` will load `world/[world_name]/options.py` and make +its `MyGameOptions` accessible. -When imported names pile up it may be easier to use `from . import options` -and access the variable as `options.MyGameOptions`. +When imported names pile up, it may be easier to use `from . import options` and access the variable as +`options.MyGameOptions`. -Imports from directories outside your world should use absolute imports. -Correct use of relative / absolute imports is required for zipped worlds to -function, see [apworld specification.md](apworld%20specification.md). +Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is +required for zipped worlds to function, see [apworld specification.md](apworld%20specification.md). ### Your Item Type -Each world uses its own subclass of `BaseClasses.Item`. The constructor can be -overridden to attach additional data to it, e.g. "price in shop". -Since the constructor is only ever called from your code, you can add whatever -arguments you like to the constructor. +Each world uses its own subclass of `BaseClasses.Item`. The constructor can be overridden to attach additional data to +it, e.g. "price in shop". Since the constructor is only ever called from your code, you can add whatever arguments you +like to the constructor. + +In its simplest form, we only set the game name and use the default constructor: -In its simplest form we only set the game name and use the default constructor ```python from BaseClasses import Item + class MyGameItem(Item): game: str = "My Game" ``` -By convention this class definition will either be placed in your `__init__.py` -or your `items.py`. For a more elaborate example see `worlds/oot/Items.py`. -### Your location type +By convention, this class definition will either be placed in your `__init__.py` or your `items.py`. For a more +elaborate example see +[`worlds/oot/Items.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py). + +### Your Location Type + +The same thing we did for items above, we will now do for locations: -The same we have done for items above, we will do for locations ```python from BaseClasses import Location + class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = "", code = None, parent = None) -> None: + def __init__(self, player: int, name="", code=None, parent=None) -> None: super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` + in your `__init__.py` or your `locations.py`. -### Options - -By convention options are defined in `options.py` and will be used when parsing -the players' yaml files. - -Each option has its own class, inherits from a base option type, has a docstring -to describe it and a `display_name` property for display on the website and in -spoiler logs. - -The actual name as used in the yaml is defined via the field names of a `dataclass` that is -assigned to the world under `self.options_dataclass`. By convention, the strings -that define your option names should be in `snake_case`. - -Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. -For more see `Options.py` in AP's base directory. - -#### Toggle, DefaultOnToggle - -These don't need any additional properties defined. After parsing the option, -its `value` will either be True or False. - -#### Range - -Define properties `range_start`, `range_end` and `default`. Ranges will be -displayed as sliders on the website and can be set to random in the yaml. - -#### Choice - -Choices are like toggles, but have more options than just True and False. -Define a property `option_ = ` per selectable value and -`default = ` to set the default selection. Aliases can be set by -defining a property `alias_ = `. - -```python -option_off = 0 -option_on = 1 -option_some = 2 -alias_disabled = 0 -alias_enabled = 1 -default = 0 -``` - -#### Sample -```python -# options.py - -from dataclasses import dataclass -from Options import Toggle, Range, Choice, PerGameCommonOptions - -class Difficulty(Choice): - """Sets overall game difficulty.""" - display_name = "Difficulty" - option_easy = 0 - option_normal = 1 - option_hard = 2 - alias_beginner = 0 # same as easy - alias_expert = 2 # same as hard - default = 1 # default to normal - -class FinalBossHP(Range): - """Sets the HP of the final boss""" - display_name = "Final Boss HP" - range_start = 100 - range_end = 10000 - default = 2000 - -class FixXYZGlitch(Toggle): - """Fixes ABC when you do XYZ""" - display_name = "Fix XYZ Glitch" - -# By convention, we call the options dataclass `Options`. -# It has to be derived from 'PerGameCommonOptions'. -@dataclass -class MyGameOptions(PerGameCommonOptions): - difficulty: Difficulty - final_boss_hp: FinalBossHP - fix_xyz_glitch: FixXYZGlitch -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .options import MyGameOptions # import the options dataclass - - -class MyGameWorld(World): - # ... - options_dataclass = MyGameOptions # assign the options dataclass to the world - options: MyGameOptions # typing for option results - # ... -``` - ### A World Class Skeleton ```python @@ -483,7 +402,6 @@ from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification - class MyGameItem(Item): # or from Items import MyGameItem game = "My Game" # name of the game/world this item is from @@ -492,7 +410,6 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation game = "My Game" # name of the game/world this location is in - class MyGameSettings(settings.Group): class RomFile(settings.SNESRomPath): """Insert help text for host.yaml here.""" @@ -511,7 +428,7 @@ class MyGameWorld(World): # ID of first item and location, could be hard-coded but code may be easier # to read with this as a property. base_id = 1234 - # Instead of dynamic numbering, IDs could be part of data. + # instead of dynamic numbering, IDs could be part of data # The following two dicts are required for the generation to know which # items exist. They could be generated from json or something else. They can @@ -530,74 +447,106 @@ class MyGameWorld(World): ### Generation -The world has to provide the following things for generation +The world has to provide the following things for generation: -* the properties mentioned above +* the properties mentioned above * additions to the item pool * additions to the regions list: at least one called "Menu" * locations placed inside those regions * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand -* applying `self.multiworld.push_precollected` for world defined start inventory -* `required_client_version: Tuple[int, int, int]` - Optional client version as tuple of 3 ints to make sure the client is compatible to - this world (e.g. implements all required features) when connecting. +* applying `self.multiworld.push_precollected` for world-defined start inventory -In addition, the following methods can be implemented and are called in this order during generation +In addition, the following methods can be implemented and are called in this order during generation: -* `stage_assert_generate(cls, multiworld)` is a class method called at the start of - generation to check the existence of prerequisite files, usually a ROM for +* `stage_assert_generate(cls, multiworld: MultiWorld)` + a class method called at the start of generation to check for the existence of prerequisite files, usually a ROM for games which require one. * `generate_early(self)` - called per player before any items or locations are created. You can set properties on your world here. Already has - access to player options and RNG. This is the earliest step where the world should start setting up for the current - multiworld as any steps before this, the multiworld itself is still getting set up + called per player before any items or locations are created. You can set properties on your + world here. Already has access to player options and RNG. This is the earliest step where the world should start + setting up for the current multiworld, as the multiworld itself is still setting up before this point. * `create_regions(self)` - called to place player's regions and their locations into the MultiWorld's regions list. If it's - hard to separate, this can be done during `generate_early` or `create_items` as well. + called to place player's regions and their locations into the MultiWorld's regions list. + If it's hard to separate, this can be done during `generate_early` or `create_items` as well. * `create_items(self)` - called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in - the MultiWorld's regions and itempool, and these lists should not be modified afterwards. + called to place player's items into the MultiWorld's itempool. After this step all regions + and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward. * `set_rules(self)` - called to set access and item rules on locations and entrances. - Locations have to be defined before this, or rule application can miss them. + called to set access and item rules on locations and entrances. * `generate_basic(self)` - called after the previous steps. Some placement and player specific - randomizations can be done here. -* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` are called to modify item placement - before, during and after the regular fill process, before `generate_output`. - If items need to be placed during pre_fill, these items can be determined - and created using `get_prefill_items` -* `generate_output(self, output_directory: str)` that creates the output - files if there is output to be generated. When this is - called, `self.multiworld.get_locations(self.player)` has all locations for the player, with - attribute `item` pointing to the item. - `location.item.player` can be used to see if it's a local item. + player-specific randomization that does not affect logic can be done here. +* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` + called to modify item placement before, during, and after the regular fill process; all finishing before + `generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there + are any items that need to be filled this way, but need to be in state while you fill other items, they can be + returned from `get_prefill_items`. +* `generate_output(self, output_directory: str)` + creates the output files if there is output to be generated. When this is called, + `self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the + item. `location.item.player` can be used to see if it's a local item. * `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that will be used by the server to host the MultiWorld. +All instance methods can, optionally, have a class method defined which will be called after all instance methods are +finished running, by defining a method with `stage_` in front of the method name. These class methods will have the +args `(cls, multiworld: MultiWorld)`, followed by any other args that the relevant instance method has. #### generate_early ```python def generate_early(self) -> None: - # read player settings to world instance + # read player options to world instance self.final_boss_hp = self.options.final_boss_hp.value ``` +#### create_regions + +```python +def create_regions(self) -> None: + # Add regions to the multiworld. "Menu" is the required starting point. + # Arguments to Region() are name, player, multiworld, and optionally hint_text + menu_region = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu_region) # or use += [menu_region...] + + main_region = Region("Main Area", self.player, self.multiworld) + # add main area's locations to main area (all but final boss) + main_region.add_locations(main_region_locations, MyGameLocation) + # or + # main_region.locations = \ + # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] + self.multiworld.regions.append(main_region) + + boss_region = Region("Boss Room", self.player, self.multiworld) + # add event to Boss Room + boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) + + # if entrances are not randomized, they should be connected here, otherwise they can also be connected at a later stage + # create Entrances and connect the Regions + menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule + # or + main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) + # connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse + + # if setting location access rules from data is easier here, set_rules can possibly be omitted +``` + #### create_item ```python -# we need a way to know if an item provides progress in the game ("key item") -# this can be part of the items definition, or depend on recipe randomization +# we need a way to know if an item provides progress in the game ("key item") this can be part of the items definition, +# or depend on recipe randomization from .items import is_progression # this is just a dummy + def create_item(self, item: str) -> MyGameItem: - # This is called when AP wants to create an item by name (for plando) or - # when you call it from your own code. - classification = ItemClassification.progression if is_progression(item) else \ - ItemClassification.filler - return MyGameItem(item, classification, self.item_name_to_id[item], - self.player) + # this is called when AP wants to create an item by name (for plando) or when you call it from your own code + classification = ItemClassification.progression if is_progression(item) else + ItemClassification.filler + + +return MyGameItem(item, classification, self.item_name_to_id[item], + self.player) + def create_event(self, event: str) -> MyGameItem: # while we are at it, we can also add a helper to create events @@ -610,8 +559,7 @@ def create_event(self, event: str) -> MyGameItem: def create_items(self) -> None: # Add items to the Multiworld. # If there are two of the same item, the item has to be twice in the pool. - # Which items are added to the pool may depend on player settings, - # e.g. custom win condition like triforce hunt. + # Which items are added to the pool may depend on player options, e.g. custom win condition like triforce hunt. # Having an item in the start inventory won't remove it from the pool. # If an item can't have duplicates it has to be excluded manually. @@ -627,67 +575,10 @@ def create_items(self) -> None: # itempool and number of locations should match up. # If this is not the case we want to fill the itempool with junk. - junk = 0 # calculate this based on player settings + junk = 0 # calculate this based on player options self.multiworld.itempool += [self.create_item("nothing") for _ in range(junk)] ``` -#### create_regions - -```python -def create_regions(self) -> None: - # Add regions to the multiworld. "Menu" is the required starting point. - # Arguments to Region() are name, player, world, and optionally hint_text - menu_region = Region("Menu", self.player, self.multiworld) - self.multiworld.regions.append(menu_region) # or use += [menu_region...] - - main_region = Region("Main Area", self.player, self.multiworld) - # Add main area's locations to main area (all but final boss) - main_region.add_locations(main_region_locations, MyGameLocation) - # or - # main_region.locations = \ - # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] - self.multiworld.regions.append(main_region) - - boss_region = Region("Boss Room", self.player, self.multiworld) - # Add event to Boss Room - boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) - - # If entrances are not randomized, they should be connected here, - # otherwise they can also be connected at a later stage. - # Create Entrances and connect the Regions - menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule - # or - main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) - # Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse - - # If setting location access rules from data is easier here, set_rules can - # possibly omitted. -``` - -#### generate_basic - -```python -def generate_basic(self) -> None: - # place "Victory" at "Final Boss" and set collection as win condition - self.multiworld.get_location("Final Boss", self.player) - .place_locked_item(self.create_event("Victory")) - self.multiworld.completion_condition[self.player] = - lambda state: state.has("Victory", self.player) - - # place item Herb into location Chest1 for some reason - item = self.create_item("Herb") - self.multiworld.get_location("Chest1", self.player).place_locked_item(item) - # in most cases it's better to do this at the same time the itempool is - # filled to avoid accidental duplicates: - # manually placed and still in the itempool - - # for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to - # write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations - # are connected and placed as desired - # from Utils import visualize_regions - # visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") -``` - ### Setting Rules ```python @@ -703,6 +594,7 @@ def set_rules(self) -> None: # set a simple rule for an region set_rule(self.multiworld.get_entrance("Boss Door", self.player), lambda state: state.has("Boss Key", self.player)) + # location.access_rule = ... is likely to be a bit faster # combine rules to require two items add_rule(self.multiworld.get_location("Chest2", self.player), lambda state: state.has("Sword", self.player)) @@ -730,59 +622,80 @@ def set_rules(self) -> None: # get_item_type needs to take player/world into account # if MyGameItem has a type property, a more direct implementation would be add_item_rule(self.multiworld.get_location("Chest5", self.player), - lambda item: item.player != self.player or\ + lambda item: item.player != self.player or item.my_type == "weapon") # location.item_rule = ... is likely to be a bit faster + + # place "Victory" at "Final Boss" and set collection as win condition + self.multiworld.get_location("Final Boss", self.player).place_locked_item(self.create_event("Victory")) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + +# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to +# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations +# are connected and placed as desired +# from Utils import visualize_regions +# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") ``` -### Logic Mixin +### Custom Logic Rules -While lambdas and events could do pretty much anything, by convention we -implement more complex logic in logic mixins, even if there is no need to add -properties to the `BaseClasses.CollectionState` state object. - -When importing a file that defines a class that inherits from -`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by -the mixin's members. These members should be prefixed with underscore following -the name of the implementing world. This is due to sharing a namespace with all -other logic mixins. - -Typical uses are defining methods that are used instead of `state.has` -in lambdas, e.g.`state.mygame_has(custom, player)` or recurring checks -like `state.mygame_can_do_something(player)` to simplify lambdas. -Private members, only accessible from mixins, should start with `_mygame_`, -public members with `mygame_`. - -More advanced uses could be to add additional variables to the state object, -override `World.collect(self, state, item)` and `remove(self, state, item)` -to update the state object, and check those added variables in added methods. -Please do this with caution and only when necessary. - -#### Sample +Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or +Entrance should be +a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9). +Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other +functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly. +For an example, see [The Messenger](/worlds/messenger/rules.py). ```python # logic.py -from worlds.AutoWorld import LogicMixin +from BaseClasses import CollectionState -class MyGameLogic(LogicMixin): - def mygame_has_key(self, player: int) -> bool: - # Arguments above are free to choose - # MultiWorld can be accessed through self.multiworld, explicitly passing in - # MyGameWorld instance for easy options access is also a valid approach - return self.has("key", player) # or whatever + +def mygame_has_key(self, state: CollectionState, player: int) -> bool: + # More arguments above are free to choose, since you can expect this is only called in your world + # MultiWorld can be accessed through state.multiworld. + # Explicitly passing in MyGameWorld instance for easy options access is also a valid approach, but it's generally + # better to check options before rule assignment since the individual functions can be called thousands of times + return state.has("key", player) # or whatever ``` + ```python # __init__.py from worlds.generic.Rules import set_rule -import .logic # apply the mixin by importing its file +from . import logic + class MyGameWorld(World): # ... def set_rules(self) -> None: set_rule(self.multiworld.get_location("A Door", self.player), - lambda state: state.mygame_has_key(self.player)) + lambda state: logic.mygame_has_key(state, self.player)) +``` + +### Logic Mixin + +While lambdas and events can do pretty much anything, more complex logic can be handled in logic mixins. + +When importing a file that defines a class that inherits from `worlds.AutoWorld.LogicMixin`, the `CollectionState` class +is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing +world since the namespace is shared with all other logic mixins. + +Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified +with the state. +Please do this with caution and only when necessary. + +#### pre_fill + +```python +def pre_fill(self) -> None: + # place item Herb into location Chest1 for some reason + item = self.create_item("Herb") + self.multiworld.get_location("Chest1", self.player).place_locked_item(item) + # in most cases it's better to do this at the same time the itempool is + # filled to avoid accidental duplicates, such as manually placed and still in the itempool ``` ### Generate Output @@ -792,9 +705,9 @@ from .mod import generate_mod def generate_output(self, output_directory: str) -> None: - # How to generate the mod or ROM highly depends on the game - # if the mod is written in Lua, Jinja can be used to fill a template - # if the mod reads a json file, `json.dump()` can be used to generate that + # How to generate the mod or ROM highly depends on the game. + # If the mod is written in Lua, Jinja can be used to fill a template. + # If the mod reads a json file, `json.dump()` can be used to generate that. # code below is a dummy data = { "seed": self.multiworld.seed_name, # to verify the server's multiworld @@ -804,8 +717,7 @@ def generate_output(self, output_directory: str) -> None: for location in self.multiworld.get_filled_locations(self.player)}, # store start_inventory from player's .yaml # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod - "starter_items": [item.name for item - in self.multiworld.precollected_items[self.player]], + "starter_items": [item.name for item in self.multiworld.precollected_items[self.player]], } # add needed option results to the dictionary @@ -824,20 +736,20 @@ def generate_output(self, output_directory: str) -> None: ### Slot Data If the game client needs to know information about the generated seed, a preferred method of transferring the data -is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`, -but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once -it has successfully [connected](network%20protocol.md#connected). -If you need to know information about locations in your world, instead -of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that -data already exists on the server. The most common usage of slot data is to send option results that the client needs -to be aware of. +is through the slot data. This is filled with the `fill_slot_data` method of your world by returning +a `Dict[str, Any]`, but, to not waste resources, should be limited to data that is absolutely necessary. Slot data is +sent to your client once it has successfully [connected](network%20protocol.md#connected). +If you need to know information about locations in your world, instead of propagating the slot data, it is preferable +to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most +common usage of slot data is sending option results that the client needs to be aware of. ```python def fill_slot_data(self) -> Dict[str, Any]: - # in order for our game client to handle the generated seed correctly we need to know what the user selected - # for their difficulty and final boss HP - # a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting - # the options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the option's value + # In order for our game client to handle the generated seed correctly we need to know what the user selected + # for their difficulty and final boss HP. + # A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting. + # The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant + # option's value. return self.options.as_dict("difficulty", "final_boss_hp") ``` @@ -847,15 +759,17 @@ Each world implementation should have a tutorial and a game info page. These are the `.md` files in your world's `/docs` directory. #### Game Info + The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional information that may be useful to the player when learning your randomizer should also go here. The file name format is `_.md`. While you can write these docs for multiple languages, currently only the english version is displayed on the website. #### Tutorials + Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial` -defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what -the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, +defined in the `WebWorld`. The file name you use isn't particularly important, but it should be descriptive of what +the tutorial covers, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where `game/document/lang` is the provided URL. @@ -874,12 +788,13 @@ from test.bases import WorldTestBase class MyGameTestBase(WorldTestBase): - game = "My Game" + game = "My Game" ``` -Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. +Next, using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. Example `test_chest_access.py` + ```python from . import MyGameTestBase @@ -889,15 +804,15 @@ class TestChestAccess(MyGameTestBase): """Test locations that require a sword""" locations = ["Chest1", "Chest2"] items = [["Sword"]] - # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained. + # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained self.assertAccessDependency(locations, items) def test_any_weapon_chests(self) -> None: """Test locations that require any weapon""" locations = [f"Chest{i}" for i in range(3, 6)] items = [["Sword"], ["Axe"], ["Spear"]] - # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them. + # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them self.assertAccessDependency(locations, items) ``` -For more information on tests check the [tests doc](tests.md). +For more information on tests, check the [tests doc](tests.md). From 3a51c035ac0fa6c562f46bc3e4276be9455c022a Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 31 Jan 2024 01:56:35 -0500 Subject: [PATCH 74/78] Lingo: Enable start_inventory_from_pool (#2781) --- worlds/lingo/__init__.py | 7 +++++-- worlds/lingo/options.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 0889674450..2f93541932 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -82,9 +82,8 @@ class LingoWorld(World): skips = int(non_traps * skip_percentage / 100.0) non_skips = non_traps - skips - filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] for i in range(0, non_skips): - pool.append(self.create_item(filler_list[i % len(filler_list)])) + pool.append(self.create_item(self.get_filler_item_name())) for i in range(0, skips): pool.append(self.create_item("Puzzle Skip")) @@ -130,3 +129,7 @@ class LingoWorld(World): slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data + + def get_filler_item_name(self) -> str: + filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] + return self.random.choice(filler_list) diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index ec6158fab5..ed1426450e 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool class ShuffleDoors(Choice): @@ -136,3 +136,4 @@ class LingoOptions(PerGameCommonOptions): trap_percentage: TrapPercentage puzzle_skip_percentage: PuzzleSkipPercentage death_link: DeathLink + start_inventory_from_pool: StartInventoryPool From 140f8025647ab38ecf9d8acefe5f48f36ab69ad1 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 31 Jan 2024 02:01:55 -0500 Subject: [PATCH 75/78] LTTP: Update playerSettings.yaml to require AP version 0.4.4 (#2737) Updating yaml to make sure people have the proper required version if they ever use this template to play with KDS --- playerSettings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index f9585da246..b6b474a9ff 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc game: # Pick a game to play A Link to the Past: 1 requires: - version: 0.4.3 # Version of Archipelago required for this yaml to work as expected. + version: 0.4.4 # Version of Archipelago required for this yaml to work as expected. A Link to the Past: progression_balancing: # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. From 57cb971177c22f723dd173f3fe29446514b61541 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:07:07 +0100 Subject: [PATCH 76/78] The Witness: Junk hints for Shivers, Mystic Quest and Heretic (#2592) --- worlds/witness/hints.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index c00827feee..e2d1069bd1 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -75,6 +75,9 @@ joke_hints = [ "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.", "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.", "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.", + "Have you tried Final Fantasy Mystic Quest?\nApparently, it was made in an attempt to simplify Final Fantasy for the western market.\nThey were right, I suck at RPGs.", + "Have you tried Shivers?\nWitness 2 should totally feature a haunted Museum.", + "Have you tried Heretic?\nWait, there is a Doom Engine game where you can look UP AND DOWN???", "One day I was fascinated by the subject of generation of waves by wind.", "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", @@ -148,7 +151,7 @@ joke_hints = [ "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!", "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.", "How many of you have personally witnessed a total solar eclipse?", - "In the Treehouse area, you will find \n[Error: Data not found] progression items.", + "In the Treehouse area, you will find 69 progression items.\nNice.\n(Source: Just trust me)", "Lingo\nLingoing\nLingone", "The name of the captain was Albert Einstein.", "Panel impossible Sigma plz fix", From 33237bd5c0530830e3cdd5097596ff86a42042ed Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 3 Feb 2024 00:45:37 -0500 Subject: [PATCH 77/78] LTTP: Create Hyrule Castle Big Key Rule On Universal Small Keys Option (#2787) --- worlds/alttp/Rules.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 8a04f87afa..98ab805b5c 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -968,6 +968,9 @@ def standard_rules(world, player): set_rule(world.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)) + else: + set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + lambda state: state.has('Big Key (Hyrule Castle)', player)) def toss_junk_item(world, player): items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', From 6c19bc42bb714ca0f5ef7e435e79b10543a1f318 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 4 Feb 2024 09:09:07 +0100 Subject: [PATCH 78/78] Tests: add world load benchmark (#2768) --- test/benchmark/__init__.py | 132 ++-------------------------------- test/benchmark/load_worlds.py | 27 +++++++ test/benchmark/locations.py | 101 ++++++++++++++++++++++++++ test/benchmark/path_change.py | 16 +++++ test/benchmark/time_it.py | 23 ++++++ worlds/__init__.py | 10 ++- 6 files changed, 181 insertions(+), 128 deletions(-) create mode 100644 test/benchmark/load_worlds.py create mode 100644 test/benchmark/locations.py create mode 100644 test/benchmark/path_change.py create mode 100644 test/benchmark/time_it.py diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py index 5f890e8530..6c80c60b89 100644 --- a/test/benchmark/__init__.py +++ b/test/benchmark/__init__.py @@ -1,127 +1,7 @@ -import time - - -class TimeIt: - def __init__(self, name: str, time_logger=None): - self.name = name - self.logger = time_logger - self.timer = None - self.end_timer = None - - def __enter__(self): - self.timer = time.perf_counter() - return self - - @property - def dif(self): - return self.end_timer - self.timer - - def __exit__(self, exc_type, exc_val, exc_tb): - if not self.end_timer: - self.end_timer = time.perf_counter() - if self.logger: - self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") - - if __name__ == "__main__": - import argparse - import logging - import gc - import collections - import typing - - # makes this module runnable from its folder. - import sys - import os - sys.path.remove(os.path.dirname(__file__)) - new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) - os.chdir(new_home) - sys.path.append(new_home) - - from Utils import init_logging, local_path - local_path.cached_path = new_home - from BaseClasses import MultiWorld, CollectionState, Location - from worlds import AutoWorld - from worlds.AutoWorld import call_all - - init_logging("Benchmark Runner") - logger = logging.getLogger("Benchmark") - - - class BenchmarkRunner: - gen_steps: typing.Tuple[str, ...] = ( - "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") - rule_iterations: int = 100_000 - - if sys.version_info >= (3, 9): - @staticmethod - def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: - return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) - else: - @staticmethod - def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str: - return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) - - def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: - with TimeIt(f"{test_location.game} {self.rule_iterations} " - f"runs of {test_location}.access_rule({state_name})", logger) as t: - for _ in range(self.rule_iterations): - test_location.access_rule(state) - # if time is taken to disentangle complex ref chains, - # this time should be attributed to the rule. - gc.collect() - return t.dif - - def main(self): - for game in sorted(AutoWorld.AutoWorldRegister.world_types): - summary_data: typing.Dict[str, collections.Counter[str]] = { - "empty_state": collections.Counter(), - "all_state": collections.Counter(), - } - try: - multiworld = MultiWorld(1) - multiworld.game[1] = game - multiworld.player_name = {1: "Tester"} - multiworld.set_seed(0) - multiworld.state = CollectionState(multiworld) - args = argparse.Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items(): - setattr(args, name, { - 1: option.from_any(getattr(option, "default")) - }) - multiworld.set_options(args) - - gc.collect() - for step in self.gen_steps: - with TimeIt(f"{game} step {step}", logger): - call_all(multiworld, step) - gc.collect() - - locations = sorted(multiworld.get_unfilled_locations()) - if not locations: - continue - - all_state = multiworld.get_all_state(False) - for location in locations: - time_taken = self.location_test(location, multiworld.state, "empty_state") - summary_data["empty_state"][location.name] = time_taken - - time_taken = self.location_test(location, all_state, "all_state") - summary_data["all_state"][location.name] = time_taken - - total_empty_state = sum(summary_data["empty_state"].values()) - total_all_state = sum(summary_data["all_state"].values()) - - logger.info(f"{game} took {total_empty_state/len(locations):.4f} " - f"seconds per location in empty_state and {total_all_state/len(locations):.4f} " - f"in all_state. (all times summed for {self.rule_iterations} runs.)") - logger.info(f"Top times in empty_state:\n" - f"{self.format_times_from_counter(summary_data['empty_state'])}") - logger.info(f"Top times in all_state:\n" - f"{self.format_times_from_counter(summary_data['all_state'])}") - - except Exception as e: - logger.exception(e) - - runner = BenchmarkRunner() - runner.main() + import path_change + path_change.change_home() + import load_worlds + load_worlds.run_load_worlds_benchmark() + import locations + locations.run_locations_benchmark() diff --git a/test/benchmark/load_worlds.py b/test/benchmark/load_worlds.py new file mode 100644 index 0000000000..3b001699f4 --- /dev/null +++ b/test/benchmark/load_worlds.py @@ -0,0 +1,27 @@ +def run_load_worlds_benchmark(): + """List worlds and their load time. + Note that any first-time imports will be attributed to that world, as it is cached afterwards. + Likely best used with isolated worlds to measure their time alone.""" + import logging + + from Utils import init_logging + + # get some general imports cached, to prevent it from being attributed to one world. + import orjson + orjson.loads("{}") # orjson runs initialization on first use + + import BaseClasses, Launcher, Fill + + from worlds import world_sources + + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + for module in world_sources: + logger.info(f"{module} took {module.time_taken:.4f} seconds.") + + +if __name__ == "__main__": + from path_change import change_home + change_home() + run_load_worlds_benchmark() diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py new file mode 100644 index 0000000000..f2209eb689 --- /dev/null +++ b/test/benchmark/locations.py @@ -0,0 +1,101 @@ +def run_locations_benchmark(): + import argparse + import logging + import gc + import collections + import typing + import sys + + from time_it import TimeIt + + from Utils import init_logging + from BaseClasses import MultiWorld, CollectionState, Location + from worlds import AutoWorld + from worlds.AutoWorld import call_all + + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + if sys.version_info >= (3, 9): + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + else: + @staticmethod + def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + for game in sorted(AutoWorld.AutoWorldRegister.world_types): + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + multiworld = MultiWorld(1) + multiworld.game[1] = game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed(0) + multiworld.state = CollectionState(multiworld) + args = argparse.Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(getattr(option, "default")) + }) + multiworld.set_options(args) + + gc.collect() + for step in self.gen_steps: + with TimeIt(f"{game} step {step}", logger): + call_all(multiworld, step) + gc.collect() + + locations = sorted(multiworld.get_unfilled_locations()) + if not locations: + continue + + all_state = multiworld.get_all_state(False) + for location in locations: + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state/len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state/len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() + + +if __name__ == "__main__": + from path_change import change_home + change_home() + run_locations_benchmark() diff --git a/test/benchmark/path_change.py b/test/benchmark/path_change.py new file mode 100644 index 0000000000..2baa6273e1 --- /dev/null +++ b/test/benchmark/path_change.py @@ -0,0 +1,16 @@ +import sys +import os + + +def change_home(): + """Allow scripts to run from "this" folder.""" + old_home = os.path.dirname(__file__) + sys.path.remove(old_home) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + # fallback to local import + sys.path.append(old_home) + + from Utils import local_path + local_path.cached_path = new_home diff --git a/test/benchmark/time_it.py b/test/benchmark/time_it.py new file mode 100644 index 0000000000..95c0314682 --- /dev/null +++ b/test/benchmark/time_it.py @@ -0,0 +1,23 @@ +import time + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") diff --git a/worlds/__init__.py b/worlds/__init__.py index 66c91639b9..168bba7abf 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -3,7 +3,9 @@ import os import sys import warnings import zipimport -from typing import Dict, List, NamedTuple, TypedDict +import time +import dataclasses +from typing import Dict, List, TypedDict, Optional from Utils import local_path, user_path @@ -34,10 +36,12 @@ class DataPackage(TypedDict): games: Dict[str, GamesPackage] -class WorldSource(NamedTuple): +@dataclasses.dataclass(order=True) +class WorldSource: path: str # typically relative path from this module is_zip: bool = False relative: bool = True # relative to regular world import folder + time_taken: Optional[float] = None def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})" @@ -50,6 +54,7 @@ class WorldSource(NamedTuple): def load(self) -> bool: try: + start = time.perf_counter() if self.is_zip: importer = zipimport.zipimporter(self.resolved_path) if hasattr(importer, "find_spec"): # new in Python 3.10 @@ -69,6 +74,7 @@ class WorldSource(NamedTuple): importer.exec_module(mod) else: importlib.import_module(f".{self.path}", "worlds") + self.time_taken = time.perf_counter()-start return True except Exception: