-
Notifications
You must be signed in to change notification settings - Fork 723
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Landstalker: implement new game (#1808)
Co-authored-by: Anthony Demarcy <[email protected]> Co-authored-by: Phar <[email protected]>
- Loading branch information
1 parent
2ccf11f
commit d46e68c
Showing
21 changed files
with
6,447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.