diff --git a/.gitignore b/.gitignore index 791f7b1bb7fe..30bbddba390c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,8 @@ *multisave *.archipelago *.apsave -*.BIN *.puml +*.BIN setups build @@ -200,3 +200,9 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini + +# FF4 Free Enterprise +/worlds/ff4fe/FreeEnterpriseForAP/FreeEnt/.build/ +!/worlds/ff4fe/FreeEnterpriseForAP/FreeEnt/*.bin +/data/ff4fe/harp +/data/ff4fe/zsprite diff --git a/inno_setup.iss b/inno_setup.iss index eb794650f3a6..9b4249b56ee5 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -221,6 +221,11 @@ Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Ar Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apff4fe"; ValueData: "{#MyAppName}ff4fepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ff4fepatch"; ValueData: "Archipelago Final Fantasy IV Free Enterprise Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ff4fepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ff4fepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/ff4fe/Client.py b/worlds/ff4fe/Client.py new file mode 100644 index 000000000000..0b152f4a377d --- /dev/null +++ b/worlds/ff4fe/Client.py @@ -0,0 +1,431 @@ +import typing +import logging +from logging import Logger + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient +from . import rom as Rom +from . import items +from . import locations +from .rom import objective_threshold_size + +if typing.TYPE_CHECKING: + from SNIClient import SNIContext, snes_buffered_write +else: + SNIContext = typing.Any + +snes_logger: Logger = logging.getLogger("SNES") + + +class FF4FEClient(SNIClient): + game: str = "Final Fantasy IV Free Enterprise" + patch_suffix = ".apff4fe" + def __init__(self): + super() + self.location_name_to_id = None + self.key_item_names = None + self.key_items_with_flags = None + self.json_doc = None + self.flags = None + self.junked_items = None + self.kept_items = None + + async def validate_rom(self, ctx: SNIContext) -> bool: + from SNIClient import snes_read + + rom_name: bytes = await snes_read(ctx, Rom.ROM_NAME, 20) + if rom_name is None or rom_name[:3] != b"4FE": + return False + + ctx.game = self.game + + # We're not actually full remote items, but some are so we let the server know to send us all items; + # we'll handle the ones that are actually local separately. + ctx.items_handling = 0b111 + + ctx.rom = rom_name + + + + return True + + async def game_watcher(self, ctx: SNIContext) -> None: + from SNIClient import snes_flush_writes + # We check victory before the connection check because victory is set in a cutscene. + # Thus, we would no longer be in a "valid" state to send or receive items. + if await self.connection_check(ctx) == False: + return + await self.location_check(ctx) + await self.reward_check(ctx) + await self.objective_check(ctx) + await self.received_items_check(ctx) + await self.resolve_key_items(ctx) + await snes_flush_writes(ctx) + + async def connection_check(self, ctx: SNIContext): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + rom: bytes = await snes_read(ctx, Rom.ROM_NAME, 20) + if rom != ctx.rom: + ctx.rom = None + return False + + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + return False + + # Caching of useful information. + if self.location_name_to_id is None: + from . import FF4FEWorld + self.location_name_to_id = FF4FEWorld.location_name_to_id + + if self.key_item_names is None: + from . import FF4FEWorld + self.key_item_names = {item: id for item, id in FF4FEWorld.item_name_to_id.items() + if item in items.key_item_names} + + if self.key_items_with_flags is None: + from . import FF4FEWorld + self.key_items_with_flags = {item: id for item, id in FF4FEWorld.item_name_to_id.items() + if item in Rom.special_flag_key_items.keys()} + + if self.junked_items is None: + junked_items_length_data = await snes_read(ctx, Rom.junked_items_length_byte, 1) + if junked_items_length_data is None: + return + junked_items_array_length = junked_items_length_data[0] + junked_items_array_data = await snes_read(ctx, Rom.junked_items_array_start, junked_items_array_length) + if junked_items_array_data is None: + return + self.junked_items = [] + for item_byte in junked_items_array_data: + item_data = [item for item in items.all_items if item.fe_id == item_byte].pop() + self.junked_items.append(item_data.name) + + if self.kept_items is None: + kept_items_length_data = await snes_read(ctx, Rom.kept_items_length_byte, 1) + if kept_items_length_data is None: + return + kept_items_array_length = kept_items_length_data[0] + kept_items_array_data = await snes_read(ctx, Rom.kept_items_array_start, kept_items_array_length) + if kept_items_array_data is None: + return + self.kept_items = [] + for item_byte in kept_items_array_data: + item_data = [item for item in items.all_items if item.fe_id == item_byte].pop() + self.kept_items.append(item_data.name) + + + await self.check_victory(ctx) + + # If we're not in a safe state, _don't do anything_. + for sentinel in Rom.sentinel_addresses: + sentinel_data = await snes_read(ctx, sentinel, 1) + if sentinel_data is None: + return False + sentinel_value = sentinel_data[0] + if sentinel_value != 0: + return False + # Cache the game's internal settings document. + # This is used to track any special flags in lieu of slot data. + if self.json_doc is None: + await self.load_json_data(ctx) + + return True + + async def load_json_data(self, ctx: SNIContext): + from SNIClient import snes_read + import json + json_length_data = await snes_read(ctx, Rom.json_doc_length_location, 4) + if json_length_data is None: + return + json_length = int.from_bytes(json_length_data, "little") + json_data = await snes_read(ctx, Rom.json_doc_location, json_length) + if json_data is None: + return + self.json_doc = json.loads(json_data) + self.flags = self.json_doc["flags"] + + + async def location_check(self, ctx: SNIContext): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + treasure_data = await snes_read(ctx, Rom.treasure_found_locations_start, Rom.treasure_found_size) + if treasure_data is None: + return False + # Go through every treasure, and if it's been opened, find the location and send it if need be. + for i in range(Rom.treasure_found_size * 8): + byte = i // 8 + bit = i % 8 + checked = treasure_data[byte] & (2**bit) + if checked > 0: + treasure_found = [treasure for treasure in locations.all_locations if treasure.fe_id == i] + if len(treasure_found) > 0: + treasure_found = treasure_found.pop() + location_id = self.location_name_to_id[treasure_found.name] + if location_id not in ctx.locations_checked: + ctx.locations_checked.add(location_id) + snes_logger.info( + f'New Check: {treasure_found.name} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + + + async def reward_check(self, ctx: SNIContext): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + reward_data = await snes_read(ctx, Rom.checked_reward_locations_start, Rom.checked_reward_size) + if reward_data is None: + return False + # Same as the treasure location check. + for i in range(Rom.checked_reward_size * 8): + byte = i // 8 + bit = i % 8 + checked = reward_data[byte] & (2 ** bit) + if i == 1: + checked = 1 # FE never actually flags your starting character as obtained, amazingly + if checked > 0: + reward_found = [reward for reward in locations.all_locations if reward.fe_id == i + 0x200] + if len(reward_found) > 0: + reward_found = reward_found.pop() + location_id = self.location_name_to_id[reward_found.name] + if location_id not in ctx.locations_checked: + ctx.locations_checked.add(location_id) + snes_logger.info( + f'New Check: {reward_found.name} ' + f'({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + + + async def objective_check(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + objective_progress_data = await snes_read(ctx, Rom.objective_progress_start_location, Rom.objective_progress_size) + if objective_progress_data is None: + return False + objective_count_data = await snes_read(ctx, Rom.objective_count_location, 1) + if objective_count_data is None: + return False + objective_count = objective_count_data[0] + if objective_count == 0: + return + objectives_needed_count_data = await snes_read(ctx, Rom.objectives_needed_count_location, 1) + if objectives_needed_count_data is None: + return False + objectives_needed = objectives_needed_count_data[0] + # Go through every FE objective, and flag it if we've done it. + for i in range(objective_count): + objective_progress = objective_progress_data[i] + if objective_progress > 0: + location_id = self.location_name_to_id[f"Objective {i + 1} Status"] + if location_id not in ctx.locations_checked: + ctx.locations_checked.add(location_id) + snes_logger.info( + f'New Check: Objective {i + 1} Cleared! ' + f'({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + # Check if we've cleared the required number of objectives, and send the appropriate location if so. + # If this results in victory, that check is handled elsewhere. + objectives_cleared = 0 + for i in range(objective_count): + location_id = self.location_name_to_id[f"Objective {i + 1} Status"] + if location_id in ctx.locations_checked: + objectives_cleared += 1 + if objectives_cleared >= objectives_needed: + location_id = self.location_name_to_id["Objectives Status"] + if location_id not in ctx.locations_checked: + ctx.locations_checked.add(location_id) + reward_location_id = self.location_name_to_id["Objective Reward"] + ctx.locations_checked.add(reward_location_id) + snes_logger.info( + f'All Objectives Cleared! ' + f'({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id, reward_location_id]}]) + + + async def received_items_check(self, ctx: SNIContext): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + items_received_data = await snes_read(ctx, Rom.items_received_location_start, Rom.items_received_size) + if items_received_data is None: + return + items_received_amount = int.from_bytes(items_received_data, "big") + if items_received_amount >= len(ctx.items_received): + return + inventory_data = await snes_read(ctx, Rom.inventory_start_location, Rom.inventory_size) + if inventory_data is None: + return + junk_tier_data = await snes_read(ctx, Rom.junk_tier_byte, 1) + if junk_tier_data is None: + return + time_is_money_data = await snes_read(ctx, Rom.sell_value_byte, 1) + if time_is_money_data is None: + return + + # Get all the useful data about the latest unhandled item. + item_received = ctx.items_received[items_received_amount] + item_received_id = item_received.item + item_received_name = ctx.item_names.lookup_in_game(item_received_id, ctx.game) + item_received_location_name = ctx.location_names.lookup_in_game(item_received.location, ctx.game) + item_received_game_data = [item for item in items.all_items if item.name == item_received_name].pop() + item_received_game_id = item_received_game_data.fe_id + # Characters are handled entirely ingame. + if item_received_name in items.characters: + self.increment_items_received(ctx, items_received_amount) + return + # Five key items require specific flags to be set in addition to existing in the inventory. + if item_received_name in Rom.special_flag_key_items.keys(): + flag_byte = Rom.special_flag_key_items[item_received_name][0] + flag_bit = Rom.special_flag_key_items[item_received_name][1] + key_item_received_data = await snes_read(ctx, flag_byte, 1) + if key_item_received_data is None: + return + key_item_received_value = key_item_received_data[0] + key_item_received_value = key_item_received_value | flag_bit + snes_buffered_write(ctx, flag_byte, bytes([key_item_received_value])) + # The Hook doesn't actually go in the inventory, though, so it gets its own special case. + if item_received_name == "Hook": + self.increment_items_received(ctx, items_received_amount) + snes_logger.info('Received %s from %s (%s)' % ( + item_received_name, + ctx.player_names[item_received.player], + ctx.location_names[item_received.location])) + return + # Any non MIAB items that come from ourself are actually local items in a trenchcoat and so we just move on, + # since we got them ingame. + if item_received.player == ctx.slot and item_received.location >= 0: + if "Monster in a Box" not in item_received_location_name: + self.increment_items_received(ctx, items_received_amount) + return + # Any item that hits our junk tier settings is automatically sold. + if item_received_name in items.sellable_item_names and item_received.location >= 0: + if self.check_junk_item(item_received_game_data, junk_tier_data[0]): + # The Time is Money wacky prevents us from getting cash through any means other than time. + time_is_money = False if time_is_money_data[0] != 0 else True + # Item sale prices are capped at 127000 GP. + item_price = min(item_received_game_data.price // 2, 127000 if not time_is_money else 0) + current_gp_data = await snes_read(ctx, Rom.gp_byte_location, Rom.gp_byte_size) + if current_gp_data is None: + return + current_gp_amount = int.from_bytes(current_gp_data, "little") + current_gp_amount += item_price + + lower_byte = current_gp_amount % (2**8) + middle_byte = (current_gp_amount // (2**8)) % (2**8) + upper_byte = current_gp_amount // (2**16) + snes_buffered_write(ctx, Rom.gp_byte_location, bytes([lower_byte])) + snes_buffered_write(ctx, Rom.gp_byte_location + 1, bytes([middle_byte])) + snes_buffered_write(ctx, Rom.gp_byte_location + 2, bytes([upper_byte])) + self.increment_items_received(ctx, items_received_amount) + snes_logger.info('Received %s from %s (%s)' % ( + item_received_name, + ctx.player_names[item_received.player], + ctx.location_names[item_received.location])) + snes_logger.info(f"Automatically sold {item_received_name} for {item_price} GP.") + return + # If we've made it this far, this is an item that actually goes in the inventory. + for i, byte in enumerate(inventory_data): + # Every other slot in the inventory data is the quantity. + if i % 2 == 1: + continue + # We need a free slot or a slot that already has our item (if Unstackable wacky isn't in play) + if inventory_data[i] == 0 or (inventory_data[i] == item_received_game_id and "unstackable" not in self.flags): + snes_buffered_write(ctx, Rom.inventory_start_location + i, bytes([item_received_game_id])) + if inventory_data[i] == 0: + snes_buffered_write(ctx, + Rom.inventory_start_location + i + 1, + bytes([10 if ("Arrows" in item_received_name and "unstackable" not in self.flags) else 1])) + else: + item_count = inventory_data[i + 1] + item_count = min((item_count + 10) if "Arrows" in item_received_name else (item_count + 1), 99) + snes_buffered_write(ctx, Rom.inventory_start_location + i + 1, bytes([item_count])) + self.increment_items_received(ctx, items_received_amount) + + snes_logger.info('Received %s from %s (%s)' % ( + item_received_name, + ctx.player_names[item_received.player], + ctx.location_names[item_received.location])) + break + + async def check_victory(self, ctx): + from SNIClient import snes_buffered_write, snes_read + for sentinel in Rom.sentinel_addresses: # Defend against RAM initialized to static values everywhere. + sentinel_data = await snes_read(ctx, sentinel, 1) + if sentinel_data is None: + return + sentinel_value = sentinel_data[0] + if sentinel_value >= 0x02: + return + victory_data = await snes_read(ctx, Rom.victory_byte_location, 1) + if victory_data is None: + return + if victory_data[0] > 0: + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + async def resolve_key_items(self, ctx): + # We need to write key items into the ingame tracker. + from SNIClient import snes_buffered_write, snes_read + tracker_data = await snes_read(ctx, Rom.key_items_tracker_start_location, Rom.key_items_tracker_size) + if tracker_data is None: + return + new_tracker_bytes = bytearray(tracker_data[:Rom.key_items_tracker_size]) + key_items_collected = [item.item for item in ctx.items_received + if item.item in self.key_item_names.values() + and item.item != "Pass"] + key_items_flag_byte = None + buffered_flag_byte = None + for key_item in self.key_items_with_flags.keys(): + if self.key_items_with_flags[key_item] in key_items_collected: + flag_byte = Rom.special_flag_key_items[key_item][0] + if key_item == "Hook": + flag_bit = Rom.special_flag_key_items[key_item][1] + key_item_received_data = await snes_read(ctx, flag_byte, 1) + if key_item_received_data is None: + return + hook_received_value = key_item_received_data[0] + hook_received_value = hook_received_value | flag_bit + # The bitwise AND isn't an error: there's a flag that can erroneously get set that stops us from + # flying the airship. + # Which is bad. + hook_received_value = hook_received_value & Rom.airship_flyable_flag[1] + snes_buffered_write(ctx, flag_byte, bytes([hook_received_value])) + drill_attached_data = await snes_read(ctx, Rom.drill_attached_flag[0], 1) + if drill_attached_data is None: + return + drill_attached_value = drill_attached_data[0] + snes_buffered_write(ctx, Rom.drill_attached_flag[0], bytes([drill_attached_value | Rom.drill_attached_flag[1]])) + elif key_items_flag_byte is None: + flag_bit = Rom.special_flag_key_items[key_item][1] + key_item_received_data = await snes_read(ctx, flag_byte, 1) + if key_item_received_data is None: + return + key_items_flag_byte = key_item_received_data[0] + key_items_flag_byte = key_items_flag_byte | flag_bit + buffered_flag_byte = flag_byte + else: + flag_bit = Rom.special_flag_key_items[key_item][1] + key_items_flag_byte = key_items_flag_byte | flag_bit + if key_items_flag_byte is not None and buffered_flag_byte is not None: + snes_buffered_write(ctx, buffered_flag_byte, bytes([key_items_flag_byte])) + for key_item in key_items_collected: + item_received_name = ctx.item_names.lookup_in_game(key_item, ctx.game) + if item_received_name != "Pass": + key_item_index = items.key_items_tracker_ids[item_received_name] + byte = key_item_index // 8 + bit = key_item_index % 8 + new_tracker_bytes[byte] = new_tracker_bytes[byte] | (2 ** bit) + for i in range(Rom.key_items_tracker_size): + snes_buffered_write(ctx, Rom.key_items_tracker_start_location + i, bytes([new_tracker_bytes[i]])) + snes_buffered_write(ctx, Rom.key_items_found_location, bytes([len(key_items_collected)])) + + def check_junk_item(self, item_received_game_data, junk_tier): + if item_received_game_data.name in self.kept_items: + return False + if item_received_game_data.name in self.junked_items: + return True + return item_received_game_data.tier <= junk_tier + + + def increment_items_received(self, ctx, items_received_amount): + from SNIClient import snes_buffered_write + new_count = items_received_amount + 1 + lower_byte = new_count % 256 + upper_byte = new_count // 256 + snes_buffered_write(ctx, Rom.items_received_location_start, bytes([upper_byte])) + snes_buffered_write(ctx, Rom.items_received_location_start + 1, bytes([lower_byte])) diff --git a/worlds/ff4fe/__init__.py b/worlds/ff4fe/__init__.py new file mode 100644 index 000000000000..af784a98f444 --- /dev/null +++ b/worlds/ff4fe/__init__.py @@ -0,0 +1,478 @@ +import json +import os +import threading +import typing +from typing import Mapping, Any + +import Utils +import settings +from BaseClasses import Region, ItemClassification, MultiWorld, Tutorial +from worlds.AutoWorld import World, WebWorld +from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_items_for_player +from . import events, items, locations +from . import rules as FERules +from .Client import FF4FEClient +from .itempool import create_itempool +from .items import FF4FEItem, all_items, ItemData +from .locations import FF4FELocation, all_locations, LocationData +from .options import FF4FEOptions, ff4fe_option_groups +from . import topology, flags +from .rom import FF4FEProcedurePatch + + +class FF4FEWebWorld(WebWorld): + theme = "grassFlowers" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Final Fantasy IV: Free Enterprise randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["Rosalie"] + ) + + tutorials = [setup_en] + + option_groups = ff4fe_option_groups + + +class FF4FESettings(settings.Group): + class RomFile(settings.SNESRomPath): + """File name of the FF4 USA 1.1 US rom""" + + copy_to = "Final Fantasy II (USA) (Rev A).sfc" + description = "FFII SNES 1.1 ROM File" + md5s = ["27D02A4F03E172E029C9B82AC3DB79F7"] + + rom_file: RomFile = RomFile(RomFile.copy_to) + rom_start: bool = True + + +class FF4FEWorld(World): + """Final Fantasy IV: Free Enterprise is an open world randomizer for the classic SNES RPG. Explore the world, + find the Crystal, and defeat Zeromus on the moon. Adapted from the open source release of FE 4.6.0.""" + game = "Final Fantasy IV Free Enterprise" + options_dataclass = FF4FEOptions + options: FF4FEOptions + settings: typing.ClassVar[FF4FESettings] + + web = FF4FEWebWorld() + base_id = 7191991 + item_name_to_id = {item.name: id for + id, item in enumerate(all_items, base_id)} + location_name_to_id = {location.name: id for + id, location in enumerate(all_locations, base_id)} + item_name_groups = items.item_name_groups + + + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) + self.rom_name_available_event = threading.Event() + self.chosen_character = "None" + self.objective_count = -1 + + def is_vanilla_game(self): + return self.get_objective_count() == 0 + + def get_objective_count(self): + if self.objective_count != -1: + return self.objective_count + else: + objective_count = 0 + if self.options.ForgeTheCrystal: + objective_count += 1 + if self.options.ConquerTheGiant: + objective_count += 1 + if self.options.DefeatTheFiends: + objective_count += 6 + if self.options.FindTheDarkMatter: + objective_count += 1 + objective_count += self.options.AdditionalObjectives.value + objective_count = min(objective_count, 32) + self.objective_count = objective_count + return min(objective_count, 32) + + def create_regions(self) -> None: + menu = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu) + + overworld = Region("Overworld", self.player, self.multiworld) + underworld = Region("Underworld", self.player, self.multiworld) + moon = Region("Moon", self.player, self.multiworld) + + self.multiworld.regions.append(overworld) + self.multiworld.regions.append(underworld) + self.multiworld.regions.append(moon) + + menu.connect(overworld) + overworld.connect(underworld, "Underworld Access", lambda state: state.has("Hook", self.player) + or state.has("Magma Key", self.player)) + overworld.connect(moon, "Moon Access", lambda state: state.has("Darkness Crystal", self.player)) + + + for area in topology.areas: + new_region = Region(area, self.player, self.multiworld) + self.multiworld.regions.append(new_region) + if area in topology.overworld_areas: + overworld.connect(new_region, "Overworld to " + area) + if area in topology.hook_areas: + underworld.connect(new_region, "Hook route to " + area, lambda state: state.has("Hook", self.player)) + if area in topology.underworld_areas: + underworld.connect(new_region, "Underworld to " + area) + if area in topology.moon_areas: + moon.connect(new_region, "Moon to " + area) + + for location in all_locations: + if location.name.startswith("Objective"): # Objectives aren't "real" locations + continue + if (self.options.ForgeTheCrystal.current_key == "forge" # Forge the Crystal doesn't have a Kokkol location. + and location.name == "Kokkol's House 2F -- Kokkol -- forge item"): + continue + region = self.multiworld.get_region(location.area, self.player) + new_location = FF4FELocation(self.player, location.name, self.location_name_to_id[location.name], region) + region.locations.append(new_location) + + for event in events.boss_events: + region = self.multiworld.get_region(event.area, self.player) + new_location = FF4FELocation(self.player, event.name, None, region) + region.locations.append(new_location) + + if not self.is_vanilla_game(): + new_location = FF4FELocation(self.player, "Objectives Status", None, overworld) + overworld.locations.append(new_location) + new_location = FF4FELocation(self.player, "Objective Reward", + self.location_name_to_id["Objective Reward"], overworld) + overworld.locations.append(new_location) + + for i in range(self.get_objective_count()): + new_location = FF4FELocation(self.player, f"Objective {i + 1} Status", None, overworld) + overworld.locations.append(new_location) + + def create_item(self, item: str) -> FF4FEItem: + item_data: ItemData = next((item_data for item_data in all_items if item_data.name == item), None) + if not item_data: + raise Exception(f"{item} is not a valid item name for Final Fantasy 4 Free Enterprise") + return FF4FEItem(item, item_data.classification, self.item_name_to_id[item], self.player) + + def create_event(self, event: str) -> FF4FEItem: + return FF4FEItem(event, ItemClassification.progression, None, self.player) + + def create_items(self) -> None: + item_pool, self.chosen_character, self.second_character = create_itempool(locations.all_locations, self) + chosen_character_placed = False + second_character_placed = False + + character_locations = locations.character_locations.copy() + character_locations.remove("Starting Character 1") + character_locations.remove("Starting Character 2") + if self.options.ConquerTheGiant: + character_locations.remove("Giant of Bab-il Character") + if self.options.NoFreeCharacters: + for location in locations.free_character_locations: + character_locations.remove(location) + self.get_location(location).place_locked_item(self.create_item("None")) + item_pool.remove("None") + if self.options.NoEarnedCharacters: + for location in locations.earned_character_locations: + character_locations.remove(location) + self.get_location(location).place_locked_item(self.create_item("None")) + item_pool.remove("None") + + restricted_character_allow_locations = sorted(set(character_locations) - set(locations.restricted_character_locations)) + restricted_character_forbid_locations = sorted(set(character_locations) - set(restricted_character_allow_locations)) + self.random.shuffle(restricted_character_allow_locations) + self.random.shuffle(restricted_character_forbid_locations) + + for item in map(self.create_item, item_pool): + # If we've specifically chosen a starting character, we place them directly even though we're not normally + # allowed to have a restricted character as a starter. + if item.name == self.chosen_character and not chosen_character_placed: + self.get_location("Starting Character 1").place_locked_item(self.create_item(self.chosen_character)) + chosen_character_placed = True + continue + elif item.name == self.second_character and not second_character_placed: + self.get_location("Starting Character 2").place_locked_item(self.create_item(self.second_character)) + second_character_placed = True + continue + elif item.name in items.characters: + if item.name in self.options.RestrictedCharacters.value: + # Place restricted characters where they're allowed first, then into the other spots. + if len(restricted_character_allow_locations) > 0: + self.get_location(restricted_character_allow_locations.pop()).place_locked_item(self.create_item(item.name)) + else: + self.get_location(restricted_character_forbid_locations.pop()).place_locked_item(self.create_item(item.name)) + else: + # Inverse of the above: unrestricted characters go into the unrestricted slots first to leave room + # for the restricted characters. + if len(restricted_character_forbid_locations) > 0: + self.get_location(restricted_character_forbid_locations.pop()).place_locked_item(self.create_item(item.name)) + else: + self.get_location(restricted_character_allow_locations.pop()).place_locked_item(self.create_item(item.name)) + continue + if item.name == "Crystal": + if not self.is_vanilla_game(): + (self.get_location("Objective Reward") + .place_locked_item(self.create_item("Crystal"))) + continue + if item.name.startswith("Objective"): # Objectives get manually placed later. + continue + self.multiworld.itempool.append(item) + + def set_rules(self) -> None: + # Unplaced characters don't go in noncharacter slots, and we force slots with no character to None. + for location in locations.character_locations: + add_item_rule(self.get_location(location), + lambda item: item.name in items.characters and item.player == self.player) + if self.options.NoFreeCharacters: + if location in locations.free_character_locations: + add_item_rule(self.get_location(location), + lambda item: item.name == "None") + if self.options.NoEarnedCharacters: + if location in locations.earned_character_locations: + add_item_rule(self.get_location(location), + lambda item: item.name == "None") + if len(self.options.AllowedCharacters.value.difference(self.options.RestrictedCharacters.value)) > 0: + if location in locations.restricted_character_locations: + add_item_rule(self.get_location(location), + lambda item: item.name not in self.options.RestrictedCharacters.value) + + + for location in locations.all_locations: + if location.name not in locations.character_locations: + # Skip over any objectives or the Kokkol slot if they're not actually in this location pool due to options. + if location.name.startswith("Objective") or location.name == "Kokkol's House 2F -- Kokkol -- forge item": + try: + self.get_location(location.name) + except KeyError: + continue + # No characters in noncharacter slots, of course + add_item_rule(self.get_location(location.name), + lambda item: item.name not in items.characters) + # No key items except Dark Matters in minor slots when we're doing major/minor split. + if (self.options.ItemPlacement.current_key == "major_minor_split" and not location.major_slot + and location.name not in self.options.priority_locations): + forbid_items_for_player(self.get_location(location.name), set(items.characters), self.player) + + # Conquer the Giant doesn't have a character, so we force it to None. + if self.options.ConquerTheGiant: + (self.get_location( + "Giant of Bab-il Character") + .place_locked_item(self.create_item("None"))) + + # If we're doing Hero Challenge and we're not doing Forge the Crystal, Kokkol has a fancy weapon for our Hero. + # The actual weapon is determined by Free Enterprise, so you can't hint if it's an Excalipur and remove + # the potential comedy. + if (self.options.HeroChallenge.current_key != "none" + and not self.options.ForgeTheCrystal): + self.get_location( + "Kokkol's House 2F -- Kokkol -- forge item").place_locked_item(self.create_item("Advance Weapon")) + + # Zeromus requires the Pass or Moon access in addition to the Crystal. Mostly just makes the spoiler log nicer. + set_rule(self.get_location("Zeromus"), + lambda state: state.has("Pass", self.player) + or state.has("Darkness Crystal", self.player)) + + for location in [location for location in all_locations]: + # Hook areas, of course, require Hook. + if location.area in topology.hook_areas: + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player)) + if location.area in topology.underworld_areas: + # Skip over Kokkol on Forge the Crystal... + if location.name == "Kokkol's House 2F -- Kokkol -- forge item": + if not location.name in self.multiworld.regions.location_cache[self.player]: + continue + # ...but otherwise all underground locations require underground access. + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player) or state.has("Magma Key", self.player)) + # Moon needs the ability to get to the moon. + if location.area in topology.moon_areas: + add_rule(self.get_location(location.name), + lambda state: state.has("Darkness Crystal", self.player)) + if self.options.UnsafeKeyItemPlacement: + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player) or state.has("Magma Key", self.player)) + # Otherwise, we consult the list of area-specific rules (e.g. Baron Castle requires Baron Key).. + if location.area in FERules.area_rules.keys(): + for requirement in FERules.area_rules[location.area]: + add_rule(self.get_location(location.name), + lambda state, true_requirement=requirement: state.has(true_requirement, self.player)) + # Major slots must have useful or better. + if location.major_slot and location.name not in self.options.exclude_locations: + add_item_rule(self.get_location(location.name), + lambda item: (item.classification & (ItemClassification.useful | ItemClassification.progression)) > 0) + # The "harder" an area, the more key items and characters we need to access. + # This does two things. First, it ensures a wider distribution of key items so sphere 1 can't just be one + # piece of progression, which isn't fun in FE. + # Second of all, it includes characters in the requirements so a lategame FE area isn't equivalent to an early + # Zelda dungeon for progression balancing. + # Also it makes the spoiler playthrough nicer. + for i in range(len(FERules.location_tiers.keys())): + if (location.area in FERules.location_tiers[i] + and self.options.ItemPlacement.current_key == "normal" + and location.name not in locations.character_locations + and not location.name.startswith("Objective")): + add_rule(self.get_location(location.name), + lambda state, tier=i: state.has_group("characters", + self.player, + FERules.logical_gating[tier]["characters"]) and + state.has_group("key_items", + self.player, + FERules.logical_gating[tier]["key_items"])) + + # Boss events follow the same rules, but they're not real locations. + for location in [event for event in events.boss_events]: + if location.area in topology.hook_areas: + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player)) + if location.area in topology.underworld_areas: + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player) or state.has("Magma Key", self.player)) + if location.area in topology.moon_areas: + add_rule(self.get_location(location.name), + lambda state: state.has("Darkness Crystal", self.player)) + if self.options.UnsafeKeyItemPlacement: + add_rule(self.get_location(location.name), + lambda state: state.has("Hook", self.player) or state.has("Magma Key", self.player)) + if location.name in FERules.boss_rules.keys(): + for requirement in FERules.boss_rules[location.name]: + add_rule(self.get_location(location.name), + lambda state, true_requirement=requirement: state.has(true_requirement, self.player)) + for i in range(len(FERules.location_tiers.keys())): + if location.area in FERules.location_tiers[i]: + add_rule(self.get_location(location.name), + lambda state, tier=i: state.has_group("characters", + self.player, + FERules.logical_gating[tier]["characters"]) and + state.has_group("key_items", + self.player, + FERules.logical_gating[tier]["key_items"])) + + # Some locations need bespoke rules. This applies them. + for location in FERules.individual_location_rules.keys(): + if location in self.multiworld.regions.location_cache[self.player]: + ap_location = self.get_location(location) + for requirement in FERules.individual_location_rules[location]: + add_rule(ap_location, + lambda state, true_requirement=requirement: state.has(true_requirement, self.player)) + + # Bosses get locked events for spoiler log niceness. + for event in events.boss_events: + self.get_location(event.name).place_locked_item( + self.create_event(event.name + " Defeated") + ) + + # Zeromus also requires the Crystal...when he needs to be fought. + add_item_rule(self.get_location("Zeromus"), + lambda state: state.has("Crystal", self.player) + or (self.options.ObjectiveReward.current_key == "win" + and not self.is_vanilla_game())) + + # Specific objectives are FE's problem, not AP's. So on the AP side, clearing all objective + # requires all relevant key items. + for i in range(self.get_objective_count()): + (self.get_location(f"Objective {i + 1} Status") + .place_locked_item(self.create_item(f"Objective {i + 1} Cleared"))) + for requirement in FERules.individual_location_rules["Objectives Status"]: + add_rule(self.get_location(f"Objective {i + 1} Status"), + lambda state, true_requirement=requirement: state.has(true_requirement, self.player)) + + # Just an event to make the spoiler playthrough look nicer. + if not self.is_vanilla_game(): + self.get_location("Objectives Status").place_locked_item( + self.create_event("All Objectives Cleared") + ) + + # If we have no objectives or objectives award the crystal, Zeromus is the win condition. + # Otherwise, clearing all objectives secures victory. + if (self.options.ObjectiveReward.current_key == "crystal" + or self.is_vanilla_game()): + self.multiworld.completion_condition[self.player] = lambda state: state.has("Zeromus Defeated", self.player) + + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has("All Objectives Cleared", self.player) + + def post_fill(self) -> None: + unfilled_locations = self.multiworld.get_unfilled_locations(self.player) + for location in unfilled_locations: + location.item = self.create_item(self.get_filler_item_name()) + + def generate_output(self, output_directory: str) -> None: + # Standard rom name stuff. + self.rom_name_text = f'4FE{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}' + self.rom_name_text = self.rom_name_text[:20] + self.rom_name = bytearray(self.rom_name_text, 'utf-8') + self.rom_name.extend([0] * (20 - len(self.rom_name))) + self.rom_name_available_event.set() + + # The placement dictionary file is what FE will use to place things. + placement_dict = self.create_placement_file(str(self.rom_name_text)) + # We need the seed for FE to be deterministic, but not the same as another player in the game. + placement_dict["seed"] = self.player + self.multiworld.seed + placement_dict["output_file"] = f'{self.multiworld.get_out_file_name_base(self.player)}' + '.sfc' + placement_dict["flags"] = flags.create_flags_from_options(self.options, self.objective_count) + placement_dict["junk_tier"] = self.options.JunkTier.value + placement_dict["junked_items"] = list(self.options.JunkedItems.value) + placement_dict["kept_items"] = list(self.options.KeptItems.value) + placement_dict["data_dir"] = Utils.user_path("data", "ff4fe") + + # Our actual patch is just a set of instructions and data for FE to use. + patch = FF4FEProcedurePatch(player=self.player, player_name=self.player_name) + patch.write_file("placement_file.json" , json.dumps(placement_dict).encode("UTF-8")) + rom_path = os.path.join( + output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" f"{patch.patch_file_ending}" + ) + patch.write(rom_path) + + def fill_slot_data(self) -> Mapping[str, Any]: + # Slot data needed for tracker logic. + slot_data = { + "DarkMatterHunt": self.options.FindTheDarkMatter.current_key, + "NoEarnedCharacters": self.options.NoEarnedCharacters.current_key, + "NoFreeCharacters": self.options.NoEarnedCharacters.current_key, + "PassEnabled": self.options.PassEnabled.current_key, + "AdditionalObjectives": self.options.AdditionalObjectives.value, + "ObjectiveReward": self.options.ObjectiveReward.current_key, + "UnsafeKeyItemPlacement": self.options.UnsafeKeyItemPlacement.current_key, + "ObjectivesRequired": self.options.RequiredObjectiveCount.value + } + return slot_data + + def create_placement_file(self, rom_name): + placement_dict = {"rom_name": rom_name} + for location in self.multiworld.get_filled_locations(self.player): + # Placement dictionary doesn't need AP event logic stuff. + if location.name in [event.name for event in events.boss_events] or location.name.startswith("Objective"): + continue + location_data = [loc for loc in all_locations if loc.name == location.name].pop() + if location.item.player == self.player: + item_data = [item for item in all_items if item.name == location.item.name].pop() + placement_dict[location_data.fe_id] = { + "location_data": location_data.to_json(), + "item_data": item_data.to_json(), + "item_name": item_data.name, + "player_name": self.multiworld.player_name[location.item.player] + } + else: + placement_dict[location_data.fe_id] = { + "location_data": location_data.to_json(), + "item_data": ItemData.create_ap_item().to_json(), + "item_name": location.item.name, + "player_name": self.multiworld.player_name[location.item.player] + } + return placement_dict + + def modify_multidata(self, multidata: dict): + import base64 + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + 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: + filler_items = [item.name for item in items.filler_items] + return self.random.choice(filler_items) diff --git a/worlds/ff4fe/common.lark b/worlds/ff4fe/common.lark new file mode 100644 index 000000000000..d2e86d17c696 --- /dev/null +++ b/worlds/ff4fe/common.lark @@ -0,0 +1,59 @@ +// Basic terminals for common use + + +// +// Numbers +// + +DIGIT: "0".."9" +HEXDIGIT: "a".."f"|"A".."F"|DIGIT + +INT: DIGIT+ +SIGNED_INT: ["+"|"-"] INT +DECIMAL: INT "." INT? | "." INT + +// float = /-?\d+(\.\d+)?([eE][+-]?\d+)?/ +_EXP: ("e"|"E") SIGNED_INT +FLOAT: INT _EXP | DECIMAL _EXP? +SIGNED_FLOAT: ["+"|"-"] FLOAT + +NUMBER: FLOAT | INT +SIGNED_NUMBER: ["+"|"-"] NUMBER + +// +// Strings +// +_STRING_INNER: /.*?/ +_STRING_ESC_INNER: _STRING_INNER /(?In Free Enterprise, you are given the airship at the beginning of the game, and you may complete whatever quests are +>available to you in any order. Further, the Pass opens a passage in Toroia that will take you directly to Zeromus, +>whenever you are ready to defeat him. +>Additional options allow for randomization of key items, characters, treasures, shops, and more. +>There are some important gameplay changes from the original game to note; the NPCs in the training room cover the +>essentials, and a full detailed list of Free Enterprise's changes can be found at +>the [Free Enterprise Wiki](https://wiki.ff4fe.com/). + +Additionally, the Archipelago version of the randomizer adds in a couple of additional changes: + +- Key Items may be located anywhere, +- Monster-In-A-Box chests now include an additional item to incentivize checking them. +- Up to thirty-two objectives may be included when generating a game. + +## What is the goal of Final Fantasy IV: Free Enterprise when randomized? + +By default, the goal is to locate the Crystal and get to Zeromus and defeat him, either through the Pass item or by +proceding through the Lunar Subterrane. When objectives are enabled, clearing them can be set to reward the Crystal +or to clear the game outright, skipping Zeromus entirely. + +## What items and locations get shuffled? + +All 399 treasure locations (chests, pots, hidden items) and forty-three event reward locations have been shuffled, and +all items available in Final Fantasy IV: Free Enterprise are possibilities to appear in the item pool. + +Characters are shuffled amongst their normal recruitment locations. + +## Which items can be in another player's world? + +Any key item, piece of gear, or consumable item can appear in another world. + +## What does another world's item look like in Final Fantasy IV: Free Enterprise + +All items are unknown until earned, at which point the ingame text box will describe what has ben obtained or sent. + +## When the player receives an item, what happens? + +Items are placed directly into the inventory as long as the player is not in battle, in a cutscene, or the menu. +If no slots are available, items will not be sent until room is made using the ingame Trash functionality. diff --git a/worlds/ff4fe/docs/setup_en.md b/worlds/ff4fe/docs/setup_en.md new file mode 100644 index 000000000000..c98859e3f60d --- /dev/null +++ b/worlds/ff4fe/docs/setup_en.md @@ -0,0 +1,165 @@ +# Final Fantasy IV: Free Enterprise Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk) + - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware +- Your legally obtained Final Fantasy IV 1.1 ROM file, probably named ` +Final Fantasy II (USA) (Rev 1).sfc ` + +## Optional Software +- Final Fantasy IV: Free Enterprise Tracker + - PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/) + - Final Fantasy IV: Free Enterprise Archipelago PopTracker pack from: + [FFIV FE AP Tracker Releases Page](https://github.com/seto10987/Final-Fantasy-4-Free-Enterprise-AP-Poptracker-Pack/releases) + +## Installation Procedures + +### Windows Setup + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information.** +2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file. + This is yourFinal Fantasy IV ROM file. This only needs to be done once. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## 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 Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: +[Final Fantasy IV: Free Enterprise Player Options Page](/games/Final%20Fantasy%20IV%20Free%20Enterprise/player-options) + +### 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](/check) + +## 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 whomever 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 `.apff4fe` 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. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. This is done with the main menubar, under: + - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` + - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` +2. Load your ROM file if it hasn't already been loaded. + If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R). +3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window. + - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. + - You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua` + with the file picker. + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. + +## Frequently Asked Questions + +- I defeated a Monster in a Box (MIAB) and received a Cure1 potion but the client says I sent something else. +Did I break something? +- - For technical reasons, MIAB chests have to hand out an actual item. A Cure1 was chosen as the placeholder. +Everything is as it should be. +- Why are there dashes in my name? +- - Final Fantasy IV has a limited font and not every character can be displayed. These characters are replaced with +dashes. \ No newline at end of file diff --git a/worlds/ff4fe/events.py b/worlds/ff4fe/events.py new file mode 100644 index 000000000000..2fe25a32e024 --- /dev/null +++ b/worlds/ff4fe/events.py @@ -0,0 +1,82 @@ +from .locations import LocationData +boss_names = [ + 'D. Mist', + 'Officer', + 'Octomamm', + 'Antlion', + 'MomBomb', + 'Fabul Gauntlet', + 'Milon', + 'Milon Z.', + 'Mirror Cecil', + 'Guards', + 'Karate', + 'Baigan', + 'Kainazzo', + 'Dark Elf', + 'Magus Sisters', + 'Valvalis', + 'Calbrena', + 'Golbez', + 'Lugae', + 'Dark Imps', + 'King and Queen', + 'Rubicant', + 'EvilWall', + 'Asura', + 'Leviatan', + 'Odin', + 'Bahamut', + 'Elements', + 'CPU', + 'Pale Dim', + 'Wyvern', + 'Plague', + 'D. Lunars', + 'Ogopogo' +] + +boss_event_data = [ + ("Overworld", "MistCave", "D. Mist Slot"), + ("Overworld", "Kaipo", "Officer Slot"), + ("Overworld", "WateryPass", "Octomamm Slot"), + ("Overworld", "AntlionCave", "Antlion Slot"), + ("Overworld", "MountHobs", "MomBomb Slot"), + ("Overworld", "Fabul", "Fabul Gauntlet Slot"), + ("Overworld", "MountOrdeals", "Milon Slot"), + ("Overworld", "MountOrdeals", "Milon Z. Slot"), + ("Overworld", "MountOrdeals", "Mirror Cecil Slot"), + ("Overworld", "BaronWeaponShop", "Karate Slot"), + ("Overworld", "BaronWeaponShop", "Guards Slot"), + ("Overworld", "Sewer", "Baigan Slot"), + ("Overworld", "BaronCastle", "Kainazzo Slot"), + ("Overworld", "CaveMagnes", "Dark Elf Slot"), + ("Overworld", "Zot", "Magus Sisters Slot"), + ("Overworld", "Zot", "Valvalis Slot"), + ("Underworld", "DwarfCastle", "Calbrena Slot"), + ("Underworld", "DwarfCastle", "Golbez Slot"), + ("Overworld", "LowerBabil", "Lugae Slot"), + ("Overworld", "LowerBabil", "Dark Imp Slot"), + ("Underworld", "UpperBabil", "King and Queen Slot"), + ("Underworld", "UpperBabil", "Rubicant Slot"), + ("Underworld", "SealedCave", "Evilwall Slot"), + ("Underworld", "Feymarch", "Asura Slot"), + ("Underworld", "Feymarch", "Leviatan Slot"), + ("Overworld", "BaronCastle", "Odin Slot"), + ("Moon", "BahamutCave", "Bahamut Slot"), + ("Moon", "Giant", "Elements Slot"), + ("Moon", "Giant", "CPU Slot"), + ("Moon", "LunarCore", "Pale Dim Slot"), + ("Moon", "LunarCore", "Wyvern Slot"), + ("Moon", "LunarCore", "Plague Slot"), + ("Moon", "LunarCore", "D. Lunars Slot"), + ("Moon", "LunarCore", "Ogopogo Slot"), + ("Moon", "LunarCore", "Zeromus") +] + +boss_events = [] +boss_status_events = {boss: (f"{boss} Defeated") for boss in boss_names} +boss_slots = [(f"{boss} Slot Defeated") for boss in boss_names] + +for event in boss_event_data: + boss_events.append(LocationData(event[2], event[0], event[1], 0xFFFF, True)) diff --git a/worlds/ff4fe/flags.py b/worlds/ff4fe/flags.py new file mode 100644 index 000000000000..9c5087f5410d --- /dev/null +++ b/worlds/ff4fe/flags.py @@ -0,0 +1,218 @@ +from . import FF4FEOptions +# This file just translates our AP options into FE flagsets. + +def create_flags_from_options(options: FF4FEOptions, objective_count: int): + flags = (f"{build_objective_flags(options, objective_count)} " + f"Kmain/summon/moon/unsafe/miab " + f"{build_pass_flags(options)} " + f"{build_characters_flags(options)} " + f"Twild/junk " + f"{build_shops_flags(options)} " + f"{build_bosses_flags(options)} " + f"{build_encounters_flags(options)} " + f"Gdupe/mp/warp/life/sylph/backrow " + f"{build_misc_flags(options)}" + f"{build_starter_kit_flags(options)}") + return flags + +def build_objective_flags(options: FF4FEOptions, objective_count: int): + objective_flags = "Omode:" + primary_objectives = [] + if options.ForgeTheCrystal: + primary_objectives.append("classicforge") + if options.ConquerTheGiant: + primary_objectives.append("classicgiant") + if options.DefeatTheFiends: + primary_objectives.append("fiends") + if options.FindTheDarkMatter: + primary_objectives.append("dkmatter") + objective_flags += ",".join(primary_objectives) + if objective_flags == "Omode:": + objective_flags += "none" + if options.AdditionalObjectives.value != 0: + objective_flags += f"/random:{options.AdditionalObjectives.value}" + if options.ObjectiveReward.current_key == "crystal" or options.ForgeTheCrystal: + objective_flags += "/win:crystal" + else: + objective_flags += "/win:game" + objective_flags += f"/req:{min(objective_count, options.RequiredObjectiveCount.value)}" + return objective_flags + +def build_key_items_flags(options: FF4FEOptions): + pass + +def build_pass_flags(options: FF4FEOptions): + pass_flags = "" + pass_key_flags = "" + pass_shop_flags = "" + if options.PassEnabled: + pass_key_flags = f"key" + if options.PassInShops: + pass_shop_flags = f"shop" + if pass_key_flags != "" and pass_shop_flags != "": + pass_flags = f"P{pass_key_flags}/{pass_shop_flags}" + elif pass_key_flags == "" and pass_shop_flags == "": + pass_flags = " " + else: + pass_flags = (f"P{pass_key_flags}{pass_shop_flags}") + return pass_flags + +def build_characters_flags(options: FF4FEOptions): + party_size_flags = f"Cparty:{options.PartySize}/" + hero_challenge_flags = "" + if options.HeroChallenge.current_key != "none": + hero_challenge_flags = f"hero/start:{options.HeroChallenge.current_key.lower()}/" + free_character_flags = "" + if options.NoFreeCharacters: + free_character_flags += "nofree/" + if options.NoEarnedCharacters: + free_character_flags += "noearned/" + duplicate_character_flags = "" + if not options.AllowDuplicateCharacters: + duplicate_character_flags += "nodupes/" + permajoin_flags = "" + if options.CharactersPermajoin: + permajoin_flags = "permajoin/" + permadeath_flags = "" + if options.CharactersPermadie.current_key == "yes": + permadeath_flags = "permadeath/" + if options.CharactersPermadie.current_key == "extreme": + permadeath_flags = "permadeader/" + flags = (f"{party_size_flags}{hero_challenge_flags}" + f"{free_character_flags}{duplicate_character_flags}" + f"{permajoin_flags}{permadeath_flags}j:spells,abilities") + return flags + +def build_treasures_flags(options: FF4FEOptions): + pass + +def build_shops_flags(options: FF4FEOptions): + shops_flags = f"Sempty" + if options.ShopRandomization.current_key == "vanilla": + shops_flags = f"Svanilla" + elif options.ShopRandomization.current_key == "shuffle": + shops_flags = f"Sshuffle" + elif options.ShopRandomization.current_key == "standard": + shops_flags = f"Sstandard" + elif options.ShopRandomization.current_key == "pro": + shops_flags = f"Spro" + elif options.ShopRandomization.current_key == "wild": + shops_flags = f"Swild" + elif options.ShopRandomization.current_key == "cabins": + shops_flags = f"Scabins" + if options.FreeShops: + shops_flags += "/free" + return shops_flags + +def build_bosses_flags(options: FF4FEOptions): + bosses_flags = "Bstandard/alt:gauntlet/whichburn" + if options.NoFreeBosses: + bosses_flags += "/nofree" + return bosses_flags + +def build_encounters_flags(options: FF4FEOptions): + encounters_flags = "Etoggle" + if options.KeepDoorsBehemoths: + encounters_flags += "/keep:doors,behemoths" + return encounters_flags + +def build_glitches_flags(options: FF4FEOptions): + pass + +def build_misc_flags(options: FF4FEOptions): + spoon_flag = "-spoon" + adamant_flag = "-noadamants" if options.NoAdamantArmors else "" + wacky_flag = process_wacky_name(options.WackyChallenge.current_key) + return f"{spoon_flag} {adamant_flag} {wacky_flag}" + +def build_starter_kit_flags(options: FF4FEOptions): + kit1 = process_kit_name(options.StarterKitOne.current_key) + kit2 = process_kit_name(options.StarterKitTwo.current_key) + kit3 = process_kit_name(options.StarterKitThree.current_key) + if kit1 == "none": + kit1 = "" + else: + kit1 = f"-kit:{kit1}" + if kit2 == "none": + kit2 = "" + else: + kit2 = f"-kit2:{kit2}" + if kit3 == "none": + kit3 = "" + else: + kit3 = f"-kit3:{kit3}" + return f"{kit1} {kit2} {kit3}" + + +def process_kit_name(kit: str): + if kit == "grab_bag": + return "grabbag" + elif kit == "MIAB": + return "miab" + elif kit == "not_deme": + return "notdeme" + elif kit == "green_names": + return "green" + elif kit == "random_kit": + return "random" + else: + return kit + +def process_wacky_name(wacky: str): + wacky_flag = "" + if wacky == "none": + return wacky_flag + wacky_flag = "-wacky:" + if wacky == "battle_scars": + wacky_flag += "battlescars" + elif wacky == "the_bodyguard": + wacky_flag += "bodyguard" + elif wacky == "enemy_unknown": + wacky_flag += "enemyunknown" + elif wacky == "ff4_the_musical": + wacky_flag += "musical" + elif wacky == "fist_fight": + wacky_flag += "fistfight" + elif wacky == "the_floor_is_made_of_lava": + wacky_flag += "floorislava" + elif wacky == "forward_is_the_new_back": + wacky_flag += "forwardisback" + elif wacky == "friendly_fire": + wacky_flag += "friendlyfire" + elif wacky == "gotta_go_fast": + wacky_flag += "gottagofast" + elif wacky == "holy_onomatopoeia_batman": + wacky_flag += "batman" + elif wacky == "imaginary_numbers": + wacky_flag += "imaginarynumbers" + elif wacky == "is_this_even_randomized": + wacky_flag += "isthisrandomized" + elif wacky == "men_are_pigs": + wacky_flag += "menarepigs" + elif wacky == "a_much_bigger_magnet": + wacky_flag += "biggermagnet" + elif wacky == "mystery_juice": + wacky_flag += "mysteryjuice" + elif wacky == "neat_freak": + wacky_flag += "neatfreak" + elif wacky == "night_mode": + wacky_flag += "nightmode" + elif wacky == "payable_golbez": + wacky_flag += "payablegolbez" + elif wacky == "save_us_big_chocobo": + wacky_flag += "saveusbigchocobo" + elif wacky == "six_legged_race": + wacky_flag += "sixleggedrace" + elif wacky == "the_sky_warriors": + wacky_flag += "skywarriors" + elif wacky == "three_point_system": + wacky_flag += "3point" + elif wacky == "time_is_money": + wacky_flag += "timeismoney" + elif wacky == "world_championship_of_darts": + wacky_flag += "darts" + elif wacky == "random_challenge": + wacky_flag += "random" + else: + wacky_flag += wacky + return wacky_flag \ No newline at end of file diff --git a/worlds/ff4fe/itempool.py b/worlds/ff4fe/itempool.py new file mode 100644 index 000000000000..d2f281d62f9d --- /dev/null +++ b/worlds/ff4fe/itempool.py @@ -0,0 +1,104 @@ +from typing import List, Tuple + +from . import items +from .locations import LocationData, free_character_locations, earned_character_locations +from ..AutoWorld import World + + +def create_itempool(locations: List[LocationData], world: World) -> Tuple[List[str], str, str]: + chosen_character = get_chosen_character(world) + character_pool = create_character_pool(world, chosen_character) + second_starter = get_second_character(world, chosen_character, character_pool) + key_item_pool = create_key_item_pool(world) + location_count = len(locations) - len(character_pool) - len(key_item_pool) - 33 # Objective Status locations hack + if world.is_vanilla_game(): + location_count -= 1 # We aren't using the Objective Reward location. + if (world.options.HeroChallenge.current_key != "none" + and not world.options.ForgeTheCrystal): + location_count -= 1 # We're manually placing the Advance Weapon at Kokkol + if world.options.ConquerTheGiant: + location_count -= 1 # No Kain3 location in Giant% + useful_percentage = world.options.UsefulPercentage.value + useful_count = location_count * useful_percentage // 100 + result_pool = [] + result_pool.extend(character_pool) + result_pool.extend(key_item_pool) + result_pool.extend(world.random.choices([item.name for item in items.useful_items + if item.tier <= world.options.MaxTier.value + and not (item.name == "Adamant Armor" and world.options.NoAdamantArmors)], + k=useful_count)) + result_pool.extend(world.random.choices([item.name for item in items.filler_items + if item.tier >= world.options.MinTier.value], + k=location_count - useful_count)) + return (result_pool, chosen_character, second_starter) + + +def create_character_pool(world: World, chosen_character: str) -> List[str]: + # Create the pool of characters to place. + character_pool = [] + allowed_characters = sorted([character for character in world.options.AllowedCharacters.value if character != "None"]) + # If we have a Hero, we only get the one unless there's no other choice. + if chosen_character != "None" and world.options.HeroChallenge.current_key != "none": + if chosen_character in allowed_characters and len(allowed_characters) > 1: + allowed_characters.remove(chosen_character) + character_slots = 18 # All slots + filled_character_slots = 0 + character_pool.append(chosen_character) + filled_character_slots += 1 + if world.options.NoFreeCharacters: + filled_character_slots += len(free_character_locations) + if world.options.NoEarnedCharacters: + filled_character_slots += len(earned_character_locations) + elif world.options.ConquerTheGiant: + character_slots -= 1 # Kain3 slot goes unused in this objective + if (character_slots - filled_character_slots) > len(allowed_characters): + if world.options.EnsureAllCharacters: + character_pool.extend([character for character in allowed_characters if character != "None"]) + filled_character_slots += len(allowed_characters) + character_pool.extend(world.random.choices(allowed_characters, k=(character_slots - filled_character_slots))) + else: + character_pool.extend(world.random.sample(allowed_characters, character_slots - filled_character_slots)) + for x in range(len(character_pool), character_slots): + character_pool.append("None") + return character_pool[:18] + + +def get_chosen_character(world: World): + # Get a starting character. A Hero if applicable, otherwise random from the list of unrestricted characters. + if world.options.HeroChallenge.current_key != "none": + option_value = str(world.options.HeroChallenge.current_key) + if option_value == "random_character": + chosen_character = world.random.choice([character_choice for character_choice in items.characters if character_choice != "None"]) + else: + chosen_character = option_value.capitalize() + else: + allowed_characters = world.options.AllowedCharacters.value - world.options.RestrictedCharacters.value - {"None"} + if len(allowed_characters) > 0: + chosen_character = world.random.choice(sorted(allowed_characters)) + else: + chosen_character = world.random.choice(sorted(world.options.AllowedCharacters.value)) + return chosen_character + +def get_second_character(world: World, chosen_character: str, character_pool: List[str]): + pruned_pool = [character for character in character_pool if character != "None" + and character not in world.options.RestrictedCharacters.value] + if world.options.AllowDuplicateCharacters.value == True: + pruned_pool = [character for character in pruned_pool if character != chosen_character] + if len(pruned_pool) > 0: + return world.random.choice(sorted(pruned_pool)) + else: + return chosen_character + + + +def create_key_item_pool(world: World) -> List[str]: + key_item_pool = [item.name for item in items.key_items] + if not world.options.PassEnabled: + key_item_pool.remove("Pass") + if world.options.FindTheDarkMatter: + dark_matter_count = 45 # Placeholder until option for number of Dark Matters is made + for i in range(dark_matter_count): + key_item_pool.append("DkMatter") + return key_item_pool + + diff --git a/worlds/ff4fe/items.csvdb b/worlds/ff4fe/items.csvdb new file mode 100644 index 000000000000..d26ac32e3fed --- /dev/null +++ b/worlds/ff4fe/items.csvdb @@ -0,0 +1,257 @@ +code,const,name,spoilername,category,subtype,j,flag,tier,shopoverride,price,throw,metal,longrange,twohanded,equip +0x00,#item.NoWeapon,,,,,,D,,,,,,,, +0x01,#item.FireClaw,[claw]FireClaw,Fire Claw,weapon,claw,,,2,,350,,,,,"yang,edge" +0x02,#item.IceClaw,[claw]IceClaw,Ice Claw,weapon,claw,,,2,,450,,,,,"yang,edge" +0x03,#item.ThunderClaw,[claw]Thunder,Thunder Claw,weapon,claw,,,3,,5000,,,,,"yang,edge" +0x04,#item.CharmClaw,[claw]Charm,Charm Claw,weapon,claw,,,2,,1000,,,,,"yang,edge" +0x05,#item.PoisonClaw,[claw]Poison,Poison Claw,weapon,claw,,,3,,3000,,,,,"yang,edge" +0x06,#item.CatClaw,[claw]CatClaw,Cat Claw,weapon,claw,,,4,,10000,,,,,"yang,edge" +0x07,#item.Rod,[rod]Rod,Rod,weapon,rod,,,1,,100,,,,,"crydia,tellah,palom,arydia,fusoya" +0x08,#item.IceRod,[rod]IceRod,Ice Rod,weapon,rod,,,1,,220,,,,,"crydia,tellah,palom,arydia,fusoya" +0x09,#item.FlameRod,[rod]FlameRod,Flame Rod,weapon,rod,,,2,,380,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0A,#item.ThunderRod,[rod]Thunder,Thunder Rod,weapon,rod,,,2,,700,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0B,#item.Change,[rod]Change,Change Rod,weapon,rod,,,3,,8000,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0C,#item.CharmRod,[rod]Charm,Charm Rod,weapon,rod,,,5,,20000,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0D,#item.StardustRod,[rod]Stardust,Stardust Rod,weapon,rod,,,6,,60000,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0E,#item.Lilith,[rod]Lilith,Lilith Rod,weapon,rod,,,3,,8000,,,,,"crydia,tellah,palom,arydia,fusoya" +0x0F,#item.Staff,[staff]Staff,Staff,weapon,staff,,,1,,160,,,,,"crydia,tellah,rosa,porom,pcecil,fusoya" +0x10,#item.Cure,[staff]Cure,Cure Staff,weapon,staff,,,1,,480,,,,,"crydia,tellah,rosa,porom,pcecil,fusoya" +0x11,#item.SilverStaff,[staff]Silver,Silver Staff,weapon,staff,,,2,,4000,,yes,,,"crydia,tellah,rosa,porom,pcecil,fusoya" +0x12,#item.PowerStaff,[staff]Power,Power Staff,weapon,staff,,,3,,2000,,,,,"crydia,tellah,rosa,porom,fusoya" +0x13,#item.Lunar,[staff]Lunar,Lunar Staff,weapon,staff,,,5,,30000,,,,,"crydia,tellah,rosa,porom,fusoya" +0x14,#item.LifeStaff,[staff]Life,Life Staff,weapon,staff,,,6,,50000,,,,,"crydia,tellah,rosa,porom,fusoya" +0x15,#item.Silence,[staff]Silence,Silence Staff,weapon,staff,,,5,,30000,,,,,"crydia,tellah,rosa,porom,fusoya" +0x16,#item.ShadowSword,[darksword]Shadow,Shadow Sword,weapon,darksword,,,1,,700,,yes,,,dkcecil +0x17,#item.DarknessSword,[darksword]Darkness,Darkness Sword,weapon,darksword,,,1,,1200,,yes,,,dkcecil +0x18,#item.BlackSword,[darksword]Black,Black Sword,weapon,darksword,,,3,,3000,,yes,,,dkcecil +0x19,#item.Legend,[lightsword]Legend,Legend Sword,weapon,lightsword,,K,,,,,yes,,,pcecil +0x1A,#item.Light,[lightsword]Light,Light Sword,weapon,lightsword,,,5,,66000,yes,yes,,,pcecil +0x1B,#item.Excalibur,[lightsword]Excalbur,Excalibur,weapon,lightsword,,,8,,190000,yes,yes,,,pcecil +0x1C,#item.FireBrand,[sword]Fire,Fire Sword,weapon,sword,,,3,,14000,,yes,,,"kain,pcecil" +0x1D,#item.IceBrand,[sword]IceBrand,Ice Brand,weapon,sword,,,4,,26000,,yes,,,"kain,pcecil" +0x1E,#item.Defense,[sword]Defense,Defense Sword,weapon,sword,,,6,,98000,yes,yes,,,"kain,pcecil" +0x1F,#item.DrainSword,[sword]Drain,Drain Sword,weapon,sword,,,3,,5000,yes,yes,,,"kain,pcecil" +0x20,#item.Ancient,[sword]Ancient,Ancient Sword,weapon,sword,,,1,,3000,yes,yes,,,"kain,pcecil" +0x21,#item.Sleep,[sword]Slumber,Slumber Sword,weapon,sword,,,3,,6000,yes,yes,,,"kain,pcecil" +0x22,#item.MedusaSword,[sword]Medusa,Medusa Sword,weapon,sword,,D,,,18000,yes,yes,,,"kain,pcecil" +0x23,#item.Spear,[spear]Spear,Spear,weapon,spear,,,1,,60,,yes,,,kain +0x24,#item.Wind,[spear]Wind,Wind Spear,weapon,spear,,,2,,7000,,yes,,,kain +0x25,#item.FlameSpear,[spear]Flame,Flame Spear,weapon,spear,,,3,,11000,,yes,,,kain +0x26,#item.BlizzardSpear,[spear]Blizzard,Blizzard Spear,weapon,spear,,,4,,21000,,yes,,,kain +0x27,#item.DragoonSpear,[spear]Dragoon,Dragoon Spear,weapon,spear,,,7,,110000,yes,yes,,,kain +0x28,#item.WhiteSpear,[spear]White,White Spear,weapon,spear,,,6,,130000,yes,yes,,,kain +0x29,#item.DrainSpear,[spear]Drain,Drain Spear,weapon,spear,,,5,,12000,yes,yes,,,kain +0x2A,#item.Gungnir,[spear]Gungnir,Gungnir,weapon,spear,,,5,,66000,yes,yes,,,kain +0x2B,#item.Short,[katana]Short,Short Katana,weapon,ninjasword,,,2,,4000,yes,yes,,,edge +0x2C,#item.Middle,[katana]Middle,Middle Katana,weapon,ninjasword,,,3,,7000,,yes,,,edge +0x2D,#item.Long,[katana]Long,Long Katana,weapon,ninjasword,,,4,,22000,,yes,,,edge +0x2E,#item.NinjaSword,[katana]Ninja,Ninja Sword,weapon,ninjasword,,,5,,44000,yes,yes,,,edge +0x2F,#item.Murasame,[katana]Murasame,Murasame,weapon,ninjasword,,,6,,66000,yes,yes,,,edge +0x30,#item.Masamune,[katana]Masamune,Masamune,weapon,ninjasword,,,7,,88000,yes,yes,,,edge +0x31,#item.Assassin,[knife]Assassin,Assassin Dagger,weapon,dagger,,,5,,20000,yes,yes,,,"kain,crydia,edward,palom,pcecil,arydia,edge" +0x32,#item.MuteDagger,[knife]Mute,Mute Dagger,weapon,dagger,,,4,,15000,yes,yes,,,"kain,crydia,edward,palom,pcecil,arydia,edge" +0x33,#item.Whip,[whip]Whip,Whip,weapon,whip,,,1,,3000,,,yes,,"crydia,arydia" +0x34,#item.Chain,[whip]Chain,Chain Whip,weapon,whip,,,1,,6000,,,yes,,"crydia,arydia" +0x35,#item.Blitz,[whip]Blitz,Blitz Whip,weapon,whip,,,3,,10000,,,yes,,"crydia,arydia" +0x36,#item.FlameWhip,[whip]Flame,Flame Whip,weapon,whip,,,4,,16000,,,yes,,"crydia,arydia" +0x37,#item.DragonWhip,[whip]Dragon,Dragon Whip,weapon,whip,,,5,,31000,,,yes,,"crydia,arydia" +0x38,#item.HandAxe,[axe]HandAxe,Hand Axe,weapon,axe,,,1,,3000,yes,yes,,,"kain,pcecil,cid" +0x39,#item.Dwarf,[axe]Dwarf,Dwarf Axe,weapon,axe,,,4,,15000,,yes,yes,,"kain,pcecil,cid" +0x3A,#item.Ogre,[axe]Ogre,Ogre Axe,weapon,axe,,,4,,45000,,yes,,,"kain,pcecil,cid" +0x3B,#item.SilverDagger,[knife]Silver,Silver Dagger,weapon,dagger,,,2,,3000,yes,yes,,,"kain,crydia,edward,palom,pcecil,arydia,edge" +0x3C,#item.Dancing,[knife]Dancing,Dancing Dagger,weapon,dagger,,,3,,5000,yes,yes,,,"kain,crydia,edward,palom,pcecil,arydia,edge" +0x3D,#item.SilverSword,[sword]Silver,Silver Sword,weapon,sword,,,2,,6000,,yes,,,"kain,pcecil" +0x3E,#item.Spoon,[knife]Spoon,Spoon,weapon,dagger,,K,,,10000,yes,yes,,, +0x3F,#item.CrystalSword,[lightsword]Crystal,Crystal Sword,weapon,lightsword,,,8,,215000,,yes,,,pcecil +0x40,#item.Shuriken,[shuriken]Shuriken,Shuriken,weapon,star,,,3,,5000,yes,yes,yes,, +0x41,#item.NinjaStar,[shuriken]Ninja,Ninja Star,weapon,star,,,5,,10000,yes,yes,yes,, +0x42,#item.Boomrang,[boomerang]Boomrang,Boomerang,weapon,boomerang,,,2,,3000,,,yes,,edge +0x43,#item.FullMoon,[boomerang]FullMoon,Full Moon,weapon,boomerang,,,5,,24000,,,yes,,edge +0x44,#item.Dreamer,[harp]Dreamer,Dreamer Harp,weapon,harp,,,1,,480,,,yes,yes,edward +0x45,#item.CharmHarp,[harp]Charm,Charm Harp,weapon,harp,,,2,,1200,,,yes,yes,edward +0x46,#item.fe_CustomWeapon,,,,,,D,,,,,yes,,yes, +0x47,#item.PoisonAxe,[axe]Poison,Poison Axe,weapon,axe,,,4,,25000,,yes,,yes,"kain,pcecil,cid" +0x48,#item.RuneAxe,[axe]RuneAxe,Rune Axe,weapon,axe,,,5,,45000,,yes,,yes,"kain,pcecil,cid" +0x49,#item.SilverHammer,[wrench]Silver,Silver Hammer,weapon,hammer,,,2,,8000,,yes,,yes,cid +0x4A,#item.EarthHammer,[wrench]Earth,Earth Hammer,weapon,hammer,,,4,,30000,,yes,,yes,cid +0x4B,#item.Wooden,[wrench]Wooden,Wooden Hammer,weapon,hammer,,,1,,80,,,,yes,cid +0x4C,#item.Avenger,[sword]Avenger,Avenger,weapon,sword,,,7,,150000,yes,yes,,yes,"kain,pcecil" +0x4D,#item.ShortBow,[bow]ShortBow,Short Bow,weapon,bow,,,1,,220,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x4E,#item.CrossBow,[bow]CrossBow,Cross Bow,weapon,bow,,,1,,700,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x4F,#item.GreatBow,[bow]GreatBow,Great Bow,weapon,bow,,,2,,2000,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x50,#item.Archer,[bow]Archer,Archer Bow,weapon,bow,,,3,,3000,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x51,#item.ElvenBow,[bow]ElvenBow,Elven Bow,weapon,bow,,,4,,10000,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x52,#item.SamuraiBow,[bow]Samurai,Samurai Bow,weapon,bow,,,5,,17000,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x53,#item.ArtemisBow,[bow]Artemis,Artemis Bow,weapon,bow,,,6,,80000,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x54,#item.IronArrow,[arrow]Iron,Iron Arrows,weapon,arrow,,,1,,10,,yes,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x55,#item.WhiteArrow,[arrow]White,White Arrows,weapon,arrow,,,2,,20,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x56,#item.FireArrow,[arrow]Fire,Fire Arrows,weapon,arrow,,,2,,30,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x57,#item.IceArrow,[arrow]Ice,Ice Arrows,weapon,arrow,,,2,,30,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x58,#item.LitArrow,[arrow]Lit,Lit Arrows,weapon,arrow,,,2,,30,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x59,#item.DarknessArrow,[arrow]Darkness,Darkness Arrows,weapon,arrow,,,2,,40,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5A,#item.PoisonArrow,[arrow]Poison,Poison Arrows,weapon,arrow,,,3,,70,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5B,#item.MuteArrow,[arrow]Mute,Mute Arrows,weapon,arrow,,,3,,100,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5C,#item.CharmArrow,[arrow]Charm,Charm Arrows,weapon,arrow,,,3,,110,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5D,#item.SamuraiArrow,[arrow]Samurai,Samurai Arrows,weapon,arrow,,,5,,140,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5E,#item.MedusaArrow,[arrow]Medusa,Medusa Arrows,weapon,arrow,,,3,,1250,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x5F,#item.ArtemisArrow,[arrow]Artemis,Artemis Arrows,weapon,arrow,,,7,,500,,,yes,,"crydia,edward,rosa,palom,porom,pcecil,cid,arydia,fusoya" +0x60,#item.NoArmor,,,,,,D,,,,,,,, +0x61,#item.IronShield,[shield]Iron,Iron Shield,armor,shield,,,1,,100,,yes,,,"kain,pcecil,cid" +0x62,#item.ShadowShield,[shield]Shadow,Shadow Shield,armor,shield,,,1,,200,,yes,,,dkcecil +0x63,#item.BlackShield,[shield]Black,Black Shield,armor,shield,,,1,,400,,yes,,,dkcecil +0x64,#item.PaladinShield,[shield]Paladin,Paladin Shield,armor,shield,,,1,,700,,yes,,,pcecil +0x65,#item.SilverShield,[shield]Silver,Silver Shield,armor,shield,,,1,,1000,,yes,,,"kain,pcecil,cid" +0x66,#item.FireShield,[shield]Fire,Fire Shield,armor,shield,,,2,,1250,,yes,,,"kain,pcecil,cid" +0x67,#item.IceShield,[shield]Ice,Ice Shield,armor,shield,,,2,,10000,,yes,,,"kain,pcecil,cid" +0x68,#item.DiamondShield,[shield]Diamond,Diamond Shield,armor,shield,,,3,,15000,,,,,"kain,pcecil,cid" +0x69,#item.Aegis,[shield]Aegis,Aegis Shield,armor,shield,,,4,,20000,,yes,,,"kain,pcecil,cid" +0x6A,#item.SamuraiShield,[shield]Samurai,Samurai Shield,armor,shield,,,4,,20000,,yes,,,"kain,pcecil,cid" +0x6B,#item.DragoonShield,[shield]Dragoon,Dragoon Shield,armor,shield,,,4,,25000,,,,,"kain,pcecil,cid" +0x6C,#item.CrystalShield,[shield]Crystal,Crystal Shield,armor,shield,,,5,,35000,,,,,pcecil +0x6D,#item.IronHelm,[helmet]Iron,Iron Helm,armor,helmet,,,1,,150,,yes,,,"kain,pcecil,cid" +0x6E,#item.ShadowHelm,[helmet]Shadow,Shadow Helm,armor,helmet,,,1,,360,,yes,,,dkcecil +0x6F,#item.DarknessHelm,[helmet]Darkness,Darkness Helm,armor,helmet,,,1,,640,,yes,,,dkcecil +0x70,#item.BlackHelm,[helmet]Black,Black Helm,armor,helmet,,,1,,980,,yes,,,dkcecil +0x71,#item.PaladinHelm,[helmet]Paladin,Paladin Helm,armor,helmet,,,1,,4000,,yes,,,pcecil +0x72,#item.SilverHelm,[helmet]Silver,Silver Helm,armor,helmet,,,2,,3000,,yes,,,"kain,pcecil,cid" +0x73,#item.DiamondHelm,[helmet]Diamond,Diamond Helm,armor,helmet,,,3,,10000,,,,,"kain,pcecil,cid" +0x74,#item.SamuraiHelm,[helmet]Samurai,Samurai Helm,armor,helmet,,,4,,15000,,,,,"kain,pcecil,cid,edge" +0x75,#item.DragoonHelm,[helmet]Dragoon,Dragoon Helm,armor,helmet,,,5,,20000,,,,,"kain,pcecil,cid" +0x76,#item.CrystalHelm,[helmet]Crystal,Crystal Helm,armor,helmet,,,5,,30000,,,,,pcecil +0x77,#item.Cap,[helmet]Cap,Cap,armor,hat,,,1,,100,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x78,#item.LeatherHat,[helmet]Leather,Leather Hat,armor,hat,,,1,,330,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x79,#item.GaeaHat,[helmet]Gaea,Gaea Hat,armor,hat,,,3,,4000,,,,,"crydia,tellah,rosa,palom,porom,pcecil,arydia,fusoya" +0x7A,#item.WizardHat,[helmet]Wizard,Wizard Hat,armor,hat,,,4,,9000,,,,,"crydia,tellah,rosa,palom,porom,pcecil,arydia,fusoya" +0x7B,#item.Tiara,[helmet]Tiara,Tiara,armor,hat,,,5,,20000,,yes,,,"crydia,rosa,porom,arydia" +0x7C,#item.Ribbon,[helmet]Ribbon,Ribbon,armor,hat,,,6,,65000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x7D,#item.Headband,[helmet]Headband,Headband,armor,hat,,,3,,10000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x7E,#item.Bandanna,[helmet]Bandanna,Bandanna,armor,hat,,,4,,12000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x7F,#item.NinjaHelm,[helmet]Ninja,Ninja Mask,armor,hat,,,5,,25000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x80,#item.Glass,[helmet]Glass,Glass Mask,armor,hat,,,6,,45000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x81,#item.IronArmor,[armor]Iron,Iron Armor,armor,armor,,,1,,600,,yes,,,"kain,pcecil,cid" +0x82,#item.ShadowArmor,[armor]Shadow,Shadow Armor,armor,armor,,,1,,1100,,yes,,,dkcecil +0x83,#item.DarknessArmor,[armor]Darkness,Darkness Armor,armor,armor,,,1,,2000,,yes,,,dkcecil +0x84,#item.BlackArmor,[armor]Black,Black Armor,armor,armor,,,2,,3000,,yes,,,dkcecil +0x85,#item.PaladinArmor,[armor]Paladin,Paladin Armor,armor,armor,,,1,,8000,,yes,,,pcecil +0x86,#item.SilverArmor,[armor]Silver,Silver Armor,armor,armor,,,2,,17000,,yes,,,"kain,pcecil,cid" +0x87,#item.FireArmor,[armor]Fire,Fire Armor,armor,armor,,,3,,15000,,yes,,,"kain,pcecil,cid" +0x88,#item.IceArmor,[armor]Ice,Ice Armor,armor,armor,,,3,,18000,,yes,,,"kain,pcecil,cid" +0x89,#item.DiamondArmor,[armor]Diamond,Diamond Armor,armor,armor,,,4,,20000,,,,,"kain,pcecil,cid" +0x8A,#item.Genji,[armor]Samurai,Samurai Armor,armor,armor,,,4,,20000,,,,,"kain,pcecil,cid,edge" +0x8B,#item.DragonArmor,[armor]Dragoon,Dragoon Armor,armor,armor,,,5,,30000,,,,,"kain,pcecil,cid" +0x8C,#item.CrystalArmor,[armor]Crystal,Crystal Armor,armor,armor,,,5,,65000,,,,,pcecil +0x8D,#item.Cloth,[shirt]Cloth,Cloth Armor,armor,robe,,,1,,50,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x8E,#item.LeatherArmor,[shirt]Leather,Leather Armor,armor,robe,,,1,,200,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x8F,#item.GaeaArmor,[shirt]Gaea,Gaea Robe,armor,robe,,,2,,2000,,,,,"crydia,tellah,rosa,palom,porom,pcecil,arydia,fusoya" +0x90,#item.WizardArmor,[shirt]Wizard,Wizard Robe,armor,robe,,,4,,11000,,,,,"crydia,tellah,rosa,palom,porom,pcecil,arydia,fusoya" +0x91,#item.BlackRobe,[shirt]Black,Black Shirt,armor,robe,,,5,,20000,,,,,"crydia,tellah,palom,arydia,fusoya" +0x92,#item.Sorcerer,[shirt]Sorcerer,Sorcerer Robe,armor,robe,,,5,,30000,,,,,"crydia,tellah,rosa,palom,porom,pcecil,arydia,fusoya" +0x93,#item.WhiteRobe,[shirt]White,White Shirt,armor,robe,,,7,,85000,,,,,"crydia,tellah,rosa,porom,pcecil,fusoya" +0x94,#item.PowerRobe,[shirt]Power,Power Shirt,armor,robe,,,6,,70000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x95,#item.Heroine,[shirt]Heroine,Heroine Armor,armor,robe,,,5,,40000,,,,,"crydia,rosa,porom,arydia" +0x96,#item.Prisoner,[shirt]Prisoner,Prisoner Clothes,armor,robe,,,1,,70,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x97,#item.Bard,[shirt]Bard,Bard Tunic,armor,robe,,,1,,70,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x98,#item.Karate,[shirt]Karate,Karate Gi,armor,robe,,,3,,7000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x99,#item.BlBelt,[shirt]Bl.Belt,Black Belt,armor,robe,,,4,,20000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x9A,#item.AdamantArmor,[armor]Adamant,Adamant Armor,armor,armor,,,8,,500000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0x9B,#item.NinjaArmor,[shirt]Ninja,Ninja Shirt,armor,robe,,,5,,50000,,,,,edge +0x9C,#item.IronGauntlet,[gauntlet]Iron,Iron Gauntlet,armor,gauntlet,,,1,,130,,yes,,,"kain,pcecil,cid,edge" +0x9D,#item.ShadowGauntlet,[gauntlet]Shadow,Shadow Gauntlet,armor,gauntlet,,,1,,260,,yes,,,dkcecil +0x9E,#item.DarknessGauntlet,[gauntlet]Darkness,Darkness Gauntlet,armor,gauntlet,,,1,,520,,yes,,,dkcecil +0x9F,#item.BlackGauntlet,[gauntlet]Black,Black Gauntlet,armor,gauntlet,,,1,,800,,yes,,,dkcecil +0xA0,#item.PaladinGauntlet,[gauntlet]Paladin,Paladin Gauntlet,armor,gauntlet,,,1,,3000,,yes,,,pcecil +0xA1,#item.SilverGauntlet,[gauntlet]Silver,Silver Gauntlet,armor,gauntlet,,,1,,2000,,yes,,,"kain,pcecil,cid,edge" +0xA2,#item.DiamondGauntlet,[gauntlet]Diamond,Diamond Gauntlet,armor,gauntlet,,,2,,5000,,,,,"kain,pcecil,cid" +0xA3,#item.Zeus,[gauntlet]Zeus,Zeus Gauntlet,armor,gauntlet,,,6,,65000,,,,,"kain,yang,pcecil,cid,arydia,edge" +0xA4,#item.SamuraiGauntlet,[gauntlet]Samurai,Samurai Gauntlet,armor,gauntlet,,,3,,10000,,,,,"kain,pcecil,cid,edge" +0xA5,#item.DragoonGauntlet,[gauntlet]Dragoon,Dragoon Gauntlet,armor,gauntlet,,,4,,20000,,,,,"kain,pcecil,cid" +0xA6,#item.CrystalGauntlet,[gauntlet]Crystal,Crystal Gauntlet,armor,gauntlet,,,5,,20000,,,,,pcecil +0xA7,#item.IronRing,[ring]IronRing,Iron Ring,armor,ring,,,1,,100,,yes,,,"crydia,tellah,edward,rosa,yang,palom,porom,arydia,edge,fusoya" +0xA8,#item.RubyRing,[ring]RubyRing,Ruby Ring,armor,ring,,,1,,500,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0xA9,#item.SilverRing,[ring]Silver,Silver Ring,armor,ring,,,2,,2000,,yes,,,"crydia,tellah,edward,rosa,yang,palom,porom,arydia,edge,fusoya" +0xAA,#item.Strength,[ring]Strength,Strength Ring,armor,ring,,,5,,15000,,yes,,,"kain,yang,pcecil,cid,arydia,edge" +0xAB,#item.Rune,[ring]Rune,Rune Ring,armor,ring,,,4,,20000,,,,,"crydia,tellah,edward,rosa,yang,palom,porom,arydia,edge,fusoya" +0xAC,#item.CrystalRing,[ring]Crystal,Crystal Ring,armor,ring,,,6,,50000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0xAD,#item.DiamondRing,[ring]Diamond,Diamond Ring,armor,ring,,,3,,4000,,,,,"crydia,tellah,edward,rosa,yang,palom,porom,arydia,edge,fusoya" +0xAE,#item.Protect,[ring]Protect,Protect Ring,armor,ring,,,6,,30000,,,,,"kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0xAF,#item.Cursed,[ring]Cursed,Cursed Ring,armor,ring,,,4,,66000,,,,,"dkcecil,kain,crydia,tellah,edward,rosa,yang,palom,porom,pcecil,cid,arydia,edge,fusoya" +0xB0,#item.Bomb,[$15]Bomb,Bomb,item,attack,J,,1,,200,,,,, +0xB1,#item.BigBomb,[$15]BigBomb,BigBomb,item,attack,J,,4,wild,7000,,,,, +0xB2,#item.Notus,[$15]Notus,Notus,item,attack,J,,1,,200,,,,, +0xB3,#item.Boreas,[$15]Boreas,Boreas,item,attack,J,,4,wild,7000,,,,, +0xB4,#item.ThorRage,[$15]ThorRage,ThorRage,item,attack,J,,2,,200,,,,, +0xB5,#item.ZeusRage,[$15]ZeusRage,ZeusRage,item,attack,J,,4,wild,7000,,,,, +0xB6,#item.Stardust,[$15]Stardust,Stardust,item,attack,J,,4,wild,8000,,,,, +0xB7,#item.Succubus,[$15]Succubus,Succubus,item,support,J,,3,,350,,,,, +0xB8,#item.Vampire,[$15]Vampire,Vampire,item,support,J,,4,,4000,,,,, +0xB9,#item.Bacchus,[$15]Bacchus,Bacchus,item,support,J,,5,,5000,,,,, +0xBA,#item.Hermes,[$15]Hermes,Hermes,item,support,J,,2,,2000,,,,, +0xBB,#item.HrGlass1,[$15]HrGlass1,HrGlass1,item,attack,J,,99,wild,6000,,,,, +0xBC,#item.HrGlass2,[$15]HrGlass2,HrGlass2,item,attack,J,,5,,6000,,,,, +0xBD,#item.HrGlass3,[$15]HrGlass3,HrGlass3,item,attack,J,,99,wild,6000,,,,, +0xBE,#item.SilkWeb,[$15]SilkWeb,SilkWeb,item,attack,J,,3,,3000,,,,, +0xBF,#item.Illusion,[$15]Illusion,Illusion,item,support,J,,4,,4000,,,,, +0xC0,#item.FireBomb,[$15]FireBomb,FireBomb,item,attack,J,,4,wild,3000,,,,, +0xC1,#item.Blizzard,[$15]Blizzard,Blizzard,item,attack,J,,4,wild,3000,,,,, +0xC2,#item.LitBolt,[$15]Lit-Bolt,Lit-Bolt,item,attack,J,,4,wild,3000,,,,, +0xC3,#item.StarVeil,[$15]StarVeil,StarVeil,item,support,J,,2,,3000,,,,, +0xC4,#item.Kamikaze,[$15]Kamikaze,Kamikaze,item,attack,J,,3,,2000,,,,, +0xC5,#item.MoonVeil,[$15]MoonVeil,MoonVeil,item,support,J,,7,,20000,,,,, +0xC6,#item.MuteBell,[$15]MuteBell,MuteBell,item,attack,J,,2,,500,,,,, +0xC7,#item.GaiaDrum,[$15]GaiaDrum,GaiaDrum,item,attack,J,,4,wild,8000,,,,, +0xC8,#item.Crystal,[crystal]Crystal,Crystal,item,key,,K,,,,,,,, +0xC9,#item.Coffin,[$15]Coffin,Coffin,item,attack,J,,5,,5000,,,,, +0xCA,#item.Grimoire,[$15]Grimoire,Grimoire,item,attack,J,,4,wild,6000,,,,, +0xCB,#item.Bestiary,[$15]Bestiary,Bestiary,item,support,J,,1,,300,,,,, +0xCC,#item.Alarm,[$15]Alarm,Alarm,item,support,J,,1,,200,,,,, +0xCD,#item.Unihorn,[$15]Unihorn,Unihorn,item,support,J,,2,,500,,,,, +0xCE,#item.Cure1,[potion]Cure1,Cure1,item,potion,,,1,,30,,,,, +0xCF,#item.Cure2,[potion]Cure2,Cure2,item,potion,,,3,,300,,,,, +0xD0,#item.Cure3,[potion]Cure3,Cure3,item,potion,,,4,,3000,,,,, +0xD1,#item.Ether1,[potion]Ether1,Ether1,item,potion,,,3,,1000,,,,, +0xD2,#item.Ether2,[potion]Ether2,Ether2,item,potion,,,4,,5000,,,,, +0xD3,#item.Elixir,[potion]Elixir,Elixir,item,potion,,,5,,30000,,,,, +0xD4,#item.Life,[potion]Life,Life,item,potion,,,2,,1000,,,,, +0xD5,#item.Soft,[$15]Soft,Soft,item,potion,J,,1,,400,,,,, +0xD6,#item.MaidKiss,[$15]MaidKiss,MaidKiss,item,potion,J,,1,,60,,,,, +0xD7,#item.Mallet,[$15]Mallet,Mallet,item,potion,J,,1,,80,,,,, +0xD8,#item.DietFood,[$15]DietFood,DietFood,item,potion,J,,1,,100,,,,, +0xD9,#item.EchoNote,[$15]EchoNote,EchoNote,item,potion,J,,1,,50,,,,, +0xDA,#item.EyeDrops,[$15]Eyedrops,Eyedrops,item,potion,J,,1,,30,,,,, +0xDB,#item.Antidote,[$15]Antidote,Antidote,item,potion,J,,1,,40,,,,, +0xDC,#item.Cross,[$15]Cross,Cross,item,potion,J,,1,,100,,,,, +0xDD,#item.Heal,[potion]Heal,Heal,item,potion,,,3,,100,,,,, +0xDE,#item.Siren,[$15]Siren,Siren,item,siren,J,,5,,4000,,,,, +0xDF,#item.AuApple,[$15]AuApple,AuApple,item,apple,J,,6,,40000,,,,, +0xE0,#item.AgApple,[$15]AgApple,AgApple,item,apple,J,,6,,25000,,,,, +0xE1,#item.SomaDrop,[$15]SomaDrop,SomaDrop,item,apple,J,,4,,20000,,,,, +0xE2,#item.Tent,[tent]Tent,Tent,item,tent,,,2,,200,,,,, +0xE3,#item.Cabin,[tent]Cabin,Cabin,item,tent,,,4,,1000,,,,, +0xE4,#item.fe_EagleEye,[$15]EagleEye,EagleEye,item,map,J,,1,,100,,,,, +0xE5,#item.Exit,[$15]Exit,Exit,item,map,J,,2,,2000,,,,, +0xE6,#item.Sylph,[callmagic]Sylph,Sylph Summon,item,summon,,,4,,30000,,,,, +0xE7,#item.Odin,[callmagic]Odin,Odin Summon,item,summon,,,4,,20000,,,,, +0xE8,#item.Levia,[callmagic]Levia,Levia Summon,item,summon,,,5,,40000,,,,, +0xE9,#item.Asura,[callmagic]Asura,Asura Summon,item,summon,,,4,,20000,,,,, +0xEA,#item.Baham,[callmagic]Baham,Baham Summon,item,summon,,,6,,60000,,,,, +0xEB,#item.Carrot,[$15]Carrot,Carrot,item,map,,,1,,50,,,,, +0xEC,#item.Pass,[$15]Pass,Pass,item,key,,K,,,10000,,,,, +0xED,#item.Whistle,[$15]Whistle,Whistle,item,map,,,2,,20000,,,,, +0xEE,#item.Package,[$15]Package,Package,item,key,,K,,,,,,,, +0xEF,#item.Baron,[key]Baron,Baron Key,item,key,,K,,,,,,,, +0xF0,#item.SandRuby,[$15]SandRuby,SandRuby,item,key,,K,,,,,,,, +0xF1,#item.EarthCrystal,[crystal]Earth,Earth Crystal,item,key,,K,,,,,,,, +0xF2,#item.Magma,[key]Magma,Magma Key,item,key,,K,,,,,,,, +0xF3,#item.Luca,[key]Luca,Luca Key,item,key,,K,,,,,,,, +0xF4,#item.TwinHarp,[harp]TwinHarp,TwinHarp,item,key,,K,,,,,,,, +0xF5,#item.DarkCrystal,[crystal]Darkness,Darkness Crystal,item,key,,K,,,,,,,, +0xF6,#item.Rat,[tail]Rat,Rat Tail,item,key,,K,,,,,,,, +0xF7,#item.Adamant,[$15]Adamant,Adamant,item,key,,K,,,,,,,, +0xF8,#item.Pan,[$15]Pan,Pan,item,key,,K,,,,,,,, +0xF9,#item.Pink,[tail]Pink,Pink Tail,item,key,,K,,,,,,,, +0xFA,#item.Tower,[key]Tower,Tower Key,item,key,,K,,,,,,,, +0xFB,#item.DkMatter,[$15]DkMatter,DkMatter,item,key,J,K,,,,,,,, +0xFC,#item.fe_Hook,Hook,Hook,item,key,,K,,,,,,,, +0xFD,#item.fe_RatTailChestItem,,,,,,D,,,,,,,, +0xFE,#item.Sort,,,,,,D,,,,,,,, +0xFF,#item.TrashCan,,,,,,D,,,,,,,, \ No newline at end of file diff --git a/worlds/ff4fe/items.py b/worlds/ff4fe/items.py new file mode 100644 index 000000000000..c7a83a89ccf7 --- /dev/null +++ b/worlds/ff4fe/items.py @@ -0,0 +1,141 @@ +import pkgutil + +from BaseClasses import Item, ItemClassification +from . import csvdb + +class FF4FEItem(Item): + game = 'Final Fantasy IV Free Enterprise' + +class ItemData: + name: str + classification: ItemClassification + tier: int + fe_id: int + + def __init__(self, name: str, classification: ItemClassification, tier: int, fe_id: int, price: int = 0): + self.name = name + self.classification = classification + self.tier = tier + self.fe_id = fe_id + self.price = price + + def to_json(self): + return { + "name": self.name, + "fe_id": self.fe_id + } + + @classmethod + def create_ap_item(cls): + return ItemData("Archipelago Item", ItemClassification.filler, 100, 0x500) + + +all_items: list[ItemData] = [] +filler_items: list[ItemData] = [] +useful_items: list[ItemData] = [] + +itemscsv = csvdb.CsvDb(pkgutil.get_data(__name__, "items.csvdb").decode().splitlines()) + +for item in itemscsv.create_view(): + item_tier = int(item.tier) if item.tier.isdecimal() else -1 + item_classification = ItemClassification.filler + if item.spoilername == "" or item.spoilername == "Medusa Sword": # Medusa Sword is bugged, so we don't include it. + continue + # Legend Sword isn't flagged as a key item, weirdly. + if item.subtype == "key" or item.spoilername == "Legend Sword": + item_classification = ItemClassification.progression if item.spoilername != "DkMatter" \ + else ItemClassification.progression_skip_balancing + # Spoon is, though...but it doesn't unlock anything. + elif item.spoilername == "Spoon" or ((int(item.tier) > 4) if item.tier.isdecimal() else False): + item_classification = ItemClassification.useful + item_price = int(item.price, 10) if item.price != '' else 0 + new_item = ItemData(item.spoilername, item_classification, item_tier, int(item.code, 16), item_price) + all_items.append(new_item) + + +useful_items = [item for item in all_items if item.classification == ItemClassification.useful and item.name != "Spoon"] +filler_items = [item for item in all_items if item.classification == ItemClassification.filler and item.name != "Spoon"] + +all_items.append(ItemData("Advance Weapon", ItemClassification.useful, 8, 0x46, 100000)) + +useful_item_names = [item.name for item in [*useful_items]] +filler_item_names = [item.name for item in [*filler_items]] +sellable_item_names = [item for item in [*useful_item_names, *filler_item_names]] + +character_data = [ + ("Cecil", 0x01), + ("Kain", 0x02), + ("Rydia", 0x03), + ("Tellah", 0x04), + ("Edward", 0x05), + ("Rosa", 0x06), + ("Yang", 0x07), + ("Palom", 0x08), + ("Porom", 0x09), + ("Cid", 0x0E), + ("Edge", 0x12), + ("Fusoya", 0x13), + ("None", 0xFF) +] + +characters = [ + "Cecil", + "Kain", + "Rydia", + "Tellah", + "Edward", + "Rosa", + "Yang", + "Palom", + "Porom", + "Cid", + "Edge", + "Fusoya", + "None", +] + +for character in character_data: + new_item = ItemData(character[0], ItemClassification.progression, -1, character[1] + 0x300) + all_items.append(new_item) + +key_items = [item for item in all_items if + (item.classification == ItemClassification.progression or item.name == "Spoon") + and item.name not in characters] + +key_item_names = [item.name for item in all_items if + (item.classification == ItemClassification.progression or item.name == "Spoon") + and item.name not in characters] + +key_items_tracker_order = [ + "Package", + "SandRuby", + "Legend Sword", + "Baron Key", + "TwinHarp", + "Earth Crystal", + "Magma Key", + "Tower Key", + "Hook", + "Luca Key", + "Darkness Crystal", + "Rat Tail", + "Adamant", + "Pan", + "Spoon", + "Pink Tail", + "Crystal" +] + +key_items_tracker_ids = {k: i for i, k in enumerate(key_items_tracker_order)} + +item_name_groups = { + "characters": [*characters], + "key_items": [*key_items_tracker_order], + "non_key_items": [*sellable_item_names], + "filler_items": [*filler_item_names], + "useful_items": [*useful_item_names] +} + + +for i in range(32): + all_items.append(ItemData(f"Objective {i + 1} Cleared", ItemClassification.progression, 8, 0)) \ No newline at end of file diff --git a/worlds/ff4fe/lark.lark b/worlds/ff4fe/lark.lark new file mode 100644 index 000000000000..cdb4d1ca7c49 --- /dev/null +++ b/worlds/ff4fe/lark.lark @@ -0,0 +1,62 @@ +# Lark grammar of Lark's syntax +# Note: Lark is not bootstrapped, its parser is implemented in load_grammar.py + +start: (_item? _NL)* _item? + +_item: rule + | token + | statement + +rule: RULE rule_params priority? ":" expansions +token: TOKEN token_params priority? ":" expansions + +rule_params: ["{" RULE ("," RULE)* "}"] +token_params: ["{" TOKEN ("," TOKEN)* "}"] + +priority: "." NUMBER + +statement: "%ignore" expansions -> ignore + | "%import" import_path ["->" name] -> import + | "%import" import_path name_list -> multi_import + | "%override" rule -> override_rule + | "%declare" name+ -> declare + +!import_path: "."? name ("." name)* +name_list: "(" name ("," name)* ")" + +?expansions: alias (_VBAR alias)* + +?alias: expansion ["->" RULE] + +?expansion: expr* + +?expr: atom [OP | "~" NUMBER [".." NUMBER]] + +?atom: "(" expansions ")" + | "[" expansions "]" -> maybe + | value + +?value: STRING ".." STRING -> literal_range + | name + | (REGEXP | STRING) -> literal + | name "{" value ("," value)* "}" -> template_usage + +name: RULE + | TOKEN + +_VBAR: _NL? "|" +OP: /[+*]|[?](?![a-z])/ +RULE: /!?[_?]?[a-z][_a-z0-9]*/ +TOKEN: /_?[A-Z][_A-Z0-9]*/ +STRING: _STRING "i"? +REGEXP: /\/(?!\/)(\\\/|\\\\|[^\/])*?\/[imslux]*/ +_NL: /(\r?\n)+\s*/ + +%import common.ESCAPED_STRING -> _STRING +%import common.SIGNED_INT -> NUMBER +%import common.WS_INLINE + +COMMENT: /\s*/ "//" /[^\n]/* | /\s*/ "#" /[^\n]/* + +%ignore WS_INLINE +%ignore COMMENT diff --git a/worlds/ff4fe/locations.py b/worlds/ff4fe/locations.py new file mode 100644 index 000000000000..ef0c651123d3 --- /dev/null +++ b/worlds/ff4fe/locations.py @@ -0,0 +1,143 @@ +import os.path +import pkgutil +from pkgutil import get_data +from typing import List + +from BaseClasses import Location +from . import csvdb + +class FF4FELocation(Location): + game = 'Final Fantasy IV Free Enterprise' + surface = "" + area = "" + +class LocationData(): + name: str + surface: str + area: str + fe_id: int + major_slot: bool + + def __init__(self, name, surface, area, fe_id, major_slot): + self.name = name + self.surface = surface + self.area = area + self.fe_id = fe_id + self.major_slot = major_slot + + + def to_json(self): + return { + "name": self.name + } + + +all_locations: list[LocationData] = [] + +locationscsv = csvdb.CsvDb(pkgutil.get_data(__name__, "treasure.csvdb").decode().splitlines()) + +miab_count = 0 + +for location in locationscsv.create_view(): + if location.exclude != "": + if location.exclude == "key": + # Rat Tail and Ribbon chests are a little special in FE, as they're treasure chests but are considered + # major reward locations. + new_location = LocationData("", location.world, location.area, int(location.flag, 16), True) + if location.world == "Underworld": # Rat Tail location + new_location.name = f"Town of Monsters -- B4F (first area) -- Rat Tail" + if location.world == "Moon": # Ribbon location + if location.x == "2": # Left Ribbon + new_location.name = f"Lunar Subterrane -- B7 (right room) -- Ribbon Left" + else: # Right Ribbon + new_location.name = f"Lunar Subterrane -- B7 (right room) -- Ribbon Right" + all_locations.append(new_location) + continue + new_location = LocationData("", location.world, location.area, int(location.flag, 16), False) + subname = f"{((' -- ' + location.spoilersubarea) if location.spoilersubarea != '' else '')}" + new_location.name = (f"{location.spoilerarea}" + f"{subname}" + f" -- {location.spoilerdetail}") + all_locations.append(new_location) + +# This is actually a custom data table for the reward locations, mimicking the format of the treasure locations. +locationscsv = csvdb.CsvDb(pkgutil.get_data(__name__, "rewardslots.csvdb").decode().splitlines()) + +for location in locationscsv.create_view(): + # All reward locations are given their ID plus 512 so we can't confuse them with regular chests. + new_location = LocationData("", location.world, location.area, int(location.fecode, 16) + 0x200, True) + subname = f"{((' -- ' + location.spoilersubarea) if location.spoilersubarea != '' else '')}" + new_location.name = (f"{location.spoilerarea}" + f"{subname}" + f" -- {location.spoilerdetail}") + all_locations.append(new_location) + +character_slots = [ + ("Starting Character 1", "Overworld", "BaronTown", 0x01), + ("Starting Character 2", "Overworld", "BaronTown", 0x02), + ("Mist Character", "Overworld", "Mist", 0x03), + ("Watery Pass Character", "Overworld", "WateryPass", 0x04), + ("Damcyan Character", "Overworld", "Damcyan", 0x05), + ("Kaipo Character", "Overworld", "Kaipo", 0x06), + ("Mt. Hobs Character", "Overworld", "MountHobs", 0x07), + ("Mysidia Character 1", "Overworld", "Mysidia", 0x08), + ("Mysidia Character 2", "Overworld", "Mysidia", 0x09), + ("Mt. Ordeals Character", "Overworld", "MountOrdeals", 0x0A), + ("Baron Inn Character", "Overworld", "BaronWeaponShop", 0x0D), + ("Baron Castle Character", "Overworld", "BaronCastle", 0x0E), + ("Tower of Zot Character 1", "Overworld", "Zot", 0x0F), + ("Tower of Zot Character 2", "Overworld", "Zot", 0x10), + ("Dwarf Castle Character", "Underworld", "DwarfCastle", 0x11), + ("Cave Eblana Character", "Underworld", "CaveEblan", 0x12), + ("Lunar Palace Character", "Moon", "LunarPalace", 0x13), + ("Giant of Bab-il Character", "Moon", "Giant", 0x14) +] + +character_locations = [location[0] for location in character_slots] + +free_character_locations = [ + "Watery Pass Character", + "Damcyan Character", + "Mysidia Character 1", + "Mysidia Character 2", + "Mt. Ordeals Character", +] + +earned_character_locations = [ + "Mist Character", + "Kaipo Character", + "Mt. Hobs Character", + "Baron Inn Character", + "Baron Castle Character", + "Tower of Zot Character 1", + "Tower of Zot Character 2", + "Dwarf Castle Character", + "Cave Eblana Character", + "Lunar Palace Character", + "Giant of Bab-il Character" +] + +# The name's a little unintuitive, I admit: this is the list of spots a restricted character _can't_ be. +restricted_character_locations = [ + "Starting Character 1", + "Starting Character 2", + *free_character_locations, + "Baron Inn Character", + "Mt. Hobs Character" +] + +for location in character_slots: + # Just like event reward locations, character locations get a constant added to separate them from treasures. + all_locations.append(LocationData(location[0], location[1], location[2], location[3] + 0x200, True)) + +all_locations.append(LocationData("Objectives Status", "Overworld", "BaronTown", 0xEEEE, False)) +all_locations.append(LocationData("Objective Reward", "Overworld", "BaronTown", 0xEEEF, False)) + +areas = [] + +for location in all_locations: + if location.area not in areas: + areas.append(location.area) + +for i in range(32): + all_locations.append(LocationData(f"Objective {i + 1} Status", "Overworld", "BaronTown", 0xEE00 + i, False)) diff --git a/worlds/ff4fe/options.py b/worlds/ff4fe/options.py new file mode 100644 index 000000000000..62d04b103c51 --- /dev/null +++ b/worlds/ff4fe/options.py @@ -0,0 +1,436 @@ +from dataclasses import dataclass +from .items import sellable_item_names +from Options import (Toggle, Range, Choice, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool, OptionGroup, + OptionSet, ItemSet, Visibility) + + +class ForgeTheCrystal(Toggle): + """Bring the Adamant and Legend Sword to clear this objective. + Forces the objective reward to be the Crystal.""" + display_name = "Forge the Crystal" + +class ConquerTheGiant(Toggle): + """Clear the Giant of Bab-il to clear this objective. + No character will be available from the Giant with this objective.""" + display_name = "Conquer the Giant" + +class DefeatTheFiends(Toggle): + """Defeat every elemental fiend to clear this objective. + Your targets are Milon, Milon Z., Kainazzo, Valvalis, Rubicant, and the Elements bosses. Good hunting.""" + display_name = "Defeat The Fiends" + +class FindTheDarkMatter(Toggle): + """Find thirty Dark Matters and deliver them to Kory in Agart to clear this objective. + There are forty-five Dark Matters in total.""" + display_name = "Find The Dark Matter" + +class AdditionalObjectives(Range): + """The number of additional random objectives. Can be quests, boss fights, or character recruitments. Note that + no matter what this is set to, no more than thirty-two objectives will be set.""" + display_name = "Additional Objectives" + range_start = 0 + range_end = 32 + default = 0 + +class RequiredObjectiveCount(Range): + """The number of objectives required for victory. Note that this is ignored when no objectives are set. If this + count is greater than the total number of objectives available, then it will be reduced to match the number of + available objectives.""" + display_name = "Max Number of Required Objectives" + range_start = 1 + range_end = 32 + default = 32 + +class ObjectiveReward(Choice): + """The reward for clearing all objectives. Note that this is ignored when no objectives are set, + and Forge the Crystal forces this to the Crystal setting.""" + display_name = "Objective Reward" + option_crystal = 0 + option_win = 1 + default = 0 + +class ItemPlacement(Choice): + """Where items can and will be placed. + Setting this to Full Shuffle will allow any items to be anywhere. + Setting this to Major Minor Split will force all non-major locations to never have progression. + In either case, major locations can only have useful or progression items. + Major locations are any MIAB or event locations.""" + display_name = "Item Placement" + option_full_shuffle = 0 + option_major_minor_split = 1 + default = 0 + +class NoFreeCharacters(Toggle): + """If set, characters will not be available at locations with no requirements or bosses. These locations are + Mysidia, Damcyan Watery Pass, and Mt. Ordeals.""" + display_name = "No Free Characters" + +class NoEarnedCharacters(Toggle): + """If set, characters will not be available at locations with requirements or bosses. These locations are Mist, + Kaipo, Mt. Hobs, Baron, the Tower of Zot, Cave Eblana, Lunar Palace, and the Giant of Bab-il.""" + display_name = "No Earned Characters" + +class HeroChallenge(Choice): + """Enable the Hero Challenge. In Hero Challenge, your starting character is your main character and cannot be + dismissed. They will face the top of Mt. Ordeals on their own, and Kokkol will forge a weapon from FFIV Advance + for them (unless Forge the Crystal is set).""" + display_name = "Hero Challenge" + option_none = 0 + option_cecil = 1 + option_kain = 2 + option_rydia = 3 + option_tellah = 4 + option_edward = 5 + option_rosa = 6 + option_yang = 7 + option_palom = 8 + option_porom = 9 + option_cid = 10 + option_edge = 11 + option_fusoya = 12 + option_random_character = 13 + default = 0 + +class PassEnabled(Toggle): + """Will the Pass be included in the Key Item Pool?""" + display_name = "Pass In Key Item Pool" + +class UsefulPercentage(Range): + """The percentage of useful high tier items in the pool as opposed to filler low tier items.""" + display_name = "Useful Item Percentage" + range_start = 25 + range_end = 100 + default = 35 + +class UnsafeKeyItemPlacement(Toggle): + """Normally, underground access is guaranteed to be available without taking a trip to the moon. + Toggling this on disables this check.""" + display_name = "Unsafe Key Item Placement" + +class PassInShops(Toggle): + """Can the pass show up in shops? This is a convenience feature and will never be required by the logic.""" + display_name = "Enable Pass in Shops" + +class AllowedCharacters(OptionSet): + """Pool of characters allowed to show up. Note that if Hero Challenge is enabled, your hero will still appear.""" + display_name = "Allowed Characters" + valid_keys = ["Cecil", "Kain", "Rydia", "Tellah", "Edward", "Rosa", "Yang", "Palom", "Porom", "Cid", "Edge", "Fusoya"] + default = ["Cecil", "Kain", "Rydia", "Tellah", "Edward", "Rosa", "Yang", "Palom", "Porom", "Cid", "Edge", "Fusoya"] + +class EnsureAllCharacters(DefaultOnToggle): + """Ensure at least one instance of each allowed character is available, if possible.""" + display_name = "Ensure All Characters" + +class AllowDuplicateCharacters(DefaultOnToggle): + """Allows multiple instances of the same character to join your party.""" + display_name = "Allow Duplicate Characters" + +class RestrictedCharacters(OptionSet): + """List of characters that can't appear in the easiest to access locations if possible.""" + display_name = "Restricted Characters" + valid_keys = ["Cecil", "Kain", "Rydia", "Tellah", "Edward", "Rosa", "Yang", "Palom", "Porom", "Cid", "Edge", "Fusoya"] + default = ["Edge", "Fusoya"] + +class PartySize(Range): + """Maximum party size.""" + display_name = "Party Size" + range_start = 1 + range_end = 5 + default = 5 + +class CharactersPermajoin(Toggle): + """If enabled, characters may not be dismissed from the party.""" + display_name = "Characters Permanently Join" + +class CharactersPermadie(Choice): + """If enabled, characters petrified or dead at the end of battle will be removed from your party forever. + On Extreme difficulty, this also includes "cutscene" fights (Mist, Dark Elf, etc.)""" + display_name = "Characters Permanently Die" + option_no = 0 + option_yes = 1 + option_extreme = 2 + default = 0 + +class MinTier(Range): + """The minimum tier of items that can appear in the item pool.""" + display_name = "Minimum Treasure Tier" + range_start = 1 + range_end = 4 + default = 1 + +class MaxTier(Range): + """The maximum tier of items that can appear in the item pool.""" + display_name = "Maximum Treasure Tier" + range_start = 5 + range_end = 8 + default = 8 + +class JunkTier(Range): + """Items of this tier or below will automatically be sold for cold hard cash.""" + display_name = "Junk Tier" + range_start = 0 + range_end = 8 + default = 1 + +class ShopRandomization(Choice): + """Affects the placement of items in shops. See FE documentation for more for now.""" + display_name = "Shop Randomization" + option_vanilla = 0 + option_shuffle = 1 + option_standard = 2 + option_pro = 3 + option_wild = 4 + option_cabins = 5 + default = 4 + +class FreeShops(Toggle): + """Everything must go!""" + display_name = "Free Shops" + +class NoAdamantArmors(Toggle): + """Remove Adamant Armor from the item and shop pool.""" + display_name = "No Adamant Armor" + +class KeepDoorsBehemoths(Toggle): + """Should Trap Door and Behemoth Fights be enabled even when encounters are off?""" + display_name = "Keep TrapDoor and Behemoth Fights" + +class NoFreeBosses(Toggle): + """Removes alternate win conditions for bosses other than good old fashioned violence.""" + display_name = "No Free Bosses" + +class WackyChallenge(Choice): + """Wacky challenges are not fair, balanced, stable, or even necessarily interesting. + They are, however, quite wacky. See FE documentation for more info, or pick one for a fun surprise!""" + display_name = "Wacky Challenge" + option_none = 0 + option_afflicted = 1 + option_battle_scars = 2 + option_the_bodyguard = 3 + option_enemy_unknown = 4 + option_ff4_the_musical = 5 + option_fist_fight = 6 + option_forward_is_the_new_back = 7 + option_friendly_fire = 8 + option_the_floor_is_made_of_lava = 9 + option_gotta_go_fast = 10 + option_holy_onomatopoeia_batman = 11 + option_imaginary_numbers = 12 + option_is_this_even_randomized = 13 + option_kleptomania = 14 + option_men_are_pigs = 15 + option_misspelled = 16 + option_a_much_bigger_magnet = 17 + option_mystery_juice = 18 + option_neat_freak = 19 + option_night_mode = 20 + option_omnidextrous = 21 + option_payable_golbez = 22 + option_save_us_big_chocobo = 23 + option_six_legged_race = 24 + option_the_sky_warriors = 25 + option_something_worth_fighting_for = 26 + option_the_tellah_maneuver = 27 + option_three_point_system = 28 + option_time_is_money = 29 + option_unstackable = 30 + option_world_championship_of_darts = 31 + option_zombies = 32 + option_random_challenge = 33 + +class StarterKitOne(Choice): + """FE Starter Kit 1. See FE Documentation for details. Or just pick one, they can't hurt you.""" + display_name = "Starter Kit One" + option_none = 0 + option_basic = 1 + option_better = 2 + option_loaded = 3 + option_cata = 4 + option_freedom = 5 + option_cid = 6 + option_yang = 7 + option_money = 8 + option_grab_Bag = 9 + option_MIAB = 10 + option_archer = 11 + option_fabul = 12 + option_castlevania = 13 + option_summon = 14 + option_not_Deme = 15 + option_meme = 16 + option_defense = 17 + option_mist = 18 + option_mysidia = 19 + option_baron = 20 + option_dwarf = 21 + option_eblan = 22 + option_libra = 23 + option_99 = 24 + option_green_names = 25 + option_random_kit = 26 + default = 0 + +class StarterKitTwo(Choice): + """FE Starter Kit 2. See FE Documentation for details. Or just pick one, they can't hurt you.""" + display_name = "Starter Kit Two" + option_none = 0 + option_basic = 1 + option_better = 2 + option_loaded = 3 + option_cata = 4 + option_freedom = 5 + option_cid = 6 + option_yang = 7 + option_money = 8 + option_grab_Bag = 9 + option_MIAB = 10 + option_archer = 11 + option_fabul = 12 + option_castlevania = 13 + option_summon = 14 + option_not_Deme = 15 + option_meme = 16 + option_defense = 17 + option_mist = 18 + option_mysidia = 19 + option_baron = 20 + option_dwarf = 21 + option_eblan = 22 + option_libra = 23 + option_99 = 24 + option_green_names = 25 + option_random_kit = 26 + default = 0 + +class StarterKitThree(Choice): + """FE Starter Kit 3. See FE Documentation for details. Or just pick one, they can't hurt you.""" + display_name = "Starter Kit Three" + option_none = 0 + option_basic = 1 + option_better = 2 + option_loaded = 3 + option_cata = 4 + option_freedom = 5 + option_cid = 6 + option_yang = 7 + option_money = 8 + option_grab_bag = 9 + option_MIAB = 10 + option_archer = 11 + option_fabul = 12 + option_castlevania = 13 + option_summon = 14 + option_not_deme = 15 + option_meme = 16 + option_defense = 17 + option_mist = 18 + option_mysidia = 19 + option_baron = 20 + option_dwarf = 21 + option_eblan = 22 + option_libra = 23 + option_99 = 24 + option_green_names = 25 + option_random_kit = 26 + default = 0 + +class JunkedItems(OptionSet): + """Items that will always be sold for GP regardless of your junk tier settings.""" + display_name = "Junked Items" + valid_keys = sorted(sellable_item_names) + visibility = Visibility.complex_ui | Visibility.template | Visibility.spoiler + +class KeptItems(OptionSet): + """Items that will never be sold for GP regardless of your junk tier settings. Takes priority over Junked Items.""" + display_name = "Kept Items" + valid_keys = sorted(sellable_item_names) + visibility = Visibility.complex_ui | Visibility.template | Visibility.spoiler + +@dataclass +class FF4FEOptions(PerGameCommonOptions): + ForgeTheCrystal: ForgeTheCrystal + ConquerTheGiant: ConquerTheGiant + DefeatTheFiends: DefeatTheFiends + FindTheDarkMatter: FindTheDarkMatter + AdditionalObjectives: AdditionalObjectives + RequiredObjectiveCount: RequiredObjectiveCount + ObjectiveReward: ObjectiveReward + ItemPlacement: ItemPlacement + NoFreeCharacters: NoFreeCharacters + NoEarnedCharacters: NoEarnedCharacters + HeroChallenge: HeroChallenge + PassEnabled: PassEnabled + UsefulPercentage: UsefulPercentage + UnsafeKeyItemPlacement: UnsafeKeyItemPlacement + PassInShops: PassInShops + AllowedCharacters: AllowedCharacters + EnsureAllCharacters: EnsureAllCharacters + AllowDuplicateCharacters: AllowDuplicateCharacters + RestrictedCharacters: RestrictedCharacters + PartySize: PartySize + CharactersPermajoin: CharactersPermajoin + CharactersPermadie: CharactersPermadie + MinTier: MinTier + MaxTier: MaxTier + JunkTier: JunkTier + ShopRandomization: ShopRandomization + FreeShops: FreeShops + NoAdamantArmors: NoAdamantArmors + KeepDoorsBehemoths: KeepDoorsBehemoths + NoFreeBosses: NoFreeBosses + WackyChallenge: WackyChallenge + StarterKitOne: StarterKitOne + StarterKitTwo: StarterKitTwo + StarterKitThree: StarterKitThree + JunkedItems: JunkedItems + KeptItems: KeptItems + start_inventory_from_pool: StartInventoryPool + +ff4fe_option_groups = [ + OptionGroup("Objective Options", [ + ForgeTheCrystal, + ConquerTheGiant, + DefeatTheFiends, + FindTheDarkMatter, + AdditionalObjectives, + RequiredObjectiveCount, + ObjectiveReward + ]), + OptionGroup("Character Options", [ + NoFreeCharacters, + NoEarnedCharacters, + AllowedCharacters, + EnsureAllCharacters, + AllowDuplicateCharacters, + RestrictedCharacters + ]), + OptionGroup("Item Options", [ + ItemPlacement, + UsefulPercentage, + PassEnabled, + PassInShops, + MinTier, + MaxTier, + JunkTier, + JunkedItems, + KeptItems + ]), + OptionGroup("Challenge Flags", [ + HeroChallenge, + PartySize, + CharactersPermajoin, + CharactersPermadie, + UnsafeKeyItemPlacement, + NoAdamantArmors, + KeepDoorsBehemoths, + NoFreeBosses, + WackyChallenge + ]), + OptionGroup("Miscellaneous Flags", [ + ShopRandomization, + FreeShops, + StarterKitOne, + StarterKitTwo, + StarterKitThree + ]) +] diff --git a/worlds/ff4fe/python.lark b/worlds/ff4fe/python.lark new file mode 100644 index 000000000000..8a75966b2964 --- /dev/null +++ b/worlds/ff4fe/python.lark @@ -0,0 +1,302 @@ +// Python 3 grammar for Lark + +// This grammar should parse all python 3.x code successfully. + +// Adapted from: https://docs.python.org/3/reference/grammar.html + +// Start symbols for the grammar: +// single_input is a single interactive statement; +// file_input is a module or sequence of commands read from an input file; +// eval_input is the input for the eval() functions. +// NB: compound_stmt in single_input is followed by extra NEWLINE! +// + +single_input: _NEWLINE | simple_stmt | compound_stmt _NEWLINE +file_input: (_NEWLINE | stmt)* +eval_input: testlist _NEWLINE* + +decorator: "@" dotted_name [ "(" [arguments] ")" ] _NEWLINE +decorators: decorator+ +decorated: decorators (classdef | funcdef | async_funcdef) + +async_funcdef: "async" funcdef +funcdef: "def" name "(" [parameters] ")" ["->" test] ":" suite + +parameters: paramvalue ("," paramvalue)* ["," SLASH ("," paramvalue)*] ["," [starparams | kwparams]] + | starparams + | kwparams + +SLASH: "/" // Otherwise the it will completely disappear and it will be undisguisable in the result +starparams: (starparam | starguard) poststarparams +starparam: "*" typedparam +starguard: "*" +poststarparams: ("," paramvalue)* ["," kwparams] +kwparams: "**" typedparam ","? + +?paramvalue: typedparam ("=" test)? +?typedparam: name (":" test)? + + +lambdef: "lambda" [lambda_params] ":" test +lambdef_nocond: "lambda" [lambda_params] ":" test_nocond +lambda_params: lambda_paramvalue ("," lambda_paramvalue)* ["," [lambda_starparams | lambda_kwparams]] + | lambda_starparams + | lambda_kwparams +?lambda_paramvalue: name ("=" test)? +lambda_starparams: "*" [name] ("," lambda_paramvalue)* ["," [lambda_kwparams]] +lambda_kwparams: "**" name ","? + + +?stmt: simple_stmt | compound_stmt +?simple_stmt: small_stmt (";" small_stmt)* [";"] _NEWLINE +?small_stmt: (expr_stmt | assign_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | nonlocal_stmt | assert_stmt) +expr_stmt: testlist_star_expr +assign_stmt: annassign | augassign | assign + +annassign: testlist_star_expr ":" test ["=" test] +assign: testlist_star_expr ("=" (yield_expr|testlist_star_expr))+ +augassign: testlist_star_expr augassign_op (yield_expr|testlist) +!augassign_op: "+=" | "-=" | "*=" | "@=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>=" | "**=" | "//=" +?testlist_star_expr: test_or_star_expr + | test_or_star_expr ("," test_or_star_expr)+ ","? -> tuple + | test_or_star_expr "," -> tuple + +// For normal and annotated assignments, additional restrictions enforced by the interpreter +del_stmt: "del" exprlist +pass_stmt: "pass" +?flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt +break_stmt: "break" +continue_stmt: "continue" +return_stmt: "return" [testlist] +yield_stmt: yield_expr +raise_stmt: "raise" [test ["from" test]] +import_stmt: import_name | import_from +import_name: "import" dotted_as_names +// note below: the ("." | "...") is necessary because "..." is tokenized as ELLIPSIS +import_from: "from" (dots? dotted_name | dots) "import" ("*" | "(" import_as_names ")" | import_as_names) +!dots: "."+ +import_as_name: name ["as" name] +dotted_as_name: dotted_name ["as" name] +import_as_names: import_as_name ("," import_as_name)* [","] +dotted_as_names: dotted_as_name ("," dotted_as_name)* +dotted_name: name ("." name)* +global_stmt: "global" name ("," name)* +nonlocal_stmt: "nonlocal" name ("," name)* +assert_stmt: "assert" test ["," test] + +?compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | match_stmt + | with_stmt | funcdef | classdef | decorated | async_stmt +async_stmt: "async" (funcdef | with_stmt | for_stmt) +if_stmt: "if" test ":" suite elifs ["else" ":" suite] +elifs: elif_* +elif_: "elif" test ":" suite +while_stmt: "while" test ":" suite ["else" ":" suite] +for_stmt: "for" exprlist "in" testlist ":" suite ["else" ":" suite] +try_stmt: "try" ":" suite except_clauses ["else" ":" suite] [finally] + | "try" ":" suite finally -> try_finally +finally: "finally" ":" suite +except_clauses: except_clause+ +except_clause: "except" [test ["as" name]] ":" suite +// NB compile.c makes sure that the default except clause is last + + +with_stmt: "with" with_items ":" suite +with_items: with_item ("," with_item)* +with_item: test ["as" name] + +match_stmt: "match" test ":" _NEWLINE _INDENT case+ _DEDENT + +case: "case" pattern ["if" test] ":" suite + +?pattern: sequence_item_pattern "," _sequence_pattern -> sequence_pattern + | as_pattern +?as_pattern: or_pattern ("as" NAME)? +?or_pattern: closed_pattern ("|" closed_pattern)* +?closed_pattern: literal_pattern + | NAME -> capture_pattern + | "_" -> any_pattern + | attr_pattern + | "(" as_pattern ")" + | "[" _sequence_pattern "]" -> sequence_pattern + | "(" (sequence_item_pattern "," _sequence_pattern)? ")" -> sequence_pattern + | "{" (mapping_item_pattern ("," mapping_item_pattern)* ","?)?"}" -> mapping_pattern + | "{" (mapping_item_pattern ("," mapping_item_pattern)* ",")? "**" NAME ","? "}" -> mapping_star_pattern + | class_pattern + +literal_pattern: inner_literal_pattern + +?inner_literal_pattern: "None" -> const_none + | "True" -> const_true + | "False" -> const_false + | STRING -> string + | number + +attr_pattern: NAME ("." NAME)+ -> value + +name_or_attr_pattern: NAME ("." NAME)* -> value + +mapping_item_pattern: (literal_pattern|attr_pattern) ":" as_pattern + +_sequence_pattern: (sequence_item_pattern ("," sequence_item_pattern)* ","?)? +?sequence_item_pattern: as_pattern + | "*" NAME -> star_pattern + +class_pattern: name_or_attr_pattern "(" [arguments_pattern ","?] ")" +arguments_pattern: pos_arg_pattern ["," keyws_arg_pattern] + | keyws_arg_pattern -> no_pos_arguments + +pos_arg_pattern: as_pattern ("," as_pattern)* +keyws_arg_pattern: keyw_arg_pattern ("," keyw_arg_pattern)* +keyw_arg_pattern: NAME "=" as_pattern + + + +suite: simple_stmt | _NEWLINE _INDENT stmt+ _DEDENT + +?test: or_test ("if" or_test "else" test)? + | lambdef + | assign_expr + +assign_expr: name ":=" test + +?test_nocond: or_test | lambdef_nocond + +?or_test: and_test ("or" and_test)* +?and_test: not_test_ ("and" not_test_)* +?not_test_: "not" not_test_ -> not_test + | comparison +?comparison: expr (comp_op expr)* +star_expr: "*" expr + +?expr: or_expr +?or_expr: xor_expr ("|" xor_expr)* +?xor_expr: and_expr ("^" and_expr)* +?and_expr: shift_expr ("&" shift_expr)* +?shift_expr: arith_expr (_shift_op arith_expr)* +?arith_expr: term (_add_op term)* +?term: factor (_mul_op factor)* +?factor: _unary_op factor | power + +!_unary_op: "+"|"-"|"~" +!_add_op: "+"|"-" +!_shift_op: "<<"|">>" +!_mul_op: "*"|"@"|"/"|"%"|"//" +// <> isn't actually a valid comparison operator in Python. It's here for the +// sake of a __future__ import described in PEP 401 (which really works :-) +!comp_op: "<"|">"|"=="|">="|"<="|"<>"|"!="|"in"|"not" "in"|"is"|"is" "not" + +?power: await_expr ("**" factor)? +?await_expr: AWAIT? atom_expr +AWAIT: "await" + +?atom_expr: atom_expr "(" [arguments] ")" -> funccall + | atom_expr "[" subscriptlist "]" -> getitem + | atom_expr "." name -> getattr + | atom + +?atom: "(" yield_expr ")" + | "(" _tuple_inner? ")" -> tuple + | "(" comprehension{test_or_star_expr} ")" -> tuple_comprehension + | "[" _exprlist? "]" -> list + | "[" comprehension{test_or_star_expr} "]" -> list_comprehension + | "{" _dict_exprlist? "}" -> dict + | "{" comprehension{key_value} "}" -> dict_comprehension + | "{" _exprlist "}" -> set + | "{" comprehension{test} "}" -> set_comprehension + | name -> var + | number + | string_concat + | "(" test ")" + | "..." -> ellipsis + | "None" -> const_none + | "True" -> const_true + | "False" -> const_false + + +?string_concat: string+ + +_tuple_inner: test_or_star_expr (("," test_or_star_expr)+ [","] | ",") + +?test_or_star_expr: test + | star_expr + +?subscriptlist: subscript + | subscript (("," subscript)+ [","] | ",") -> subscript_tuple +?subscript: test | ([test] ":" [test] [sliceop]) -> slice +sliceop: ":" [test] +?exprlist: (expr|star_expr) + | (expr|star_expr) (("," (expr|star_expr))+ [","]|",") +?testlist: test | testlist_tuple +testlist_tuple: test (("," test)+ [","] | ",") +_dict_exprlist: (key_value | "**" expr) ("," (key_value | "**" expr))* [","] + +key_value: test ":" test + +_exprlist: test_or_star_expr ("," test_or_star_expr)* [","] + +classdef: "class" name ["(" [arguments] ")"] ":" suite + + + +arguments: argvalue ("," argvalue)* ("," [ starargs | kwargs])? + | starargs + | kwargs + | comprehension{test} + +starargs: stararg ("," stararg)* ("," argvalue)* ["," kwargs] +stararg: "*" test +kwargs: "**" test ("," argvalue)* + +?argvalue: test ("=" test)? + + +comprehension{comp_result}: comp_result comp_fors [comp_if] +comp_fors: comp_for+ +comp_for: [ASYNC] "for" exprlist "in" or_test +ASYNC: "async" +?comp_if: "if" test_nocond + +// not used in grammar, but may appear in "node" passed from Parser to Compiler +encoding_decl: name + +yield_expr: "yield" [testlist] + | "yield" "from" test -> yield_from + +number: DEC_NUMBER | HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | FLOAT_NUMBER | IMAG_NUMBER +string: STRING | LONG_STRING + +// Other terminals + +_NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+ + +%ignore /[\t \f]+/ // WS +%ignore /\\[\t \f]*\r?\n/ // LINE_CONT +%ignore COMMENT +%declare _INDENT _DEDENT + + +// Python terminals + +!name: NAME | "match" | "case" +NAME: /[^\W\d]\w*/ +COMMENT: /#[^\n]*/ + +STRING: /([ubf]?r?|r[ubf])("(?!"").*?(? bytes: + with open(get_settings().ff4fe_options.rom_file, "rb") as infile: + base_rom_bytes = bytes(Utils.read_snes_rom(infile)) + return base_rom_bytes + + +class FF4FEPatchExtension(APPatchExtension): + game = "Final Fantasy IV Free Enterprise" + + @staticmethod + def call_fe(caller, rom, placement_file): + placements = json.loads(caller.get_file(placement_file)) + seed = placements["seed"] + output_file = placements["output_file"] + rom_name = placements["rom_name"] + flags = placements["flags"] + junk_tier = placements["junk_tier"] + junked_items = placements["junked_items"] + kept_items = placements["kept_items"] + data_dir = placements["data_dir"] + placements = json.dumps(json.loads(caller.get_file(placement_file))) + # We try to import FE, assuming it's been installed by requirements... + try: + from FreeEnt.cmd_make import MakeCommand + except: + # ...but that won't fly in a standalone APWorld, so we then try and grab it from the data directory. + # This could be removed for the merged build but maintaining two versions is pain. + try: + import sys + sys.path.append(data_dir) + from FreeEnterprise4.FreeEnt.cmd_make import MakeCommand + except ImportError: + raise ImportError("Free Enterprise not found. Try reinstalling it.") + cmd = MakeCommand() + parser = argparse.ArgumentParser() + cmd.add_parser_arguments(parser) + directory = tempfile.gettempdir() + with open(os.path.join(directory, "ff4base.sfc"), "wb") as file: + file.write(rom) + arguments = [ + os.path.join(directory, "ff4base.sfc"), + f"-s={seed}", + f"-f={flags}", + f"-o={os.path.join(directory, output_file)}", + f"-a={placements}" + ] + args = parser.parse_args(arguments) + cmd.execute(args) + with open(os.path.join(directory, output_file), "rb") as file: + rom_data = bytearray(file.read()) + rom_data[ROM_NAME:ROM_NAME+20] = bytes(rom_name, encoding="utf-8") + rom_data[junk_tier_byte:junk_tier_byte + 1] = bytes([junk_tier]) + rom_data[junked_items_length_byte] = len(junked_items) + rom_data[kept_items_length_byte] = len(kept_items) + for i, item_name in enumerate(junked_items): + item_id = [item for item in all_items if item.name == item_name].pop().fe_id + rom_data[junked_items_array_start + i] = item_id + for i, item_name in enumerate(kept_items): + item_id = [item for item in all_items if item.name == item_name].pop().fe_id + rom_data[kept_items_array_start + i] = item_id + return rom_data + + +class FF4FEProcedurePatch(APProcedurePatch, APTokenMixin): + game = "Final Fantasy IV Free Enterprise" + hash = "27D02A4F03E172E029C9B82AC3DB79F7" + patch_file_ending = ".apff4fe" + result_file_ending = ".sfc" + + procedure = [ + ("call_fe", ["placement_file.json"]) + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_as_bytes() + diff --git a/worlds/ff4fe/rules.py b/worlds/ff4fe/rules.py new file mode 100644 index 000000000000..620ab844cea8 --- /dev/null +++ b/worlds/ff4fe/rules.py @@ -0,0 +1,116 @@ +from . import items + +area_rules = { + "BaronWeaponShop": ["Baron Key"], + "BaronCastle": ["Baron Key", "Baigan Slot Defeated"], + "Sewer": ["Baron Key"], + "ToroiaTreasury": ["Earth Crystal"], + "Adamant": ["Hook"], + "UpperBabilAfterFall": ["King and Queen Slot Defeated", "Rubicant Slot Defeated"], + "SealedCave": ["Luca Key"] +} + +boss_rules = { + "Officer Slot": ["Package"], + "Milon Z. Slot": ["Milon Slot Defeated"], + "Mirror Cecil Slot": ["Milon Slot Defeated", "Milon Z. Slot Defeated"], + "Karate Slot": ["Guards Slot Defeated"], + "Baigan Slot": ["Baron Key"], + "Kainazzo Slot": ["Baron Key", "Baigan Slot Defeated"], + "Dark Elf Slot": ["TwinHarp"], + "Valvalis Slot": ["Earth Crystal", "Magus Sisters Slot Defeated"], + "Golbez Slot": ["Calbrena Slot Defeated"], + "Dark Imps Slot": ["Tower Key"], + "Rubicant Slot": ["King and Queen Slot Defeated"], + "Odin Slot": ["Baron Key", "Baigan Slot Defeated"], + "CPU Slot": ["Elements Slot Defeated"], + "Zeromus": ["Crystal"] +} + +individual_location_rules = { + # This is a really janky way of ensuring we can find D. Mist without having to randomize the bosses through AP + "Mist -- Exterior -- Rydia's Mom item": [ + "Hook", "Earth Crystal", "Package", + "TwinHarp", "Tower Key", "Darkness Crystal", + "Luca Key", "Baron Key" + ], + "Mist Character": ["Package"], + "Kaipo Character": ["SandRuby"], + "Mt. Hobs Character": ["MomBomb Slot Defeated"], + "Baron Inn Character": ["Karate Slot Defeated"], + "Baron Castle Character": ["Kainazzo Slot Defeated"], + "Tower of Zot Character 1": ["Valvalis Slot Defeated"], + "Tower of Zot Character 2": ["Valvalis Slot Defeated"], + "Dwarf Castle Character": ["Golbez Slot Defeated"], + "Cave Eblana Character": ["Hook"], + "Lunar Palace Character": ["Darkness Crystal"], + "Giant of Bab-il Character": ["CPU Slot Defeated"], + "Antlion Cave -- B3F -- Antlion Nest item": ["Antlion Slot Defeated"], + "Baron Town -- Inn -- item": ["Karate Slot Defeated"], + "Baron Castle -- Throne Room -- item": ["Kainazzo Slot Defeated"], + "Tower of Zot -- 6F -- item": ["Valvalis Slot Defeated"], + "Dwarf Castle -- Throne Room -- Luca item": ["Golbez Slot Defeated"], + "Sealed Cave -- Crystal Room -- item": ["Evilwall Slot Defeated"], + "Fabul -- West tower 3F (Yang's room) -- Found Yang item": ["Hook", "Magma Key"], + "Fabul -- West tower 3F (Yang's room) -- Pan Trade item": ["Hook", "Magma Key", "Pan"], + "Sylvan Cave -- B3F -- Sylph item": ["Hook", "Magma Key", "Pan"], + "Mt. Ordeals -- Mirror Room -- item": ["Mirror Cecil Slot Defeated"], + "Cave Magnes -- Crystal Room -- item": ["Dark Elf Slot Defeated"], + "Tower of Bab-il (lower) -- 5F -- item (Super Cannon destruction)": ["Dark Imp Slot Defeated"], + "Baron Castle -- Basement -- Odin item": ["Odin Slot Defeated"], + "Cave Bahamut -- B3F -- Baham item": ["Bahamut Slot Defeated"], + "Town of Monsters -- Throne Room -- Asura item": ["Asura Slot Defeated"], + "Town of Monsters -- Throne Room -- Levia item": ["Leviatan Slot Defeated"], + "Adamant -- Cave -- Rat Tail Trade item": ["Rat Tail"], + "Adamant -- Cave -- Pink Tail Trade item": ["Pink Tail"], + "Kokkol's House 2F -- Kokkol -- forge item": ["Legend Sword", "Adamant"], + "Lunar Subterrane -- B7 (right room) -- Ribbon Left": ["D. Lunars Slot Defeated"], + "Lunar Subterrane -- B7 (right room) -- Ribbon Right": ["D. Lunars Slot Defeated"], + "Objectives Status": [*[item for item in items.key_item_names if item not in ["Crystal", "Spoon", "Pass"]]], + "Objective Reward": ["All Objectives Cleared"] +} + +location_tiers = { + # -- Tier 0 -- + # -- Available from start of game with no gating or difficulty -- + 0: [ + "BaronTown", "Mist", "Kaipo", "Silvera", "ToroiaTown", "Agart", + "ChocoboForest", "Damcyan", "Fabul", "ToroiaCastle", "Mysidia" + ], + # -- Tier 1 -- + # -- Available from start of game with minimal gating or difficulty -- + 1: [ + "MistCave", "WateryPass", "AntlionCave", "MountOrdeals" + ], + # -- Tier 2 -- + # -- Available after defeating a boss or receiving a single key item -- + 2: [ + "BaronWeaponShop", "Sewer", "ToroiaTreasury", "Waterfall", "MountHobs", + "DwarfCastle", "Smithy", "Tomra" + ], + # -- Tier 3 -- + # -- Available after defeating a boss or receiving a key item with some difficulty + 3: [ + "CaveMagnes", "Zot", "CaveEblan", "LowerBabil", "CaveOfSummons", + "UpperBabil", "UpperBabilAfterFall", "SealedCave" + ], + # -- Tier 4 -- + # -- Requires multiple key items and/or bosses to access + 4: [ + "BaronCastle", "Eblan", "Feymarch", "SylvanCave", "Adamant" + ], + # -- Tier 5 -- + # -- THE MOON -- + 5: [ + "Giant", "BahamutCave", "LunarPath", "LunarCore", "LunarPalace" + ] +} + +logical_gating = { + 0: {"characters": 0, "key_items": 0}, + 1: {"characters": 3, "key_items": 3}, + 2: {"characters": 6, "key_items": 6}, + 3: {"characters": 9, "key_items": 9}, + 4: {"characters": 12, "key_items": 12}, + 5: {"characters": 15, "key_items": 15}, +} diff --git a/worlds/ff4fe/topology.py b/worlds/ff4fe/topology.py new file mode 100644 index 000000000000..8c6e0935faee --- /dev/null +++ b/worlds/ff4fe/topology.py @@ -0,0 +1,77 @@ +surfaces = [ + "Overworld", + "Underworld", + "Moon" +] + +areas = [ + "BaronTown", + "Mist", + "Kaipo", + "Silvera", + "ToroiaTown", + "Agart", + "BaronWeaponShop", + "ChocoboForest", + "BaronCastle", + "Sewer", + "Damcyan", + "Fabul", + "ToroiaCastle", + "ToroiaTreasury", + "Eblan", + "MistCave", + "WateryPass", + "Waterfall", + "AntlionCave", + "MountHobs", + "MountOrdeals", + "CaveMagnes", + "Zot", + "UpperBabil", + "Giant", + "CaveEblan", + "Smithy", + "Tomra", + "DwarfCastle", + "LowerBabil", + "UpperBabilAfterFall", + "CaveOfSummons", + "Feymarch", + "SylvanCave", + "SealedCave", + "BahamutCave", + "LunarPath", + "LunarCore", + "Adamant", + "Mysidia", + "LunarPalace" +] + +hook_areas = [ + "UpperBabil", + "UpperBabilAfterFall", + "CaveEblan", + "Adamant" +] + +underworld_areas = [ + "Smithy", + "Tomra", + "DwarfCastle", + "LowerBabil", + "CaveOfSummons", + "Feymarch", + "SylvanCave", + "SealedCave" +] + +moon_areas = [ + "Giant", + "BahamutCave", + "LunarPath", + "LunarCore", + "LunarPalace" +] + +overworld_areas = [area for area in areas if area not in [*hook_areas, *underworld_areas, *moon_areas]] diff --git a/worlds/ff4fe/treasure.csvdb b/worlds/ff4fe/treasure.csvdb new file mode 100644 index 000000000000..2a07dc17dc03 --- /dev/null +++ b/worlds/ff4fe/treasure.csvdb @@ -0,0 +1,404 @@ +ordr,flag,world,area,map,index,x,y,contents,jcontents,fight,exclude,spoilerarea,spoilersubarea,spoilerdetail +0,0x00,Overworld,BaronTown,#BaronTown,0,8,2,#item.Heal,#item.Soft,,,Baron Town,Exterior,"grass, top" +1,0x01,Overworld,BaronTown,#BaronTown,1,3,6,#item.Life,,,,Baron Town,Exterior,"grass, left" +2,0x02,Overworld,BaronTown,#BaronTown,2,12,10,#item.Heal,#item.Soft,,,Baron Town,Exterior,"grass, bottom" +3,0x03,Overworld,BaronTown,#BaronTown,3,18,19,#item.Cure1,,,,Baron Town,Exterior,pot outside Rosa's +4,0x04,Overworld,BaronTown,#BaronTown,4,24,27,#item.Cure1,,,,Baron Town,Exterior,pot outside inn +5,0x05,Overworld,BaronTown,#BaronTown,5,27,26,#item.Tent,,,,Baron Town,Exterior,"water, top" +6,0x06,Overworld,BaronTown,#BaronTown,6,29,29,#item.Tent,#item.HrGlass1,,,Baron Town,Exterior,"water, bottom" +7,0x07,Overworld,Mist,#Mist,0,21,8,100 gp,#item.Bomb,,,Mist Village,Exterior,north grass +8,0x08,Overworld,Mist,#Mist,1,28,20,#item.Cure1,#item.Bomb,,,Mist Village,Exterior,"south grass, left" +9,0x09,Overworld,Mist,#Mist,2,29,20,#item.Heal,#item.Bomb,,,Mist Village,Exterior,"south grass, right" +10,0x0A,Overworld,Kaipo,#Kaipo,0,24,18,#item.Ether1,,,,Kaipo,Exterior,pot +11,0x0B,Overworld,Silvera,#Silvera,0,13,8,5000 gp,,,,Silvera,Exterior,north grass +12,0x0C,Overworld,Silvera,#Silvera,1,23,8,#item.SilverDagger,,,,Silvera,Exterior,northeast grass +13,0x0D,Overworld,Silvera,#Silvera,2,30,17,#item.SilverStaff,,,,Silvera,Exterior,east grass +14,0x0E,Overworld,ToroiaTown,#ToroiaTown,0,4,3,1000 gp,,,,Toroia Town,Exterior,"grass, top right" +15,0x0F,Overworld,ToroiaTown,#ToroiaTown,1,1,5,#item.Cure2,#item.Illusion,,,Toroia Town,Exterior,"grass, left" +16,0x10,Overworld,ToroiaTown,#ToroiaTown,2,1,7,#item.Ether1,,,,Toroia Town,Exterior,"grass, bottom left" +17,0x11,Overworld,ToroiaTown,#ToroiaTown,3,4,7,#item.Ether2,,,,Toroia Town,Exterior,"grass, bottom right" +18,0x12,Overworld,Agart,#Agart,0,9,15,#item.Cure2,#item.Boreas,,,Agart,Exterior,grass +19,0x13,Overworld,BaronTown,#BaronInn,0,16,3,#item.Tent,#item.fe_EagleEye,,,Baron Town,Inn,"treasury, left" +20,0x14,Overworld,BaronTown,#BaronInn,1,17,3,#item.Heal,#item.Tent,,,Baron Town,Inn,"treasury, middle" +21,0x15,Overworld,BaronTown,#BaronInn,2,19,3,#item.Cure1,#item.EyeDrops,,,Baron Town,Inn,"treasury, right" +22,0x16,Overworld,BaronTown,#BaronInn,3,10,17,#item.Cure1,,,,Baron Town,Inn,pot +23,0x17,Overworld,BaronWeaponShop,#BaronEquipment,0,5,4,#item.ThunderClaw,#item.ZeusRage,,,Baron Town,Weapon/armor shop,weapon +24,0x18,Overworld,BaronWeaponShop,#BaronEquipment,1,9,4,2000 gp,,,,Baron Town,Weapon/armor shop,armor +25,0x19,Overworld,BaronTown,#RosaHouse,0,1,3,#item.Tent,#item.MaidKiss,,,Baron Town,Rosa's house,pot +26,0x1A,Overworld,BaronTown,#RosaHouse,1,5,3,#item.Ether1,,,,Baron Town,Rosa's house,shelf +27,0x1B,Overworld,Mist,#RydiaHouse,0,22,5,#item.Tiara,,,,Mist Village,Rydia's house,"crawlspace, top left" +28,0x1C,Overworld,Mist,#RydiaHouse,1,23,5,#item.Cloth,,,,Mist Village,Rydia's house,"crawlspace, top right" +29,0x1D,Overworld,Mist,#RydiaHouse,2,23,7,#item.RubyRing,,,,Mist Village,Rydia's house,"crawlspace, bottom right" +30,0x1E,Overworld,Mist,#RydiaHouse,3,22,25,#item.Change,,,,Mist Village,Rydia's house,tiara room +31,0x1F,Overworld,ChocoboForest,#BlackChocoboForest,0,31,8,#item.Carrot,,,,Black Chocobo Forest,,"northeast, top" +32,0x20,Overworld,ChocoboForest,#BlackChocoboForest,1,31,9,#item.Carrot,,,,Black Chocobo Forest,,"northeast, bottom" +33,0x21,Overworld,ChocoboForest,#BlackChocoboForest,2,20,29,#item.Carrot,,,,Black Chocobo Forest,,south grass +34,0x22,Overworld,BaronCastle,#BaronCastleLobby,0,13,3,300 gp,480 gp,,,Baron Castle,1F,left +35,0x23,Overworld,BaronCastle,#BaronCastleLobby,1,14,3,#item.Cure1,#item.Ether1,,,Baron Castle,1F,middle +36,0x24,Overworld,BaronCastle,#BaronCastleLobby,2,15,3,#item.Tent,,,,Baron Castle,1F,right +37,0x25,Overworld,BaronCastle,#BaronCastleEastHall,0,2,15,#item.Ether1,,,,Baron Castle,East hall treasury,top left +38,0x26,Overworld,BaronCastle,#BaronCastleEastHall,1,3,15,#item.Ether1,,,,Baron Castle,East hall treasury,top middle +39,0x27,Overworld,BaronCastle,#BaronCastleEastHall,2,4,15,#item.Cure2,#item.Unihorn,,,Baron Castle,East hall treasury,top right +40,0x28,Overworld,BaronCastle,#BaronCastleEastHall,3,2,16,#item.Cure2,#item.Unihorn,,,Baron Castle,East hall treasury,bottom left +41,0x29,Overworld,BaronCastle,#BaronCastleEastHall,4,3,16,#item.Life,,,,Baron Castle,East hall treasury,bottom middle +42,0x2A,Overworld,BaronCastle,#BaronCastleEastHall,5,4,16,#item.Life,,,,Baron Castle,East hall treasury,bottom right +43,0x2B,Overworld,BaronCastle,#BaronCastleEastTower1F,0,3,3,#item.Cure1,#item.Bacchus,,,Baron Castle,East tower 1F,top +44,0x2C,Overworld,BaronCastle,#BaronCastleEastTower1F,1,1,4,#item.Cure1,#item.Bacchus,,,Baron Castle,East tower 1F,leftmost +45,0x2D,Overworld,BaronCastle,#BaronCastleEastTower1F,2,2,4,#item.Heal,#item.Hermes,,,Baron Castle,East tower 1F,middle left +46,0x2E,Overworld,BaronCastle,#BaronCastleEastTower1F,3,8,4,#item.Tent,#item.Hermes,,,Baron Castle,East tower 1F,right +47,0x2F,Overworld,BaronCastle,#BaronCastleEastTower2F,0,1,4,#item.Life,#item.Cure2,,,Baron Castle,East tower 2F,"left, top" +48,0x30,Overworld,BaronCastle,#BaronCastleEastTower2F,1,1,5,#item.Cure2,,,,Baron Castle,East tower 2F,"left, bottom" +49,0x31,Overworld,BaronCastle,#BaronCastleEastTower3F,0,5,3,#item.Cure3,,,map tile is not trigger,Baron Castle,East tower 3F, +50,0x32,Overworld,BaronCastle,#BaronCastleEastTower3F,1,7,4,#item.Ether1,,,,Baron Castle,East tower 3F,pot +51,0x33,Overworld,BaronCastle,#BaronCastleEastTower3F,2,4,5,#item.Ether1,,,,Baron Castle,East tower 3F,left chest +52,0x34,Overworld,BaronCastle,#BaronCastleEastTower3F,3,5,5,#item.Tent,,,,Baron Castle,East tower 3F,middle chest +53,0x35,Overworld,BaronCastle,#BaronCastleEastTower3F,4,6,5,#item.Tent,,,,Baron Castle,East tower 3F,right chest +54,0x36,Overworld,BaronCastle,#BaronCastleEastTowerB1,0,4,3,#item.Elixir,,,,Baron Castle,East tower B1,pot +55,0x37,Overworld,Sewer,#SewerEntrance,0,13,29,#item.Cure2,,,,Old Water-way,B1F,left +56,0x38,Overworld,Sewer,#SewerEntrance,1,16,29,#item.Ether1,,,,Old Water-way,B1F,middle +57,0x39,Overworld,Sewer,#SewerEntrance,2,19,29,#item.Life,#item.ZeusRage,,,Old Water-way,B1F,right +58,0x3A,Overworld,Sewer,#SewerB3,0,1,12,500 gp,#item.HrGlass1,,,Old Water-way,B3F,west +59,0x3B,Overworld,Sewer,#SewerB3,1,7,19,#item.Life,#item.SilkWeb,,,Old Water-way,B3F,southwest +60,0x3C,Overworld,Sewer,#SewerB3,2,20,17,#item.Life,#item.Hermes,,,Old Water-way,B3F,center +61,0x3D,Overworld,Sewer,#SewerSaveRoom,0,17,5,#item.Ancient,,,,Old Water-way,Save room,hidden chest +62,0x3E,Overworld,Sewer,#SewerB2,0,2,7,#item.Ether1,,,,Old Water-way,B2F,chest +63,0x3F,Overworld,Damcyan,#Damcyan2F,0,11,4,#item.Tent,,,,Damcyan,2F,chest +64,0x40,Overworld,Damcyan,#DamcyanTreasuryEntrance,0,1,3,#item.RubyRing,,,,Damcyan,Treasury entrance,west cell +65,0x41,Overworld,Damcyan,#DamcyanTreasuryEntrance,1,4,3,#item.LeatherHat,,,,Damcyan,Treasury entrance,"middle cell, left" +66,0x42,Overworld,Damcyan,#DamcyanTreasuryEntrance,2,5,3,#item.CrossBow,,,,Damcyan,Treasury entrance,"middle cell, right" +67,0x43,Overworld,Damcyan,#DamcyanTreasuryDownstairs,0,3,4,#item.Cure1,,,,Damcyan,Treasury,top left chest +68,0x44,Overworld,Damcyan,#DamcyanTreasuryDownstairs,1,4,4,#item.Cure1,#item.Antidote,,,Damcyan,Treasury,top middle chest +69,0x45,Overworld,Damcyan,#DamcyanTreasuryDownstairs,2,5,4,300 gp,#item.EyeDrops,,,Damcyan,Treasury,top right chest +70,0x46,Overworld,Damcyan,#DamcyanTreasuryDownstairs,3,7,4,#item.IronArrow,,,,Damcyan,Treasury,top right pot +71,0x47,Overworld,Damcyan,#DamcyanTreasuryDownstairs,4,1,5,#item.WhiteArrow,,,,Damcyan,Treasury,bottom left pot +72,0x48,Overworld,Damcyan,#DamcyanTreasuryDownstairs,5,3,5,#item.Ether1,,,,Damcyan,Treasury,bottom left chest +73,0x49,Overworld,Damcyan,#DamcyanTreasuryDownstairs,6,4,5,#item.Life,,,,Damcyan,Treasury,bottom middle chest +74,0x4A,Overworld,Damcyan,#DamcyanTreasuryDownstairs,7,5,5,200 gp,#item.Soft,,,Damcyan,Treasury,bottom right chest +75,0x4B,Overworld,Damcyan,#DamcyanTreasuryDownstairs,8,7,5,#item.WhiteArrow,,,,Damcyan,Treasury,bottom right pot +76,0x4C,Overworld,Sewer,#RoomToSewer,0,14,5,1000 gp,,,,Baron Town,Access to Old Water-way,hidden chest +77,0x4D,Overworld,Fabul,#FabulThroneRoom,0,1,7,#item.Cure1,#item.Hermes,,,Fabul,Throne room,west pot +78,0x4E,Overworld,Fabul,#FabulThroneRoom,1,28,7,#item.Cure1,#item.SilkWeb,,,Fabul,Throne room,top chest +79,0x4F,Overworld,Fabul,#FabulThroneRoom,2,28,8,#item.BlackShield,,,,Fabul,Throne room,middle chest +80,0x50,Overworld,Fabul,#FabulThroneRoom,3,28,9,#item.Ether1,,,,Fabul,Throne room,bottom chest +81,0x51,Overworld,Fabul,#FabulEastTower2F,0,9,6,500 gp,#item.Bacchus,,,Fabul,East tower 2F,pot +82,0x52,Overworld,Fabul,#FabulKingRoom,0,3,7,#item.Tent,,,,Fabul,East tower 3F (king's bedroom),chest +83,0x53,Overworld,Fabul,#FabulWestTower1F,0,2,3,#item.Cure1,#item.Bomb,,,Fabul,West tower 1F,top chest +84,0x54,Overworld,Fabul,#FabulWestTower1F,1,6,3,#item.Cure1,#item.ZeusRage,,,Fabul,West tower 1F,pot +85,0x55,Overworld,Fabul,#FabulWestTower1F,2,1,4,#item.Heal,#item.Notus,,,Fabul,West tower 1F,middle chest +86,0x56,Overworld,Fabul,#FabulWestTower1F,3,1,5,#item.Tent,#item.Cure1,,,Fabul,West tower 1F,bottom chest +87,0x57,Overworld,ToroiaCastle,#ToroiaCastlePotRoom,0,5,6,#item.Ether1,#item.Bacchus,,,Toroia Castle,East wing pot room,pot +88,0x58,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,0,1,3,#item.Tent,,,,Toroia Castle,East wing chest room,"left cell, left" +89,0x59,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,1,2,3,#item.Tent,,,,Toroia Castle,East wing chest room,"left cell, right" +90,0x5A,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,2,4,3,#item.Cure2,,,,Toroia Castle,East wing chest room,"middle-left cell, left" +91,0x5B,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,3,5,3,#item.Cure2,#item.RubyRing,,,Toroia Castle,East wing chest room,"middle-left cell, right" +92,0x5C,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,4,7,3,#item.Ether1,,,,Toroia Castle,East wing chest room,"middle-right cell, left" +93,0x5D,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,5,8,3,#item.Ether1,,,,Toroia Castle,East wing chest room,"middle-right cell, right" +94,0x5E,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,6,10,3,#item.RubyRing,,,,Toroia Castle,East wing chest room,"right cell, left" +95,0x5F,Overworld,ToroiaCastle,#ToroiaCastleChestRoom,7,11,3,#item.RubyRing,,,,Toroia Castle,East wing chest room,"right cell, right" +96,0x60,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,0,3,5,#item.Cure2,,,,Toroia Castle,Treasury,"top row, 1" +97,0x61,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,1,4,5,#item.Heal,,,,Toroia Castle,Treasury,"top row, 2" +98,0x62,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,2,5,5,#item.Ether1,,,,Toroia Castle,Treasury,"top row, 3" +99,0x63,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,3,6,5,#item.Ether2,,,,Toroia Castle,Treasury,"top row, 4" +100,0x64,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,4,7,5,#item.Elixir,,,,Toroia Castle,Treasury,"top row, 5" +101,0x65,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,5,8,5,1000 gp,#item.EchoNote,,,Toroia Castle,Treasury,"top row, 6" +102,0x66,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,6,9,5,#item.GreatBow,,,,Toroia Castle,Treasury,"top row, 7" +103,0x67,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,7,10,5,#item.FireArrow,,,,Toroia Castle,Treasury,"top row, 8" +104,0x68,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,8,11,5,#item.FireArrow,,,,Toroia Castle,Treasury,"top row, 9" +105,0x69,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,9,3,6,#item.Cure2,,,,Toroia Castle,Treasury,"bottom row, 1" +106,0x6A,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,10,4,6,#item.Heal,,,,Toroia Castle,Treasury,"bottom row, 2" +107,0x6B,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,11,5,6,#item.Ether1,,,,Toroia Castle,Treasury,"bottom row, 3" +108,0x6C,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,12,6,6,#item.Ether2,,,,Toroia Castle,Treasury,"bottom row, 4" +109,0x6D,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,13,7,6,#item.Elixir,#item.AgApple,,,Toroia Castle,Treasury,"bottom row, 5" +110,0x6E,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,14,8,6,1000 gp,#item.EchoNote,,,Toroia Castle,Treasury,"bottom row, 6" +111,0x6F,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,15,9,6,#item.IceArrow,,,,Toroia Castle,Treasury,"bottom row, 7" +112,0x70,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,16,10,6,#item.IceArrow,,,,Toroia Castle,Treasury,"bottom row, 8" +113,0x71,Overworld,ToroiaTreasury,#ToroiaCastleTreasury,17,11,6,#item.LitArrow,,,,Toroia Castle,Treasury,"bottom row, 9" +114,0x72,Overworld,Eblan,#Eblan1F,0,13,4,#item.Cure2,,,,Eblan Castle,1F,chest +115,0x73,Overworld,Eblan,#Eblan2F,0,4,3,#item.Heal,#item.Unihorn,,,Eblan Castle,2F,"west room, top" +116,0x74,Overworld,Eblan,#Eblan2F,1,4,4,#item.Cure2,#item.Alarm,,,Eblan Castle,2F,"west room, bottom" +117,0x75,Overworld,Eblan,#Eblan2F,2,14,3,#item.Cure2,#item.Soft,,,Eblan Castle,2F,"east room, top" +118,0x76,Overworld,Eblan,#Eblan2F,3,14,4,#item.Cabin,,,,Eblan Castle,2F,"east room, bottom-left" +119,0x77,Overworld,Eblan,#Eblan2F,4,15,4,#item.Life,#item.MaidKiss,,,Eblan Castle,2F,"east room, bottom-right" +120,0x78,Overworld,Eblan,#EblanWestTower1F,0,1,5,#item.Ether1,#item.Kamikaze,,,Eblan Castle,West tower 1F,left +121,0x79,Overworld,Eblan,#EblanWestTower1F,1,11,5,#item.Sleep,,0x1C0,,Eblan Castle,West tower 1F,right +122,0x7A,Overworld,Eblan,#EblanWestTower2F,0,4,4,#item.Cure2,#item.Bacchus,,,Eblan Castle,West tower 2F,top-left pot +123,0x7B,Overworld,Eblan,#EblanWestTower2F,1,8,8,#item.Cure2,#item.Bacchus,,,Eblan Castle,West tower 2F,bottom-right pot +124,0x7C,Overworld,Eblan,#EblanWestTower2F,2,10,5,2000 gp,10000 gp,,,Eblan Castle,West tower 2F,left chest +125,0x7D,Overworld,Eblan,#EblanWestTower2F,3,11,5,#item.MuteArrow,,,,Eblan Castle,West tower 2F,right chest +126,0x7E,Overworld,Eblan,#EblanEastTower1F,0,2,5,600 gp,#item.Coffin,,,Eblan Castle,East tower 1F,left +127,0x7F,Overworld,Eblan,#EblanEastTower1F,1,12,5,800 gp,#item.HrGlass2,,,Eblan Castle,East tower 1F,right +128,0x80,Overworld,Eblan,#EblanEastTower2F,0,1,5,#item.Cure2,#item.Exit,,,Eblan Castle,East tower 2F,"left side, top chest" +129,0x81,Overworld,Eblan,#EblanEastTower2F,1,1,6,#item.Ether1,,,,Eblan Castle,East tower 2F,"left side, bottom chest" +130,0x82,Overworld,Eblan,#EblanEastTower2F,2,6,3,#item.Life,#item.Hermes,,,Eblan Castle,East tower 2F,pot +131,0x83,Overworld,Eblan,#EblanEastTower2F,3,11,5,#item.DrainSpear,,0x1C1,,Eblan Castle,East tower 2F,"right side, top chest" +132,0x84,Overworld,Eblan,#EblanEastTower2F,4,11,6,#item.Cabin,,,,Eblan Castle,East tower 2F,"right side, bottom chest" +133,0x85,Overworld,Eblan,#EblanBasement,0,2,11,#item.Ether1,,,,Eblan Castle,Basement,left +134,0x86,Overworld,Eblan,#EblanBasement,1,3,11,#item.Ether1,,,,Eblan Castle,Basement,middle +135,0x87,Overworld,Eblan,#EblanBasement,2,8,12,#item.Elixir,#item.AgApple,0x1C2,,Eblan Castle,Basement,right +136,0x88,Overworld,MistCave,#MistCave,0,6,3,#item.Cure1,,,,Mist Cave,,top left +137,0x89,Overworld,MistCave,#MistCave,1,24,3,#item.Heal,#item.EyeDrops,,,Mist Cave,,top right +138,0x8A,Overworld,MistCave,#MistCave,2,13,12,#item.Tent,,,,Mist Cave,,center +139,0x8B,Overworld,MistCave,#MistCave,3,30,18,#item.Cure1,,,,Mist Cave,,bottom right +140,0x8C,Overworld,WateryPass,#WateryPass1F,0,29,14,#item.Heal,#item.MaidKiss,,,Watery Pass,"Entrance (""South"")",rightmost +141,0x8D,Overworld,WateryPass,#WateryPass1F,1,22,16,#item.Cure1,,,,Watery Pass,"Entrance (""South"")",center-right +142,0x8E,Overworld,WateryPass,#WateryPass1F,2,4,28,#item.Tent,,,,Watery Pass,"Entrance (""South"")",bottom left +143,0x8F,Overworld,WateryPass,#WateryPass1F,3,13,14,120 gp,#item.Bomb,,,Watery Pass,"Entrance (""South"")",center +144,0x90,Overworld,WateryPass,#WateryPass1F,4,25,3,#item.IronRing,,,,Watery Pass,"Entrance (""South"")",top right +145,0x91,Overworld,WateryPass,#WateryPass2F,0,21,5,#item.Cure1,,,,Watery Pass,"Camp level (""B2F"")",top right +146,0x92,Overworld,WateryPass,#WateryPass2F,1,9,18,#item.Ether1,,,,Watery Pass,"Camp level (""B2F"")",bottom left +147,0x93,Overworld,WateryPass,#WateryPass2F,2,13,3,#item.IceRod,,,,Watery Pass,"Camp level (""B2F"")",top left +148,0x94,Overworld,WateryPass,#WateryPass3F,0,22,24,200 gp,580 gp,,,Watery Pass,B3F,right +149,0x95,Overworld,WateryPass,#WateryPass3F,1,12,24,#item.Cure1,#item.Bomb,,,Watery Pass,B3F,left +150,0x96,Overworld,WateryPass,#WateryPass4F,0,5,17,#item.Cure1,,,,Watery Pass,"Passage to exit (""B2F"")",left +151,0x97,Overworld,WateryPass,#WateryPass4F,1,19,22,#item.LeatherHat,,,,Watery Pass,"Passage to exit (""B2F"")",right +152,0x98,Overworld,WateryPass,#WateryPass5F,0,3,15,#item.Ether1,,,,Watery Pass,"Exit (""North"")",leftmost +153,0x99,Overworld,WateryPass,#WateryPass5F,1,5,10,#item.Ether1,#item.Notus,,,Watery Pass,"Exit (""North"")",top left +154,0x9A,Overworld,WateryPass,#WateryPass5F,2,17,17,#item.DarknessSword,,,,Watery Pass,"Exit (""North"")",bridge chest +155,0x9B,Overworld,WateryPass,#WateryPass5F,3,20,20,#item.Heal,#item.HrGlass1,,,Watery Pass,"Exit (""North"")",bottom right +156,0x9C,Overworld,Waterfall,#Waterfall1F,0,12,27,#item.DarknessHelm,,,,Waterfall,B2F,left +157,0x9D,Overworld,Waterfall,#Waterfall1F,1,16,27,#item.DarknessGauntlet,,,,Waterfall,B2F,right +158,0x9E,Overworld,Waterfall,#Waterfall2F,0,25,22,#item.DarknessArmor,,,,Waterfall,Lake,right +159,0x9F,Overworld,Waterfall,#Waterfall2F,1,24,23,#item.Ether1,#item.Hermes,,,Waterfall,Lake,bottom +160,0xA0,Overworld,AntlionCave,#AntlionCave1F,0,3,7,190 gp,,,,Antlion Cave,B1F,top left +161,0xA1,Overworld,AntlionCave,#AntlionCave1F,1,10,4,#item.Cure1,,,,Antlion Cave,B1F,top center-left +162,0xA2,Overworld,AntlionCave,#AntlionCave1F,2,2,23,#item.Tent,,,,Antlion Cave,B1F,bottom left +163,0xA3,Overworld,AntlionCave,#AntlionCave1F,3,6,17,#item.Cure1,,,,Antlion Cave,B1F,middle left +164,0xA4,Overworld,AntlionCave,#AntlionCave1F,4,27,4,210 gp,#item.SilkWeb,,,Antlion Cave,B1F,top right +165,0xA5,Overworld,AntlionCave,#AntlionCave1F,5,23,9,#item.Cure1,#item.Cure2,,,Antlion Cave,B1F,middle right +166,0xA6,Overworld,AntlionCave,#AntlionCave2F,0,23,4,#item.Heal,#item.HrGlass1,,,Antlion Cave,B2F,top right +167,0xA7,Overworld,AntlionCave,#AntlionCave2F,1,29,23,#item.Life,#item.Notus,,,Antlion Cave,B2F,bottom right +168,0xA8,Overworld,AntlionCave,#AntlionCave2F,2,8,5,250 gp,#item.SilkWeb,,,Antlion Cave,B2F,top left +169,0xA9,Overworld,AntlionCave,#AntlionCaveSaveRoom,0,8,7,#item.Life,,,,Antlion Cave,Save room,right +170,0xAA,Overworld,AntlionCave,#AntlionCaveSaveRoom,1,6,3,#item.Ether1,,,,Antlion Cave,Save room,top +171,0xAB,Overworld,AntlionCave,#AntlionCaveSaveRoom,2,1,4,#item.Tent,#item.Exit,,,Antlion Cave,Save room,left +172,0xAC,Overworld,AntlionCave,#AntlionCaveTreasureRoom,0,3,4,#item.CharmHarp,,,,Antlion Cave,Treasure room,chest +173,0xAD,Overworld,MountHobs,#MountHobsSummit,0,20,24,#item.WhiteArrow,,,,Mt. Hobs,Summit,chest +174,0xAE,Overworld,MountHobs,#MountHobsSave,0,11,19,#item.Tent,,,,Mt. Hobs,Save area,leftmost +175,0xAF,Overworld,MountHobs,#MountHobsSave,1,16,17,#item.Cure1,,,,Mt. Hobs,Save area,center-left +176,0xB0,Overworld,MountHobs,#MountHobsSave,2,18,18,#item.Heal,#item.Soft,,,Mt. Hobs,Save area,center-right +177,0xB1,Overworld,MountHobs,#MountHobsSave,3,22,18,350 gp,960 gp,,,Mt. Hobs,Save area,rightmost +178,0xB2,Overworld,WateryPass,#WateryPassWaterfallRoom,0,1,7,#item.Cure2,#item.Cure3,,,Watery Pass,Behind waterfall,left +179,0xB3,Overworld,WateryPass,#WateryPassWaterfallRoom,1,2,8,#item.Elixir,#item.Ether2,,,Watery Pass,Behind waterfall,middle +180,0xB4,Overworld,WateryPass,#WateryPassWaterfallRoom,2,7,6,1000 gp,#item.Life,,,Watery Pass,Behind waterfall,right +181,0xB5,Overworld,MountOrdeals,#MountOrdeals1F,0,23,19,#item.Cure1,,,,Mt. Ordeals,1F,bottom +182,0xB6,Overworld,MountOrdeals,#MountOrdeals1F,1,22,15,#item.Cure1,,,,Mt. Ordeals,1F,top +183,0xB7,Overworld,MountOrdeals,#MountOrdeals3F,0,9,17,#item.Ether1,,,,Mt. Ordeals,"3F (""7th Station"")",left +184,0xB8,Overworld,MountOrdeals,#MountOrdeals3F,1,16,13,#item.Ether1,,,,Mt. Ordeals,"3F (""7th Station"")",center +185,0xB9,Overworld,CaveMagnes,#CaveMagnes1F,0,27,7,#item.Cure2,,,,Cave Magnes,B1F,top right +186,0xBA,Overworld,CaveMagnes,#CaveMagnes1F,1,2,20,#item.Heal,#item.Unihorn,,,Cave Magnes,B1F,bottom left +187,0xBB,Overworld,CaveMagnes,#CaveMagnes2F,0,4,19,#item.Ether1,,,,Cave Magnes,B2F,chest +188,0xBC,Overworld,CaveMagnes,#CaveMagnesPitTreasureRoom,0,2,5,500 gp,2000 gp,,,Cave Magnes,Pit room,left +189,0xBD,Overworld,CaveMagnes,#CaveMagnesPitTreasureRoom,1,11,7,#item.Ether1,,,,Cave Magnes,Pit room,right +190,0xBE,Overworld,CaveMagnes,#CaveMagnesTorchTreasureRoom,0,4,6,#item.Cure2,,,,Cave Magnes,Torch room,left +191,0xBF,Overworld,CaveMagnes,#CaveMagnesTorchTreasureRoom,1,8,5,#item.Cure3,#item.SilkWeb,,,Cave Magnes,Torch room,center +192,0xC0,Overworld,CaveMagnes,#CaveMagnesTorchTreasureRoom,2,12,6,#item.Life,#item.HrGlass1,,,Cave Magnes,Torch room,right +193,0xC1,Overworld,CaveMagnes,#CaveMagnes4F,0,14,5,#item.CharmClaw,,,,Cave Magnes,B3F to B4F passage,chest +194,0xC2,Overworld,CaveMagnes,#CaveMagnes5F,0,7,8,#item.Ether2,#item.Exit,,,Cave Magnes,B4F,chest (left of crystal room entrance) +195,0xC3,Overworld,Zot,#Zot1F,0,3,4,#item.FireArmor,,,,Tower of Zot,1F,chest +196,0xC4,Overworld,Zot,#Zot2F,0,7,15,#item.FireBrand,,0x1C3,,Tower of Zot,2F,chest +197,0xC5,Overworld,Zot,#Zot5F,0,11,9,#item.PoisonClaw,,,,Tower of Zot,5F (through 4F center-left door),top +198,0xC6,Overworld,Zot,#Zot5F,1,11,10,#item.EarthHammer,,,,Tower of Zot,5F (through 4F center door),chest +199,0xC7,Overworld,Zot,#Zot5F,2,10,23,#item.FireShield,,,,Tower of Zot,5F (through 4F center-left door),bottom +200,0xC8,Overworld,Zot,#Zot5F,3,19,14,#item.WizardArmor,,,,Tower of Zot,5F (through 4F center-right door),chest +201,0xC9,Underworld,UpperBabil,#BabilB1,0,18,4,#item.Cure3,#item.Unihorn,,,Tower of Bab-il (upper),1F,top +202,0xCA,Underworld,UpperBabil,#BabilB1,1,26,12,#item.Cure3,#item.HrGlass2,,,Tower of Bab-il (upper),1F,right +203,0xCB,Underworld,UpperBabil,#BabilB2,0,14,20,#item.Ogre,,0x1C4,,Tower of Bab-il (upper),B2F,chest (long bridge) +204,0xCC,Underworld,UpperBabil,#BabilB3,0,20,21,2000 gp,#item.Succubus,,,Tower of Bab-il (upper),B3F,chest (ring bridge) +205,0xCD,Underworld,UpperBabil,#BabilB4,0,12,18,#item.Middle,,,,Tower of Bab-il (upper),B4F,chest (U bridge) +206,0xCE,Underworld,UpperBabil,#BabilB5,0,5,15,82000 gp,,,,Tower of Bab-il (upper),B5F,chest (left of crystal room entrance) +207,0xCF,Moon,Giant,#GiantChest,0,9,8,#item.Shuriken,,,,Giant of Bab-il,Chest,top left +208,0xD0,Moon,Giant,#GiantChest,1,11,19,#item.Cure2,,,,Giant of Bab-il,Chest,bottom left +209,0xD1,Moon,Giant,#GiantChest,2,21,20,#item.Ether1,,,,Giant of Bab-il,Chest,middle +210,0xD2,Moon,Giant,#GiantChest,3,27,6,#item.SamuraiArrow,,,,Giant of Bab-il,Chest,top right +211,0xD3,Moon,Giant,#GiantChest,4,14,18,#item.Cabin,#item.Siren,,,Giant of Bab-il,Chest,bottom right +212,0xD4,Moon,Giant,#GiantStomach,0,19,9,#item.Life,#item.AgApple,,,Giant of Bab-il,Stomach,center +213,0xD5,Moon,Giant,#GiantStomach,1,26,6,#item.Life,#item.SomaDrop,,,Giant of Bab-il,Stomach,top right +214,0xD6,Moon,Giant,#GiantPassage,0,14,21,#item.Elixir,,0x1C7,,Giant of Bab-il,Passage,chest +215,0xD7,Underworld,CaveEblan,#CaveEblanEntrance,0,18,4,#item.Shuriken,,,,Cave Eblana,B1F,top middle +216,0xD8,Underworld,CaveEblan,#CaveEblanEntrance,1,22,3,#item.Heal,,,,Cave Eblana,B1F,top right +217,0xD9,Underworld,CaveEblan,#CaveEblanEntrance,2,26,25,1200 gp,#item.Vampire,,,Cave Eblana,B1F,bottom right +218,0xDA,Underworld,CaveEblan,#CaveEblanPass,0,8,28,#item.Ether1,,,,Cave Eblana,Pass to Bab-il (west half),southwest entrance +219,0xDB,Underworld,CaveEblan,#CaveEblanPass,1,4,10,#item.Tent,,,,Cave Eblana,Pass to Bab-il (west half),northwest exit +220,0xDC,Underworld,CaveEblan,#CaveEblanPass,2,12,11,#item.Cure2,,,,Cave Eblana,Pass to Bab-il (east half),"north entrance, left" +221,0xDD,Underworld,CaveEblan,#CaveEblanPass,3,16,11,#item.Cabin,,,,Cave Eblana,Pass to Bab-il (east half),"north entrance, right top" +222,0xDE,Underworld,CaveEblan,#CaveEblanPass,4,16,12,#item.Cure2,,,,Cave Eblana,Pass to Bab-il (east half),"north entrance, right bottom" +223,0xDF,Underworld,CaveEblan,#CaveEblanPass,5,20,12,#item.Elixir,,,,Cave Eblana,Pass to Bab-il (east half),"north entrance, secret path, top" +224,0xE0,Underworld,CaveEblan,#CaveEblanPass,6,20,14,#item.Elixir,,,,Cave Eblana,Pass to Bab-il (east half),"north entrance, secret path, bottom" +225,0xE1,Underworld,CaveEblan,#CaveEblanPass,7,10,26,#item.Cure2,,,,Cave Eblana,Pass to Bab-il (west half),"bottom, through secret path" +226,0xE2,Underworld,CaveEblan,#CaveEblanPass,8,15,28,800 gp,#item.SilkWeb,,,Cave Eblana,Pass to Bab-il (east half),bottom +227,0xE3,Underworld,CaveEblan,#CaveEblanPass,9,27,27,850 gp,#item.HrGlass2,,,Cave Eblana,Pass to Bab-il (east half),"southeast corner, left" +228,0xE4,Underworld,CaveEblan,#CaveEblanPass,10,28,27,#item.Life,,,,Cave Eblana,Pass to Bab-il (east half),"southeast corner, middle" +229,0xE5,Underworld,CaveEblan,#CaveEblanPass,11,29,27,#item.Life,,,,Cave Eblana,Pass to Bab-il (east half),"southeast corner, right" +230,0xE6,Underworld,CaveEblan,#CaveEblanExit,0,14,8,#item.Cure3,#item.Kamikaze,,,Cave Eblana,Pass to Bab-il (north connection),top right +231,0xE7,Underworld,CaveEblan,#CaveEblanExit,1,17,9,#item.Ether2,,,,Cave Eblana,Exit,"top left, through secret path" +232,0xE8,Underworld,CaveEblan,#CaveEblanExit,2,18,16,#item.Life,#item.Soft,,,Cave Eblana,Exit,middle left +233,0xE9,Underworld,CaveEblan,#CaveEblanExit,3,18,22,#item.Shuriken,,,,Cave Eblana,Exit,bottom left +234,0xEA,Underworld,CaveEblan,#CaveEblanSaveRoom,0,17,5,#item.DrainSword,,0x1C6,,Cave Eblana,Save Room,hidden chest +235,0xEB,Underworld,CaveEblan,#CaveEblanHospital,0,2,5,#item.Cure1,,,,Cave Eblana,Hospital,left pot +236,0xEC,Underworld,CaveEblan,#CaveEblanHospital,1,14,7,#item.Cure1,,,,Cave Eblana,Hospital,right pot +237,0xED,Overworld,ChocoboForest,#FabulChocoboForest,0,9,7,#item.Carrot,,,,Fabul Chocobo Forest,,grass +238,0xEE,Overworld,ChocoboForest,#MountOrdealsChocoboForest,0,9,7,#item.Carrot,,,,Mt. Ordeals Chocobo Forest,,grass +239,0xEF,Overworld,ChocoboForest,#BaronChocoboForest,0,9,7,#item.Carrot,,,,Baron Chocobo Forest,,grass +240,0xF0,Overworld,ChocoboForest,#TroiaChocoboForest,0,9,7,#item.Carrot,,,,Toroia South Chocobo Forest,,grass +241,0xF1,Overworld,ChocoboForest,#IslandChocoboForest,0,9,7,#item.Carrot,,,,Island Chocobo Forest,,grass +242,0x100,Underworld,Smithy,#SmithyHouseMainFloor,0,10,3,#item.Cure2,,,,Kokkol's House,1F,top pot +243,0x101,Underworld,Smithy,#SmithyHouseMainFloor,1,2,10,#item.Heal,,,,Kokkol's House,1F,left pot +244,0x102,Underworld,Smithy,#SmithyHouseMainFloor,2,25,5,1000 gp,,,,Kokkol's House,1F,hidden chest +245,0x103,Underworld,Smithy,#SmithyRoom,0,5,3,#item.Elixir,#item.SomaDrop,,,Kokkol's House,2F,shelf +246,0x104,Underworld,Tomra,#TomraTreasury,0,3,5,490 gp,#item.Bomb,,,Tomra,Treasury,leftmost chest +247,0x105,Underworld,Tomra,#TomraTreasury,1,3,6,470 gp,#item.Notus,,,Tomra,Treasury,left pot +248,0x106,Underworld,Tomra,#TomraTreasury,2,4,6,#item.Cure2,#item.ZeusRage,,,Tomra,Treasury,bottom left chest +249,0x107,Underworld,Tomra,#TomraTreasury,3,10,4,#item.Cure2,#item.Bestiary,,,Tomra,Treasury,right pot +250,0x108,Underworld,Tomra,#TomraTreasury,4,11,5,#item.Cabin,#item.Ether2,,,Tomra,Treasury,right top chest +251,0x109,Underworld,Tomra,#TomraTreasury,5,11,6,480 gp,2000 gp,,,Tomra,Treasury,right bottom chest +252,0x10A,Underworld,DwarfCastle,#DwarfCastleFatChocobo,0,23,4,#item.Carrot,,,,Dwarf Castle,B1F (Fat Chocobo),left pot +253,0x10B,Underworld,DwarfCastle,#DwarfCastleFatChocobo,1,24,3,#item.Carrot,,,,Dwarf Castle,B1F (Fat Chocobo),top pot +254,0x10C,Underworld,DwarfCastle,#DwarfCastleFatChocobo,2,26,4,#item.Carrot,,,,Dwarf Castle,B1F (Fat Chocobo),right pot +255,0x10D,Underworld,DwarfCastle,#DwarfCastleTunnel,0,27,18,#item.Cabin,,,,Dwarf Castle,B2F (tunnel),left chest +256,0x10E,Underworld,DwarfCastle,#DwarfCastleTunnel,1,28,18,#item.Cabin,,,,Dwarf Castle,B2F (tunnel),middle chest +257,0x10F,Underworld,DwarfCastle,#DwarfCastleTunnel,2,29,19,#item.Cabin,,,,Dwarf Castle,B2F (tunnel),right chest +258,0x110,Underworld,DwarfCastle,#DwarfCastleEastTower1F,0,6,3,#item.Dwarf,,,,Dwarf Castle,East tower 1F,chest +259,0x111,Underworld,DwarfCastle,#DwarfCastleInn,0,8,3,1000 gp,5000 gp,,,Dwarf Castle,Inn,pot +260,0x112,Underworld,LowerBabil,#BabilIcebrandRoom,0,5,4,#item.IceBrand,,0x1E0,,Tower of Bab-il (lower),2F,east room chest +261,0x113,Underworld,LowerBabil,#BabilBlizzardRoom,0,5,4,#item.BlizzardSpear,,0x1E1,,Tower of Bab-il (lower),2F,south room chest +262,0x114,Underworld,LowerBabil,#BabilIceShieldRoom,0,5,4,#item.IceShield,,0x1E2,,Tower of Bab-il (lower),4F,east left room chest +263,0x115,Underworld,LowerBabil,#BabilIceMailRoom,0,5,4,#item.IceArmor,,0x1E3,,Tower of Bab-il (lower),4F,northeast room chest +264,0x116,Underworld,DwarfCastle,#DwarfCastleEastTower3F,0,6,3,#item.Strength,,,,Dwarf Castle,East tower 3F,top +265,0x117,Underworld,DwarfCastle,#DwarfCastleEastTower3F,1,1,7,#item.Ether1,,,,Dwarf Castle,East tower 3F,left +266,0x118,Underworld,DwarfCastle,#DwarfCastleEastTower3F,2,11,7,#item.Ether2,#item.HrGlass2,,,Dwarf Castle,East tower 3F,right +267,0x119,Underworld,DwarfCastle,#DwarfCastleEastTower3F,3,6,11,#item.Elixir,,,,Dwarf Castle,East tower 3F,bottom +268,0x11A,Underworld,DwarfCastle,#DwarfCastleWestTower3F,0,6,3,#item.Elixir,,,,Dwarf Castle,West tower 3F,top +269,0x11B,Underworld,DwarfCastle,#DwarfCastleWestTower3F,1,1,7,#item.Cure2,,,,Dwarf Castle,West tower 3F,left +270,0x11C,Underworld,DwarfCastle,#DwarfCastleWestTower3F,2,11,7,#item.Ether1,,,,Dwarf Castle,West tower 3F,right +271,0x11D,Underworld,DwarfCastle,#DwarfCastleWestTower3F,3,6,11,#item.BlBelt,,,,Dwarf Castle,West tower 3F,bottom +272,0x11E,Underworld,DwarfCastle,#DwarfCastleTower2F,0,8,10,500 gp,#item.Bacchus,,,Dwarf Castle,West tower 2F,pot +273,0x11F,Underworld,DwarfCastle,#DwarfCastleTower2F,1,21,24,500 gp,#item.Bacchus,,,Dwarf Castle,East tower 2F,pot +274,0x120,Underworld,UpperBabilAfterFall,#BabilFloorLugae,0,6,3,#item.Cure2,,,,Tower of Bab-il (upper),Trapdoor landing,chest +275,0x121,Underworld,UpperBabilAfterFall,#BabilFloorAirship,0,28,11,#item.Cure2,,,,Tower of Bab-il (upper),Falcon level,chest +276,0x122,Underworld,LowerBabil,#Babil1F,0,5,10,#item.IceArrow,,,,Tower of Bab-il (lower),1F,left top +277,0x123,Underworld,LowerBabil,#Babil1F,1,6,20,#item.IceArrow,,,,Tower of Bab-il (lower),1F,left bottom +278,0x124,Underworld,LowerBabil,#Babil1F,2,26,12,#item.Ether1,,,,Tower of Bab-il (lower),1F,right +279,0x125,Underworld,LowerBabil,#Babil2F,0,8,7,#item.Bandanna,,,,Tower of Bab-il (lower),2F,chest +280,0x126,Underworld,LowerBabil,#Babil3F,0,13,11,#item.CatClaw,,,,Tower of Bab-il (lower),3F,top left +281,0x127,Underworld,LowerBabil,#Babil3F,1,26,19,#item.Cure2,#item.MoonVeil,,,Tower of Bab-il (lower),3F,right +282,0x128,Underworld,LowerBabil,#Babil3F,2,15,25,#item.Life,,,,Tower of Bab-il (lower),3F,bottom +283,0x129,Underworld,LowerBabil,#Babil4F,0,16,19,#item.Archer,,,,Tower of Bab-il (lower),4F (through 3F southeast door),left +284,0x12A,Underworld,LowerBabil,#Babil4F,1,29,23,#item.Life,#item.Notus,,,Tower of Bab-il (lower),4F (through 3F southeast door),right +285,0x12B,Underworld,LowerBabil,#Babil5F,0,6,12,2000 gp,#item.Boreas,,,Tower of Bab-il (lower),5F,left +286,0x12C,Underworld,LowerBabil,#Babil5F,1,29,22,#item.Cure2,,,,Tower of Bab-il (lower),5F,right +287,0x12D,Underworld,LowerBabil,#BabilFloorAirship2,0,28,11,#item.Cure2,,,unreachable,,, +288,0x12E,Underworld,LowerBabil,#BabilFloorIceMail2,0,16,24,#item.Ether2,,,,Tower of Bab-il (lower),7F,chest +289,0x12F,Underworld,LowerBabil,#BabilFloorLugae2,0,6,3,#item.PoisonArrow,,,unreachable,,, +290,0x130,Underworld,CaveOfSummons,#CaveOfSummons1F,0,8,6,#item.Ether1,,,,Land of Monsters,B1F,"top left, through secret path" +291,0x131,Underworld,CaveOfSummons,#CaveOfSummons1F,1,26,6,#item.Life,,,,Land of Monsters,B1F,top right +292,0x132,Underworld,CaveOfSummons,#CaveOfSummons1F,2,25,21,#item.Cure2,,,,Land of Monsters,B1F,bottom right +293,0x133,Underworld,CaveOfSummons,#CaveOfSummons2F,0,9,5,#item.Cabin,,,,Land of Monsters,B2F,top left +294,0x134,Underworld,CaveOfSummons,#CaveOfSummons2F,1,24,25,#item.Cure2,,,,Land of Monsters,B2F,bottom right +295,0x135,Underworld,CaveOfSummons,#CaveOfSummons3F,0,18,6,#item.Defense,,0x1E4,,Land of Monsters,B3F,top +296,0x136,Underworld,CaveOfSummons,#CaveOfSummons3F,1,3,21,#item.Cure2,,,,Land of Monsters,B3F,"southwest room, through secret path, left" +297,0x137,Underworld,CaveOfSummons,#CaveOfSummons3F,2,4,21,#item.PoisonAxe,,,,Land of Monsters,B3F,"southwest room, through secret path, middle" +298,0x138,Underworld,CaveOfSummons,#CaveOfSummons3F,3,5,21,#item.NinjaSword,,,,Land of Monsters,B3F,"southwest room, through secret path, right" +299,0x139,Underworld,CaveOfSummons,#CaveOfSummons3F,4,11,23,#item.Life,,,,Land of Monsters,B3F,bottom middle +300,0x13A,Underworld,CaveOfSummons,#CaveOfSummons3F,5,25,21,#item.Cure3,#item.Bestiary,,,Land of Monsters,B3F,bottom right +301,0x13B,Underworld,Feymarch,#Feymarch1F,0,6,3,2000 gp,#item.Bestiary,,,Town of Monsters,B4F (first area),top left +302,0x13C,Underworld,Feymarch,#Feymarch1F,1,5,10,#item.Ether1,,,,Town of Monsters,B4F (first area),middle left +303,0x13D,Underworld,Feymarch,#Feymarch1F,2,20,8,#item.Rat,,,key,,, +304,0x13E,Underworld,Feymarch,#Feymarch1F,3,25,12,2000 gp,5000 gp,,,Town of Monsters,B4F (first area),right +305,0x13F,Underworld,Feymarch,#Feymarch1F,4,18,21,3000 gp,6000 gp,,,Town of Monsters,B4F (first area),bottom +306,0x140,Underworld,Feymarch,#FeymarchTreasury,0,15,9,#item.SamuraiBow,,,,Town of Monsters,Treasure platform,top left +307,0x141,Underworld,Feymarch,#FeymarchTreasury,1,15,10,#item.SamuraiArrow,,,,Town of Monsters,Treasure platform,top right +308,0x142,Underworld,Feymarch,#FeymarchTreasury,2,16,9,#item.Ether2,,,,Town of Monsters,Treasure platform,bottom left +309,0x143,Underworld,Feymarch,#FeymarchTreasury,3,16,10,#item.Elixir,,,,Town of Monsters,Treasure platform,bottom right +310,0x144,Underworld,Feymarch,#FeymarchSaveRoom,0,1,6,#item.Heal,#item.Bestiary,,,Town of Monsters,Save Room,left +311,0x145,Underworld,Feymarch,#FeymarchSaveRoom,1,9,6,#item.Life,,,,Town of Monsters,Save Room,right +312,0x146,Underworld,SylvanCave,#SylvanCave1F,0,5,4,#item.FireArrow,,,,Sylvan Cave,B1F (northwest area),"top row, left" +313,0x147,Underworld,SylvanCave,#SylvanCave1F,1,6,4,#item.IceArrow,,,,Sylvan Cave,B1F (northwest area),"top row, middle" +314,0x148,Underworld,SylvanCave,#SylvanCave1F,2,7,4,#item.LitArrow,,,,Sylvan Cave,B1F (northwest area),"top row, right" +315,0x149,Underworld,SylvanCave,#SylvanCave1F,3,10,5,#item.Ether1,,,,Sylvan Cave,B1F (northwest area),"right side, top" +316,0x14A,Underworld,SylvanCave,#SylvanCave1F,4,10,6,#item.Cure2,,,,Sylvan Cave,B1F (northwest area),"right side, bottom" +317,0x14B,Underworld,SylvanCave,#SylvanCave1F,5,3,18,#item.Cabin,,,,Sylvan Cave,B1F (save area),"west end, left" +318,0x14C,Underworld,SylvanCave,#SylvanCave1F,6,4,18,1000 gp,10000 gp,,,Sylvan Cave,B1F (save area),"west end, right" +319,0x14D,Underworld,SylvanCave,#SylvanCave1F,7,28,5,#item.CharmArrow,,,,Sylvan Cave,B1F (entry area),"east side, top" +320,0x14E,Underworld,SylvanCave,#SylvanCave1F,8,28,6,#item.Cure2,#item.Bestiary,,,Sylvan Cave,B1F (entry area),"east side, bottom" +321,0x14F,Underworld,SylvanCave,#SylvanCave1F,9,29,5,#item.ElvenBow,,,,Sylvan Cave,B1F (save area),through northeast secret path +322,0x150,Underworld,SylvanCave,#SylvanCave2F,0,29,29,#item.Cure3,#item.Exit,,,Sylvan Cave,B2F (east half),"pit chamber, bottom right" +323,0x151,Underworld,SylvanCave,#SylvanCave2F,1,23,29,#item.Ether1,,,,Sylvan Cave,B2F (east half),"pit chamber, bottom left" +324,0x152,Underworld,SylvanCave,#SylvanCave2F,2,29,28,#item.Cure2,#item.Bestiary,,,Sylvan Cave,B2F (east half),"pit chamber, right" +325,0x153,Underworld,SylvanCave,#SylvanCave2F,3,25,28,#item.Heal,#item.MaidKiss,,,Sylvan Cave,B2F (east half),"pit chamber, middle" +326,0x154,Underworld,SylvanCave,#SylvanCave2F,4,23,27,#item.Heal,#item.MaidKiss,,,Sylvan Cave,B2F (east half),"pit chamber, top left" +327,0x155,Underworld,SylvanCave,#SylvanCave2F,5,23,13,#item.Heal,,,,Sylvan Cave,B2F (east half),"square chamber, top left" +328,0x156,Underworld,SylvanCave,#SylvanCave2F,6,25,13,#item.Cure2,#item.Kamikaze,,,Sylvan Cave,B2F (east half),"square chamber, top right" +329,0x157,Underworld,SylvanCave,#SylvanCave2F,7,23,15,2000 gp,,,,Sylvan Cave,B2F (east half),"square chamber, bottom left" +330,0x158,Underworld,SylvanCave,#SylvanCave2F,8,25,15,3000 gp,,,,Sylvan Cave,B2F (east half),"square chamber, bottom right" +331,0x159,Underworld,SylvanCave,#SylvanCave2F,9,11,6,#item.MuteDagger,,0x1E5,,Sylvan Cave,B2F (west half),"north room, through secret path" +332,0x15A,Underworld,SylvanCave,#SylvanCave3F,0,10,4,#item.Cure2,,,,Sylvan Cave,B3F (northeast area),top left +333,0x15B,Underworld,SylvanCave,#SylvanCave3F,1,11,4,#item.CharmRod,,,,Sylvan Cave,B3F (northeast area),top right +334,0x15C,Underworld,SylvanCave,#SylvanCave3F,2,11,5,#item.Heal,#item.MaidKiss,,,Sylvan Cave,B3F (northeast area),bottom right +335,0x15D,Underworld,SylvanCave,#SylvanCave3F,3,1,9,#item.Elixir,,,,Sylvan Cave,B3F (path to house),left of entrance +336,0x15E,Underworld,SylvanCave,#SylvanCaveTreasury,0,7,5,#item.Elixir,#item.FireBomb,0x1E6,,Sylvan Cave,Poison treasury,top left +337,0x15F,Underworld,SylvanCave,#SylvanCaveTreasury,1,9,5,#item.Elixir,#item.Blizzard,0x1E6,,Sylvan Cave,Poison treasury,top middle +338,0x160,Underworld,SylvanCave,#SylvanCaveTreasury,2,11,5,#item.Elixir,#item.LitBolt,0x1E6,,Sylvan Cave,Poison treasury,top right +339,0x161,Underworld,SylvanCave,#SylvanCaveTreasury,3,7,7,#item.FullMoon,,0x1E7,,Sylvan Cave,Poison treasury,bottom left +340,0x162,Underworld,SylvanCave,#SylvanCaveTreasury,4,9,7,#item.Avenger,,0x1E8,,Sylvan Cave,Poison treasury,bottom middle +341,0x163,Underworld,SylvanCave,#SylvanCaveTreasury,5,11,7,#item.MedusaArrow,,0x1E9,,Sylvan Cave,Poison treasury,bottom right +342,0x164,Underworld,SylvanCave,#SylvanCaveYangRoom,0,13,3,#item.PoisonClaw,,,,Sylvan Cave,House,top +343,0x165,Underworld,SylvanCave,#SylvanCaveYangRoom,1,13,5,#item.CatClaw,,,,Sylvan Cave,House,bottom +344,0x166,Underworld,SealedCave,#SealedCave1F,0,18,28,#item.Life,#item.Bestiary,,,Sealed Cave,B1F,chest +345,0x167,Underworld,SealedCave,#SealedCaveRoomKatanaEther,0,2,5,#item.Long,,,,Sealed Cave,B1F (southeast room),left +346,0x168,Underworld,SealedCave,#SealedCaveRoomKatanaEther,1,14,5,#item.Ether1,,,,Sealed Cave,B1F (southeast room),right +347,0x169,Underworld,SealedCave,#SealedCave2F,0,2,6,#item.Cure2,,,,Sealed Cave,B1F to B2F passage,left +348,0x16A,Underworld,SealedCave,#SealedCave2F,1,16,7,#item.Ether1,,,,Sealed Cave,B1F to B2F passage,right +349,0x16B,Underworld,SealedCave,#SealedCave3F,0,30,19,#item.Life,,,,Sealed Cave,B2F,center right +350,0x16C,Underworld,SealedCave,#SealedCave3F,1,29,26,#item.Life,#item.StarVeil,,,Sealed Cave,B2F,bottom right +351,0x16D,Underworld,SealedCave,#SealedCaveRoomKatanaNinjaHat,0,3,5,#item.Long,,,,Sealed Cave,"B2F (north side, leftmost door)",left +352,0x16E,Underworld,SealedCave,#SealedCaveRoomKatanaNinjaHat,1,9,5,#item.NinjaHelm,,,,Sealed Cave,"B2F (north side, leftmost door)",right +353,0x16F,Underworld,SealedCave,#SealedCaveRoomNinjaStarElixir,0,3,5,#item.Elixir,,,,Sealed Cave,"B2F (north side, third door from right)","left, top" +354,0x170,Underworld,SealedCave,#SealedCaveRoomNinjaStarElixir,1,4,7,#item.NinjaStar,,,,Sealed Cave,"B2F (north side, third door from right)","left, bottom" +355,0x171,Underworld,SealedCave,#SealedCaveRoomNinjaStarElixir,2,19,9,5000 gp,#item.StarVeil,,,Sealed Cave,"B2F (north side, third door from right)",right +356,0x172,Underworld,SealedCave,#SealedCaveRoomLightSword,0,6,3,#item.Light,,,,Sealed Cave,"B2F,(north side, second door from right)",chest +357,0x173,Underworld,SealedCave,#SealedCave4F,0,9,5,#item.Ether1,,,,Sealed Cave,B2F to B3F passage,chest +358,0x174,Underworld,SealedCave,#SealedCave5F,0,25,9,#item.Life,#item.MuteBell,,,Sealed Cave,B3F,chest +359,0x175,Underworld,SealedCave,#SealedCave6F,0,12,8,#item.Cure2,,,,Sealed Cave,B3F to B4F passage,right +360,0x176,Underworld,SealedCave,#SealedCave6F,1,10,10,#item.Life,,,,Sealed Cave,B3F to B4F passage,bottom +361,0x177,Underworld,SealedCave,#SealedCaveRoomBoxes,0,9,5,#item.Cure3,,,,Sealed Cave,Box room,top right +362,0x178,Underworld,SealedCave,#SealedCaveRoomBoxes,1,3,10,#item.Ether2,,,,Sealed Cave,Box room,bottom left +363,0x179,Moon,BahamutCave,#Bahamut1F,0,28,11,#item.SamuraiShield,,,,Cave Bahamut,B1F,"right, through secret path" +364,0x17A,Moon,BahamutCave,#Bahamut1F,1,8,24,#item.SamuraiGauntlet,,,,Cave Bahamut,B1F,bottom left +365,0x17B,Moon,BahamutCave,#Bahamut2F,0,6,11,#item.Genji,,,,Cave Bahamut,B2F,left +366,0x17C,Moon,BahamutCave,#Bahamut2F,1,26,13,#item.SamuraiHelm,,,,Cave Bahamut,B2F,right +367,0x17D,Moon,LunarPath,#LunarPassage1,0,26,22,#item.Elixir,#item.AuApple,0x1EA,,Lunar Path,,bottom right +368,0x17E,Moon,LunarPath,#LunarPassage1,1,28,6,#item.Cure2,#item.MoonVeil,,,Lunar Path,,"northeast chamber, top" +369,0x17F,Moon,LunarPath,#LunarPassage1,2,29,7,#item.Heal,#item.Stardust,,,Lunar Path,,"northeast chamber, right" +370,0x180,Moon,LunarCore,#LunarSubterran1F,0,6,24,#item.NinjaArmor,,0x1EB,,Lunar Subterrane,B1,through secret path +371,0x181,Moon,LunarCore,#LunarSubterran2F,0,10,4,#item.DragoonShield,,,,Lunar Subterrane,B2 (main route),top left +372,0x182,Moon,LunarCore,#LunarSubterran2F,1,26,12,#item.LifeStaff,,0x1EC,,Lunar Subterrane,B2 (route to altar),chest +373,0x183,Moon,LunarCore,#LunarSubterran2F,2,25,23,#item.FlameWhip,,,,Lunar Subterrane,B2 (main route),secret path chest +374,0x184,Moon,LunarCore,#LunarSubterran3F,0,25,16,#item.DragoonHelm,,,,Lunar Subterrane,B3,secret path first chamber +375,0x185,Moon,LunarCore,#LunarSubterran3F,1,23,24,#item.DragonArmor,,,,Lunar Subterrane,B3,"secret path second chamber, top" +376,0x186,Moon,LunarCore,#LunarSubterran3F,2,14,28,#item.DragoonGauntlet,,,,Lunar Subterrane,B3,"secret path second chamber, left" +377,0x187,Moon,LunarCore,#LunarSubterran4F,0,28,7,#item.ArtemisArrow,,,,Lunar Subterrane,B4,top right +378,0x188,Moon,LunarCore,#LunarSubterran4F,1,6,24,#item.StardustRod,,0x1F3,,Lunar Subterrane,B4,bottom left +379,0x189,Moon,LunarCore,#LunarSubterran5F,0,7,9,#item.CrystalShield,,0x1EE,,Lunar Subterrane,B5 (main route),top left +380,0x18A,Moon,LunarCore,#LunarSubterran5F,1,1,15,#item.Protect,,0x1F4,,Lunar Subterrane,B5,on bridge to hidden altar +381,0x18B,Moon,LunarCore,#LunarSubterran5F,2,26,11,#item.CrystalArmor,,0x1F0,,Lunar Subterrane,B5 (through first interior passage south exit),chest +382,0x18C,Moon,LunarCore,#LunarSubterran5F,3,27,16,#item.CrystalGauntlet,,0x1F1,,Lunar Subterrane,B5 (main route),right +383,0x18D,Moon,LunarCore,#LunarSubterran5F,4,5,20,#item.CrystalHelm,,0x1F2,,Lunar Subterrane,B5 (main route),bottom left +384,0x18E,Moon,LunarCore,#LunarSubterran6F,0,15,5,#item.ArtemisArrow,,,,Lunar Subterrane,B6,left of entrance +385,0x18F,Moon,LunarCore,#LunarSubterran6F,1,25,5,#item.NinjaStar,,,,Lunar Subterrane,B6,right of entrance +386,0x190,Moon,LunarCore,#LunarSubterran6F,2,18,17,#item.Cabin,,,,Lunar Subterrane,B6,by hidden bridge +387,0x191,Moon,LunarCore,#LunarSubterran6F,3,6,27,#item.Life,#item.AuApple,,,Lunar Subterrane,B6,en route to altar +388,0x192,Moon,LunarCore,#LunarCore1F,0,7,11,#item.NinjaStar,,,,Lunar Subterrane,Core B1,chest +389,0x193,Moon,LunarCore,#LunarCore1F,1,23,19,#item.Elixir,,,unreachable,,, +390,0x194,Moon,LunarCore,#LunarCore2F,0,21,10,#item.Elixir,,,,Lunar Subterrane,Core B2,top right +391,0x195,Moon,LunarCore,#LunarCore2F,1,13,24,#item.Whistle,,,,Lunar Subterrane,Core B2,bottom +392,0x196,Moon,LunarCore,#LunarCore3F,0,7,15,#item.NinjaStar,,,,Lunar Subterrane,Core B3,left +393,0x197,Moon,LunarCore,#LunarCore3F,1,24,14,#item.NinjaStar,,,,Lunar Subterrane,Core B3,right +394,0x198,Moon,LunarCore,#LunarSubterranRoomElixir,0,3,6,#item.Elixir,,,,Lunar Subterrane,B4 (west room),chest +395,0x199,Moon,LunarCore,#LunarSubterranTunnelCure3,0,23,5,#item.Cure3,#item.Blizzard,,,Lunar Subterrane,B4 (interior passage),top right +396,0x19A,Moon,LunarCore,#LunarSubterranTunnelCure3,1,2,14,#item.Cure3,,,,Lunar Subterrane,B4 (interior passage),bottom left +397,0x19B,Moon,LunarCore,#LunarSubterranTunnelProtectRing,0,13,4,#item.Protect,,,,Lunar Subterrane,B5 (first interior passage),chest +398,0x19C,Moon,LunarCore,#LunarSubterranTunnelWhiteRobe,0,3,14,#item.WhiteRobe,,,,Lunar Subterrane,B5 (second interior passage),chest +399,0x19D,Moon,LunarCore,#LunarSubterranPinkpuff,0,7,5,#item.Cabin,#item.FireBomb,,,Lunar Subterrane,B5 (PinkPuff room),chest +400,0x19E,Moon,LunarCore,#LunarSubterranTunnelMinerva,0,2,4,#item.Heroine,,0x1ED,,Lunar Subterrane,B5 to B6 passage,chest +401,0x19F,Moon,LunarCore,#LunarSubterranRoomRibbons,0,2,5,#item.Ribbon,,,key,,, +402,0x1A0,Moon,LunarCore,#LunarSubterranRoomRibbons,1,4,5,#item.Ribbon,,,key,,, \ No newline at end of file diff --git a/worlds/ff4fe/unicode.lark b/worlds/ff4fe/unicode.lark new file mode 100644 index 000000000000..0ab849e3fd86 --- /dev/null +++ b/worlds/ff4fe/unicode.lark @@ -0,0 +1,7 @@ +// TODO: LETTER, WORD, etc. + +// +// Whitespace +// +WS_INLINE: /[ \t\xa0]/+ +WS: /[ \t\xa0\f\r\n]/+