Landstalker: implement new game (#1808)

Co-authored-by: Anthony Demarcy <anthony.demarcy@lumiplan.com>
Co-authored-by: Phar <zach@alliware.com>
This commit is contained in:
Dinopony 2023-11-25 16:00:15 +01:00 committed by GitHub
parent 2ccf11f3d7
commit d46e68cb5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 6447 additions and 0 deletions

View File

@ -56,6 +56,7 @@ Currently, the following games are supported:
* DOOM II
* Shivers
* Heretic
* Landstalker: The Treasures of King Nole
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

View File

@ -67,6 +67,9 @@
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
# Landstalker: The Treasures of King Nole
/worlds/landstalker/ @Dinopony
# Lingo
/worlds/lingo/ @hatkirby

140
worlds/landstalker/Hints.py Normal file
View File

@ -0,0 +1,140 @@
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"):
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"
if location_name_words:
random_word_1 = random.choice(location_name_words)
location_name_words.remove(random_word_1)
if location_name_words:
random_word_2 = random.choice(location_name_words)
return [random_word_1, random_word_2]
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, 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 != world.player:
# Add player name if it's not in our own world
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")
def generate_random_hints(world: "LandstalkerWorld"):
hints = {}
hint_texts = []
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
excluded_items = ["Life Stock", "EkeEke"]
progression_items = [item for item in multiworld.itempool if item.advancement and
item.name not in excluded_items]
local_own_progression_items = [item for item in progression_items if item.player == this_player
and item.location.player == this_player]
remote_own_progression_items = [item for item in progression_items if item.player == this_player
and item.location.player != this_player]
local_unowned_progression_items = [item for item in progression_items if item.player != this_player
and item.location.player == this_player]
remote_unowned_progression_items = [item for item in progression_items if item.player != this_player
and item.location.player != this_player]
# Hint-type #1: Own progression item in own world
for item in local_own_progression_items:
region_hint = item.location.parent_region.hint_text
hint_texts.append(f"I can sense {item.name} {region_hint}.")
# Hint-type #2: Remote progression item in own world
for item in local_unowned_progression_items:
other_player = multiworld.get_player_name(item.player)
own_local_region = item.location.parent_region.hint_text
hint_texts.append(f"You might find something useful for {other_player} {own_local_region}. "
f"It is a {item.name}, to be precise.")
# 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 - 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:
[word_1, word_2] = generate_blurry_location_hint(item.location, random)
if word_1 == "mysterious" and word_2 == "place":
continue
hint_texts.append(f"Looking for {item.name}? I read something about {other_player}'s world... "
f"Does \"{word_1} {word_2}\" remind you anything?")
# Hint-type #4: Remote progression item in remote location
for item in remote_unowned_progression_items:
owner_name = multiworld.get_player_name(item.player)
if item.location.player == item.player:
world_name = "their own world"
else:
world_name = f"{multiworld.get_player_name(item.location.player)}'s world"
[word_1, word_2] = generate_blurry_location_hint(item.location, random)
if word_1 == "mysterious" and word_2 == "place":
continue
hint_texts.append(f"I once found {owner_name}'s {item.name} in {world_name}. "
f"I remember \"{word_1} {word_2}\"... Does that make any sense?")
# Hint-type #5: Jokes
other_player_names = [multiworld.get_player_name(player) for player in multiworld.player_ids if
player != this_player]
if other_player_names:
random_player_name = random.choice(other_player_names)
hint_texts.append(f"{random_player_name}'s world is objectively better than yours.")
hint_texts.append(f"Have you found all of the {len(multiworld.itempool)} items in this universe?")
local_progression_item_count = len(local_own_progression_items) + len(local_unowned_progression_items)
remote_progression_item_count = len(remote_own_progression_items) + len(remote_unowned_progression_items)
percent = (local_progression_item_count / (local_progression_item_count + remote_progression_item_count)) * 100
hint_texts.append(f"Did you know that your world contains {int(percent)} percent of all progression items?")
# Shuffle hint texts and hint source names, and pair the two of those together
hint_texts = list(set(hint_texts))
random.shuffle(hint_texts)
hint_count = world.options.hint_count.value
del hint_texts[hint_count:]
hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if
source["description"].startswith("Foxy")]
random.shuffle(hint_source_names)
for i in range(hint_count):
hints[hint_source_names[i]] = hint_texts[i]
return hints

105
worlds/landstalker/Items.py Normal file
View File

@ -0,0 +1,105 @@
from typing import Dict, List, NamedTuple
from BaseClasses import Item, ItemClassification
BASE_ITEM_ID = 4000
class LandstalkerItem(Item):
game: str = "Landstalker - The Treasures of King Nole"
price_in_shops: int
class LandstalkerItemData(NamedTuple):
id: int
classification: ItemClassification
price_in_shops: int
quantity: int = 1
item_table: Dict[str, LandstalkerItemData] = {
"EkeEke": LandstalkerItemData(0, ItemClassification.filler, 20, 0), # Variable amount
"Magic Sword": LandstalkerItemData(1, ItemClassification.useful, 300),
"Sword of Ice": LandstalkerItemData(2, ItemClassification.useful, 300),
"Thunder Sword": LandstalkerItemData(3, ItemClassification.useful, 500),
"Sword of Gaia": LandstalkerItemData(4, ItemClassification.progression, 300),
"Fireproof": LandstalkerItemData(5, ItemClassification.progression, 150),
"Iron Boots": LandstalkerItemData(6, ItemClassification.progression, 150),
"Healing Boots": LandstalkerItemData(7, ItemClassification.useful, 300),
"Snow Spikes": LandstalkerItemData(8, ItemClassification.progression, 400),
"Steel Breast": LandstalkerItemData(9, ItemClassification.useful, 200),
"Chrome Breast": LandstalkerItemData(10, ItemClassification.useful, 350),
"Shell Breast": LandstalkerItemData(11, ItemClassification.useful, 500),
"Hyper Breast": LandstalkerItemData(12, ItemClassification.useful, 700),
"Mars Stone": LandstalkerItemData(13, ItemClassification.useful, 150),
"Moon Stone": LandstalkerItemData(14, ItemClassification.useful, 150),
"Saturn Stone": LandstalkerItemData(15, ItemClassification.useful, 200),
"Venus Stone": LandstalkerItemData(16, ItemClassification.useful, 300),
# Awakening Book: 17
"Detox Grass": LandstalkerItemData(18, ItemClassification.filler, 25, 9),
"Statue of Gaia": LandstalkerItemData(19, ItemClassification.filler, 75, 12),
"Golden Statue": LandstalkerItemData(20, ItemClassification.filler, 150, 10),
"Mind Repair": LandstalkerItemData(21, ItemClassification.filler, 25, 7),
"Casino Ticket": LandstalkerItemData(22, ItemClassification.progression, 50),
"Axe Magic": LandstalkerItemData(23, ItemClassification.progression, 400),
"Blue Ribbon": LandstalkerItemData(24, ItemClassification.filler, 50),
"Buyer Card": LandstalkerItemData(25, ItemClassification.progression, 150),
"Lantern": LandstalkerItemData(26, ItemClassification.progression, 200),
"Garlic": LandstalkerItemData(27, ItemClassification.progression, 150, 2),
"Anti Paralyze": LandstalkerItemData(28, ItemClassification.filler, 20, 7),
"Statue of Jypta": LandstalkerItemData(29, ItemClassification.useful, 250),
"Sun Stone": LandstalkerItemData(30, ItemClassification.progression, 300),
"Armlet": LandstalkerItemData(31, ItemClassification.progression, 300),
"Einstein Whistle": LandstalkerItemData(32, ItemClassification.progression, 200),
"Blue Jewel": LandstalkerItemData(33, ItemClassification.progression, 500, 0), # Detox Book in base game
"Yellow Jewel": LandstalkerItemData(34, ItemClassification.progression, 500, 0), # AntiCurse Book in base game
# Record Book: 35
# Spell Book: 36
# Hotel Register: 37
# Island Map: 38
"Lithograph": LandstalkerItemData(39, ItemClassification.progression, 250),
"Red Jewel": LandstalkerItemData(40, ItemClassification.progression, 500, 0),
"Pawn Ticket": LandstalkerItemData(41, ItemClassification.useful, 200, 4),
"Purple Jewel": LandstalkerItemData(42, ItemClassification.progression, 500, 0),
"Gola's Eye": LandstalkerItemData(43, ItemClassification.progression, 400),
"Death Statue": LandstalkerItemData(44, ItemClassification.filler, 150),
"Dahl": LandstalkerItemData(45, ItemClassification.filler, 100, 18),
"Restoration": LandstalkerItemData(46, ItemClassification.filler, 40, 9),
"Logs": LandstalkerItemData(47, ItemClassification.progression, 100, 2),
"Oracle Stone": LandstalkerItemData(48, ItemClassification.progression, 250),
"Idol Stone": LandstalkerItemData(49, ItemClassification.progression, 200),
"Key": LandstalkerItemData(50, ItemClassification.progression, 150),
"Safety Pass": LandstalkerItemData(51, ItemClassification.progression, 250),
"Green Jewel": LandstalkerItemData(52, ItemClassification.progression, 500, 0), # No52 in base game
"Bell": LandstalkerItemData(53, ItemClassification.useful, 200),
"Short Cake": LandstalkerItemData(54, ItemClassification.useful, 250),
"Gola's Nail": LandstalkerItemData(55, ItemClassification.progression, 800),
"Gola's Horn": LandstalkerItemData(56, ItemClassification.progression, 800),
"Gola's Fang": LandstalkerItemData(57, ItemClassification.progression, 800),
# Broad Sword: 58
# Leather Breast: 59
# Leather Boots: 60
# No Ring: 61
"Life Stock": LandstalkerItemData(62, ItemClassification.filler, 250, 0), # Variable amount
"No Item": LandstalkerItemData(63, ItemClassification.filler, 0, 0),
"1 Gold": LandstalkerItemData(64, ItemClassification.filler, 1),
"20 Golds": LandstalkerItemData(65, ItemClassification.filler, 20, 15),
"50 Golds": LandstalkerItemData(66, ItemClassification.filler, 50, 7),
"100 Golds": LandstalkerItemData(67, ItemClassification.filler, 100, 5),
"200 Golds": LandstalkerItemData(68, ItemClassification.useful, 200, 2),
"Progressive Armor": LandstalkerItemData(69, ItemClassification.useful, 250, 0),
"Kazalt Jewel": LandstalkerItemData(70, ItemClassification.progression, 500, 0)
}
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(data.quantity)]
return weighted_item_names
def build_item_name_to_id_table():
return {name: data.id + BASE_ITEM_ID for name, data in item_table.items()}

View File

@ -0,0 +1,53 @@
from typing import Dict, Optional
from BaseClasses import Location
from .Regions import LandstalkerRegion
from .data.item_source import ITEM_SOURCES_JSON
BASE_LOCATION_ID = 4000
BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256
BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30
BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50
class LandstalkerLocation(Location):
game: str = "Landstalker - The Treasures of King Nole"
type_string: str
price: int = 0
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, 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"]
region = regions_table[region_id]
new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"])
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)
def build_location_name_to_id_table():
location_name_to_id_table = {}
for data in ITEM_SOURCES_JSON:
if data["type"] == "chest":
location_id = BASE_LOCATION_ID + int(data["chestId"])
elif data["type"] == "ground":
location_id = BASE_GROUND_LOCATION_ID + int(data["groundItemId"])
elif data["type"] == "shop":
location_id = BASE_SHOP_LOCATION_ID + int(data["shopItemId"])
else: # if data["type"] == "reward":
location_id = BASE_REWARD_LOCATION_ID + int(data["rewardId"])
location_name_to_id_table[data["name"]] = location_id
# Win condition location ID
location_name_to_id_table["Gola"] = BASE_REWARD_LOCATION_ID + 10
return location_name_to_id_table

View File

@ -0,0 +1,228 @@
from dataclasses import dataclass
from Options import Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, Range, Toggle
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
"""
display_name = "Goal"
option_beat_gola = 0
option_reach_kazalt = 1
option_beat_dark_nole = 2
default = 0
class JewelCount(Range):
"""
Determines the number of jewels to find to be able to reach Kazalt.
"""
display_name = "Jewel Count"
range_start = 0
range_end = 9
default = 5
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"
class UseRecordBook(DefaultOnToggle):
"""
Gives a Record Book item in starting inventory, allowing to save the game anywhere.
This makes the game significantly less frustrating and enables interesting save-scumming strategies in some places.
"""
display_name = "Use Record Book"
class UseSpellBook(DefaultOnToggle):
"""
Gives a Spell Book item in starting inventory, allowing to warp back to the starting location at any time.
This prevents any kind of softlock and makes the world easier to explore.
"""
display_name = "Use Spell Book"
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"
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 it.
"""
display_name = "Remove Boulder After Gumi"
class EnemyJumpingInLogic(Toggle):
"""
Adds jumping on enemies' heads as a logical rule.
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"
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"
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"
class WhistleUsageBehindTrees(DefaultOnToggle):
"""
In Greenmaze, Einstein Whistle can only be used to call Cutter from the intended side by default.
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"
class SpawnRegion(Choice):
"""
List of spawn locations that can be picked by the randomizer.
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"
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 = 0
class TeleportTreeRequirements(Choice):
"""
Determines the requirements to be able to use a teleport tree pair.
- None: All teleport trees are available right from the start
- Clear Tibor: Tibor needs to be cleared before unlocking any tree
- 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"
option_none = 0
option_clear_tibor = 1
option_visit_trees = 2
option_clear_tibor_and_visit_trees = 3
default = 3
class ShuffleTrees(Toggle):
"""
If enabled, all teleportation trees will be shuffled into new pairs.
"""
display_name = "Shuffle Teleportation Trees"
class ReviveUsingEkeeke(DefaultOnToggle):
"""
In the vanilla game, when you die, you are automatically revived by Friday using an EkeEke.
This setting allows disabling this feature, making the game extremely harder.
USE WITH CAUTION!
"""
display_name = "Revive Using EkeEke"
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 (%)"
range_start = 50
range_end = 200
default = 100
class CombatDifficulty(Choice):
"""
Determines the overall combat difficulty in the game by modifying both monsters HP & damage.
- Peaceful: 50% HP & damage
- Easy: 75% HP & damage
- Normal: 100% HP & damage
- Hard: 140% HP & damage
- Insane: 200% HP & damage
"""
display_name = "Combat Difficulty"
option_peaceful = 0
option_easy = 1
option_normal = 2
option_hard = 3
option_insane = 4
default = 2
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"
range_start = 0
range_end = 25
default = 12
@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
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
hint_count: HintCount
revive_using_ekeeke: ReviveUsingEkeeke
death_link: DeathLink

View File

@ -0,0 +1,118 @@
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
def __init__(self, code: str, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
super().__init__(name, player, multiworld, hint)
self.code = code
class LandstalkerRegionData(NamedTuple):
locations: Optional[List[str]]
region_exits: Optional[List[str]]
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)
regions_table["menu"] = menu_region
multiworld.regions.append(menu_region)
# Create regions from world_nodes
for code, region_data in WORLD_NODES_JSON.items():
random_hint_name = None
if "hints" in region_data:
random_hint_name = multiworld.random.choice(region_data["hints"])
region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name)
regions_table[code] = region
multiworld.regions.append(region)
# 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, regions_table)
# Create a path between the fake Menu location and the starting location
starting_region = get_starting_region(world, regions_table)
menu_region.connect(starting_region, f"menu -> {starting_region.code}")
add_specific_paths(world, regions_table)
return regions_table
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 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 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 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, regions_table: Dict[str, LandstalkerRegion]):
created_entrances = []
name = from_id + " -> " + to_id
from_region = regions_table[from_id]
to_region = regions_table[to_id]
created_entrances.append(from_region.connect(to_region, name))
if two_way:
reverse_name = to_id + " -> " + from_id
created_entrances.append(to_region.connect(from_region, reverse_name))
return created_entrances
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 = world.options.spawn_region.current_key
if spawn_id == "waterfall":
return regions_table["greenmaze_post_whistle"]
elif spawn_id == "kado":
return regions_table["route_gumi_ryuma"]
elif spawn_id == "greenmaze":
return regions_table["greenmaze_pre_whistle"]
return regions_table[spawn_id]
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"],
'region': pair[0]["nodeId"]
}
second_tree = {
'name': pair[1]["name"],
'region': pair[1]["nodeId"]
}
pairs.append([first_tree, second_tree])
return pairs

134
worlds/landstalker/Rules.py Normal file
View File

@ -0,0 +1,134 @@
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
if TYPE_CHECKING:
from . import LandstalkerWorld
def _landstalker_has_visited_regions(state: CollectionState, player: int, regions):
return all([state.can_reach(region, None, player) for region in regions])
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(world)
add_specific_path_requirements(world)
# Location rules to forbid some item types depending on location types
add_location_rules(world)
# Win condition
world.multiworld.completion_condition[world.player] = lambda state: state.has("King Nole's Treasure", world.player)
# 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"]
# Determine required items to reach this region
required_items = data["requiredItems"] if "requiredItems" in data else []
if "itemsPlacedWhenCrossing" in data:
required_items += data["itemsPlacedWhenCrossing"]
if data["toId"] in world.dark_region_ids:
# Make Lantern required to reach the randomly selected dark regions
required_items.append("Lantern")
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 = [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(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"]
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
# Make the jewels required to reach Kazalt
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
required_jewels = ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"]
del required_jewels[jewel_count:]
path_to_kazalt.access_rule = make_path_requirement_lambda(player, required_jewels, [])
else:
# 6+ jewels => the player needs to find as many "Kazalt Jewel" items
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 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 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 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: 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 _landstalker_has_visited_regions(state, player, required_regions)
def make_shop_location_requirement_lambda(player: int, location: LandstalkerLocation):
"""
Lambdas are created in a for loop, so values need to be captured
"""
# Prevent local golds in shops, as well as duplicates
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
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(world: "LandstalkerWorld", entrance_name: str):
entrance = world.multiworld.get_entrance(entrance_name, world.player)
entrance.access_rule = lambda state: True
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)
elif location.type_string == "shop":
location.item_rule = make_shop_location_requirement_lambda(world.player, location)
# Add a special rule for Fahl
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)

View File

@ -0,0 +1,262 @@
from typing import ClassVar, Set
from BaseClasses import LocationProgressType, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Hints import *
from .Items import *
from .Locations import *
from .Options import JewelCount, LandstalkerGoal, LandstalkerOptions, ProgressiveArmors, TeleportTreeRequirements
from .Regions import *
from .Rules import *
class LandstalkerWeb(WebWorld):
theme = "grass"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Landstalker Randomizer software on your computer.",
"English",
"landstalker_setup_en.md",
"landstalker_setup/en",
["Dinopony"]
)]
class LandstalkerWorld(World):
"""
Landstalker: The Treasures of King Nole is a classic Action-RPG with an isometric view (also known as "2.5D").
You play Nigel, a treasure hunter exploring the island of Mercator trying to find the legendary treasure.
Roam freely on the island, get stronger to beat dungeons and gather the required key items in order to reach the
hidden palace and claim the treasure.
"""
game = "Landstalker - The Treasures of King Nole"
options_dataclass = LandstalkerOptions
options: LandstalkerOptions
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()
cached_spheres: ClassVar[List[Set[Location]]]
def __init__(self, multiworld, player):
super().__init__(multiworld, player)
self.regions_table: Dict[str, LandstalkerRegion] = {}
self.dark_dungeon_id = "None"
self.dark_region_ids = []
self.teleport_tree_pairs = []
self.jewel_items = []
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",
"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["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):
# Randomly pick a set of dark regions where Lantern is needed
darkenable_regions = get_darkenable_regions()
self.dark_dungeon_id = self.random.choice(list(darkenable_regions))
self.dark_region_ids = darkenable_regions[self.dark_dungeon_id]
def create_regions(self):
self.regions_table = Regions.create_regions(self)
Locations.create_locations(self.player, self.regions_table, self.location_name_to_id)
self.create_teleportation_trees()
def create_item(self, name: str, classification_override: Optional[ItemClassification] = None) -> LandstalkerItem:
data = item_table[name]
classification = classification_override or data.classification
item = LandstalkerItem(name, classification, BASE_ITEM_ID + data.id, self.player)
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.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.options.ensure_ekeeke_in_shops:
shops_to_fill = [
"Massan: Shop item #1",
"Gumi: Inn item #1",
"Ryuma: Inn item",
"Mercator: Shop item #1",
"Verla: Shop item #1",
"Destel: Inn item",
"Route to Lake Shrine: Greedly's shop item #1",
"Kazalt: Shop item #1"
]
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)]
# Add a unique progression EkeEke for a specific requirement (Cutter)
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
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.options.jewel_count)]
item_pool += self.jewel_items
# Add a pre-placed fake 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(self.get_filler_item_name()) for _ in range(remaining_items)]
self.multiworld.itempool += item_pool
def create_teleportation_trees(self):
self.teleport_tree_pairs = load_teleport_trees()
def pairwise(iterable):
"""Yields pairs of elements from the given list -> [0,1], [2,3]..."""
a = iter(iterable)
return zip(a, a)
# Shuffle teleport tree pairs if the matching setting is on
if self.options.shuffle_trees:
all_trees = [item for pair in self.teleport_tree_pairs for item in pair]
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.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.regions_table)
created_entrances += entrances
# 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"]])
def set_rules(self):
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
# King Nole's Cave to Kazalt must go to the end region instead.
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 = [
"kazalt",
"king_nole_labyrinth_pre_door",
"king_nole_labyrinth_post_door",
"king_nole_labyrinth_exterior",
"king_nole_labyrinth_fall_from_exterior",
"king_nole_labyrinth_raft_entrance",
"king_nole_labyrinth_raft",
"king_nole_labyrinth_sacred_tree",
"king_nole_labyrinth_path_to_palace",
"king_nole_palace"
]
for location in self.multiworld.get_locations(self.player):
if location.parent_region.name in excluded_regions:
location.progress_type = LocationProgressType.EXCLUDED
def get_starting_health(self):
spawn_id = self.options.spawn_region.current_key
if spawn_id == "destel":
return 20
elif spawn_id == "verla":
return 16
elif spawn_id in ["waterfall", "mercator", "greenmaze"]:
return 10
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
earlygame_price_factor = 1.0
endgame_price_factor = 2.0
factor_diff = endgame_price_factor - earlygame_price_factor
global_price_factor = self.options.shop_prices_factor / 100.0
spheres = self.cached_spheres
sphere_count = len(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)
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
price = max(price, 5)
location.price = int(price)
@staticmethod
def get_jewel_names(count: JewelCount):
if count < 6:
return ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"][:count]
return ["Kazalt Jewel"] * count

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,411 @@
WORLD_NODES_JSON = {
"massan": {
"name": "Massan",
"hints": [
"in a village",
"in a region inhabited by bears",
"in the village of Massan"
]
},
"massan_cave": {
"name": "Massan Cave",
"hints": [
"in a large cave",
"in a region inhabited by bears",
"in Massan cave"
]
},
"route_massan_gumi": {
"name": "Route between Massan and Gumi",
"hints": [
"on a route",
"in a region inhabited by bears",
"between Massan and Gumi"
]
},
"waterfall_shrine": {
"name": "Waterfall Shrine",
"hints": [
"in a shrine",
"close to a waterfall",
"in a region inhabited by bears",
"in Waterfall Shrine"
]
},
"swamp_shrine": {
"name": "Swamp Shrine",
"hints": [
"in a shrine",
"near a swamp",
"in a region inhabited by bears",
"in Swamp Shrine"
]
},
"massan_after_swamp_shrine": {
"name": "Massan (after Swamp Shrine)",
"hints": [
"in a village",
"in a region inhabited by bears",
"in the village of Massan"
]
},
"gumi_after_swamp_shrine": {
"name": "Gumi (after Swamp Shrine)",
"hints": [
"in a village",
"in a region inhabited by bears",
"in the village of Gumi"
]
},
"gumi": {
"name": "Gumi",
"hints": [
"in a village",
"in a region inhabited by bears",
"in the village of Gumi"
]
},
"route_gumi_ryuma": {
"name": "Route from Gumi to Ryuma",
"hints": [
"on a route",
"in a region inhabited by bears",
"between Gumi and Ryuma"
]
},
"tibor": {
"name": "Tibor",
"hints": [
"among the trees",
"inside the elder tree called Tibor"
]
},
"ryuma": {
"name": "Ryuma",
"hints": [
"in a town",
"in the town of Ryuma"
]
},
"ryuma_after_thieves_hideout": {
"name": "Ryuma (after Thieves Hideout)",
"hints": [
"in a town",
"in the town of Ryuma"
]
},
"ryuma_lighthouse_repaired": {
"name": "Ryuma (repaired lighthouse)",
"hints": [
"in a town",
"in the town of Ryuma"
]
},
"thieves_hideout_pre_key": {
"name": "Thieves Hideout (before keydoor)",
"hints": [
"close to a waterfall",
"in a large cave",
"in the Thieves' Hideout"
]
},
"thieves_hideout_post_key": {
"name": "Thieves Hideout (after keydoor)",
"hints": [
"close to a waterfall",
"in a large cave",
"in the Thieves' Hideout"
]
},
"helga_hut": {
"name": "Witch Helga's Hut",
"hints": [
"near a swamp",
"in the hut of a witch called Helga"
]
},
"mercator": {
"name": "Mercator",
"hints": [
"in a town",
"in the town of Mercator"
]
},
"mercator_repaired_docks": {
"name": "Mercator (docks with repaired lighthouse)",
"hints": [
"in a town",
"in the town of Mercator"
]
},
"mercator_casino": {
"name": "Mercator casino"
},
"mercator_dungeon": {
"name": "Mercator Dungeon"
},
"crypt": {
"name": "Crypt",
"hints": [
"hidden in the depths of Mercator",
"in Mercator crypt"
]
},
"mercator_special_shop": {
"name": "Mercator special shop",
"hints": [
"in a town",
"in the town of Mercator"
]
},
"mir_tower_sector": {
"name": "Mir Tower sector",
"hints": [
"on a route",
"near Mir Tower"
]
},
"mir_tower_sector_tree_ledge": {
"name": "Mir Tower sector (ledge behind sacred tree)",
"hints": [
"on a route",
"among the trees",
"near Mir Tower"
]
},
"mir_tower_sector_tree_coast": {
"name": "Mir Tower sector (coast behind sacred tree)",
"hints": [
"on a route",
"among the trees",
"near Mir Tower"
]
},
"twinkle_village": {
"name": "Twinkle village",
"hints": [
"in a village",
"in Twinkle village"
]
},
"mir_tower_pre_garlic": {
"name": "Mir Tower (pre-garlic)",
"hints": [
"inside a tower",
"in Mir Tower"
]
},
"mir_tower_post_garlic": {
"name": "Mir Tower (post-garlic)",
"hints": [
"inside a tower",
"in Mir Tower"
]
},
"greenmaze_pre_whistle": {
"name": "Greenmaze (pre-whistle)",
"hints": [
"among the trees",
"in the infamous Greenmaze"
]
},
"greenmaze_cutter": {
"name": "Greenmaze (Cutter hidden sector)",
"hints": [
"among the trees",
"in the infamous Greenmaze"
]
},
"greenmaze_post_whistle": {
"name": "Greenmaze (post-whistle)",
"hints": [
"among the trees",
"in the infamous Greenmaze"
]
},
"verla_shore": {
"name": "Verla shore",
"hints": [
"on a route",
"near the town of Verla"
]
},
"verla_shore_cliff": {
"name": "Verla shore cliff (accessible from Verla Mines)",
"hints": [
"on a route",
"near the town of Verla"
]
},
"verla": {
"name": "Verla",
"hints": [
"in a town",
"in the town of Verla"
]
},
"verla_after_mines": {
"name": "Verla (after mines)",
"hints": [
"in a town",
"in the town of Verla"
]
},
"verla_mines": {
"name": "Verla Mines",
"hints": [
"in Verla Mines"
]
},
"verla_mines_behind_lava": {
"name": "Verla Mines (behind lava)",
"hints": [
"in Verla Mines"
]
},
"route_verla_destel": {
"name": "Route between Verla and Destel",
"hints": [
"on a route",
"in Destel region",
"between Verla and Destel"
]
},
"destel": {
"name": "Destel",
"hints": [
"in a village",
"in Destel region",
"in the village of Destel"
]
},
"route_after_destel": {
"name": "Route after Destel",
"hints": [
"on a route",
"near a lake",
"in Destel region",
"on the route to the lake after Destel"
]
},
"destel_well": {
"name": "Destel Well",
"hints": [
"in Destel region",
"in a large cave",
"in Destel Well"
]
},
"route_lake_shrine": {
"name": "Route to Lake Shrine",
"hints": [
"on a route",
"near a lake",
"on the mountainous path to Lake Shrine"
]
},
"route_lake_shrine_cliff": {
"name": "Route to Lake Shrine cliff",
"hints": [
"on a route",
"near a lake",
"on the mountainous path to Lake Shrine"
]
},
"lake_shrine": {
"name": "Lake Shrine",
"hints": [
"in a shrine",
"near a lake",
"in Lake Shrine"
]
},
"mountainous_area": {
"name": "Mountainous Area",
"hints": [
"in a mountainous area"
]
},
"king_nole_cave": {
"name": "King Nole's Cave",
"hints": [
"in a large cave",
"in King Nole's cave"
]
},
"kazalt": {
"name": "Kazalt",
"hints": [
"in King Nole's domain",
"in Kazalt"
]
},
"king_nole_labyrinth_pre_door": {
"name": "King Nole's Labyrinth (before door)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_post_door": {
"name": "King Nole's Labyrinth (after door)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_exterior": {
"name": "King Nole's Labyrinth (exterior)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_fall_from_exterior": {
"name": "King Nole's Labyrinth (fall from exterior)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_raft_entrance": {
"name": "King Nole's Labyrinth (raft entrance)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_raft": {
"name": "King Nole's Labyrinth (raft)",
"hints": [
"close to a waterfall",
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_sacred_tree": {
"name": "King Nole's Labyrinth (sacred tree)",
"hints": [
"among the trees",
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_labyrinth_path_to_palace": {
"name": "King Nole's Labyrinth (path to palace)",
"hints": [
"in King Nole's domain",
"in King Nole's labyrinth"
]
},
"king_nole_palace": {
"name": "King Nole's Palace",
"hints": [
"in King Nole's domain",
"in King Nole's palace"
]
},
"end": {
"name": "The End"
}
}

View File

@ -0,0 +1,446 @@
WORLD_PATHS_JSON = [
{
"fromId": "massan",
"toId": "massan_cave",
"twoWay": True,
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "massan",
"toId": "massan_after_swamp_shrine",
"requiredNodes": [
"swamp_shrine"
]
},
{
"fromId": "massan",
"toId": "route_massan_gumi",
"twoWay": True
},
{
"fromId": "route_massan_gumi",
"toId": "waterfall_shrine",
"twoWay": True
},
{
"fromId": "route_massan_gumi",
"toId": "swamp_shrine",
"twoWay": True,
"weight": 2,
"requiredItems": [
"Idol Stone"
]
},
{
"fromId": "route_massan_gumi",
"toId": "gumi",
"twoWay": True
},
{
"fromId": "gumi",
"toId": "gumi_after_swamp_shrine",
"requiredNodes": [
"swamp_shrine"
]
},
{
"fromId": "gumi",
"toId": "route_gumi_ryuma"
},
{
"fromId": "route_gumi_ryuma",
"toId": "ryuma",
"twoWay": True
},
{
"fromId": "ryuma",
"toId": "ryuma_after_thieves_hideout",
"requiredNodes": [
"thieves_hideout_post_key"
]
},
{
"fromId": "ryuma",
"toId": "ryuma_lighthouse_repaired",
"twoWay": True,
"requiredItems": [
"Sun Stone"
]
},
{
"fromId": "ryuma",
"toId": "thieves_hideout_pre_key",
"twoWay": True
},
{
"fromId": "thieves_hideout_pre_key",
"toId": "thieves_hideout_post_key",
"requiredItems": [
"Key"
]
},
{
"fromId": "thieves_hideout_post_key",
"toId": "thieves_hideout_pre_key"
},
{
"fromId": "route_gumi_ryuma",
"toId": "tibor",
"twoWay": True
},
{
"fromId": "route_gumi_ryuma",
"toId": "helga_hut",
"twoWay": True,
"requiredItems": [
"Einstein Whistle"
],
"requiredNodes": [
"massan"
]
},
{
"fromId": "route_gumi_ryuma",
"toId": "mercator",
"twoWay": True,
"weight": 2,
"requiredItems": [
"Safety Pass"
]
},
{
"fromId": "mercator",
"toId": "mercator_dungeon",
"twoWay": True
},
{
"fromId": "mercator",
"toId": "crypt",
"twoWay": True
},
{
"fromId": "mercator",
"toId": "mercator_special_shop",
"twoWay": True,
"requiredItems": [
"Buyer Card"
]
},
{
"fromId": "mercator",
"toId": "mercator_casino",
"twoWay": True,
"requiredItems": [
"Casino Ticket"
]
},
{
"fromId": "mercator",
"toId": "mir_tower_sector",
"twoWay": True
},
{
"fromId": "mir_tower_sector",
"toId": "twinkle_village",
"twoWay": True
},
{
"fromId": "mir_tower_sector",
"toId": "mir_tower_sector_tree_ledge",
"twoWay": True,
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "mir_tower_sector",
"toId": "mir_tower_sector_tree_coast",
"twoWay": True,
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "mir_tower_sector",
"toId": "mir_tower_pre_garlic",
"requiredItems": [
"Armlet"
]
},
{
"fromId": "mir_tower_pre_garlic",
"toId": "mir_tower_sector"
},
{
"fromId": "mir_tower_pre_garlic",
"toId": "mir_tower_post_garlic",
"requiredItems": [
"Garlic"
]
},
{
"fromId": "mir_tower_post_garlic",
"toId": "mir_tower_pre_garlic"
},
{
"fromId": "mir_tower_post_garlic",
"toId": "mir_tower_sector"
},
{
"fromId": "mercator",
"toId": "greenmaze_pre_whistle",
"weight": 2,
"requiredItems": [
"Key"
]
},
{
"fromId": "greenmaze_pre_whistle",
"toId": "greenmaze_post_whistle",
"requiredItems": [
"Einstein Whistle"
]
},
{
"fromId": "greenmaze_pre_whistle",
"toId": "greenmaze_cutter",
"requiredItems": [
"EkeEke"
],
"twoWay": True
},
{
"fromId": "greenmaze_post_whistle",
"toId": "route_massan_gumi"
},
{
"fromId": "mercator",
"toId": "mercator_repaired_docks",
"requiredNodes": [
"ryuma_lighthouse_repaired"
]
},
{
"fromId": "mercator_repaired_docks",
"toId": "verla_shore"
},
{
"fromId": "verla_shore",
"toId": "verla",
"twoWay": True
},
{
"fromId": "verla",
"toId": "verla_after_mines",
"requiredNodes": [
"verla_mines"
],
"twoWay": True
},
{
"fromId": "verla_shore",
"toId": "verla_mines",
"twoWay": True
},
{
"fromId": "verla_mines",
"toId": "verla_shore_cliff",
"twoWay": True
},
{
"fromId": "verla_shore_cliff",
"toId": "verla_shore"
},
{
"fromId": "verla_shore",
"toId": "mir_tower_sector",
"requiredNodes": [
"verla_mines"
],
"twoWay": True
},
{
"fromId": "verla_mines",
"toId": "route_verla_destel"
},
{
"fromId": "verla_mines",
"toId": "verla_mines_behind_lava",
"twoWay": True,
"requiredItems": [
"Fireproof"
]
},
{
"fromId": "route_verla_destel",
"toId": "destel",
"twoWay": True
},
{
"fromId": "destel",
"toId": "route_after_destel",
"twoWay": True
},
{
"fromId": "destel",
"toId": "destel_well",
"twoWay": True
},
{
"fromId": "destel_well",
"toId": "route_lake_shrine",
"twoWay": True
},
{
"fromId": "route_lake_shrine",
"toId": "lake_shrine",
"itemsPlacedWhenCrossing": [
"Sword of Gaia"
]
},
{
"fromId": "lake_shrine",
"toId": "route_lake_shrine"
},
{
"fromId": "lake_shrine",
"toId": "mir_tower_sector"
},
{
"fromId": "greenmaze_pre_whistle",
"toId": "mountainous_area",
"twoWay": True,
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "mountainous_area",
"toId": "route_lake_shrine_cliff",
"twoWay": True,
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "route_lake_shrine_cliff",
"toId": "route_lake_shrine"
},
{
"fromId": "mountainous_area",
"toId": "king_nole_cave",
"twoWay": True,
"weight": 2,
"requiredItems": [
"Gola's Eye"
]
},
{
"fromId": "king_nole_cave",
"toId": "mercator"
},
{
"fromId": "king_nole_cave",
"toId": "kazalt",
"itemsPlacedWhenCrossing": [
"Lithograph"
]
},
{
"fromId": "kazalt",
"toId": "king_nole_cave"
},
{
"fromId": "kazalt",
"toId": "king_nole_labyrinth_pre_door",
"twoWay": True
},
{
"fromId": "king_nole_labyrinth_pre_door",
"toId": "king_nole_labyrinth_post_door",
"requiredItems": [
"Key"
]
},
{
"fromId": "king_nole_labyrinth_post_door",
"toId": "king_nole_labyrinth_pre_door"
},
{
"fromId": "king_nole_labyrinth_pre_door",
"toId": "king_nole_labyrinth_exterior",
"requiredItems": [
"Iron Boots"
]
},
{
"fromId": "king_nole_labyrinth_exterior",
"toId": "king_nole_labyrinth_fall_from_exterior",
"requiredItems": [
"Axe Magic"
]
},
{
"fromId": "king_nole_labyrinth_fall_from_exterior",
"toId": "king_nole_labyrinth_pre_door"
},
{
"fromId": "king_nole_labyrinth_post_door",
"toId": "king_nole_labyrinth_raft_entrance",
"requiredItems": [
"Snow Spikes"
]
},
{
"fromId": "king_nole_labyrinth_raft_entrance",
"toId": "king_nole_labyrinth_post_door"
},
{
"fromId": "king_nole_labyrinth_raft_entrance",
"toId": "king_nole_labyrinth_raft",
"requiredItems": [
"Logs"
]
},
{
"fromId": "king_nole_labyrinth_raft",
"toId": "king_nole_labyrinth_raft_entrance"
},
{
"fromId": "king_nole_labyrinth_post_door",
"toId": "king_nole_labyrinth_path_to_palace",
"requiredItems": [
"Snow Spikes"
]
},
{
"fromId": "king_nole_labyrinth_path_to_palace",
"toId": "king_nole_labyrinth_post_door"
},
{
"fromId": "king_nole_labyrinth_post_door",
"toId": "king_nole_labyrinth_sacred_tree",
"requiredItems": [
"Axe Magic"
],
"requiredNodes": [
"king_nole_labyrinth_raft_entrance"
]
},
{
"fromId": "king_nole_labyrinth_path_to_palace",
"toId": "king_nole_palace",
"twoWay": True
},
{
"fromId": "king_nole_palace",
"toId": "end",
"requiredItems": [
"Gola's Fang",
"Gola's Horn",
"Gola's Nail"
]
}
]

View File

@ -0,0 +1,299 @@
WORLD_REGIONS_JSON = [
{
"name": "Massan",
"hintName": "in the village of Massan",
"nodeIds": [
"massan",
"massan_after_swamp_shrine"
]
},
{
"name": "Massan Cave",
"hintName": "in the cave near Massan",
"nodeIds": [
"massan_cave"
],
"darkMapIds": [
803, 804, 805, 806, 807
]
},
{
"name": "Route between Massan and Gumi",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_massan_gumi"
]
},
{
"name": "Waterfall Shrine",
"hintName": "in the waterfall shrine",
"nodeIds": [
"waterfall_shrine"
],
"darkMapIds": [
174, 175, 176, 177, 178, 179, 180, 181, 182
]
},
{
"name": "Swamp Shrine",
"hintName": "in the swamp shrine",
"canBeHintedAsRequired": False,
"nodeIds": [
"swamp_shrine"
],
"darkMapIds": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 30
]
},
{
"name": "Gumi",
"hintName": "in the village of Gumi",
"nodeIds": [
"gumi",
"gumi_after_swamp_shrine"
]
},
{
"name": "Route between Gumi and Ryuma",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_gumi_ryuma"
]
},
{
"name": "Tibor",
"hintName": "inside Tibor",
"nodeIds": [
"tibor"
],
"darkMapIds": [
808, 809, 810, 811, 812, 813, 814, 815
]
},
{
"name": "Ryuma",
"hintName": "in the town of Ryuma",
"nodeIds": [
"ryuma",
"ryuma_after_thieves_hideout",
"ryuma_lighthouse_repaired"
]
},
{
"name": "Thieves Hideout",
"hintName": "in the thieves' hideout",
"nodeIds": [
"thieves_hideout_pre_key",
"thieves_hideout_post_key"
],
"darkMapIds": [
185, 186, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,
203, 204, 205, 206, 207, 208, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222
]
},
{
"name": "Witch Helga's Hut",
"hintName": "in witch Helga's hut",
"nodeIds": [
"helga_hut"
]
},
{
"name": "Mercator",
"hintName": "in the town of Mercator",
"nodeIds": [
"mercator",
"mercator_repaired_docks",
"mercator_casino",
"mercator_special_shop"
]
},
{
"name": "Crypt",
"hintName": "in the crypt of Mercator",
"nodeIds": [
"crypt"
],
"darkMapIds": [
646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659
]
},
{
"name": "Mercator Dungeon",
"hintName": "in the dungeon of Mercator",
"nodeIds": [
"mercator_dungeon"
],
"darkMapIds": [
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 76, 80, 81, 82, 91, 92
]
},
{
"name": "Mir Tower sector",
"hintName": "near Mir Tower",
"canBeHintedAsRequired": False,
"nodeIds": [
"mir_tower_sector",
"mir_tower_sector_tree_ledge",
"mir_tower_sector_tree_coast",
"twinkle_village"
]
},
{
"name": "Mir Tower",
"hintName": "inside Mir Tower",
"canBeHintedAsRequired": False,
"nodeIds": [
"mir_tower_pre_garlic",
"mir_tower_post_garlic"
],
"darkMapIds": [
750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766,
767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784
]
},
{
"name": "Greenmaze",
"hintName": "in Greenmaze",
"nodeIds": [
"greenmaze_pre_whistle",
"greenmaze_post_whistle"
]
},
{
"name": "Verla Shore",
"canBeHintedAsRequired": False,
"nodeIds": [
"verla_shore",
"verla_shore_cliff"
]
},
{
"name": "Verla",
"hintName": "in the town of Verla",
"nodeIds": [
"verla",
"verla_after_mines"
]
},
{
"name": "Verla Mines",
"hintName": "in the mines near Verla",
"nodeIds": [
"verla_mines",
"verla_mines_behind_lava"
],
"darkMapIds": [
227, 228, 229, 230, 231, 232, 233, 234, 235, 237, 239, 240, 241, 242, 243, 244, 246,
247, 248, 250, 253, 254, 255, 256, 258, 259, 266, 268, 269, 471
]
},
{
"name": "Route between Verla and Destel",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_verla_destel"
]
},
{
"name": "Destel",
"hintName": "in the village of Destel",
"nodeIds": [
"destel"
]
},
{
"name": "Route after Destel",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_after_destel"
]
},
{
"name": "Destel Well",
"hintName": "in Destel well",
"nodeIds": [
"destel_well"
],
"darkMapIds": [
270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290
]
},
{
"name": "Route to Lake Shrine",
"canBeHintedAsRequired": False,
"nodeIds": [
"route_lake_shrine",
"route_lake_shrine_cliff"
]
},
{
"name": "Lake Shrine",
"hintName": "in the lake shrine",
"nodeIds": [
"lake_shrine"
],
"darkMapIds": [
291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306,
307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322,
323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354
]
},
{
"name": "Mountainous Area",
"hintName": "in the mountainous area",
"nodeIds": [
"mountainous_area"
]
},
{
"name": "King Nole's Cave",
"hintName": "in King Nole's cave",
"nodeIds": [
"king_nole_cave"
],
"darkMapIds": [
145, 147, 150, 152, 154, 155, 156, 158, 160, 161, 162, 164, 166, 170, 171, 172
]
},
{
"name": "Kazalt",
"hintName": "in the hidden town of Kazalt",
"nodeIds": [
"kazalt"
]
},
{
"name": "King Nole's Labyrinth",
"hintName": "in King Nole's labyrinth",
"nodeIds": [
"king_nole_labyrinth_pre_door",
"king_nole_labyrinth_post_door",
"king_nole_labyrinth_exterior",
"king_nole_labyrinth_fall_from_exterior",
"king_nole_labyrinth_path_to_palace",
"king_nole_labyrinth_raft_entrance",
"king_nole_labyrinth_raft",
"king_nole_labyrinth_sacred_tree"
],
"darkMapIds": [
355, 356, 357, 358, 359, 360, 361, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372,
373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389,
390, 391, 392, 393, 394, 395, 396, 397, 398, 405, 406, 408, 409, 410, 411, 412, 413,
414, 415, 416, 417, 418, 419, 420, 422, 423
]
},
{
"name": "King Nole's Palace",
"hintName": "in King Nole's palace",
"nodeIds": [
"king_nole_palace",
"end"
],
"darkMapIds": [
115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130,
131, 132, 133, 134, 135, 136, 137, 138
]
}
]

View File

@ -0,0 +1,62 @@
WORLD_TELEPORT_TREES_JSON = [
[
{
"name": "Massan tree",
"treeMapId": 512,
"nodeId": "route_massan_gumi"
},
{
"name": "Tibor tree",
"treeMapId": 534,
"nodeId": "route_gumi_ryuma"
}
],
[
{
"name": "Mercator front gate tree",
"treeMapId": 539,
"nodeId": "route_gumi_ryuma"
},
{
"name": "Verla shore tree",
"treeMapId": 537,
"nodeId": "verla_shore"
}
],
[
{
"name": "Destel sector tree",
"treeMapId": 536,
"nodeId": "route_after_destel"
},
{
"name": "Lake Shrine sector tree",
"treeMapId": 513,
"nodeId": "route_lake_shrine"
}
],
[
{
"name": "Mir Tower sector tree",
"treeMapId": 538,
"nodeId": "mir_tower_sector"
},
{
"name": "Mountainous area tree",
"treeMapId": 535,
"nodeId": "mountainous_area"
}
],
[
{
"name": "Greenmaze entrance tree",
"treeMapId": 510,
"nodeId": "greenmaze_pre_whistle"
},
{
"name": "Greenmaze end tree",
"treeMapId": 511,
"nodeId": "greenmaze_post_whistle"
}
]
]

View File

@ -0,0 +1,60 @@
# Landstalker: The Treasures of King Nole
## Where is the settings page?
The [player settings page for this game](../player-settings) contains most of the options you need to
configure and export a config file.
## What does randomization do to this game?
All items are shuffled while keeping a logic to make every seed completable.
Some key items could be obtained in a very different order compared to the vanilla game, leading to very unusual situations.
The world is made as open as possible while keeping the original locks behind the same items & triggers as vanilla
when that makes sense logic-wise. This puts the emphasis on exploration and gameplay by removing all the scenario
and story-related triggers, giving a wide open world to explore.
## What items and locations get shuffled?
All items and locations are shuffled. This includes **chests**, items on **ground**, in **shops**, and given by **NPCs**.
It's also worth noting that all of these items are shuffled among all worlds, meaning every item can be sent to you
by other players.
## What are the main differences compared to the vanilla game?
The **Key** is now a unique item and can open several doors without being consumed, making it a standard progression item.
All key doors are gone, except three of them :
- the Mercator castle backdoor (giving access to Greenmaze sector)
- Thieves Hideout middle door (cutting the level in half)
- King Nole's Labyrinth door near entrance
---
The secondary shop of Mercator requiring to do the traders sidequest in the original game is now unlocked by having
**Buyer Card** in your inventory.
You will need as many **jewels** as specified in the settings to use the teleporter to go to Kazalt and the final dungeon.
If you find and use the **Lithograph**, it will tell you in which world are each one of your jewels.
Each seed, there is a random dungeon which is chosen to be the "dark dungeon" where you won't see anything unless you
have the **Lantern** in your inventory. Unlike vanilla, King Nole's Labyrinth no longer has the few dark rooms the lantern
was originally intended for.
The **Statue of Jypta** is introduced as a real item (instead of just being an intro gimmick) and gives you gold over
time while you're walking, the same way Healing Boots heal you when you walk.
## What do I need to know for my first seed?
It's advised you keep Massan as your starting region for your first seed, since taking another starting region might
be significantly harder, both combat-wise and logic-wise.
Having fully open & shuffled teleportation trees is an interesting way to play, but is discouraged for beginners
as well since it can force you to go in late-game zones with few Life Stocks.
Overall, the default settings are good for a beginner-friendly seed, and if you don't feel too confident, you can also
lower the combat difficulty to make it more forgiving.
*Have fun on your adventure!*

View File

@ -0,0 +1,119 @@
# Landstalker Setup Guide
## Required Software
- [Landstalker Archipelago Client](https://github.com/Dinopony/randstalker-archipelago/releases) (only available on Windows)
- A compatible emulator to run the game
- [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core
- [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core
- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/))
## Installation Instructions
- Unzip the Landstalker Archipelago Client archive into its own folder
- Put your Landstalker ROM (`LandStalker_USA.SGD` on the Steam release) inside this folder
- To launch the client, launch `randstalker_archipelago.exe` inside that folder
Be aware that you might get antivirus warnings about the client program because one of its main features is to spy
on another process's memory (your emulator). This is something antiviruses obviously dislike, and sometimes mistake
for malicious software.
If you're not trusting the program, you can check its [source code](https://github.com/Dinopony/randstalker-archipelago/)
or test it on a service like Virustotal.
## Create a Config (.yaml) File
### What is a config file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
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.
## How-to-play
### Connecting to the Archipelago Server
Once the game has been created, you need to connect to the server using the Landstalker Archipelago Client.
To do so, run `randstalker_archipelago.exe` inside the folder you created while installing the software.
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 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.
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 find the cause of the issue.
### ROM Generation
When you connected to the Archipelago server, the client fetched all the required data from the server to be able to
build a randomized ROM.
You should see a window with settings to fill:
- **Input ROM file**: This is the path to your original ROM file for the game. If you are using the Steam release ROM
and placed it inside the client's folder as mentioned above, you don't need to change anything.
- **Output ROM directory**: This is where the randomized ROMs will be put. No need to change this unless you want them
to be created in a very specific folder.
![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_rom.png)
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
the same button again.
### Connecting to the emulator
Now that you're connected to the Archipelago server and have a randomized ROM, all we need is to get the client
connected to the emulator. This way, the client will be able to see what's happening while you play and give you in-game
the items you have received from other players.
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.
The easiest way to do so is to:
- open the emu of your choice
- if you're using Retroarch and it's your first time, download the Genesis Plus GX core through Retroarch user interface
- click the `Show ROM file in explorer` button
- drag-and-drop the shown ROM file on the emulator window
- press Start to reach file select screen (to ensure game RAM is properly set-up)
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`)
- 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! 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.
If you don't know all checks in the game, don't be afraid: you can click the `Where is it?` button that will show
you a screenshot of where the location actually is.
![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_client.png)
Have fun!

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB