mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
TLoZ: Implementing The Legend of Zelda (#1354)
Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
This commit is contained in:
145
worlds/tloz/ItemPool.py
Normal file
145
worlds/tloz/ItemPool.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations
|
||||
|
||||
# Swords are in starting_weapons
|
||||
overworld_items = {
|
||||
"Letter": 1,
|
||||
"Power Bracelet": 1,
|
||||
"Heart Container": 1,
|
||||
"Sword": 1
|
||||
}
|
||||
|
||||
# Bomb, Arrow, 1 Small Key and Red Water of Life are in guaranteed_shop_items
|
||||
shop_items = {
|
||||
"Magical Shield": 3,
|
||||
"Food": 2,
|
||||
"Small Key": 1,
|
||||
"Candle": 1,
|
||||
"Recovery Heart": 1,
|
||||
"Blue Ring": 1,
|
||||
"Water of Life (Blue)": 1
|
||||
}
|
||||
|
||||
# Magical Rod and Red Candle are in starting_weapons, Triforce Fragments are added in its section of get_pool_core
|
||||
major_dungeon_items = {
|
||||
"Heart Container": 8,
|
||||
"Bow": 1,
|
||||
"Boomerang": 1,
|
||||
"Magical Boomerang": 1,
|
||||
"Raft": 1,
|
||||
"Stepladder": 1,
|
||||
"Recorder": 1,
|
||||
"Magical Key": 1,
|
||||
"Book of Magic": 1,
|
||||
"Silver Arrow": 1,
|
||||
"Red Ring": 1
|
||||
}
|
||||
|
||||
minor_dungeon_items = {
|
||||
"Bomb": 23,
|
||||
"Small Key": 45,
|
||||
"Five Rupees": 17
|
||||
}
|
||||
|
||||
take_any_items = {
|
||||
"Heart Container": 4
|
||||
}
|
||||
|
||||
# Map/Compasses: 18
|
||||
# Reasoning: Adding some variety to the vanilla game.
|
||||
|
||||
map_compass_replacements = {
|
||||
"Fairy": 6,
|
||||
"Clock": 3,
|
||||
"Water of Life (Red)": 1,
|
||||
"Water of Life (Blue)": 2,
|
||||
"Bomb": 2,
|
||||
"Small Key": 2,
|
||||
"Five Rupees": 2
|
||||
}
|
||||
basic_pool = {
|
||||
item: overworld_items.get(item, 0) + shop_items.get(item, 0)
|
||||
+ major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0)
|
||||
for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements)
|
||||
}
|
||||
|
||||
starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"]
|
||||
guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"]
|
||||
starting_weapon_locations = ["Starting Sword Cave", "Letter Cave", "Armos Knights"]
|
||||
dangerous_weapon_locations = [
|
||||
"Level 1 Compass", "Level 2 Bomb Drop (Keese)", "Level 3 Key Drop (Zols Entrance)", "Level 3 Compass"]
|
||||
|
||||
def generate_itempool(tlozworld):
|
||||
(pool, placed_items) = get_pool_core(tlozworld)
|
||||
tlozworld.multiworld.itempool.extend([tlozworld.multiworld.create_item(item, tlozworld.player) for item in pool])
|
||||
for (location_name, item) in placed_items.items():
|
||||
location = tlozworld.multiworld.get_location(location_name, tlozworld.player)
|
||||
location.place_locked_item(tlozworld.multiworld.create_item(item, tlozworld.player))
|
||||
if item == "Bomb":
|
||||
location.item.classification = ItemClassification.progression
|
||||
|
||||
def get_pool_core(world):
|
||||
random = world.multiworld.random
|
||||
|
||||
pool = []
|
||||
placed_items = {}
|
||||
minor_items = dict(minor_dungeon_items)
|
||||
|
||||
# Guaranteed Shop Items
|
||||
reserved_store_slots = random.sample(shop_locations[0:9], 4)
|
||||
for location, item in zip(reserved_store_slots, guaranteed_shop_items):
|
||||
placed_items[location] = item
|
||||
|
||||
# Starting Weapon
|
||||
starting_weapon = random.choice(starting_weapons)
|
||||
if world.multiworld.StartingPosition[world.player] == 0:
|
||||
placed_items[starting_weapon_locations[0]] = starting_weapon
|
||||
elif world.multiworld.StartingPosition[world.player] in [1, 2]:
|
||||
if world.multiworld.StartingPosition[world.player] == 2:
|
||||
for location in dangerous_weapon_locations:
|
||||
if world.multiworld.ExpandedPool[world.player] or "Drop" not in location:
|
||||
starting_weapon_locations.append(location)
|
||||
placed_items[random.choice(starting_weapon_locations)] = starting_weapon
|
||||
else:
|
||||
pool.append(starting_weapon)
|
||||
for other_weapons in starting_weapons:
|
||||
if other_weapons != starting_weapon:
|
||||
pool.append(other_weapons)
|
||||
|
||||
# Triforce Fragments
|
||||
fragment = "Triforce Fragment"
|
||||
if world.multiworld.ExpandedPool[world.player]:
|
||||
possible_level_locations = [location for location in all_level_locations
|
||||
if location not in level_locations[8]]
|
||||
else:
|
||||
possible_level_locations = [location for location in standard_level_locations
|
||||
if location not in level_locations[8]]
|
||||
for level in range(1, 9):
|
||||
if world.multiworld.TriforceLocations[world.player] == 0:
|
||||
placed_items[f"Level {level} Triforce"] = fragment
|
||||
elif world.multiworld.TriforceLocations[world.player] == 1:
|
||||
placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment
|
||||
else:
|
||||
pool.append(fragment)
|
||||
|
||||
# Level 9 junk fill
|
||||
if world.multiworld.ExpandedPool[world.player] > 0:
|
||||
spots = random.sample(level_locations[8], len(level_locations[8]) // 2)
|
||||
for spot in spots:
|
||||
junk = random.choice(list(minor_items.keys()))
|
||||
placed_items[spot] = junk
|
||||
minor_items[junk] -= 1
|
||||
|
||||
# Finish Pool
|
||||
final_pool = basic_pool
|
||||
if world.multiworld.ExpandedPool[world.player]:
|
||||
final_pool = {
|
||||
item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0)
|
||||
for item in set(basic_pool) | set(minor_items) | set(take_any_items)
|
||||
}
|
||||
final_pool["Five Rupees"] -= 1
|
||||
for item in final_pool.keys():
|
||||
for i in range(0, final_pool[item]):
|
||||
pool.append(item)
|
||||
|
||||
return pool, placed_items
|
147
worlds/tloz/Items.py
Normal file
147
worlds/tloz/Items.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from BaseClasses import ItemClassification
|
||||
import typing
|
||||
from typing import Dict
|
||||
|
||||
progression = ItemClassification.progression
|
||||
filler = ItemClassification.filler
|
||||
useful = ItemClassification.useful
|
||||
trap = ItemClassification.trap
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
classification: ItemClassification
|
||||
|
||||
|
||||
item_table: Dict[str, ItemData] = {
|
||||
"Boomerang": ItemData(100, useful),
|
||||
"Bow": ItemData(101, progression),
|
||||
"Magical Boomerang": ItemData(102, useful),
|
||||
"Raft": ItemData(103, progression),
|
||||
"Stepladder": ItemData(104, progression),
|
||||
"Recorder": ItemData(105, progression),
|
||||
"Magical Rod": ItemData(106, progression),
|
||||
"Red Candle": ItemData(107, progression),
|
||||
"Book of Magic": ItemData(108, progression),
|
||||
"Magical Key": ItemData(109, useful),
|
||||
"Red Ring": ItemData(110, useful),
|
||||
"Silver Arrow": ItemData(111, progression),
|
||||
"Sword": ItemData(112, progression),
|
||||
"White Sword": ItemData(113, progression),
|
||||
"Magical Sword": ItemData(114, progression),
|
||||
"Heart Container": ItemData(115, progression),
|
||||
"Letter": ItemData(116, progression),
|
||||
"Magical Shield": ItemData(117, useful),
|
||||
"Candle": ItemData(118, progression),
|
||||
"Arrow": ItemData(119, progression),
|
||||
"Food": ItemData(120, progression),
|
||||
"Water of Life (Blue)": ItemData(121, useful),
|
||||
"Water of Life (Red)": ItemData(122, useful),
|
||||
"Blue Ring": ItemData(123, useful),
|
||||
"Triforce Fragment": ItemData(124, progression),
|
||||
"Power Bracelet": ItemData(125, useful),
|
||||
"Small Key": ItemData(126, filler),
|
||||
"Bomb": ItemData(127, filler),
|
||||
"Recovery Heart": ItemData(128, filler),
|
||||
"Five Rupees": ItemData(129, filler),
|
||||
"Rupee": ItemData(130, filler),
|
||||
"Clock": ItemData(131, filler),
|
||||
"Fairy": ItemData(132, filler)
|
||||
|
||||
}
|
||||
|
||||
item_game_ids = {
|
||||
"Bomb": 0x00,
|
||||
"Sword": 0x01,
|
||||
"White Sword": 0x02,
|
||||
"Magical Sword": 0x03,
|
||||
"Food": 0x04,
|
||||
"Recorder": 0x05,
|
||||
"Candle": 0x06,
|
||||
"Red Candle": 0x07,
|
||||
"Arrow": 0x08,
|
||||
"Silver Arrow": 0x09,
|
||||
"Bow": 0x0A,
|
||||
"Magical Key": 0x0B,
|
||||
"Raft": 0x0C,
|
||||
"Stepladder": 0x0D,
|
||||
"Five Rupees": 0x0F,
|
||||
"Magical Rod": 0x10,
|
||||
"Book of Magic": 0x11,
|
||||
"Blue Ring": 0x12,
|
||||
"Red Ring": 0x13,
|
||||
"Power Bracelet": 0x14,
|
||||
"Letter": 0x15,
|
||||
"Small Key": 0x19,
|
||||
"Heart Container": 0x1A,
|
||||
"Triforce Fragment": 0x1B,
|
||||
"Magical Shield": 0x1C,
|
||||
"Boomerang": 0x1D,
|
||||
"Magical Boomerang": 0x1E,
|
||||
"Water of Life (Blue)": 0x1F,
|
||||
"Water of Life (Red)": 0x20,
|
||||
"Recovery Heart": 0x22,
|
||||
"Rupee": 0x18,
|
||||
"Clock": 0x21,
|
||||
"Fairy": 0x23
|
||||
}
|
||||
|
||||
# Item prices are going to get a bit of a writeup here, because these are some seemingly arbitrary
|
||||
# design decisions and future contributors may want to know how these were arrived at.
|
||||
|
||||
# First, I based everything off of the Blue Ring. Since the Red Ring is twice as good as the Blue Ring,
|
||||
# logic dictates it should cost twice as much. Since you can't make something cost 500 rupees, the only
|
||||
# solution was to halve the price of the Blue Ring. Correspondingly, everything else sold in shops was
|
||||
# also cut in half.
|
||||
|
||||
# Then, I decided on a factor for swords. Since each sword does double the damage of its predecessor, each
|
||||
# one should be at least double. Since the sword saves so much time when upgraded (as, unlike other items,
|
||||
# you don't need to switch to it), I wanted a bit of a premium on upgrades. Thus, a 4x multiplier was chosen,
|
||||
# allowing the basic Sword to stay cheap while making the Magical Sword be a hefty upgrade you'll
|
||||
# feel the price of.
|
||||
|
||||
# Since arrows do the same amount of damage as the White Sword and silver arrows are the same with the Magical Sword.
|
||||
# they were given corresponding costs.
|
||||
|
||||
# Utility items were based on the prices of the shield, keys, and food. Broadly useful utility items should cost more,
|
||||
# while limited use utility items should cost less. After eyeballing those, a few editorial decisions were made as
|
||||
# deliberate thumbs on the scale of game balance. Those exceptions will be noted below. In general, prices were chosen
|
||||
# based on how a player would feel spending that amount of money as opposed to how useful an item actually is.
|
||||
|
||||
item_prices = {
|
||||
"Bomb": 10,
|
||||
"Sword": 10,
|
||||
"White Sword": 40,
|
||||
"Magical Sword": 160,
|
||||
"Food": 30,
|
||||
"Recorder": 45,
|
||||
"Candle": 30,
|
||||
"Red Candle": 60,
|
||||
"Arrow": 40,
|
||||
"Silver Arrow": 160,
|
||||
"Bow": 40,
|
||||
"Magical Key": 250, # Replacing all small keys commands a high premium
|
||||
"Raft": 80,
|
||||
"Stepladder": 80,
|
||||
"Five Rupees": 255, # This could cost anything above 5 Rupees and be fine, but 255 is the funniest
|
||||
"Magical Rod": 100, # White Sword with forever beams should cost at least more than the White Sword itself
|
||||
"Book of Magic": 60,
|
||||
"Blue Ring": 125,
|
||||
"Red Ring": 250,
|
||||
"Power Bracelet": 25,
|
||||
"Letter": 20,
|
||||
"Small Key": 40,
|
||||
"Heart Container": 80,
|
||||
"Triforce Fragment": 200, # Since I couldn't make Zelda 1 track shop purchases, this is how to discourage repeat
|
||||
# Triforce purchases. The punishment for endless Rupee grinding to avoid searching out
|
||||
# Triforce pieces is that you're doing endless Rupee grinding to avoid playing the game
|
||||
"Magical Shield": 45,
|
||||
"Boomerang": 5,
|
||||
"Magical Boomerang": 20,
|
||||
"Water of Life (Blue)": 20,
|
||||
"Water of Life (Red)": 34,
|
||||
"Recovery Heart": 5,
|
||||
"Rupee": 50,
|
||||
"Clock": 0,
|
||||
"Fairy": 10
|
||||
}
|
350
worlds/tloz/Locations.py
Normal file
350
worlds/tloz/Locations.py
Normal file
@@ -0,0 +1,350 @@
|
||||
from . import Rom
|
||||
|
||||
major_locations = [
|
||||
"Starting Sword Cave",
|
||||
"White Sword Pond",
|
||||
"Magical Sword Grave",
|
||||
"Take Any Item Left",
|
||||
"Take Any Item Middle",
|
||||
"Take Any Item Right",
|
||||
"Armos Knights",
|
||||
"Ocean Heart Container",
|
||||
"Letter Cave",
|
||||
]
|
||||
|
||||
level_locations = [
|
||||
[
|
||||
"Level 1 Item (Bow)", "Level 1 Item (Boomerang)", "Level 1 Map", "Level 1 Compass", "Level 1 Boss",
|
||||
"Level 1 Triforce", "Level 1 Key Drop (Keese Entrance)", "Level 1 Key Drop (Stalfos Middle)",
|
||||
"Level 1 Key Drop (Moblins)", "Level 1 Key Drop (Stalfos Water)",
|
||||
"Level 1 Key Drop (Stalfos Entrance)", "Level 1 Key Drop (Wallmasters)",
|
||||
],
|
||||
[
|
||||
"Level 2 Item (Magical Boomerang)", "Level 2 Map", "Level 2 Compass", "Level 2 Boss", "Level 2 Triforce",
|
||||
"Level 2 Key Drop (Ropes West)", "Level 2 Key Drop (Moldorms)",
|
||||
"Level 2 Key Drop (Ropes Middle)", "Level 2 Key Drop (Ropes Entrance)",
|
||||
"Level 2 Bomb Drop (Keese)", "Level 2 Bomb Drop (Moblins)",
|
||||
"Level 2 Rupee Drop (Gels)",
|
||||
],
|
||||
[
|
||||
"Level 3 Item (Raft)", "Level 3 Map", "Level 3 Compass", "Level 3 Boss", "Level 3 Triforce",
|
||||
"Level 3 Key Drop (Zols and Keese West)", "Level 3 Key Drop (Keese North)",
|
||||
"Level 3 Key Drop (Zols Central)", "Level 3 Key Drop (Zols South)",
|
||||
"Level 3 Key Drop (Zols Entrance)", "Level 3 Bomb Drop (Darknuts West)",
|
||||
"Level 3 Bomb Drop (Keese Corridor)", "Level 3 Bomb Drop (Darknuts Central)",
|
||||
"Level 3 Rupee Drop (Zols and Keese East)"
|
||||
],
|
||||
[
|
||||
"Level 4 Item (Stepladder)", "Level 4 Map", "Level 4 Compass", "Level 4 Boss", "Level 4 Triforce",
|
||||
"Level 4 Key Drop (Keese Entrance)", "Level 4 Key Drop (Keese Central)",
|
||||
"Level 4 Key Drop (Zols)", "Level 4 Key Drop (Keese North)",
|
||||
],
|
||||
[
|
||||
"Level 5 Item (Recorder)", "Level 5 Map", "Level 5 Compass", "Level 5 Boss", "Level 5 Triforce",
|
||||
"Level 5 Key Drop (Keese North)", "Level 5 Key Drop (Gibdos North)",
|
||||
"Level 5 Key Drop (Gibdos Central)", "Level 5 Key Drop (Pols Voice Entrance)",
|
||||
"Level 5 Key Drop (Gibdos Entrance)", "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)",
|
||||
"Level 5 Key Drop (Zols)", "Level 5 Bomb Drop (Gibdos)",
|
||||
"Level 5 Bomb Drop (Dodongos)", "Level 5 Rupee Drop (Zols)",
|
||||
],
|
||||
[
|
||||
"Level 6 Item (Magical Rod)", "Level 6 Map", "Level 6 Compass", "Level 6 Boss", "Level 6 Triforce",
|
||||
"Level 6 Key Drop (Wizzrobes Entrance)", "Level 6 Key Drop (Keese)",
|
||||
"Level 6 Key Drop (Wizzrobes North Island)", "Level 6 Key Drop (Wizzrobes North Stream)",
|
||||
"Level 6 Key Drop (Vires)", "Level 6 Bomb Drop (Wizzrobes)",
|
||||
"Level 6 Rupee Drop (Wizzrobes)"
|
||||
],
|
||||
[
|
||||
"Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Compass", "Level 7 Boss", "Level 7 Triforce",
|
||||
"Level 7 Key Drop (Ropes)", "Level 7 Key Drop (Goriyas)", "Level 7 Key Drop (Stalfos)",
|
||||
"Level 7 Key Drop (Moldorms)", "Level 7 Bomb Drop (Goriyas South)", "Level 7 Bomb Drop (Keese and Spikes)",
|
||||
"Level 7 Bomb Drop (Moldorms South)", "Level 7 Bomb Drop (Moldorms North)",
|
||||
"Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)",
|
||||
"Level 7 Bomb Drop (Digdogger)", "Level 7 Rupee Drop (Goriyas Central)",
|
||||
"Level 7 Rupee Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)",
|
||||
],
|
||||
[
|
||||
"Level 8 Item (Magical Key)", "Level 8 Map", "Level 8 Compass", "Level 8 Item (Book of Magic)", "Level 8 Boss",
|
||||
"Level 8 Triforce", "Level 8 Key Drop (Darknuts West)",
|
||||
"Level 8 Key Drop (Darknuts Far West)", "Level 8 Key Drop (Pols Voice South)",
|
||||
"Level 8 Key Drop (Pols Voice and Keese)", "Level 8 Key Drop (Darknuts Central)",
|
||||
"Level 8 Key Drop (Keese and Zols Entrance)", "Level 8 Bomb Drop (Darknuts North)",
|
||||
"Level 8 Bomb Drop (Darknuts East)", "Level 8 Bomb Drop (Pols Voice North)",
|
||||
"Level 8 Rupee Drop (Manhandla Entrance West)", "Level 8 Rupee Drop (Manhandla Entrance North)",
|
||||
"Level 8 Rupee Drop (Darknuts and Gibdos)",
|
||||
],
|
||||
[
|
||||
"Level 9 Item (Silver Arrow)", "Level 9 Item (Red Ring)",
|
||||
"Level 9 Map", "Level 9 Compass",
|
||||
"Level 9 Key Drop (Patra Southwest)", "Level 9 Key Drop (Like Likes and Zols East)",
|
||||
"Level 9 Key Drop (Wizzrobes and Bubbles East)", "Level 9 Key Drop (Wizzrobes East Island)",
|
||||
"Level 9 Bomb Drop (Blue Lanmolas)", "Level 9 Bomb Drop (Gels Lake)",
|
||||
"Level 9 Bomb Drop (Like Likes and Zols Corridor)", "Level 9 Bomb Drop (Patra Northeast)",
|
||||
"Level 9 Bomb Drop (Vires)", "Level 9 Rupee Drop (Wizzrobes West Island)",
|
||||
"Level 9 Rupee Drop (Red Lanmolas)", "Level 9 Rupee Drop (Keese Southwest)",
|
||||
"Level 9 Rupee Drop (Keese Central Island)", "Level 9 Rupee Drop (Wizzrobes Central)",
|
||||
"Level 9 Rupee Drop (Wizzrobes North Island)", "Level 9 Rupee Drop (Gels East)"
|
||||
]
|
||||
]
|
||||
|
||||
all_level_locations = []
|
||||
for level in level_locations:
|
||||
for location in level:
|
||||
all_level_locations.append(location)
|
||||
|
||||
standard_level_locations = []
|
||||
for level in level_locations:
|
||||
for location in level:
|
||||
if "Drop" not in location:
|
||||
standard_level_locations.append(location)
|
||||
|
||||
shop_locations = [
|
||||
"Arrow Shop Item Left", "Arrow Shop Item Middle", "Arrow Shop Item Right",
|
||||
"Candle Shop Item Left", "Candle Shop Item Middle", "Candle Shop Item Right",
|
||||
"Blue Ring Shop Item Left", "Blue Ring Shop Item Middle", "Blue Ring Shop Item Right",
|
||||
"Shield Shop Item Left", "Shield Shop Item Middle", "Shield Shop Item Right",
|
||||
"Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right"
|
||||
]
|
||||
|
||||
food_locations = [
|
||||
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
|
||||
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
|
||||
"Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)"
|
||||
]
|
||||
|
||||
floor_location_game_offsets_early = {
|
||||
"Level 1 Item (Bow)": 0x7F,
|
||||
"Level 1 Item (Boomerang)": 0x44,
|
||||
"Level 1 Map": 0x43,
|
||||
"Level 1 Compass": 0x54,
|
||||
"Level 1 Boss": 0x35,
|
||||
"Level 1 Triforce": 0x36,
|
||||
"Level 1 Key Drop (Keese Entrance)": 0x72,
|
||||
"Level 1 Key Drop (Moblins)": 0x23,
|
||||
"Level 1 Key Drop (Stalfos Water)": 0x33,
|
||||
"Level 1 Key Drop (Stalfos Entrance)": 0x74,
|
||||
"Level 1 Key Drop (Stalfos Middle)": 0x53,
|
||||
"Level 1 Key Drop (Wallmasters)": 0x45,
|
||||
"Level 2 Item (Magical Boomerang)": 0x4F,
|
||||
"Level 2 Map": 0x5F,
|
||||
"Level 2 Compass": 0x6F,
|
||||
"Level 2 Boss": 0x0E,
|
||||
"Level 2 Triforce": 0x0D,
|
||||
"Level 2 Key Drop (Ropes West)": 0x6C,
|
||||
"Level 2 Key Drop (Moldorms)": 0x3E,
|
||||
"Level 2 Key Drop (Ropes Middle)": 0x4E,
|
||||
"Level 2 Key Drop (Ropes Entrance)": 0x7E,
|
||||
"Level 2 Bomb Drop (Keese)": 0x3F,
|
||||
"Level 2 Bomb Drop (Moblins)": 0x1E,
|
||||
"Level 2 Rupee Drop (Gels)": 0x2F,
|
||||
"Level 3 Item (Raft)": 0x0F,
|
||||
"Level 3 Map": 0x4C,
|
||||
"Level 3 Compass": 0x5A,
|
||||
"Level 3 Boss": 0x4D,
|
||||
"Level 3 Triforce": 0x3D,
|
||||
"Level 3 Key Drop (Zols and Keese West)": 0x49,
|
||||
"Level 3 Key Drop (Keese North)": 0x2A,
|
||||
"Level 3 Key Drop (Zols Central)": 0x4B,
|
||||
"Level 3 Key Drop (Zols South)": 0x6B,
|
||||
"Level 3 Key Drop (Zols Entrance)": 0x7B,
|
||||
"Level 3 Bomb Drop (Darknuts West)": 0x69,
|
||||
"Level 3 Bomb Drop (Keese Corridor)": 0x4A,
|
||||
"Level 3 Bomb Drop (Darknuts Central)": 0x5B,
|
||||
"Level 3 Rupee Drop (Zols and Keese East)": 0x5D,
|
||||
"Level 4 Item (Stepladder)": 0x60,
|
||||
"Level 4 Map": 0x21,
|
||||
"Level 4 Compass": 0x62,
|
||||
"Level 4 Boss": 0x13,
|
||||
"Level 4 Triforce": 0x03,
|
||||
"Level 4 Key Drop (Keese Entrance)": 0x70,
|
||||
"Level 4 Key Drop (Keese Central)": 0x51,
|
||||
"Level 4 Key Drop (Zols)": 0x40,
|
||||
"Level 4 Key Drop (Keese North)": 0x01,
|
||||
"Level 5 Item (Recorder)": 0x04,
|
||||
"Level 5 Map": 0x46,
|
||||
"Level 5 Compass": 0x37,
|
||||
"Level 5 Boss": 0x24,
|
||||
"Level 5 Triforce": 0x14,
|
||||
"Level 5 Key Drop (Keese North)": 0x16,
|
||||
"Level 5 Key Drop (Gibdos North)": 0x26,
|
||||
"Level 5 Key Drop (Gibdos Central)": 0x47,
|
||||
"Level 5 Key Drop (Pols Voice Entrance)": 0x77,
|
||||
"Level 5 Key Drop (Gibdos Entrance)": 0x66,
|
||||
"Level 5 Key Drop (Gibdos, Keese, and Pols Voice)": 0x27,
|
||||
"Level 5 Key Drop (Zols)": 0x55,
|
||||
"Level 5 Bomb Drop (Gibdos)": 0x65,
|
||||
"Level 5 Bomb Drop (Dodongos)": 0x56,
|
||||
"Level 5 Rupee Drop (Zols)": 0x57,
|
||||
"Level 6 Item (Magical Rod)": 0x75,
|
||||
"Level 6 Map": 0x19,
|
||||
"Level 6 Compass": 0x68,
|
||||
"Level 6 Boss": 0x1C,
|
||||
"Level 6 Triforce": 0x0C,
|
||||
"Level 6 Key Drop (Wizzrobes Entrance)": 0x7A,
|
||||
"Level 6 Key Drop (Keese)": 0x58,
|
||||
"Level 6 Key Drop (Wizzrobes North Island)": 0x29,
|
||||
"Level 6 Key Drop (Wizzrobes North Stream)": 0x1A,
|
||||
"Level 6 Key Drop (Vires)": 0x2D,
|
||||
"Level 6 Bomb Drop (Wizzrobes)": 0x3C,
|
||||
"Level 6 Rupee Drop (Wizzrobes)": 0x28
|
||||
}
|
||||
|
||||
floor_location_game_ids_early = {}
|
||||
floor_location_game_ids_late = {}
|
||||
for key, value in floor_location_game_offsets_early.items():
|
||||
floor_location_game_ids_early[key] = value + Rom.first_quest_dungeon_items_early
|
||||
|
||||
floor_location_game_offsets_late = {
|
||||
"Level 7 Item (Red Candle)": 0x4A,
|
||||
"Level 7 Map": 0x18,
|
||||
"Level 7 Compass": 0x5A,
|
||||
"Level 7 Boss": 0x2A,
|
||||
"Level 7 Triforce": 0x2B,
|
||||
"Level 7 Key Drop (Ropes)": 0x78,
|
||||
"Level 7 Key Drop (Goriyas)": 0x0A,
|
||||
"Level 7 Key Drop (Stalfos)": 0x6D,
|
||||
"Level 7 Key Drop (Moldorms)": 0x3A,
|
||||
"Level 7 Bomb Drop (Goriyas South)": 0x69,
|
||||
"Level 7 Bomb Drop (Keese and Spikes)": 0x68,
|
||||
"Level 7 Bomb Drop (Moldorms South)": 0x7A,
|
||||
"Level 7 Bomb Drop (Moldorms North)": 0x0B,
|
||||
"Level 7 Bomb Drop (Goriyas North)": 0x1B,
|
||||
"Level 7 Bomb Drop (Dodongos)": 0x0C,
|
||||
"Level 7 Bomb Drop (Digdogger)": 0x6C,
|
||||
"Level 7 Rupee Drop (Goriyas Central)": 0x38,
|
||||
"Level 7 Rupee Drop (Dodongos)": 0x58,
|
||||
"Level 7 Rupee Drop (Goriyas North)": 0x09,
|
||||
"Level 8 Item (Magical Key)": 0x0F,
|
||||
"Level 8 Item (Book of Magic)": 0x6F,
|
||||
"Level 8 Map": 0x2E,
|
||||
"Level 8 Compass": 0x5F,
|
||||
"Level 8 Boss": 0x3C,
|
||||
"Level 8 Triforce": 0x2C,
|
||||
"Level 8 Key Drop (Darknuts West)": 0x5C,
|
||||
"Level 8 Key Drop (Darknuts Far West)": 0x4B,
|
||||
"Level 8 Key Drop (Pols Voice South)": 0x4C,
|
||||
"Level 8 Key Drop (Pols Voice and Keese)": 0x5D,
|
||||
"Level 8 Key Drop (Darknuts Central)": 0x5E,
|
||||
"Level 8 Key Drop (Keese and Zols Entrance)": 0x7F,
|
||||
"Level 8 Bomb Drop (Darknuts North)": 0x0E,
|
||||
"Level 8 Bomb Drop (Darknuts East)": 0x3F,
|
||||
"Level 8 Bomb Drop (Pols Voice North)": 0x1D,
|
||||
"Level 8 Rupee Drop (Manhandla Entrance West)": 0x7D,
|
||||
"Level 8 Rupee Drop (Manhandla Entrance North)": 0x6E,
|
||||
"Level 8 Rupee Drop (Darknuts and Gibdos)": 0x4E,
|
||||
"Level 9 Item (Silver Arrow)": 0x4F,
|
||||
"Level 9 Item (Red Ring)": 0x00,
|
||||
"Level 9 Map": 0x27,
|
||||
"Level 9 Compass": 0x35,
|
||||
"Level 9 Key Drop (Patra Southwest)": 0x61,
|
||||
"Level 9 Key Drop (Like Likes and Zols East)": 0x56,
|
||||
"Level 9 Key Drop (Wizzrobes and Bubbles East)": 0x47,
|
||||
"Level 9 Key Drop (Wizzrobes East Island)": 0x57,
|
||||
"Level 9 Bomb Drop (Blue Lanmolas)": 0x11,
|
||||
"Level 9 Bomb Drop (Gels Lake)": 0x23,
|
||||
"Level 9 Bomb Drop (Like Likes and Zols Corridor)": 0x25,
|
||||
"Level 9 Bomb Drop (Patra Northeast)": 0x16,
|
||||
"Level 9 Bomb Drop (Vires)": 0x37,
|
||||
"Level 9 Rupee Drop (Wizzrobes West Island)": 0x40,
|
||||
"Level 9 Rupee Drop (Red Lanmolas)": 0x12,
|
||||
"Level 9 Rupee Drop (Keese Southwest)": 0x62,
|
||||
"Level 9 Rupee Drop (Keese Central Island)": 0x34,
|
||||
"Level 9 Rupee Drop (Wizzrobes Central)": 0x44,
|
||||
"Level 9 Rupee Drop (Wizzrobes North Island)": 0x15,
|
||||
"Level 9 Rupee Drop (Gels East)": 0x26
|
||||
}
|
||||
|
||||
for key, value in floor_location_game_offsets_late.items():
|
||||
floor_location_game_ids_late[key] = value + Rom.first_quest_dungeon_items_late
|
||||
|
||||
dungeon_items = {**floor_location_game_ids_early, **floor_location_game_ids_late}
|
||||
|
||||
shop_location_ids = {
|
||||
"Arrow Shop Item Left": 0x18637,
|
||||
"Arrow Shop Item Middle": 0x18638,
|
||||
"Arrow Shop Item Right": 0x18639,
|
||||
"Candle Shop Item Left": 0x1863A,
|
||||
"Candle Shop Item Middle": 0x1863B,
|
||||
"Candle Shop Item Right": 0x1863C,
|
||||
"Shield Shop Item Left": 0x1863D,
|
||||
"Shield Shop Item Middle": 0x1863E,
|
||||
"Shield Shop Item Right": 0x1863F,
|
||||
"Blue Ring Shop Item Left": 0x18640,
|
||||
"Blue Ring Shop Item Middle": 0x18641,
|
||||
"Blue Ring Shop Item Right": 0x18642,
|
||||
"Potion Shop Item Left": 0x1862E,
|
||||
"Potion Shop Item Middle": 0x1862F,
|
||||
"Potion Shop Item Right": 0x18630
|
||||
}
|
||||
|
||||
shop_price_location_ids = {
|
||||
"Arrow Shop Item Left": 0x18673,
|
||||
"Arrow Shop Item Middle": 0x18674,
|
||||
"Arrow Shop Item Right": 0x18675,
|
||||
"Candle Shop Item Left": 0x18676,
|
||||
"Candle Shop Item Middle": 0x18677,
|
||||
"Candle Shop Item Right": 0x18678,
|
||||
"Shield Shop Item Left": 0x18679,
|
||||
"Shield Shop Item Middle": 0x1867A,
|
||||
"Shield Shop Item Right": 0x1867B,
|
||||
"Blue Ring Shop Item Left": 0x1867C,
|
||||
"Blue Ring Shop Item Middle": 0x1867D,
|
||||
"Blue Ring Shop Item Right": 0x1867E,
|
||||
"Potion Shop Item Left": 0x1866A,
|
||||
"Potion Shop Item Middle": 0x1866B,
|
||||
"Potion Shop Item Right": 0x1866C
|
||||
}
|
||||
|
||||
secret_money_ids = {
|
||||
"Secret Money 1": 0x18680,
|
||||
"Secret Money 2": 0x18683,
|
||||
"Secret Money 3": 0x18686
|
||||
}
|
||||
|
||||
major_location_ids = {
|
||||
"Starting Sword Cave": 0x18611,
|
||||
"White Sword Pond": 0x18617,
|
||||
"Magical Sword Grave": 0x1861A,
|
||||
"Letter Cave": 0x18629,
|
||||
"Take Any Item Left": 0x18613,
|
||||
"Take Any Item Middle": 0x18614,
|
||||
"Take Any Item Right": 0x18615,
|
||||
"Armos Knights": 0x10D05,
|
||||
"Ocean Heart Container": 0x1789A
|
||||
}
|
||||
|
||||
major_location_offsets = {
|
||||
"Starting Sword Cave": 0x77,
|
||||
"White Sword Pond": 0x0A,
|
||||
"Magical Sword Grave": 0x21,
|
||||
"Letter Cave": 0x0E,
|
||||
# "Take Any Item Left": 0x7B,
|
||||
# "Take Any Item Middle": 0x2C,
|
||||
# "Take Any Item Right": 0x47,
|
||||
"Armos Knights": 0x24,
|
||||
"Ocean Heart Container": 0x5F
|
||||
}
|
||||
|
||||
overworld_locations = [
|
||||
"Starting Sword Cave",
|
||||
"White Sword Pond",
|
||||
"Magical Sword Grave",
|
||||
"Letter Cave",
|
||||
"Armos Knights",
|
||||
"Ocean Heart Container"
|
||||
]
|
||||
|
||||
underworld1_locations = [*floor_location_game_offsets_early.keys()]
|
||||
|
||||
underworld2_locations = [*floor_location_game_offsets_late.keys()]
|
||||
|
||||
#cave_locations = ["Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"] + [*shop_locations]
|
||||
|
||||
location_table_base = [x for x in major_locations] + \
|
||||
[y for y in all_level_locations] + \
|
||||
[z for z in shop_locations]
|
||||
location_table = {}
|
||||
for i, location in enumerate(location_table_base):
|
||||
location_table[location] = i
|
||||
|
||||
location_ids = {**dungeon_items, **shop_location_ids, **major_location_ids}
|
40
worlds/tloz/Options.py
Normal file
40
worlds/tloz/Options.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Choice
|
||||
|
||||
|
||||
class ExpandedPool(DefaultOnToggle):
|
||||
"""Puts room clear drops into the pool of items and locations."""
|
||||
display_name = "Expanded Item Pool"
|
||||
|
||||
|
||||
class TriforceLocations(Choice):
|
||||
"""Where Triforce fragments can be located. Note that Triforce pieces
|
||||
obtained in a dungeon will heal and warp you out, while overworld Triforce pieces obtained will appear to have
|
||||
no immediate effect. This is normal."""
|
||||
display_name = "Triforce Locations"
|
||||
option_vanilla = 0
|
||||
option_dungeons = 1
|
||||
option_anywhere = 2
|
||||
|
||||
|
||||
class StartingPosition(Choice):
|
||||
"""How easy is the start of the game.
|
||||
Safe means a weapon is guaranteed in Starting Sword Cave.
|
||||
Unsafe means that a weapon is guaranteed between Starting Sword Cave, Letter Cave, and Armos Knight.
|
||||
Dangerous adds these level locations to the unsafe pool (if they exist):
|
||||
# Level 1 Compass, Level 2 Bomb Drop (Keese), Level 3 Key Drop (Zols Entrance), Level 3 Compass
|
||||
Very Dangerous is the same as dangerous except it doesn't guarantee a weapon. It will only mean progression
|
||||
will be there in single player seeds. In multi worlds, however, this means all bets are off and after checking
|
||||
the dangerous spots, you could be stuck until someone sends you a weapon"""
|
||||
display_name = "Starting Position"
|
||||
option_safe = 0
|
||||
option_unsafe = 1
|
||||
option_dangerous = 2
|
||||
option_very_dangerous = 3
|
||||
|
||||
|
||||
tloz_options: typing.Dict[str, type(Option)] = {
|
||||
"ExpandedPool": ExpandedPool,
|
||||
"TriforceLocations": TriforceLocations,
|
||||
"StartingPosition": StartingPosition
|
||||
}
|
78
worlds/tloz/Rom.py
Normal file
78
worlds/tloz/Rom.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import zlib
|
||||
import os
|
||||
|
||||
import Utils
|
||||
from Patch import APDeltaPatch
|
||||
|
||||
NA10CHECKSUM = 'D7AE93DF'
|
||||
ROM_PLAYER_LIMIT = 65535
|
||||
ROM_NAME = 0x10
|
||||
bit_positions = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
|
||||
candle_shop = bit_positions[5]
|
||||
arrow_shop = bit_positions[4]
|
||||
potion_shop = bit_positions[1]
|
||||
shield_shop = bit_positions[6]
|
||||
ring_shop = bit_positions[7]
|
||||
take_any = bit_positions[2]
|
||||
first_quest_dungeon_items_early = 0x18910
|
||||
first_quest_dungeon_items_late = 0x18C10
|
||||
game_mode = 0x12
|
||||
sword = 0x0657
|
||||
bombs = 0x0658
|
||||
arrow = 0x0659
|
||||
bow = 0x065A
|
||||
candle = 0x065B
|
||||
recorder = 0x065C
|
||||
food = 0x065D
|
||||
potion = 0x065E
|
||||
magical_rod = 0x065F
|
||||
raft = 0x0660
|
||||
book_of_magic = 0x0661
|
||||
ring = 0x0662
|
||||
stepladder = 0x0663
|
||||
magical_key = 0x0664
|
||||
power_bracelet = 0x0665
|
||||
letter = 0x0666
|
||||
heart_containers = 0x066F
|
||||
triforce_fragments = 0x0671
|
||||
boomerang = 0x0674
|
||||
magical_boomerang = 0x0675
|
||||
magical_shield = 0x0676
|
||||
rupees_to_add = 0x067D
|
||||
|
||||
|
||||
|
||||
|
||||
class TLoZDeltaPatch(APDeltaPatch):
|
||||
checksum = NA10CHECKSUM
|
||||
hash = NA10CHECKSUM
|
||||
game = "The Legend of Zelda"
|
||||
patch_file_ending = ".aptloz"
|
||||
result_file_ending = ".nes"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
|
||||
|
||||
basechecksum = str(hex(zlib.crc32(base_rom_bytes))).upper()[2:]
|
||||
if NA10CHECKSUM != basechecksum:
|
||||
raise Exception('Supplied Base Rom does not match known CRC-32 for NA (1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["tloz_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
147
worlds/tloz/Rules.py
Normal file
147
worlds/tloz/Rules.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..generic.Rules import add_rule
|
||||
from .Locations import food_locations, shop_locations
|
||||
from .ItemPool import dangerous_weapon_locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TLoZWorld
|
||||
|
||||
def set_rules(tloz_world: "TLoZWorld"):
|
||||
player = tloz_world.player
|
||||
world = tloz_world.multiworld
|
||||
|
||||
# Boss events for a nicer spoiler log play through
|
||||
for level in range(1, 9):
|
||||
boss = world.get_location(f"Level {level} Boss", player)
|
||||
boss_event = world.get_location(f"Level {level} Boss Status", player)
|
||||
status = tloz_world.create_event(f"Boss {level} Defeated")
|
||||
boss_event.place_locked_item(status)
|
||||
add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player))
|
||||
|
||||
# No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons
|
||||
for i, level in enumerate(tloz_world.levels[1:10]):
|
||||
for location in level.locations:
|
||||
if world.StartingPosition[player] < 1 or location.name not in dangerous_weapon_locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has_group("weapons", player))
|
||||
if i > 0: # Don't need an extra heart for Level 1
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state, hearts=i: state.has("Heart Container", player, hearts) or
|
||||
(state.has("Blue Ring", player) and
|
||||
state.has("Heart Container", player, int(hearts / 2))) or
|
||||
(state.has("Red Ring", player) and
|
||||
state.has("Heart Container", player, int(hearts / 4)))
|
||||
|
||||
)
|
||||
# No requiring anything in a shop until we can farm for money
|
||||
for location in shop_locations:
|
||||
add_rule(world.get_location(location, player),
|
||||
lambda state: state.has_group("weapons", player))
|
||||
|
||||
# Everything from 4 on up has dark rooms
|
||||
for level in tloz_world.levels[4:]:
|
||||
for location in level.locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has_group("candles", player)
|
||||
or (state.has("Magical Rod", player) and state.has("Book", player)))
|
||||
|
||||
# Everything from 5 on up has gaps
|
||||
for level in tloz_world.levels[5:]:
|
||||
for location in level.locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
|
||||
add_rule(world.get_location("Level 5 Boss", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
|
||||
add_rule(world.get_location("Level 6 Boss", player),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
|
||||
add_rule(world.get_location("Level 7 Item (Red Candle)", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(world.get_location("Level 7 Boss", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
if world.ExpandedPool[player]:
|
||||
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
|
||||
for location in food_locations:
|
||||
if world.ExpandedPool[player] or "Drop" not in location:
|
||||
add_rule(world.get_location(location, player),
|
||||
lambda state: state.has("Food", player))
|
||||
|
||||
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
if world.ExpandedPool[player]:
|
||||
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
|
||||
for location in tloz_world.levels[9].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Triforce Fragment", player, 8) and
|
||||
state.has_group("swords", player))
|
||||
|
||||
# Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop
|
||||
for level in range(1, 9):
|
||||
add_rule(world.get_location(f"Level {level} Triforce", player),
|
||||
lambda state, l=level: state.has(f"Boss {l} Defeated", player))
|
||||
|
||||
# Sword, raft, and ladder spots
|
||||
add_rule(world.get_location("White Sword Pond", player),
|
||||
lambda state: state.has("Heart Container", player, 2))
|
||||
add_rule(world.get_location("Magical Sword Grave", player),
|
||||
lambda state: state.has("Heart Container", player, 9))
|
||||
|
||||
stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"]
|
||||
stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"]
|
||||
for location in stepladder_locations:
|
||||
add_rule(world.get_location(location, player),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
if world.ExpandedPool[player]:
|
||||
for location in stepladder_locations_expanded:
|
||||
add_rule(world.get_location(location, player),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
|
||||
if world.StartingPosition[player] != 2:
|
||||
# Don't allow Take Any Items until we can actually get in one
|
||||
if world.ExpandedPool[player]:
|
||||
add_rule(world.get_location("Take Any Item Left", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
add_rule(world.get_location("Take Any Item Middle", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
add_rule(world.get_location("Take Any Item Right", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
for location in tloz_world.levels[4].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Raft", player) or state.has("Recorder", player))
|
||||
for location in tloz_world.levels[7].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
for location in tloz_world.levels[8].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Bow", player))
|
||||
|
||||
add_rule(world.get_location("Potion Shop Item Left", player),
|
||||
lambda state: state.has("Letter", player))
|
||||
add_rule(world.get_location("Potion Shop Item Middle", player),
|
||||
lambda state: state.has("Letter", player))
|
||||
add_rule(world.get_location("Potion Shop Item Right", player),
|
||||
lambda state: state.has("Letter", player))
|
||||
|
||||
add_rule(world.get_location("Shield Shop Item Left", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
||||
add_rule(world.get_location("Shield Shop Item Middle", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
||||
add_rule(world.get_location("Shield Shop Item Right", player),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
313
worlds/tloz/__init__.py
Normal file
313
worlds/tloz/__init__.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import pkgutil
|
||||
from typing import NamedTuple, Union, Dict, Any
|
||||
|
||||
import bsdiff4
|
||||
|
||||
import Utils
|
||||
from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassification, Tutorial
|
||||
from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations
|
||||
from .Items import item_table, item_prices, item_game_ids
|
||||
from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \
|
||||
standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations
|
||||
from .Options import tloz_options
|
||||
from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late
|
||||
from .Rules import set_rules
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
|
||||
class TLoZWeb(WebWorld):
|
||||
theme = "stone"
|
||||
setup = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up The Legend of Zelda for Archipelago on your computer.",
|
||||
"English",
|
||||
"multiworld_en.md",
|
||||
"multiworld/en",
|
||||
["Rosalie and Figment"]
|
||||
)
|
||||
|
||||
tutorials = [setup]
|
||||
|
||||
|
||||
class TLoZWorld(World):
|
||||
"""
|
||||
The Legend of Zelda needs almost no introduction. Gather the eight fragments of the
|
||||
Triforce of Courage, enter Death Mountain, defeat Ganon, and rescue Princess Zelda.
|
||||
This randomizer shuffles all the items in the game around, leading to a new adventure
|
||||
every time.
|
||||
"""
|
||||
option_definitions = tloz_options
|
||||
game = "The Legend of Zelda"
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
base_id = 7000
|
||||
web = TLoZWeb()
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = location_table
|
||||
|
||||
item_name_groups = {
|
||||
'weapons': starting_weapons,
|
||||
'swords': {
|
||||
"Sword", "White Sword", "Magical Sword"
|
||||
},
|
||||
"candles": {
|
||||
"Candle", "Red Candle"
|
||||
},
|
||||
"arrows": {
|
||||
"Arrow", "Silver Arrow"
|
||||
}
|
||||
}
|
||||
|
||||
for k, v in item_name_to_id.items():
|
||||
item_name_to_id[k] = v + base_id
|
||||
|
||||
for k, v in location_name_to_id.items():
|
||||
if v is not None:
|
||||
location_name_to_id[k] = v + base_id
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.generator_in_use = threading.Event()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
self.levels = None
|
||||
self.filler_items = None
|
||||
|
||||
def create_item(self, name: str):
|
||||
return TLoZItem(name, item_table[name].classification, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_event(self, event: str):
|
||||
return TLoZItem(event, ItemClassification.progression, None, self.player)
|
||||
|
||||
def create_location(self, name, id, parent, event=False):
|
||||
return_location = TLoZLocation(self.player, name, id, parent)
|
||||
return_location.event = event
|
||||
return return_location
|
||||
|
||||
def create_regions(self):
|
||||
menu = Region("Menu", self.player, self.multiworld)
|
||||
overworld = Region("Overworld", self.player, self.multiworld)
|
||||
self.levels = [None] # Yes I'm making a one-indexed array in a zero-indexed language. I hate me too.
|
||||
for i in range(1, 10):
|
||||
level = Region(f"Level {i}", self.player, self.multiworld)
|
||||
self.levels.append(level)
|
||||
new_entrance = Entrance(self.player, f"Level {i}", overworld)
|
||||
new_entrance.connect(level)
|
||||
overworld.exits.append(new_entrance)
|
||||
self.multiworld.regions.append(level)
|
||||
|
||||
for i, level in enumerate(level_locations):
|
||||
for location in level:
|
||||
if self.multiworld.ExpandedPool[self.player] or "Drop" not in location:
|
||||
self.levels[i + 1].locations.append(
|
||||
self.create_location(location, self.location_name_to_id[location], self.levels[i + 1]))
|
||||
|
||||
for level in range(1, 9):
|
||||
boss_event = self.create_location(f"Level {level} Boss Status", None,
|
||||
self.multiworld.get_region(f"Level {level}", self.player),
|
||||
True)
|
||||
boss_event.show_in_spoiler = False
|
||||
self.levels[level].locations.append(boss_event)
|
||||
|
||||
for location in major_locations:
|
||||
if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location:
|
||||
overworld.locations.append(
|
||||
self.create_location(location, self.location_name_to_id[location], overworld))
|
||||
|
||||
for location in shop_locations:
|
||||
overworld.locations.append(
|
||||
self.create_location(location, self.location_name_to_id[location], overworld))
|
||||
|
||||
ganon = self.create_location("Ganon", None, self.multiworld.get_region("Level 9", self.player))
|
||||
zelda = self.create_location("Zelda", None, self.multiworld.get_region("Level 9", self.player))
|
||||
ganon.show_in_spoiler = False
|
||||
zelda.show_in_spoiler = False
|
||||
self.levels[9].locations.append(ganon)
|
||||
self.levels[9].locations.append(zelda)
|
||||
begin_game = Entrance(self.player, "Begin Game", menu)
|
||||
menu.exits.append(begin_game)
|
||||
begin_game.connect(overworld)
|
||||
self.multiworld.regions.append(menu)
|
||||
self.multiworld.regions.append(overworld)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self):
|
||||
ganon = self.multiworld.get_location("Ganon", self.player)
|
||||
ganon.place_locked_item(self.create_event("Triforce of Power"))
|
||||
add_rule(ganon, lambda state: state.has("Silver Arrow", self.player) and state.has("Bow", self.player))
|
||||
|
||||
self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!"))
|
||||
add_rule(self.multiworld.get_location("Zelda", self.player),
|
||||
lambda state: ganon in state.locations_checked)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player)
|
||||
generate_itempool(self)
|
||||
|
||||
def apply_base_patch(self, rom):
|
||||
# The base patch source is on a different repo, so here's the summary of changes:
|
||||
# Remove Triforce check for recorder, so you can always warp.
|
||||
# Remove level check for Triforce Fragments (and maps and compasses, but this won't matter)
|
||||
# Replace some code with a jump to free space
|
||||
# Check if we're picking up a Triforce Fragment. If so, increment the local count
|
||||
# In either case, we do the instructions we overwrote with the jump and then return to normal flow
|
||||
# Remove map/compass check so they're always on
|
||||
# Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to
|
||||
# go past 0x1F items for dungeon items.
|
||||
base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4"
|
||||
with open(base_patch_location, "rb") as base_patch:
|
||||
rom_data = bsdiff4.patch(rom.read(), base_patch.read())
|
||||
rom_data = bytearray(rom_data)
|
||||
# Set every item to the new nothing value, but keep room flags. Type 2 boss roars should
|
||||
# become type 1 boss roars, so we at least keep the sound of roaring where it should be.
|
||||
for i in range(0, 0x7F):
|
||||
item = rom_data[first_quest_dungeon_items_early + i]
|
||||
if item & 0b00100000:
|
||||
rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111
|
||||
rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000
|
||||
if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing"
|
||||
rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111
|
||||
|
||||
item = rom_data[first_quest_dungeon_items_late + i]
|
||||
if item & 0b00100000:
|
||||
rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111
|
||||
rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000
|
||||
if item & 0b00011111 == 0b00000011:
|
||||
rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111
|
||||
return rom_data
|
||||
|
||||
def apply_randomizer(self):
|
||||
with open(get_base_rom_path(), 'rb') as rom:
|
||||
rom_data = self.apply_base_patch(rom)
|
||||
# Write each location's new data in
|
||||
for location in self.multiworld.get_filled_locations(self.player):
|
||||
# Zelda and Ganon aren't real locations
|
||||
if location.name == "Ganon" or location.name == "Zelda":
|
||||
continue
|
||||
|
||||
# Neither are boss defeat events
|
||||
if "Status" in location.name:
|
||||
continue
|
||||
|
||||
item = location.item.name
|
||||
# Remote items are always going to look like Rupees.
|
||||
if location.item.player != self.player:
|
||||
item = "Rupee"
|
||||
|
||||
item_id = item_game_ids[item]
|
||||
location_id = location_ids[location.name]
|
||||
|
||||
# Shop prices need to be set
|
||||
if location.name in shop_locations:
|
||||
if location.name[-5:] == "Right":
|
||||
# Final item in stores has bit 6 and 7 set. It's what marks the cave a shop.
|
||||
item_id = item_id | 0b11000000
|
||||
price_location = shop_price_location_ids[location.name]
|
||||
item_price = item_prices[item]
|
||||
if item == "Rupee":
|
||||
item_class = location.item.classification
|
||||
if item_class == ItemClassification.progression:
|
||||
item_price = item_price * 2
|
||||
elif item_class == ItemClassification.useful:
|
||||
item_price = item_price // 2
|
||||
elif item_class == ItemClassification.filler:
|
||||
item_price = item_price // 2
|
||||
elif item_class == ItemClassification.trap:
|
||||
item_price = item_price * 2
|
||||
rom_data[price_location] = item_price
|
||||
if location.name == "Take Any Item Right":
|
||||
# Same story as above: bit 6 is what makes this a Take Any cave
|
||||
item_id = item_id | 0b01000000
|
||||
rom_data[location_id] = item_id
|
||||
|
||||
# We shuffle the tiers of rupee caves. Caves that shared a value before still will.
|
||||
secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3)
|
||||
secret_cave_money_amounts = [20, 50, 100]
|
||||
for i, amount in enumerate(secret_cave_money_amounts):
|
||||
# Giving approximately double the money to keep grinding down
|
||||
amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5)
|
||||
secret_cave_money_amounts[i] = int(amount)
|
||||
for i, cave in enumerate(secret_caves):
|
||||
rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i]
|
||||
return rom_data
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
try:
|
||||
patched_rom = self.apply_randomizer()
|
||||
outfilebase = 'AP_' + self.multiworld.seed_name
|
||||
outfilepname = f'_P{self.player}'
|
||||
outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}"
|
||||
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.nes')
|
||||
self.rom_name_text = f'LOZ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0'
|
||||
self.romName = bytearray(self.rom_name_text, 'utf8')[:0x20]
|
||||
self.romName.extend([0] * (0x20 - len(self.romName)))
|
||||
self.rom_name = self.romName
|
||||
patched_rom[0x10:0x30] = self.romName
|
||||
self.playerName = bytearray(self.multiworld.player_name[self.player], 'utf8')[:0x20]
|
||||
self.playerName.extend([0] * (0x20 - len(self.playerName)))
|
||||
patched_rom[0x30:0x50] = self.playerName
|
||||
patched_filename = os.path.join(output_directory, outputFilename)
|
||||
with open(patched_filename, 'wb') as patched_rom_file:
|
||||
patched_rom_file.write(patched_rom)
|
||||
patch = TLoZDeltaPatch(os.path.splitext(outputFilename)[0] + TLoZDeltaPatch.patch_file_ending,
|
||||
player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player],
|
||||
patched_path=outputFilename)
|
||||
patch.write()
|
||||
os.unlink(patched_filename)
|
||||
finally:
|
||||
self.rom_name_available_event.set()
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
self.rom_name_available_event.wait()
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.filler_items is None:
|
||||
self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler]
|
||||
return self.multiworld.random.choice(self.filler_items)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
if self.multiworld.ExpandedPool[self.player]:
|
||||
take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item
|
||||
take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item
|
||||
take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item
|
||||
if take_any_left.player == self.player:
|
||||
take_any_left = take_any_left.code
|
||||
else:
|
||||
take_any_left = -1
|
||||
if take_any_middle.player == self.player:
|
||||
take_any_middle = take_any_middle.code
|
||||
else:
|
||||
take_any_middle = -1
|
||||
if take_any_right.player == self.player:
|
||||
take_any_right = take_any_right.code
|
||||
else:
|
||||
take_any_right = -1
|
||||
|
||||
slot_data = {
|
||||
"TakeAnyLeft": take_any_left,
|
||||
"TakeAnyMiddle": take_any_middle,
|
||||
"TakeAnyRight": take_any_right
|
||||
}
|
||||
else:
|
||||
slot_data = {
|
||||
"TakeAnyLeft": -1,
|
||||
"TakeAnyMiddle": -1,
|
||||
"TakeAnyRight": -1
|
||||
}
|
||||
return slot_data
|
||||
|
||||
|
||||
class TLoZItem(Item):
|
||||
game = 'The Legend of Zelda'
|
||||
|
||||
|
||||
class TLoZLocation(Location):
|
||||
game = 'The Legend of Zelda'
|
43
worlds/tloz/docs/en_The Legend of Zelda.md
Normal file
43
worlds/tloz/docs/en_The Legend of Zelda.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# The Legend of Zelda (NES)
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
All acquirable pickups (except maps and compasses) are shuffled among each other. Logic is in place to ensure both
|
||||
that the game is still completable, and that players aren't forced to enter dungeons under-geared.
|
||||
|
||||
Shops can contain any item in the game, with prices added for the items unavailable in stores. Rupee caves are worth
|
||||
more while shops cost less, making shop routing and money management important without requiring mindless grinding.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
In general, all item pickups in the game. More formally:
|
||||
|
||||
- Every inventory item.
|
||||
- Every item found in the five kinds of shops.
|
||||
- Optionally, Triforce Fragments can be shuffled to be within dungeons, or anywhere.
|
||||
- Optionally, enemy-held items and dungeon floor items can be included in the shuffle, along with their slots
|
||||
- Maps and compasses have been replaced with bonus items, including Clocks and Fairies.
|
||||
|
||||
## What items from The Legend of Zelda can appear in other players' worlds?
|
||||
|
||||
All items can appear in other players' worlds.
|
||||
|
||||
## What does another world's item look like in The Legend of Zelda?
|
||||
|
||||
All local items appear as normal. All remote items, no matter the game they originate from, will take on the appearance
|
||||
of a single Rupee. These single Rupees will have variable prices in shops: progression and trap items will cost more,
|
||||
filler and useful items will cost less, and uncategorized items will be in the middle.
|
||||
|
||||
## Are there any other changes made?
|
||||
|
||||
- The map and compass for each dungeon start already acquired, and other items can be found in their place.
|
||||
- The Recorder will warp you between all eight levels regardless of Triforce count
|
||||
- It's possible for this to be your route to level 4!
|
||||
- Pressing Select will cycle through your inventory.
|
||||
- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position.
|
||||
- What slots from a Take Any Cave have been chosen are similarly tracked.
|
104
worlds/tloz/docs/multiworld_en.md
Normal file
104
worlds/tloz/docs/multiworld_en.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# The Legend of Zelda (NES) Multiworld Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- The Zelda1Client
|
||||
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended
|
||||
- [BizHawk Official Website](http://tasvideos.org/BizHawk.html)
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Download and install the latest version of Archipelago.
|
||||
- On Windows, download Setup.Archipelago.<HighestVersion\>.exe and run it.
|
||||
2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files.
|
||||
- Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps
|
||||
for loading ROMs more conveniently.
|
||||
1. Right-click on a ROM file and select **Open with...**
|
||||
2. Check the box next to **Always use this app to open .nes files**.
|
||||
3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**.
|
||||
4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**.
|
||||
|
||||
## 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 on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and the Zelda 1 Client will launch automatically, create your ROM from the
|
||||
patch file, and open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||
files. Your patch file should have a `.aptloz` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||
client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
|
||||
## Running the Client Program and Connecting to the Server
|
||||
|
||||
Once the Archipelago server has been hosted:
|
||||
|
||||
1. Navigate to your Archipelago install folder and run `ArchipelagoZelda1Client.exe`.
|
||||
2. Notice the `/connect command` on the server hosting page. (It should look like `/connect archipelago.gg:*****`
|
||||
where ***** are numbers)
|
||||
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
|
||||
already say `archipelago.gg`) and click `connect`.
|
||||
|
||||
### Running Your Game and Connecting to the Client Program
|
||||
|
||||
1. Open Bizhawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the
|
||||
extension `*.nes`.
|
||||
2. Click on the Tools menu and click on **Lua Console**.
|
||||
3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**)
|
||||
4. Navigate to the location you installed Archipelago to. Open `data/lua/TLOZ/tloz_connector.lua`.
|
||||
1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
|
||||
close your emulator entirely, restart it and re-run these steps.
|
||||
2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking **
|
||||
Help** -> **About**.
|
||||
|
||||
## Play the game
|
||||
|
||||
When the client shows both NES and server are connected, you are good to go. You can check the connection status of the
|
||||
NES at any time by running `/nes`.
|
||||
|
||||
### Other Client Commands
|
||||
|
||||
All other commands may be found on the [Archipelago Server and Client Commands Guide.](/tutorial/Archipelago/commands/en)
|
||||
.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Triforce Fragments and Heart Containers may be purchased multiple times. It is up to you if you wish to take advantage
|
||||
of this; logic will not account for or require purchasing any slot more than once. Remote items, no matter what they
|
||||
are, will always only be sent once.
|
||||
- Obtaining a remote item will move the location of any existing item in that room. Should this make an item
|
||||
inaccessible, simply exit and re-enter the room. This can be used to obtain the Ocean Heart Container item without the
|
||||
stepladder; logic does not account for this.
|
||||
- Whether you've purchased from a shop is tracked via Archipelago between sessions: if you revisit a single player game,
|
||||
none of your shop pruchase statuses will be remembered. If you want them to be, connect to the client and server like
|
||||
you would in a multiplayer game.
|
1
worlds/tloz/requirements.txt
Normal file
1
worlds/tloz/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
bsdiff4>=1.2.2
|
BIN
worlds/tloz/z1_base_patch.bsdiff4
Normal file
BIN
worlds/tloz/z1_base_patch.bsdiff4
Normal file
Binary file not shown.
Reference in New Issue
Block a user