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:
Rosalie-A
2023-03-05 07:31:31 -05:00
committed by GitHub
parent 3a68ce3faa
commit efb2ab4505
21 changed files with 2994 additions and 1 deletions

145
worlds/tloz/ItemPool.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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'

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

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

View File

@@ -0,0 +1 @@
bsdiff4>=1.2.2

Binary file not shown.