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:
parent
2ccf11f3d7
commit
d46e68cb5f
|
@ -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
|
||||
|
|
|
@ -67,6 +67,9 @@
|
|||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
# Landstalker: The Treasures of King Nole
|
||||
/worlds/landstalker/ @Dinopony
|
||||
|
||||
# Lingo
|
||||
/worlds/lingo/ @hatkirby
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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()}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
|
@ -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!*
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 |
Loading…
Reference in New Issue