Skip to content

Commit

Permalink
Landstalker: implement new game (ArchipelagoMW#1808)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Demarcy <[email protected]>
Co-authored-by: Phar <[email protected]>
  • Loading branch information
3 people authored and Jouramie committed Feb 28, 2024
1 parent 8e9d6e6 commit be9d75f
Show file tree
Hide file tree
Showing 21 changed files with 6,447 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike

# Landstalker: The Treasures of King Nole
/worlds/landstalker/ @Dinopony

# Lingo
/worlds/lingo/ @hatkirby

Expand Down
140 changes: 140 additions & 0 deletions worlds/landstalker/Hints.py
Original file line number Diff line number Diff line change
@@ -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 changes: 105 additions & 0 deletions worlds/landstalker/Items.py
Original file line number Diff line number Diff line change
@@ -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()}
53 changes: 53 additions & 0 deletions worlds/landstalker/Locations.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit be9d75f

Please sign in to comment.