Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Landstalker: implement new game #1808

Merged
merged 59 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
443fbd3
Added initial support for Landstalker
Dinopony Apr 16, 2023
971694d
Added support for remote items on ground
Dinopony Apr 23, 2023
0ccc0f5
Merge branch 'ArchipelagoMW:main' into main
Dinopony Apr 23, 2023
c86b0a0
Landstalker: Fixed shop location IDs being too high for game engine
Dinopony Apr 24, 2023
1947c14
Landstalker: Added dark region forwarding to client
Dinopony Apr 24, 2023
885abd5
Landstalker: Added support for settings related to teleportation trees
Dinopony Apr 24, 2023
3fc1499
Landstalker: Prevent duplicate items in shops
Dinopony Apr 24, 2023
e2e021d
Landstalker: Added handling of NPC reward IDs
Dinopony Apr 25, 2023
7a51692
Landstalker: Improved progressive armor handling
Dinopony Apr 28, 2023
c94613b
Landstalker: Improved location names (shorter/clearer)
Dinopony Apr 28, 2023
9e1575e
Merge branch 'ArchipelagoMW:main' into main
Dinopony Apr 28, 2023
34f8906
Landstalker: Added support for remote items in shops
Dinopony Apr 29, 2023
0420e3b
Landstalker: Added two in-game hints
Dinopony Apr 29, 2023
1b21918
Landstalker: Updated docs
Dinopony May 1, 2023
816e433
Landstalker: Various improvements
Dinopony May 1, 2023
25cbe3b
Landstalker: Added missing options
Dinopony May 1, 2023
e9ef3a4
Landstalker: Changed item pool depending on starting location
Dinopony May 1, 2023
1b66a6b
Landstalker: Added an option to enforce healing items in shops
Dinopony May 1, 2023
d1f467e
Landstalker: Added an alternative shorter goal
Dinopony May 1, 2023
47759a4
Landstalker: Optimized generation times
Dinopony May 4, 2023
a9e977e
Landstalker: Fixed a problem with item pool size
Dinopony May 4, 2023
5fa5102
Landstalker: Added a new "Beat Dark Nole" option
Dinopony May 4, 2023
a5deb47
Landstalker: Fixed dynamic prices not always working
Dinopony May 5, 2023
82b0ba9
Landstalker: Fixes to make all tests pass
Dinopony May 5, 2023
f199b67
Landstalker: Improved items pricing
Dinopony May 5, 2023
dfcb9e8
Merge branch 'ArchipelagoMW:main' into main
Dinopony May 8, 2023
e2f6380
Landstalker: Small logic adjustment
Dinopony May 11, 2023
cff41e2
Landstalker: Updated docs
Dinopony May 11, 2023
be4466d
Landstalker: Fixed Life Stock all being progression
Dinopony May 17, 2023
70f53bf
Landstalker: Added in-game hints
Dinopony May 17, 2023
4ffbb79
Landstalker: Improved setup guide
Dinopony May 22, 2023
909b26b
Merge branch 'main' into main
Dinopony May 22, 2023
8e2000f
Merge branch 'ArchipelagoMW:main' into main
Dinopony May 28, 2023
6eec3a1
Landstalker: Fixed impossible gen for 1p seeds
Dinopony May 28, 2023
0b41b71
Merge branch 'main' into main
Dinopony May 31, 2023
e8a900e
Merge branch 'ArchipelagoMW:main' into main
Dinopony Jul 4, 2023
d7626c1
Landstalker: Removed dummy test
Dinopony Jul 4, 2023
46141e2
Merge branch 'ArchipelagoMW:main' into main
Dinopony Aug 7, 2023
22e1832
Landstalker: First batch of fixes after code review
ademarcy Aug 11, 2023
71c14ae
Merge branch 'ArchipelagoMW:main' into main
Dinopony Aug 11, 2023
3684042
Merge branch 'ArchipelagoMW:main' into main
Dinopony Sep 9, 2023
0cdac77
Landstalker: Lightened slot data
Dinopony Sep 9, 2023
d6a28a0
Landstalker: Updated setup guide to reflect new Bizhawk support
Dinopony Sep 26, 2023
86bc405
Landstalker: fixed an extremely rare softlock cause
Dinopony Oct 2, 2023
c4d146c
Landstalker: Added support for 6+ jewels
Dinopony Oct 2, 2023
e33774a
Merge branch 'ArchipelagoMW:main' into main
Dinopony Oct 2, 2023
ada1ab9
Landstalker: Removed hints about a specific non-precious progression …
Dinopony Oct 5, 2023
4f9e99a
Landstalker: Optimized hint generation
Dinopony Oct 5, 2023
f8593c6
Landstalker: Added a mention of a specific version of Bizhawk in the …
Dinopony Oct 10, 2023
f351da6
Merge branch 'ArchipelagoMW:main' into main
Dinopony Nov 22, 2023
d0fedd5
Landstalker: Added mentions in README.md and CODEOWNERS
Dinopony Nov 22, 2023
ecdef81
Landstalker: Refactor for PR Review
ThePhar Nov 23, 2023
e0c5e53
Landstalker: More quotes
ThePhar Nov 23, 2023
e9b0ef6
Landstalker: Move hint generation to fill_slot_data as there's no out…
ThePhar Nov 23, 2023
fbe5c78
Landstalker: Ensure name consistency with game title.
ThePhar Nov 23, 2023
b9dcbf8
Landstalker: Cache spheres.
ThePhar Nov 23, 2023
41421f2
Merge pull request #1 from ThePhar/dinopony_main
Dinopony Nov 24, 2023
89f7774
Landstalker: Removed unnecessary "topology_present" flag
Dinopony Nov 24, 2023
66706af
Merge branch 'main' into main
ThePhar Nov 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions worlds/landstalker/Hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import random
from typing import List
from BaseClasses import MultiWorld, Location
from . import LandstalkerItem
from .data.hint_source import HINT_SOURCES_JSON


def generate_blurry_location_hint(location: Location, random: random.Random):
cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in '(),:'})
cleaned_location_name.replace('-', ' ')
cleaned_location_name.replace('/', ' ')
cleaned_location_name.replace('.', ' ')
location_name_words = [w for w in cleaned_location_name.split(' ') if len(w) > 3]

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(multiworld: MultiWorld, player: int, jewel_items: List[LandstalkerItem]):
hint_text = "It's barely readable:\n"

for item in jewel_items:
# Jewel hints are composed of 4 'words' shuffled randomly:
# - the name of the player whose world contains said jewel (if not ours)
# - the color of the jewel (if relevant)
# - two random words from the location name
words = generate_blurry_location_hint(item.location, multiworld.per_slot_randoms[player])
words[0] = words[0].upper()
words[1] = words[1].upper()
if len(jewel_items) < 6:
# Add jewel color if we are not using generic jewels because jewel count is 6 or more
words.append(item.name.split(' ')[0].upper())
if item.location.player != player:
# Add player name if it's not in our own world
words.append(multiworld.get_player_name(item.location.player).upper())
multiworld.per_slot_randoms[player].shuffle(words)
hint_text += " ".join(words) + "\n"
return hint_text.rstrip('\n')


def generate_random_hints(multiworld: MultiWorld, this_player: int):
hints = {}
hint_texts = []
random = multiworld.per_slot_randoms[this_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':
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}.")
Dinopony marked this conversation as resolved.
Show resolved Hide resolved
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 = multiworld.hint_count[this_player].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
108 changes: 108 additions & 0 deletions worlds/landstalker/Items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Dict, NamedTuple, List
from BaseClasses import Item, ItemClassification

BASE_ITEM_ID = 4000


class LandstalkerItem(Item):
game: str = "Landstalker"
price_in_shops: int


class LandstalkerItemData(NamedTuple):
id: int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this id could be dropped entirely if you referenced your datapackage on item creation, for the ID, and enumerated in your build function at the bottom here. Messenger does exactly this if you want an example. Obviously not necessary, just reduces some data pollution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an expert on how datapackage is handled internally, I'm not against that change but I wouldn't know how to proceed to do so. I didn't find anything related to this in Messenger, I probably didn't look well :)

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(0, data.quantity)]
return weighted_item_names


def build_item_name_to_id_table():
item_name_to_id_table = {}
for name, data in item_table.items():
item_name_to_id_table[name] = data.id + BASE_ITEM_ID
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm i think random.choices, using those numbers as weights would be better. This is also done in Messenger lol.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand what would any method from the Random class accomplish here, since we are just building a simple matching table.

The way you do it in messenger is as follows :

    item_name_to_id = {item: item_id for item_id, item in enumerate(ALL_ITEMS, base_offset)}

return item_name_to_id_table

51 changes: 51 additions & 0 deletions worlds/landstalker/Locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Dict, Optional
from BaseClasses import Location, Region
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"
type_string: str
price: int = 0

def __init__(self, player: int, name: str, location_id: Optional[int], region: Region, type_string: str):
super().__init__(player, name, location_id, region)
self.type_string = type_string


def create_locations(player: int, regions_table: Dict[str, Region], name_to_id_table: Dict[str, int]):
# 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