From ecdef8145270a95d13e0c8dcc47419b6f17f20ae Mon Sep 17 00:00:00 2001 From: Phar Date: Thu, 23 Nov 2023 00:13:27 -0600 Subject: [PATCH 1/5] Landstalker: Refactor for PR Review --- worlds/landstalker/Hints.py | 49 ++++--- worlds/landstalker/Items.py | 11 +- worlds/landstalker/Locations.py | 12 +- worlds/landstalker/Options.py | 105 +++++++------- worlds/landstalker/Regions.py | 54 ++++---- worlds/landstalker/Rules.py | 96 +++++++------ worlds/landstalker/__init__.py | 128 +++++++++--------- .../landstalker/docs/landstalker_setup_en.md | 21 +-- 8 files changed, 247 insertions(+), 229 deletions(-) diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py index 767fbebe152d..72c4956dbbb1 100644 --- a/worlds/landstalker/Hints.py +++ b/worlds/landstalker/Hints.py @@ -1,16 +1,19 @@ -import random -from typing import List -from BaseClasses import MultiWorld, Location -from . import LandstalkerItem +from typing import TYPE_CHECKING + +from BaseClasses import Location from .data.hint_source import HINT_SOURCES_JSON +if TYPE_CHECKING: + from random import Random + from . import LandstalkerWorld + -def generate_blurry_location_hint(location: Location, random: random.Random): - cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in '(),:'}) - cleaned_location_name.replace('-', ' ') - cleaned_location_name.replace('/', ' ') - cleaned_location_name.replace('.', ' ') - location_name_words = [w for w in cleaned_location_name.split(' ') if len(w) > 3] +def generate_blurry_location_hint(location: Location, random: "Random"): + cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in "(),:"}) + cleaned_location_name.replace("-", " ") + cleaned_location_name.replace("/", " ") + cleaned_location_name.replace(".", " ") + location_name_words = [w for w in cleaned_location_name.split(" ") if len(w) > 3] random_word_1 = "mysterious" random_word_2 = "place" @@ -22,32 +25,36 @@ def generate_blurry_location_hint(location: Location, random: random.Random): return [random_word_1, random_word_2] -def generate_lithograph_hint(multiworld: MultiWorld, player: int, jewel_items: List[LandstalkerItem]): +def generate_lithograph_hint(world: "LandstalkerWorld"): hint_text = "It's barely readable:\n" + jewel_items = world.jewel_items for item in jewel_items: # Jewel hints are composed of 4 'words' shuffled randomly: # - the name of the player whose world contains said jewel (if not ours) # - the color of the jewel (if relevant) # - two random words from the location name - words = generate_blurry_location_hint(item.location, multiworld.per_slot_randoms[player]) + words = generate_blurry_location_hint(item.location, world.random) words[0] = words[0].upper() words[1] = words[1].upper() if len(jewel_items) < 6: # Add jewel color if we are not using generic jewels because jewel count is 6 or more - words.append(item.name.split(' ')[0].upper()) - if item.location.player != player: + words.append(item.name.split(" ")[0].upper()) + if item.location.player != world.player: # Add player name if it's not in our own world - words.append(multiworld.get_player_name(item.location.player).upper()) - multiworld.per_slot_randoms[player].shuffle(words) + player_name = world.multiworld.get_player_name(world.player) + words.append(player_name.upper()) + world.random.shuffle(words) hint_text += " ".join(words) + "\n" - return hint_text.rstrip('\n') + return hint_text.rstrip("\n") -def generate_random_hints(multiworld: MultiWorld, this_player: int): +def generate_random_hints(world: "LandstalkerWorld"): hints = {} hint_texts = [] - random = multiworld.per_slot_randoms[this_player] + random = world.random + multiworld = world.multiworld + this_player = world.player # Exclude Life Stock from the hints as some of them are considered as progression for Fahl, but isn't really # exciting when hinted @@ -80,7 +87,7 @@ def generate_random_hints(multiworld: MultiWorld, this_player: int): # Hint-type #3: Own progression item in remote location for item in remote_own_progression_items: other_player = multiworld.get_player_name(item.location.player) - if item.location.game == 'Landstalker': + if item.location.game == "Landstalker": region_hint_name = item.location.parent_region.hint_text hint_texts.append(f"If you need {item.name}, tell {other_player} to look {region_hint_name}.") else: @@ -121,7 +128,7 @@ def generate_random_hints(multiworld: MultiWorld, this_player: int): hint_texts = list(set(hint_texts)) random.shuffle(hint_texts) - hint_count = multiworld.hint_count[this_player].value + hint_count = world.options.hint_count.value del hint_texts[hint_count:] hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if diff --git a/worlds/landstalker/Items.py b/worlds/landstalker/Items.py index ca2e687873bf..c9bbdb65b986 100644 --- a/worlds/landstalker/Items.py +++ b/worlds/landstalker/Items.py @@ -1,4 +1,5 @@ -from typing import Dict, NamedTuple, List +from typing import Dict, List, NamedTuple + from BaseClasses import Item, ItemClassification BASE_ITEM_ID = 4000 @@ -96,13 +97,9 @@ def get_weighted_filler_item_names(): weighted_item_names: List[str] = [] for name, data in item_table.items(): if data.classification == ItemClassification.filler: - weighted_item_names += [name for _ in range(0, data.quantity)] + weighted_item_names += [name for _ in range(data.quantity)] return weighted_item_names def build_item_name_to_id_table(): - item_name_to_id_table = {} - for name, data in item_table.items(): - item_name_to_id_table[name] = data.id + BASE_ITEM_ID - return item_name_to_id_table - + return {name: data.id + BASE_ITEM_ID for name, data in item_table.items()} diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index 6d3a7a759d69..76ca5b9f10b6 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -1,5 +1,7 @@ from typing import Dict, Optional -from BaseClasses import Location, Region + +from BaseClasses import Location +from .Regions import LandstalkerRegion from .data.item_source import ITEM_SOURCES_JSON BASE_LOCATION_ID = 4000 @@ -13,12 +15,12 @@ class LandstalkerLocation(Location): type_string: str price: int = 0 - def __init__(self, player: int, name: str, location_id: Optional[int], region: Region, type_string: str): + def __init__(self, player: int, name: str, location_id: Optional[int], region: LandstalkerRegion, type_string: str): super().__init__(player, name, location_id, region) self.type_string = type_string -def create_locations(player: int, regions_table: Dict[str, Region], name_to_id_table: Dict[str, int]): +def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], name_to_id_table: Dict[str, int]): # Create real locations from the data inside the corresponding JSON file for data in ITEM_SOURCES_JSON: region_id = data["nodeId"] @@ -27,8 +29,8 @@ def create_locations(player: int, regions_table: Dict[str, Region], name_to_id_t region.locations.append(new_location) # Create a specific end location that will contain a fake win-condition item - end_location = LandstalkerLocation(player, "End", None, regions_table['end'], "reward") - regions_table['end'].locations.append(end_location) + end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") + regions_table["end"].locations.append(end_location) def build_location_name_to_id_table(): diff --git a/worlds/landstalker/Options.py b/worlds/landstalker/Options.py index a04f03b3b4a0..65ffd2c1f31e 100644 --- a/worlds/landstalker/Options.py +++ b/worlds/landstalker/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from dataclasses import dataclass -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle +from Options import Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, Range, Toggle class LandstalkerGoal(Choice): @@ -8,7 +8,8 @@ class LandstalkerGoal(Choice): The goal to accomplish in order to complete the seed. - Beat Gola: beat the usual final boss (same as vanilla) - Reach Kazalt: find the jewels and take the teleporter to Kazalt - - Beat Dark Nole: the door to King Nole's fight brings you into a final dungeon with an absurdly hard boss you have to beat to win the game + - Beat Dark Nole: the door to King Nole's fight brings you into a final dungeon with an absurdly hard boss you have + to beat to win the game """ display_name = "Goal" @@ -21,9 +22,9 @@ class LandstalkerGoal(Choice): class JewelCount(Range): """ - Determines the number of jewels to find in order to be able to reach Kazalt + Determines the number of jewels to find to be able to reach Kazalt. """ - display_name = "Jewel count" + display_name = "Jewel Count" range_start = 0 range_end = 9 default = 5 @@ -34,7 +35,7 @@ class ProgressiveArmors(DefaultOnToggle): When obtaining an armor, you get the next armor tier instead of getting the specific armor tier that was placed here by randomization. Enabling this provides a smoother progression. """ - display_name = "Progressive armors" + display_name = "Progressive Armors" class UseRecordBook(DefaultOnToggle): @@ -58,15 +59,15 @@ class EnsureEkeEkeInShops(DefaultOnToggle): Ensures an EkeEke will always be for sale in one shop per region in the game. Disabling this can lead to frustrating situations where you cannot refill your health items and might get locked. """ - display_name = "Ensure EkeEke in shops" + display_name = "Ensure EkeEke in Shops" class RemoveGumiBoulder(Toggle): """ - Removes the boulder between Gumi and Ryuma which is usually a one-way path. - This makes the vanilla early game (Massan, Gumi...) more easily accessible when starting outside of it. + Removes the boulder between Gumi and Ryuma, which is usually a one-way path. + This makes the vanilla early game (Massan, Gumi...) more easily accessible when starting outside it. """ - display_name = "Remove boulder after Gumi" + display_name = "Remove Boulder After Gumi" class EnemyJumpingInLogic(Toggle): @@ -75,7 +76,7 @@ class EnemyJumpingInLogic(Toggle): This gives access to Mountainous Area from Lake Shrine sector and to the cliff chest behind a magic tree near Mir Tower. These tricks not being easy, you should leave this disabled until practiced. """ - display_name = "Enemy jumping in logic" + display_name = "Enemy Jumping in Logic" class TreeCuttingGlitchInLogic(Toggle): @@ -83,7 +84,7 @@ class TreeCuttingGlitchInLogic(Toggle): Adds tree-cutting glitch as a logical rule, enabling access to both chests behind magic trees in Mir Tower Sector without having Axe Magic. """ - display_name = "Tree-cutting glitch in logic" + display_name = "Tree-cutting Glitch in Logic" class DamageBoostingInLogic(Toggle): @@ -91,7 +92,7 @@ class DamageBoostingInLogic(Toggle): Adds damage boosting as a logical rule, removing any requirements involving Iron Boots or Fireproof Boots. Who doesn't like walking on spikes and lava? """ - display_name = "Damage boosting in logic" + display_name = "Damage Boosting in Logic" class WhistleUsageBehindTrees(DefaultOnToggle): @@ -100,7 +101,7 @@ class WhistleUsageBehindTrees(DefaultOnToggle): Enabling this allows using Einstein Whistle from both sides of the magic trees. This is only useful in seeds starting in the "waterfall" spawn region or where teleportation trees are made open from the start. """ - display_name = "Allow using Einstein Whistle behind trees" + display_name = "Allow Using Einstein Whistle Behind Trees" class SpawnRegion(Choice): @@ -109,19 +110,19 @@ class SpawnRegion(Choice): It is advised to keep Massan as your spawn location for your first few seeds. Picking a late-game location can make the seed significantly harder, both for logic and combat. """ - display_name = "Starting region" + display_name = "Starting Region" - option_massan = "massan" - option_gumi = "gumi" - option_kado = "kado" - option_waterfall = "waterfall" - option_ryuma = "ryuma" - option_mercator = "mercator" - option_verla = "verla" - option_greenmaze = "greenmaze" - option_destel = "destel" + option_massan = 0 + option_gumi = 1 + option_kado = 2 + option_waterfall = 3 + option_ryuma = 4 + option_mercator = 5 + option_verla = 6 + option_greenmaze = 7 + option_destel = 8 - default = "massan" + default = 0 class TeleportTreeRequirements(Choice): @@ -132,7 +133,7 @@ class TeleportTreeRequirements(Choice): - Visit Trees: Both trees from a tree pair need to be visited to teleport between them Vanilla behavior is "Clear Tibor And Visit Trees" """ - display_name = "Teleportation trees requirements" + display_name = "Teleportation Trees Requirements" option_none = 0 option_clear_tibor = 1 @@ -146,7 +147,7 @@ class ShuffleTrees(Toggle): """ If enabled, all teleportation trees will be shuffled into new pairs. """ - display_name = "Shuffle teleportation trees" + display_name = "Shuffle Teleportation Trees" class ReviveUsingEkeeke(DefaultOnToggle): @@ -155,7 +156,7 @@ class ReviveUsingEkeeke(DefaultOnToggle): This setting allows disabling this feature, making the game extremely harder. USE WITH CAUTION! """ - display_name = "Revive using EkeEke" + display_name = "Revive Using EkeEke" class ShopPricesFactor(Range): @@ -163,7 +164,7 @@ class ShopPricesFactor(Range): Applies a percentage factor on all prices in shops. Having higher prices can lead to a bit of gold farming, which can make seeds longer but also sometimes more frustrating. """ - display_name = "Shop prices factor (%)" + display_name = "Shop Prices Factor (%)" range_start = 50 range_end = 200 default = 100 @@ -178,7 +179,7 @@ class CombatDifficulty(Choice): - Hard: 140% HP & damage - Insane: 200% HP & damage """ - display_name = "Combat difficulty" + display_name = "Combat Difficulty" option_peaceful = 0 option_easy = 1 @@ -193,35 +194,35 @@ class HintCount(Range): """ Determines the number of Foxy NPCs that will be scattered across the world, giving various types of hints """ - display_name = "Hint count" + display_name = "Hint Count" range_start = 0 range_end = 25 default = 12 -ls_options: Dict[str, type(Option)] = { - "goal": LandstalkerGoal, - "spawn_region": SpawnRegion, - "jewel_count": JewelCount, - "progressive_armors": ProgressiveArmors, - "use_record_book": UseRecordBook, - "use_spell_book": UseSpellBook, +@dataclass +class LandstalkerOptions(PerGameCommonOptions): + goal: LandstalkerGoal + spawn_region: SpawnRegion + jewel_count: JewelCount + progressive_armors: ProgressiveArmors + use_record_book: UseRecordBook + use_spell_book: UseSpellBook - "shop_prices_factor": ShopPricesFactor, - "combat_difficulty": CombatDifficulty, + shop_prices_factor: ShopPricesFactor + combat_difficulty: CombatDifficulty - "teleport_tree_requirements": TeleportTreeRequirements, - "shuffle_trees": ShuffleTrees, + teleport_tree_requirements: TeleportTreeRequirements + shuffle_trees: ShuffleTrees - "ensure_ekeeke_in_shops": EnsureEkeEkeInShops, - "remove_gumi_boulder": RemoveGumiBoulder, - "allow_whistle_usage_behind_trees": WhistleUsageBehindTrees, - "handle_damage_boosting_in_logic": DamageBoostingInLogic, - "handle_enemy_jumping_in_logic": EnemyJumpingInLogic, - "handle_tree_cutting_glitch_in_logic": TreeCuttingGlitchInLogic, + ensure_ekeeke_in_shops: EnsureEkeEkeInShops + remove_gumi_boulder: RemoveGumiBoulder + allow_whistle_usage_behind_trees: WhistleUsageBehindTrees + handle_damage_boosting_in_logic: DamageBoostingInLogic + handle_enemy_jumping_in_logic: EnemyJumpingInLogic + handle_tree_cutting_glitch_in_logic: TreeCuttingGlitchInLogic - "hint_count": HintCount, + hint_count: HintCount - "revive_using_ekeeke": ReviveUsingEkeeke, - "death_link": DeathLink, -} + revive_using_ekeeke: ReviveUsingEkeeke + death_link: DeathLink diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py index e9c10b1bdb55..21704194f157 100644 --- a/worlds/landstalker/Regions.py +++ b/worlds/landstalker/Regions.py @@ -1,10 +1,14 @@ -from typing import Dict, List, NamedTuple, Optional -from BaseClasses import MultiWorld, Region, Entrance +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from BaseClasses import MultiWorld, Region from .data.world_node import WORLD_NODES_JSON from .data.world_path import WORLD_PATHS_JSON from .data.world_region import WORLD_REGIONS_JSON from .data.world_teleport_tree import WORLD_TELEPORT_TREES_JSON +if TYPE_CHECKING: + from . import LandstalkerWorld + class LandstalkerRegion(Region): code: str @@ -19,8 +23,10 @@ class LandstalkerRegionData(NamedTuple): region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "LandstalkerWorld"): regions_table: Dict[str, LandstalkerRegion] = {} + multiworld = world.multiworld + player = world.player # Create the hardcoded starting "Menu" region menu_region = LandstalkerRegion("menu", "Menu", player, multiworld) @@ -39,58 +45,51 @@ def create_regions(multiworld: MultiWorld, player: int): # Create exits/entrances from world_paths for data in WORLD_PATHS_JSON: two_way = data["twoWay"] if "twoWay" in data else False - create_entrance(data["fromId"], data["toId"], two_way, player, regions_table) + create_entrance(data["fromId"], data["toId"], two_way, regions_table) # Create a path between the fake Menu location and the starting location - starting_region = get_starting_region(multiworld, player, regions_table) + starting_region = get_starting_region(world, regions_table) menu_region.connect(starting_region, f"menu -> {starting_region.code}") - add_specific_paths(multiworld, player, regions_table) + add_specific_paths(world, regions_table) return regions_table -def add_specific_paths(multiworld: MultiWorld, player: int, regions_table: Dict[str, LandstalkerRegion]): +def add_specific_paths(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): # If Gumi boulder is removed, add a path from "route_gumi_ryuma" to "gumi" - if multiworld.remove_gumi_boulder[player].value == 1: - create_entrance("route_gumi_ryuma", "gumi", False, player, regions_table) + if world.options.remove_gumi_boulder == 1: + create_entrance("route_gumi_ryuma", "gumi", False, regions_table) # If enemy jumping is in logic, Mountainous Area can be reached from route to Lake Shrine by doing a "ghost jump" # at crossroads map - if multiworld.handle_enemy_jumping_in_logic[player].value == 1: - create_entrance("route_lake_shrine", "route_lake_shrine_cliff", False, player, regions_table) + if world.options.handle_enemy_jumping_in_logic == 1: + create_entrance("route_lake_shrine", "route_lake_shrine_cliff", False, regions_table) # If using Einstein Whistle behind trees is allowed, add a new logic path there to reflect that change - if multiworld.allow_whistle_usage_behind_trees[player].value == 1: - create_entrance("greenmaze_post_whistle", "greenmaze_pre_whistle", False, player, regions_table) + if world.options.allow_whistle_usage_behind_trees == 1: + create_entrance("greenmaze_post_whistle", "greenmaze_pre_whistle", False, regions_table) -def create_entrance(from_id: str, to_id: str, two_way: bool, player: int, regions_table: Dict[str, LandstalkerRegion]): +def create_entrance(from_id: str, to_id: str, two_way: bool, regions_table: Dict[str, LandstalkerRegion]): created_entrances = [] name = from_id + " -> " + to_id from_region = regions_table[from_id] to_region = regions_table[to_id] - entrance = Entrance(player, name, from_region) - from_region.exits.append(entrance) - entrance.connect(to_region) - created_entrances.append(entrance) + created_entrances.append(from_region.connect(to_region, name)) - # If two-way, also create a reverse path if two_way: reverse_name = to_id + " -> " + from_id - entrance = Entrance(player, reverse_name, to_region) - to_region.exits.append(entrance) - entrance.connect(from_region) - created_entrances.append(entrance) + created_entrances.append(to_region.connect(from_region, reverse_name)) return created_entrances -def get_starting_region(multiworld: MultiWorld, player: int, regions_table: Dict[str, LandstalkerRegion]): +def get_starting_region(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): # Most spawn locations have the same name as the region they are bound to, but a few vary. - spawn_id = multiworld.spawn_region[player].value + spawn_id = world.options.spawn_region.current_key if spawn_id == "waterfall": return regions_table["greenmaze_post_whistle"] elif spawn_id == "kado": @@ -103,15 +102,16 @@ def get_starting_region(multiworld: MultiWorld, player: int, regions_table: Dict def get_darkenable_regions(): return {data["name"]: data["nodeIds"] for data in WORLD_REGIONS_JSON if "darkMapIds" in data} + def load_teleport_trees(): pairs = [] for pair in WORLD_TELEPORT_TREES_JSON: first_tree = { - 'name': pair[0]["name"], + 'name': pair[0]["name"], 'region': pair[0]["nodeId"] } second_tree = { - 'name': pair[1]["name"], + 'name': pair[1]["name"], 'region': pair[1]["nodeId"] } pairs.append([first_tree, second_tree]) diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py index 1879585f3171..4365eb7aa45b 100644 --- a/worlds/landstalker/Rules.py +++ b/worlds/landstalker/Rules.py @@ -1,33 +1,38 @@ -from typing import Dict, List -from BaseClasses import MultiWorld, Region -from worlds.AutoWorld import LogicMixin -from worlds.landstalker.data.world_path import WORLD_PATHS_JSON +from typing import List, TYPE_CHECKING +from BaseClasses import CollectionState +from .data.world_path import WORLD_PATHS_JSON +from .Locations import LandstalkerLocation +from .Regions import LandstalkerRegion -class LandstalkerLogic(LogicMixin): - def _landstalker_has_visited_regions(self, player, regions): - return all([self.can_reach(region, None, player) for region in regions]) +if TYPE_CHECKING: + from . import LandstalkerWorld - def _landstalker_has_health(self, player, health): - return self.has("Life Stock", player, health) +def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): + return all([state.can_reach(region, None, player) for region in regions]) -def create_rules(multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], dark_region_ids: List[str]): + +def _landstalker_has_health(state: CollectionState, player: int, health): + return state.has("Life Stock", player, health) + + +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], dark_region_ids: List[str] +def create_rules(world: "LandstalkerWorld"): # Item & exploration requirements to take paths - add_path_requirements(multiworld, player, regions_table, dark_region_ids) - add_specific_path_requirements(multiworld, player) + add_path_requirements(world) + add_specific_path_requirements(world) # Location rules to forbid some item types depending on location types - add_location_rules(multiworld, player) + add_location_rules(world) # Win condition - multiworld.completion_condition[player] = lambda state: state.has("King Nole's Treasure", player) + world.multiworld.completion_condition[world.player] = lambda state: state.has("King Nole's Treasure", world.player) -def add_path_requirements(multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], - dark_region_ids: List[str]): - can_damage_boost = multiworld.handle_damage_boosting_in_logic[player].value - +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], +# dark_region_ids: List[str] +def add_path_requirements(world: "LandstalkerWorld"): for data in WORLD_PATHS_JSON: name = data["fromId"] + " -> " + data["toId"] @@ -36,33 +41,36 @@ def add_path_requirements(multiworld: MultiWorld, player: int, regions_table: Di if "itemsPlacedWhenCrossing" in data: required_items += data["itemsPlacedWhenCrossing"] - if data["toId"] in dark_region_ids: + if data["toId"] in world.dark_region_ids: # Make Lantern required to reach the randomly selected dark regions required_items.append("Lantern") - if can_damage_boost: + if world.options.handle_damage_boosting_in_logic: # If damage boosting is handled in logic, remove all iron boots & fireproof requirements required_items = [item for item in required_items if item != "Iron Boots" and item != "Fireproof"] # Determine required other visited regions to reach this region required_region_ids = data["requiredNodes"] if "requiredNodes" in data else [] - required_regions = [regions_table[region_id] for region_id in required_region_ids] + required_regions = [world.regions_table[region_id] for region_id in required_region_ids] if not (required_items or required_regions): continue # Create the rule lambda using those requirements - access_rule = make_path_requirement_lambda(player, required_items, required_regions) - multiworld.get_entrance(name, player).access_rule = access_rule + access_rule = make_path_requirement_lambda(world.player, required_items, required_regions) + world.multiworld.get_entrance(name, world.player).access_rule = access_rule # If two-way, also apply the rule to the opposite path if "twoWay" in data and data["twoWay"] is True: reverse_name = data["toId"] + " -> " + data["fromId"] - multiworld.get_entrance(reverse_name, player).access_rule = access_rule + world.multiworld.get_entrance(reverse_name, world.player).access_rule = access_rule + +def add_specific_path_requirements(world: "LandstalkerWorld"): + multiworld = world.multiworld + player = world.player -def add_specific_path_requirements(multiworld: MultiWorld, player: int): # Make the jewels required to reach Kazalt - jewel_count = multiworld.jewel_count[player].value + jewel_count = world.options.jewel_count.value path_to_kazalt = multiworld.get_entrance("king_nole_cave -> kazalt", player) if jewel_count < 6: # 5- jewels => the player needs to find as many uniquely named jewel items @@ -74,30 +82,29 @@ def add_specific_path_requirements(multiworld: MultiWorld, player: int): path_to_kazalt.access_rule = lambda state: state.has("Kazalt Jewel", player, jewel_count) # If enemy jumping is enabled, Mir Tower sector first tree can be bypassed to reach the elevated ledge - if multiworld.handle_enemy_jumping_in_logic[player].value == 1: - remove_requirements_for(multiworld, "mir_tower_sector -> mir_tower_sector_tree_ledge", player) + if world.options.handle_enemy_jumping_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") # Both trees in Mir Tower sector can be abused using tree cutting glitch - if multiworld.handle_tree_cutting_glitch_in_logic[player].value == 1: - remove_requirements_for(multiworld, "mir_tower_sector -> mir_tower_sector_tree_ledge", player) - remove_requirements_for(multiworld, "mir_tower_sector -> mir_tower_sector_tree_coast", player) + if world.options.handle_tree_cutting_glitch_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_coast") # If Whistle can be used from behind the trees, it adds a new path that requires the whistle as well - if multiworld.allow_whistle_usage_behind_trees[player].value == 1: + if world.options.allow_whistle_usage_behind_trees == 1: entrance = multiworld.get_entrance("greenmaze_post_whistle -> greenmaze_pre_whistle", player) entrance.access_rule = make_path_requirement_lambda(player, ["Einstein Whistle"], []) -def make_path_requirement_lambda(player, required_items, required_regions): +def make_path_requirement_lambda(player: int, required_items: List[str], required_regions: List[LandstalkerRegion]): """ Lambdas are created in a for loop, so values need to be captured """ return lambda state: \ - state.has_all(set(required_items), player) \ - and state._landstalker_has_visited_regions(player, required_regions) + state.has_all(set(required_items), player) and _landstalker_has_visited_regions(state, player, required_regions) -def make_shop_location_requirement_lambda(player, location): +def make_shop_location_requirement_lambda(player: int, location: LandstalkerLocation): """ Lambdas are created in a for loop, so values need to be captured """ @@ -109,18 +116,19 @@ def make_shop_location_requirement_lambda(player, location): and item.name not in [loc.item.name for loc in other_locations_in_shop if loc.item is not None]) -def remove_requirements_for(multiworld: MultiWorld, entrance_name: str, player: int): - entrance = multiworld.get_entrance(entrance_name, player) +def remove_requirements_for(world: "LandstalkerWorld", entrance_name: str): + entrance = world.multiworld.get_entrance(entrance_name, world.player) entrance.access_rule = lambda state: True -def add_location_rules(multiworld: MultiWorld, player: int): - for location in multiworld.get_locations(player): +def add_location_rules(world: "LandstalkerWorld"): + location: LandstalkerLocation + for location in world.multiworld.get_locations(world.player): if location.type_string == "ground": - location.item_rule = lambda item: not (item.player == player and ' Gold' in item.name) + location.item_rule = lambda item: not (item.player == world.player and ' Gold' in item.name) elif location.type_string == "shop": - location.item_rule = make_shop_location_requirement_lambda(player, location) + location.item_rule = make_shop_location_requirement_lambda(world.player, location) # Add a special rule for Fahl - fahl_location = multiworld.get_location("Mercator: Fahl's dojo challenge reward", player) - fahl_location.access_rule = lambda state: state._landstalker_has_health(player, 15) + fahl_location = world.multiworld.get_location("Mercator: Fahl's dojo challenge reward", world.player) + fahl_location.access_rule = lambda state: _landstalker_has_health(state, world.player, 15) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 4f26a020f158..1e037e1d1605 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -1,12 +1,11 @@ -import threading -from BaseClasses import Tutorial, LocationProgressType +from BaseClasses import LocationProgressType, Tutorial from worlds.AutoWorld import WebWorld, World -from .Options import ls_options, ProgressiveArmors, LandstalkerGoal, TeleportTreeRequirements +from .Hints import * from .Items import * -from .Regions import * from .Locations import * +from .Options import JewelCount, LandstalkerGoal, LandstalkerOptions, ProgressiveArmors, TeleportTreeRequirements +from .Regions import * from .Rules import * -from .Hints import * class LandstalkerWeb(WebWorld): @@ -29,41 +28,42 @@ class LandstalkerWorld(World): hidden palace and claim the treasure. """ game = "Landstalker - The Treasures of King Nole" - option_definitions = ls_options + options_dataclass = LandstalkerOptions + options: LandstalkerOptions topology_present = True - data_version = 1 - required_client_version = (0, 3, 8) + required_client_version = (0, 4, 4) web = LandstalkerWeb() item_name_to_id = build_item_name_to_id_table() location_name_to_id = build_location_name_to_id_table() - """ This is needed to force fill_slot_data to happen after generate_output finished balancing shop prices. """ - can_fill_slot_data: threading.Event - - def __init__(self, multiworld: "MultiWorld", player: int): + def __init__(self, multiworld, player): super().__init__(multiworld, player) - self.regions_table: Dict[str, Region] = {} + self.regions_table: Dict[str, LandstalkerRegion] = {} self.dark_dungeon_id = "None" self.dark_region_ids = [] self.teleport_tree_pairs = [] - self.can_fill_slot_data = threading.Event() self.jewel_items = [] self.hints = {} - - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + self.locations: List[LandstalkerLocation] = [] def fill_slot_data(self) -> dict: - self.can_fill_slot_data.wait() - # Put options, locations' contents and some additional data inside slot data - slot_data = {option_name: self.get_setting(option_name).value for option_name in ls_options} - slot_data["seed"] = self.random.randint(0, 4294967295) + options = [ + "goal", "jewel_count", "progressive_armors", "use_record_book", "use_spell_book", "shop_prices_factor", + "combat_difficulty", "teleport_tree_requirements", "shuffle_trees", "ensure_ekeeke_in_shops", + "remove_gumi_boulder", "allow_whistle_usage_behind_trees", "handle_damage_boosting_in_logic", + "handle_enemy_jumping_in_logic", "handle_tree_cutting_glitch_in_logic", "hint_count", "death_link", + "revive_using_ekeeke", + ] + + slot_data = self.options.as_dict(*options) + slot_data["spawn_region"] = self.options.spawn_region.current_key + slot_data["seed"] = self.random.randint(0, 2 ** 32 - 1) slot_data["dark_region"] = self.dark_dungeon_id - slot_data["location_prices"] = {location.name: location.price for location in self.multiworld.get_locations(self.player) if location.price} + slot_data["location_prices"] = {location.name: location.price for location in self.locations if location.price} slot_data["hints"] = self.hints - slot_data["teleport_tree_pairs"] = [[pair[0]['name'], pair[1]['name']] for pair in self.teleport_tree_pairs] + slot_data["teleport_tree_pairs"] = [[pair[0]["name"], pair[1]["name"]] for pair in self.teleport_tree_pairs] return slot_data @@ -74,7 +74,7 @@ def generate_early(self): self.dark_region_ids = darkenable_regions[self.dark_dungeon_id] def create_regions(self): - self.regions_table = Regions.create_regions(self.multiworld, self.player) + self.regions_table = Regions.create_regions(self) Locations.create_locations(self.player, self.regions_table, self.location_name_to_id) self.create_teleportation_trees() @@ -85,17 +85,23 @@ def create_item(self, name: str, classification_override: Optional[ItemClassific item.price_in_shops = data.price_in_shops return item + def create_event(self, name: str) -> LandstalkerItem: + return LandstalkerItem(name, ItemClassification.progression, None, self.player) + + def get_filler_item_name(self) -> str: + return "EkeEke" + def create_items(self): item_pool: List[LandstalkerItem] = [] for name, data in item_table.items(): # If item is an armor and progressive armors are enabled, transform it into a progressive armor item - if self.get_setting('progressive_armors') and 'Breast' in name: - name = 'Progressive Armor' - item_pool += [self.create_item(name) for _ in range(0, data.quantity)] + if self.options.progressive_armors and "Breast" in name: + name = "Progressive Armor" + item_pool += [self.create_item(name) for _ in range(data.quantity)] # If the appropriate setting is on, place one EkeEke in one shop in every town in the game - if self.get_setting("ensure_ekeeke_in_shops"): - SHOPS_TO_FILL = [ + if self.options.ensure_ekeeke_in_shops: + shops_to_fill = [ "Massan: Shop item #1", "Gumi: Inn item #1", "Ryuma: Inn item", @@ -105,32 +111,31 @@ def create_items(self): "Route to Lake Shrine: Greedly's shop item #1", "Kazalt: Shop item #1" ] - for location_name in SHOPS_TO_FILL: + for location_name in shops_to_fill: self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item("EkeEke")) # Add a fixed amount of progression Life Stock for a specific requirement (Fahl) - FAHL_LIFESTOCK_REQ = 15 - item_pool += [self.create_item("Life Stock", ItemClassification.progression) for _ in range(FAHL_LIFESTOCK_REQ)] + fahl_lifestock_req = 15 + item_pool += [self.create_item("Life Stock", ItemClassification.progression) for _ in range(fahl_lifestock_req)] # Add a unique progression EkeEke for a specific requirement (Cutter) - item_pool += [self.create_item("EkeEke", ItemClassification.progression)] + item_pool.append(self.create_item("EkeEke", ItemClassification.progression)) # Add a variable amount of "useful" Life Stock to the pool, depending on the amount of starting Life Stock # (i.e. on the starting location) starting_lifestocks = self.get_starting_health() - 4 - lifestock_count = 80 - starting_lifestocks - FAHL_LIFESTOCK_REQ + lifestock_count = 80 - starting_lifestocks - fahl_lifestock_req item_pool += [self.create_item("Life Stock") for _ in range(lifestock_count)] # Add jewels to the item pool depending on the number of jewels set in generation settings - self.jewel_items = [self.create_item(name) for name in self.get_jewel_names(self.get_setting('jewel_count'))] + self.jewel_items = [self.create_item(name) for name in self.get_jewel_names(self.options.jewel_count)] item_pool += self.jewel_items # Add a pre-placed fake win condition item - win_condition_item = LandstalkerItem("King Nole's Treasure", ItemClassification.progression, None, self.player) - self.multiworld.get_location("End", self.player).place_locked_item(win_condition_item) + self.multiworld.get_location("End", self.player).place_locked_item(self.create_event("King Nole's Treasure")) # Fill the rest of the item pool with EkeEke remaining_items = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) - item_pool += [self.create_item("EkeEke") for _ in range(remaining_items)] + item_pool += [self.create_item(self.get_filler_item_name()) for _ in range(remaining_items)] self.multiworld.itempool += item_pool @@ -143,35 +148,36 @@ def pairwise(iterable): return zip(a, a) # Shuffle teleport tree pairs if the matching setting is on - if self.get_setting("shuffle_trees"): + if self.options.shuffle_trees: all_trees = [item for pair in self.teleport_tree_pairs for item in pair] - self.multiworld.random.shuffle(all_trees) + self.random.shuffle(all_trees) self.teleport_tree_pairs = [[x, y] for x, y in pairwise(all_trees)] # If a specific setting is set, teleport trees are potentially active without visiting both sides. # This means we need to add those as explorable paths for the generation algorithm. - teleport_trees_mode = self.get_setting("teleport_tree_requirements") + teleport_trees_mode = self.options.teleport_tree_requirements.value created_entrances = [] if teleport_trees_mode in [TeleportTreeRequirements.option_none, TeleportTreeRequirements.option_clear_tibor]: for pair in self.teleport_tree_pairs: - entrances = create_entrance(pair[0]['region'], pair[1]['region'], True, self.player, self.regions_table) + entrances = create_entrance(pair[0]["region"], pair[1]["region"], True, self.regions_table) created_entrances += entrances - # Teleport trees are open but require access to Tibor in order to work + + # Teleport trees are open but require access to Tibor to work if teleport_trees_mode == TeleportTreeRequirements.option_clear_tibor: for entrance in created_entrances: - entrance.access_rule = make_path_requirement_lambda(self.player, [], [self.regions_table['tibor']]) + entrance.access_rule = make_path_requirement_lambda(self.player, [], [self.regions_table["tibor"]]) def set_rules(self): - Rules.create_rules(self.multiworld, self.player, self.regions_table, self.dark_region_ids) + Rules.create_rules(self) # In "Reach Kazalt" goal, player doesn't have access to Kazalt, King Nole's Labyrinth & King Nole's Palace. - # As a consequence, all locations inside those regions must be excluded and the teleporter from + # As a consequence, all locations inside those regions must be excluded, and the teleporter from # King Nole's Cave to Kazalt must go to the end region instead. - if self.get_setting("goal") == LandstalkerGoal.option_reach_kazalt: + if self.options.goal == LandstalkerGoal.option_reach_kazalt: kazalt_tp = self.multiworld.get_entrance("king_nole_cave -> kazalt", self.player) kazalt_tp.connected_region = self.regions_table["end"] - EXCLUDED_REGIONS = [ + excluded_regions = [ "kazalt", "king_nole_labyrinth_pre_door", "king_nole_labyrinth_post_door", @@ -185,11 +191,11 @@ def set_rules(self): ] for location in self.multiworld.get_locations(self.player): - if location.parent_region.name in EXCLUDED_REGIONS: + if location.parent_region.name in excluded_regions: location.progress_type = LocationProgressType.EXCLUDED def get_starting_health(self): - spawn_id = self.get_setting('spawn_region') + spawn_id = self.options.spawn_region.current_key if spawn_id == "destel": return 20 elif spawn_id == "verla": @@ -202,12 +208,10 @@ def get_starting_health(self): def generate_output(self, output_directory: str) -> None: self.adjust_shop_prices() - self.hints = Hints.generate_random_hints(self.multiworld, self.player) - self.hints["Lithograph"] = Hints.generate_lithograph_hint(self.multiworld, self.player, self.jewel_items) + self.hints = Hints.generate_random_hints(self) + self.hints["Lithograph"] = Hints.generate_lithograph_hint(self) self.hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness." - self.can_fill_slot_data.set() - def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position unknown_items_price = 250 @@ -215,19 +219,19 @@ def adjust_shop_prices(self): endgame_price_factor = 2.0 factor_diff = endgame_price_factor - earlygame_price_factor - global_price_factor = self.get_setting("shop_prices_factor") / 100.0 + global_price_factor = self.options.shop_prices_factor / 100.0 spheres = list(self.multiworld.get_spheres()) sphere_id = 0 sphere_count = len(spheres) for sphere in spheres: for location in sphere: - if location.player != self.player or location.type_string != 'shop': + if location.player != self.player or location.type_string != "shop": continue current_playthrough_progression = sphere_id / sphere_count progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff) - price = location.item.price_in_shops if location.item.game == 'Landstalker' else unknown_items_price + price = location.item.price_in_shops if location.item.game == "Landstalker" else unknown_items_price price *= progression_price_factor price *= global_price_factor price -= price % 5 @@ -236,10 +240,8 @@ def adjust_shop_prices(self): sphere_id += 1 @staticmethod - def get_jewel_names(count): + def get_jewel_names(count: JewelCount): if count < 6: - required_jewels = ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"] - del required_jewels[count:] - return required_jewels - else: - return ["Kazalt Jewel"] * count + return ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"][:count] + + return ["Kazalt Jewel"] * count diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md index 7d631b3ef21c..9f453c146de3 100644 --- a/worlds/landstalker/docs/landstalker_setup_en.md +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -44,15 +44,15 @@ To do so, run `randstalker_archipelago.exe` inside the folder you created while A window will open with a few settings to enter: - **Host**: Put the server address and port in this field (e.g. `archipelago.gg:12345`) - **Slot name**: Put the player name you specified in your YAML config file in this field. -- **Password**: If server has a password, put it there. +- **Password**: If the server has a password, put it there. ![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_ap.png) -Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to the -Archipelago server. +Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to +the Archipelago server. If this didn't work, double-check your credentials. An error message should be displayed on the console log to the -right that might help you finding the cause of the issue. +right that might help you find the cause of the issue. ### ROM Generation @@ -70,7 +70,7 @@ You should see a window with settings to fill: There also a few cosmetic options you can fill before clicking the `Build ROM` button which should create your randomized seed if everything went right. -If it didn't, double-check your `Input ROM file` and `Output ROM path`, then retry building the ROM by clicking +If it didn't, double-check your `Input ROM file` and `Output ROM path`, then retry building the ROM by clicking the same button again. ### Connecting to the emulator @@ -83,8 +83,8 @@ You should see the following window: ![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_emu.png) -As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core. Be careful to -select that core, because any other core (e.g. BlastEm) won't work. +As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core. +Be careful to select that core, because any other core (e.g. BlastEm) won't work. The easiest way to do so is to: - open the emu of your choice @@ -98,15 +98,16 @@ Then, you can click on the `Connect to emulator` button below and it should work If this didn't work, try the following: - ensure you have loaded your ROM and reached the save select screen - ensure you are using Genesis Plus GX and not another core (e.g. BlastEm will not work) -- try launching the client in Administrator Mode (right click on `randstalker_archipelago.exe`, then `Run as administrator`) +- try launching the client in Administrator Mode (right-click on `randstalker_archipelago.exe`, then + `Run as administrator`) - if all else fails, try using one of those specific emulator versions: - RetroArch 1.9.0 and Genesis Plus GX 1.7.4 - Bizhawk 2.9.1 (x64) ### Play the game -If all indicators are green and show "Connected", you're good to go! Just play the game and enjoy the wonders of -isometric perspective. +If all indicators are green and show "Connected," you're good to go! Play the game and enjoy the wonders of isometric +perspective. The client is packaged with both an **automatic item tracker** and an **automatic map tracker** for your comfort. From e0c5e53d59217b3927bbc291cea9c8c98e52db13 Mon Sep 17 00:00:00 2001 From: Phar Date: Thu, 23 Nov 2023 00:46:30 -0600 Subject: [PATCH 2/5] Landstalker: More quotes --- worlds/landstalker/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py index 4365eb7aa45b..51357c9480b0 100644 --- a/worlds/landstalker/Rules.py +++ b/worlds/landstalker/Rules.py @@ -112,7 +112,7 @@ def make_shop_location_requirement_lambda(player: int, location: LandstalkerLoca other_locations_in_shop = [loc for loc in location.parent_region.locations if loc != location] return lambda item: \ item.player != player \ - or (' Gold' not in item.name + or (" Gold" not in item.name and item.name not in [loc.item.name for loc in other_locations_in_shop if loc.item is not None]) @@ -125,7 +125,7 @@ def add_location_rules(world: "LandstalkerWorld"): location: LandstalkerLocation for location in world.multiworld.get_locations(world.player): if location.type_string == "ground": - location.item_rule = lambda item: not (item.player == world.player and ' Gold' in item.name) + location.item_rule = lambda item: not (item.player == world.player and " Gold" in item.name) elif location.type_string == "shop": location.item_rule = make_shop_location_requirement_lambda(world.player, location) From e9b0ef6173f4e162faab4df7dec708ddd9dd69c7 Mon Sep 17 00:00:00 2001 From: Phar Date: Thu, 23 Nov 2023 11:16:48 -0600 Subject: [PATCH 3/5] Landstalker: Move hint generation to fill_slot_data as there's no output. --- worlds/landstalker/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 1e037e1d1605..c0ed19cae8c8 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -44,10 +44,14 @@ def __init__(self, multiworld, player): self.dark_region_ids = [] self.teleport_tree_pairs = [] self.jewel_items = [] - self.hints = {} - self.locations: List[LandstalkerLocation] = [] def fill_slot_data(self) -> dict: + # Generate hints. + self.adjust_shop_prices() + hints = Hints.generate_random_hints(self) + hints["Lithograph"] = Hints.generate_lithograph_hint(self) + hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness." + # Put options, locations' contents and some additional data inside slot data options = [ "goal", "jewel_count", "progressive_armors", "use_record_book", "use_spell_book", "shop_prices_factor", @@ -61,10 +65,14 @@ def fill_slot_data(self) -> dict: slot_data["spawn_region"] = self.options.spawn_region.current_key slot_data["seed"] = self.random.randint(0, 2 ** 32 - 1) slot_data["dark_region"] = self.dark_dungeon_id - slot_data["location_prices"] = {location.name: location.price for location in self.locations if location.price} - slot_data["hints"] = self.hints + slot_data["hints"] = hints slot_data["teleport_tree_pairs"] = [[pair[0]["name"], pair[1]["name"]] for pair in self.teleport_tree_pairs] + # Type hinting for location. + location: LandstalkerLocation + slot_data["location_prices"] = { + location.name: location.price for location in self.multiworld.get_locations(self.player) if location.price} + return slot_data def generate_early(self): @@ -205,13 +213,6 @@ def get_starting_health(self): else: return 4 - def generate_output(self, output_directory: str) -> None: - self.adjust_shop_prices() - - self.hints = Hints.generate_random_hints(self) - self.hints["Lithograph"] = Hints.generate_lithograph_hint(self) - self.hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness." - def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position unknown_items_price = 250 From fbe5c7894c7c40763873d653f108c613b139d3a0 Mon Sep 17 00:00:00 2001 From: Phar Date: Thu, 23 Nov 2023 11:24:09 -0600 Subject: [PATCH 4/5] Landstalker: Ensure name consistency with game title. --- worlds/landstalker/Hints.py | 2 +- worlds/landstalker/Items.py | 2 +- worlds/landstalker/Locations.py | 2 +- worlds/landstalker/__init__.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py index 72c4956dbbb1..93274f1d68bb 100644 --- a/worlds/landstalker/Hints.py +++ b/worlds/landstalker/Hints.py @@ -87,7 +87,7 @@ def generate_random_hints(world: "LandstalkerWorld"): # Hint-type #3: Own progression item in remote location for item in remote_own_progression_items: other_player = multiworld.get_player_name(item.location.player) - if item.location.game == "Landstalker": + if item.location.game == "Landstalker - The Treasures of King Nole": region_hint_name = item.location.parent_region.hint_text hint_texts.append(f"If you need {item.name}, tell {other_player} to look {region_hint_name}.") else: diff --git a/worlds/landstalker/Items.py b/worlds/landstalker/Items.py index c9bbdb65b986..ad7efa1cb27a 100644 --- a/worlds/landstalker/Items.py +++ b/worlds/landstalker/Items.py @@ -6,7 +6,7 @@ class LandstalkerItem(Item): - game: str = "Landstalker" + game: str = "Landstalker - The Treasures of King Nole" price_in_shops: int diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index 76ca5b9f10b6..5e42fbecda72 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -11,7 +11,7 @@ class LandstalkerLocation(Location): - game: str = "Landstalker" + game: str = "Landstalker - The Treasures of King Nole" type_string: str price: int = 0 diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index c0ed19cae8c8..398712b1a223 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -232,7 +232,8 @@ def adjust_shop_prices(self): current_playthrough_progression = sphere_id / sphere_count progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff) - price = location.item.price_in_shops if location.item.game == "Landstalker" else unknown_items_price + price = location.item.price_in_shops \ + if location.item.game == "Landstalker - The Treasures of King Nole" else unknown_items_price price *= progression_price_factor price *= global_price_factor price -= price % 5 From b9dcbf86d512911ebf663a7db25a09b4b3fb0384 Mon Sep 17 00:00:00 2001 From: Phar Date: Thu, 23 Nov 2023 12:32:44 -0600 Subject: [PATCH 5/5] Landstalker: Cache spheres. --- worlds/landstalker/__init__.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 398712b1a223..7efe53a2390d 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -1,3 +1,5 @@ +from typing import ClassVar, Set + from BaseClasses import LocationProgressType, Tutorial from worlds.AutoWorld import WebWorld, World from .Hints import * @@ -37,6 +39,8 @@ class LandstalkerWorld(World): item_name_to_id = build_item_name_to_id_table() location_name_to_id = build_location_name_to_id_table() + cached_spheres: ClassVar[List[Set[Location]]] + def __init__(self, multiworld, player): super().__init__(multiworld, player) self.regions_table: Dict[str, LandstalkerRegion] = {} @@ -213,6 +217,16 @@ def get_starting_health(self): else: return 4 + @classmethod + def stage_post_fill(cls, multiworld): + # Cache spheres for hint calculation after fill completes. + cls.cached_spheres = list(multiworld.get_spheres()) + + @classmethod + def stage_modify_multidata(cls, *_): + # Clean up all references in cached spheres after generation completes. + del cls.cached_spheres + def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position unknown_items_price = 250 @@ -222,13 +236,14 @@ def adjust_shop_prices(self): global_price_factor = self.options.shop_prices_factor / 100.0 - spheres = list(self.multiworld.get_spheres()) - sphere_id = 0 + spheres = self.cached_spheres sphere_count = len(spheres) - for sphere in spheres: + for sphere_id, sphere in enumerate(spheres): + location: LandstalkerLocation # after conditional, we guarantee it's this kind of location. for location in sphere: if location.player != self.player or location.type_string != "shop": continue + current_playthrough_progression = sphere_id / sphere_count progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff) @@ -239,7 +254,6 @@ def adjust_shop_prices(self): price -= price % 5 price = max(price, 5) location.price = int(price) - sphere_id += 1 @staticmethod def get_jewel_names(count: JewelCount):