mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	Lufia II Ancient Cave: implement new game (#1218)
Co-authored-by: wordfcuk <greili1985@gmail.com>
This commit is contained in:
		
							
								
								
									
										360
									
								
								worlds/lufia2ac/Client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								worlds/lufia2ac/Client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| import logging | ||||
| import time | ||||
| import typing | ||||
| from logging import Logger | ||||
| from typing import Dict | ||||
|  | ||||
| from NetUtils import ClientStatus, NetworkItem | ||||
| from worlds.AutoSNIClient import SNIClient | ||||
| from .Items import start_id as items_start_id | ||||
| from .Locations import start_id as locations_start_id | ||||
|  | ||||
| if typing.TYPE_CHECKING: | ||||
|     from SNIClient import SNIContext | ||||
| else: | ||||
|     SNIContext = typing.Any | ||||
|  | ||||
| snes_logger: Logger = logging.getLogger("SNES") | ||||
|  | ||||
| SRAM_START: int = 0xE00000 | ||||
| L2AC_ROMNAME_START: int = 0x007FC0 | ||||
| L2AC_SIGN_ADDR: int = SRAM_START + 0x2000 | ||||
| L2AC_GOAL_ADDR: int = SRAM_START + 0x2030 | ||||
| L2AC_DEATH_ADDR: int = SRAM_START + 0x203D | ||||
| L2AC_TX_ADDR: int = SRAM_START + 0x2040 | ||||
| L2AC_RX_ADDR: int = SRAM_START + 0x2800 | ||||
|  | ||||
| enemy_names: Dict[int, str] = { | ||||
|     0x00: "a Goblin", | ||||
|     0x01: "an Armor goblin", | ||||
|     0x02: "a Regal Goblin", | ||||
|     0x03: "a Goblin Mage", | ||||
|     0x04: "a Troll", | ||||
|     0x05: "an Ork", | ||||
|     0x06: "a Fighter ork", | ||||
|     0x07: "an Ork Mage", | ||||
|     0x08: "a Lizardman", | ||||
|     0x09: "a Skull Lizard", | ||||
|     0x0A: "an Armour Dait", | ||||
|     0x0B: "a Dragonian", | ||||
|     0x0C: "a Cyclops", | ||||
|     0x0D: "a Mega Cyclops", | ||||
|     0x0E: "a Flame genie", | ||||
|     0x0F: "a Well Genie", | ||||
|     0x10: "a Wind Genie", | ||||
|     0x11: "an Earth Genie", | ||||
|     0x12: "a Cobalt", | ||||
|     0x13: "a Merman", | ||||
|     0x14: "an Aqualoi", | ||||
|     0x15: "an Imp", | ||||
|     0x16: "a Fiend", | ||||
|     0x17: "an Archfiend", | ||||
|     0x18: "a Hound", | ||||
|     0x19: "a Doben", | ||||
|     0x1A: "a Winger", | ||||
|     0x1B: "a Serfaco", | ||||
|     0x1C: "a Pug", | ||||
|     0x1D: "a Salamander", | ||||
|     0x1E: "a Brinz Lizard", | ||||
|     0x1F: "a Seahorse", | ||||
|     0x20: "a Seirein", | ||||
|     0x21: "an Earth Viper", | ||||
|     0x22: "a Gnome", | ||||
|     0x23: "a Wispy", | ||||
|     0x24: "a Thunderbeast", | ||||
|     0x25: "a Lunar bear", | ||||
|     0x26: "a Shadowfly", | ||||
|     0x27: "a Shadow", | ||||
|     0x28: "a Lion", | ||||
|     0x29: "a Sphinx", | ||||
|     0x2A: "a Mad horse", | ||||
|     0x2B: "an Armor horse", | ||||
|     0x2C: "a Buffalo", | ||||
|     0x2D: "a Bruse", | ||||
|     0x2E: "a Bat", | ||||
|     0x2F: "a Big Bat", | ||||
|     0x30: "a Red Bat", | ||||
|     0x31: "an Eagle", | ||||
|     0x32: "a Hawk", | ||||
|     0x33: "a Crow", | ||||
|     0x34: "a Baby Frog", | ||||
|     0x35: "a King Frog", | ||||
|     0x36: "a Lizard", | ||||
|     0x37: "a Newt", | ||||
|     0x38: "a Needle Lizard", | ||||
|     0x39: "a Poison Lizard", | ||||
|     0x3A: "a Medusa", | ||||
|     0x3B: "a Ramia", | ||||
|     0x3C: "a Basilisk", | ||||
|     0x3D: "a Cokatoris", | ||||
|     0x3E: "a Scorpion", | ||||
|     0x3F: "an Antares", | ||||
|     0x40: "a Small Crab", | ||||
|     0x41: "a Big Crab", | ||||
|     0x42: "a Red Lobster", | ||||
|     0x43: "a Spider", | ||||
|     0x44: "a Web Spider", | ||||
|     0x45: "a Beetle", | ||||
|     0x46: "a Poison Beetle", | ||||
|     0x47: "a Mosquito", | ||||
|     0x48: "a Coridras", | ||||
|     0x49: "a Spinner", | ||||
|     0x4A: "a Tartona", | ||||
|     0x4B: "an Armour Nail", | ||||
|     0x4C: "a Moth", | ||||
|     0x4D: "a Mega  Moth", | ||||
|     0x4E: "a Big Bee", | ||||
|     0x4F: "a Dark Fly", | ||||
|     0x50: "a Stinger", | ||||
|     0x51: "an Armor Bee", | ||||
|     0x52: "a Sentopez", | ||||
|     0x53: "a Cancer", | ||||
|     0x54: "a Garbost", | ||||
|     0x55: "a Bolt Fish", | ||||
|     0x56: "a Moray", | ||||
|     0x57: "a She Viper", | ||||
|     0x58: "an Angler fish", | ||||
|     0x59: "a Unicorn", | ||||
|     0x5A: "an Evil Shell", | ||||
|     0x5B: "a Drill Shell", | ||||
|     0x5C: "a Snell", | ||||
|     0x5D: "an Ammonite", | ||||
|     0x5E: "an Evil Fish", | ||||
|     0x5F: "a Squid", | ||||
|     0x60: "a Kraken", | ||||
|     0x61: "a Killer Whale", | ||||
|     0x62: "a White Whale", | ||||
|     0x63: "a Grianos", | ||||
|     0x64: "a Behemoth", | ||||
|     0x65: "a Perch", | ||||
|     0x66: "a Current", | ||||
|     0x67: "a Vampire Rose", | ||||
|     0x68: "a Desert Rose", | ||||
|     0x69: "a Venus Fly", | ||||
|     0x6A: "a Moray Vine", | ||||
|     0x6B: "a Torrent", | ||||
|     0x6C: "a Mad Ent", | ||||
|     0x6D: "a Crow Kelp", | ||||
|     0x6E: "a Red Plant", | ||||
|     0x6F: "La Fleshia", | ||||
|     0x70: "a Wheel Eel", | ||||
|     0x71: "a Skeleton", | ||||
|     0x72: "a Ghoul", | ||||
|     0x73: "a Zombie", | ||||
|     0x74: "a Specter", | ||||
|     0x75: "a Dark Spirit", | ||||
|     0x76: "a Snatcher", | ||||
|     0x77: "a Jurahan", | ||||
|     0x78: "a Demise", | ||||
|     0x79: "a Leech", | ||||
|     0x7A: "a Necromancer", | ||||
|     0x7B: "a Hade Chariot", | ||||
|     0x7C: "a Hades", | ||||
|     0x7D: "a Dark Skull", | ||||
|     0x7E: "a Hades Skull", | ||||
|     0x7F: "a Mummy", | ||||
|     0x80: "a Vampire", | ||||
|     0x81: "a Nosferato", | ||||
|     0x82: "a Ghost Ship", | ||||
|     0x83: "a Deadly Sword", | ||||
|     0x84: "a Deadly Armor", | ||||
|     0x85: "a T Rex", | ||||
|     0x86: "a Brokion", | ||||
|     0x87: "a Pumpkin Head", | ||||
|     0x88: "a Mad Head", | ||||
|     0x89: "a Snow Gas", | ||||
|     0x8A: "a Great Coca", | ||||
|     0x8B: "a Gargoyle", | ||||
|     0x8C: "a Rogue Shape", | ||||
|     0x8D: "a Bone Gorem", | ||||
|     0x8E: "a Nuborg", | ||||
|     0x8F: "a Wood Gorem", | ||||
|     0x90: "a Mad Gorem", | ||||
|     0x91: "a Green Clay", | ||||
|     0x92: "a Sand Gorem", | ||||
|     0x93: "a Magma Gorem", | ||||
|     0x94: "an Iron Gorem", | ||||
|     0x95: "a Gold Gorem", | ||||
|     0x96: "a Hidora", | ||||
|     0x97: "a Sea Hidora", | ||||
|     0x98: "a High Hidora", | ||||
|     0x99: "a King Hidora", | ||||
|     0x9A: "an Orky", | ||||
|     0x9B: "a Waiban", | ||||
|     0x9C: "a White Dragon", | ||||
|     0x9D: "a Red Dragon", | ||||
|     0x9E: "a Blue Dragon", | ||||
|     0x9F: "a Green Dragon", | ||||
|     0xA0: "a Black Dragon", | ||||
|     0xA1: "a Copper Dragon", | ||||
|     0xA2: "a Silver Dragon", | ||||
|     0xA3: "a Gold Dragon", | ||||
|     0xA4: "a Red Jelly", | ||||
|     0xA5: "a Blue Jelly", | ||||
|     0xA6: "a Bili Jelly", | ||||
|     0xA7: "a Red Core", | ||||
|     0xA8: "a Blue Core", | ||||
|     0xA9: "a Green Core", | ||||
|     0xAA: "a No Core", | ||||
|     0xAB: "a Mimic", | ||||
|     0xAC: "a Blue Mimic", | ||||
|     0xAD: "an Ice Roge", | ||||
|     0xAE: "a Mushroom", | ||||
|     0xAF: "a Big Mushr'm", | ||||
|     0xB0: "a Minataurus", | ||||
|     0xB1: "a Gorgon", | ||||
|     0xB2: "a Ninja", | ||||
|     0xB3: "an Asashin", | ||||
|     0xB4: "a Samurai", | ||||
|     0xB5: "a Dark Warrior", | ||||
|     0xB6: "an Ochi Warrior", | ||||
|     0xB7: "a Sly Fox", | ||||
|     0xB8: "a Tengu", | ||||
|     0xB9: "a Warm Eye", | ||||
|     0xBA: "a Wizard", | ||||
|     0xBB: "a Dark Sum'ner", | ||||
|     0xBC: "the Big Catfish", | ||||
|     0xBD: "a Follower", | ||||
|     0xBE: "the Tarantula", | ||||
|     0xBF: "Pierre", | ||||
|     0xC0: "Daniele", | ||||
|     0xC1: "the Venge Ghost", | ||||
|     0xC2: "the Fire Dragon", | ||||
|     0xC3: "the Tank", | ||||
|     0xC4: "Idura", | ||||
|     0xC5: "Camu", | ||||
|     0xC6: "Gades", | ||||
|     0xC7: "Amon", | ||||
|     0xC8: "Erim", | ||||
|     0xC9: "Daos", | ||||
|     0xCA: "a Lizard Man", | ||||
|     0xCB: "a Goblin", | ||||
|     0xCC: "a Skeleton", | ||||
|     0xCD: "a Regal Goblin", | ||||
|     0xCE: "a Goblin", | ||||
|     0xCF: "a Goblin Mage", | ||||
|     0xD0: "a Slave", | ||||
|     0xD1: "a Follower", | ||||
|     0xD2: "a Groupie", | ||||
|     0xD3: "the Egg Dragon", | ||||
|     0xD4: "a Mummy", | ||||
|     0xD5: "a Troll", | ||||
|     0xD6: "Gades", | ||||
|     0xD7: "Idura", | ||||
|     0xD8: "a Lion", | ||||
|     0xD9: "the Rogue Flower", | ||||
|     0xDA: "a Gargoyle", | ||||
|     0xDB: "a Ghost Ship", | ||||
|     0xDC: "Idura", | ||||
|     0xDD: "a Soldier", | ||||
|     0xDE: "Gades", | ||||
|     0xDF: "the Master", | ||||
| } | ||||
|  | ||||
|  | ||||
| class L2ACSNIClient(SNIClient): | ||||
|     game: str = "Lufia II Ancient Cave" | ||||
|  | ||||
|     async def validate_rom(self, ctx: SNIContext) -> bool: | ||||
|         from SNIClient import snes_read | ||||
|  | ||||
|         rom_name: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) | ||||
|         if rom_name is None or rom_name[:4] != b"L2AC": | ||||
|             return False | ||||
|  | ||||
|         ctx.game = self.game | ||||
|         ctx.items_handling = 0b111  # fully remote | ||||
|  | ||||
|         ctx.rom = rom_name | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     async def game_watcher(self, ctx: SNIContext) -> None: | ||||
|         from SNIClient import snes_buffered_write, snes_flush_writes, snes_read | ||||
|  | ||||
|         rom: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) | ||||
|         if rom != ctx.rom: | ||||
|             ctx.rom = None | ||||
|             return | ||||
|  | ||||
|         if ctx.server is None or ctx.slot is None: | ||||
|             # not successfully connected to a multiworld server, cannot process the game sending items | ||||
|             return | ||||
|  | ||||
|         signature: bytes = await snes_read(ctx, L2AC_SIGN_ADDR, 16) | ||||
|         if signature != b"ArchipelagoLufia": | ||||
|             return | ||||
|  | ||||
|         # Goal | ||||
|         if not ctx.finished_game: | ||||
|             goal_data: bytes = await snes_read(ctx, L2AC_GOAL_ADDR, 10) | ||||
|             if goal_data is not None and goal_data[goal_data[0]] == 0x01: | ||||
|                 await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | ||||
|                 ctx.finished_game = True | ||||
|  | ||||
|         # DeathLink TX | ||||
|         death_data: bytes = await snes_read(ctx, L2AC_DEATH_ADDR, 3) | ||||
|         if death_data is not None: | ||||
|             await ctx.update_death_link(bool(death_data[0])) | ||||
|             if death_data[1] != 0x00: | ||||
|                 snes_buffered_write(ctx, L2AC_DEATH_ADDR + 1, b"\x00") | ||||
|                 if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): | ||||
|                     player_name: str = ctx.player_names.get(ctx.slot, str(ctx.slot)) | ||||
|                     enemy_name: str = enemy_names.get(death_data[1] - 1, hex(death_data[1] - 1)) | ||||
|                     await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.") | ||||
|  | ||||
|         # TX | ||||
|         tx_data: bytes = await snes_read(ctx, L2AC_TX_ADDR, 8) | ||||
|         if tx_data is not None: | ||||
|             snes_items_sent = int.from_bytes(tx_data[:2], "little") | ||||
|             client_items_sent = int.from_bytes(tx_data[2:4], "little") | ||||
|             client_ap_items_found = int.from_bytes(tx_data[4:6], "little") | ||||
|  | ||||
|             if client_items_sent < snes_items_sent: | ||||
|                 location_id: int = locations_start_id + client_items_sent | ||||
|                 location: str = ctx.location_names[location_id] | ||||
|                 client_items_sent += 1 | ||||
|  | ||||
|                 ctx.locations_checked.add(location_id) | ||||
|                 await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) | ||||
|  | ||||
|                 snes_logger.info("New Check: %s (%d/%d)" % ( | ||||
|                     location, | ||||
|                     len(ctx.locations_checked), | ||||
|                     len(ctx.missing_locations) + len(ctx.checked_locations))) | ||||
|                 snes_buffered_write(ctx, L2AC_TX_ADDR + 2, client_items_sent.to_bytes(2, "little")) | ||||
|  | ||||
|             ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values()) | ||||
|             if client_ap_items_found < ap_items_found: | ||||
|                 snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little")) | ||||
|  | ||||
|         # RX | ||||
|         rx_data: bytes = await snes_read(ctx, L2AC_RX_ADDR, 4) | ||||
|         if rx_data is not None: | ||||
|             snes_items_received = int.from_bytes(rx_data[:2], "little") | ||||
|  | ||||
|             if snes_items_received < len(ctx.items_received): | ||||
|                 item: NetworkItem = ctx.items_received[snes_items_received] | ||||
|                 item_code: int = item.item - items_start_id | ||||
|                 snes_items_received += 1 | ||||
|  | ||||
|                 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( | ||||
|                     ctx.item_names[item.item], | ||||
|                     ctx.player_names[item.player], | ||||
|                     ctx.location_names[item.location], | ||||
|                     snes_items_received, len(ctx.items_received))) | ||||
|                 snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, 'little')) | ||||
|                 snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) | ||||
|  | ||||
|         await snes_flush_writes(ctx) | ||||
|  | ||||
|     async def deathlink_kill_player(self, ctx: SNIContext) -> None: | ||||
|         from SNIClient import DeathState, snes_buffered_write, snes_flush_writes | ||||
|  | ||||
|         # DeathLink RX | ||||
|         if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): | ||||
|             snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x01") | ||||
|         else: | ||||
|             snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x00") | ||||
|         await snes_flush_writes(ctx) | ||||
|         ctx.death_state = DeathState.dead | ||||
							
								
								
									
										552
									
								
								worlds/lufia2ac/Items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										552
									
								
								worlds/lufia2ac/Items.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,552 @@ | ||||
| from enum import auto, Enum | ||||
| from typing import Dict, NamedTuple, Optional | ||||
|  | ||||
| from BaseClasses import Item, ItemClassification | ||||
| from . import Locations | ||||
|  | ||||
| start_id: int = Locations.start_id | ||||
|  | ||||
|  | ||||
| class ItemType(Enum): | ||||
|     BLUE_CHEST = auto() | ||||
|     CAPSULE_MONSTER = auto() | ||||
|     ENEMY_DROP = auto() | ||||
|     ENTRANCE_CHEST = auto() | ||||
|     PARTY_MEMBER = auto() | ||||
|     RED_CHEST = auto() | ||||
|     RED_CHEST_PATCH = auto() | ||||
|  | ||||
|  | ||||
| class ItemData(NamedTuple): | ||||
|     code: int | ||||
|     type: ItemType | ||||
|     classification: ItemClassification | ||||
|  | ||||
|  | ||||
| class L2ACItem(Item): | ||||
|     game: str = "Lufia II Ancient Cave" | ||||
|  | ||||
|     def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): | ||||
|         super().__init__(name, classification, code, player) | ||||
|  | ||||
|  | ||||
| l2ac_item_table: Dict[str, ItemData] = { | ||||
|     # 0x0000: "No equip" | ||||
|     # ----- CONSUMABLE ----- | ||||
|     "Charred newt": ItemData(0x0001, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Potion": ItemData(0x0002, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hi-Potion": ItemData(0x0003, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ex-Potion": ItemData(0x0004, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Magic jar": ItemData(0x0005, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Hi-Magic": ItemData(0x0006, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ex-Magic": ItemData(0x0007, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Regain": ItemData(0x0008, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Miracle": ItemData(0x0009, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Antidote": ItemData(0x000A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Awake": ItemData(0x000B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Shriek": ItemData(0x000C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mystery pin": ItemData(0x000D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power gourd": ItemData(0x000E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mind gourd": ItemData(0x000F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Magic guard": ItemData(0x0010, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Life potion": ItemData(0x0011, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Spell potion": ItemData(0x0012, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power potion": ItemData(0x0013, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Speed potion": ItemData(0x0014, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mind potion": ItemData(0x0015, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Brave": ItemData(0x0016, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0017: "Pear cider" | ||||
|     "Sour cider": ItemData(0x0018, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     # 0x0019: "Lime cider" | ||||
|     # 0x001A: "Plum cider" | ||||
|     # 0x001B: "Apple cider" | ||||
|     "Sleep ball": ItemData(0x001C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Confuse ball": ItemData(0x001D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Freeze ball": ItemData(0x001E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Smoke ball": ItemData(0x001F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ice ball": ItemData(0x0020, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fire ball": ItemData(0x0021, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Terror ball": ItemData(0x0022, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0023: "Ear pick" | ||||
|     "Boomerang": ItemData(0x0024, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Big boomer": ItemData(0x0025, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ex-boomer": ItemData(0x0026, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0027: "Dragon tooth" | ||||
|     # 0x0028: "Green tea" | ||||
|     # 0x0029: "Escape" | ||||
|     # 0x002A: "Warp" | ||||
|     # 0x002B: "Dragon egg" | ||||
|     "Curselifter": ItemData(0x002C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x002D: "Providence" | ||||
|     "Secret fruit": ItemData(0x002E, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Holy fruit": ItemData(0x002F, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Breeze fruit": ItemData(0x0030, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Charm fruit": ItemData(0x0031, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Dark fruit": ItemData(0x0032, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Earth fruit": ItemData(0x0033, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Flame fruit": ItemData(0x0034, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Magic fruit": ItemData(0x0035, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # ----- WEAPON ----- | ||||
|     # 0x0036: "Dual blade" | ||||
|     "Frypan": ItemData(0x0037, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Knife": ItemData(0x0038, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Small knife": ItemData(0x0039, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rapier": ItemData(0x003A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Battle knife": ItemData(0x003B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Dagger": ItemData(0x003C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Insect crush": ItemData(0x003D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Long knife": ItemData(0x003E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Short sword": ItemData(0x003F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Light knife": ItemData(0x0040, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Kukri": ItemData(0x0041, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gladius": ItemData(0x0042, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cold rapier": ItemData(0x0043, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Scimitar": ItemData(0x0044, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Deadly sword": ItemData(0x0045, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0046: "Deadly sword" (uncursed) | ||||
|     "SuhrCustom11": ItemData(0x0047, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bronze sword": ItemData(0x0048, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fire dagger": ItemData(0x0049, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "War rapier": ItemData(0x004A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Long sword": ItemData(0x004B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Beserk blade": ItemData(0x004C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x004D: "Beserk blade" (uncursed) | ||||
|     "Multi sword": ItemData(0x004E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rockbreaker": ItemData(0x004F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Broadsword": ItemData(0x0050, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Estok": ItemData(0x0051, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silvo rapier": ItemData(0x0052, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Burn sword": ItemData(0x0053, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Dekar blade": ItemData(0x0054, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Crazy blade": ItemData(0x0055, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Deadly sword (fake)": ItemData(0x0056, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0057: "Deadly sword" (fake, uncursed) | ||||
|     "Luck rapier": ItemData(0x0058, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0059: "Luck rapier" (uncursed) | ||||
|     "Aqua sword": ItemData(0x005A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Red saber": ItemData(0x005B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Lucky blade": ItemData(0x005C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x005D: "Lucky blade" (uncursed) | ||||
|     "Mist rapier": ItemData(0x005E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Boom sword": ItemData(0x005F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Freeze sword": ItemData(0x0060, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver sword": ItemData(0x0061, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Flying blow": ItemData(0x0062, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Super sword": ItemData(0x0063, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Buster sword": ItemData(0x0064, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rune rapier": ItemData(0x0065, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Old sword": ItemData(0x0066, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Lizard blow": ItemData(0x0067, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zirco sword": ItemData(0x0068, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Sizzle sword": ItemData(0x4069, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Blaze sword": ItemData(0x406A, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Myth blade": ItemData(0x006B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gades blade": ItemData(0x406C, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Sky sword": ItemData(0x406D, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Snow sword": ItemData(0x406E, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Fry sword": ItemData(0x406F, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x0070: "Egg sword" | ||||
|     "Franshiska": ItemData(0x0071, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Thunder ax": ItemData(0x0072, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hand ax": ItemData(0x0073, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bronze ax": ItemData(0x0074, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0075: "Flying ax" | ||||
|     "Rainy ax": ItemData(0x0076, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Great ax": ItemData(0x0077, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zirco ax": ItemData(0x0078, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mega ax": ItemData(0x4079, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Mace": ItemData(0x007A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rod": ItemData(0x007B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Staff": ItemData(0x007C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Deadly rod": ItemData(0x007D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x007E: "Deadly rod" (uncursed) | ||||
|     "Sleep rod": ItemData(0x007F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Long staff": ItemData(0x0080, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Holy staff": ItemData(0x0081, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Morning star": ItemData(0x0082, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Pounder rod": ItemData(0x0083, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Crystal wand": ItemData(0x0084, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver rod": ItemData(0x0085, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zirco rod": ItemData(0x0086, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zirco flail": ItemData(0x0087, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Spark staff": ItemData(0x4088, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Whip": ItemData(0x0089, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Wire": ItemData(0x008A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Chain": ItemData(0x008B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Aqua whip": ItemData(0x008C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cutter whip": ItemData(0x008D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Royal whip": ItemData(0x008E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Holy whip": ItemData(0x008F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zirco whip": ItemData(0x0090, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Air whip": ItemData(0x4091, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Fatal pick": ItemData(0x0092, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0093: "Fatal pick" (uncursed) | ||||
|     "Spear": ItemData(0x0094, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Trident": ItemData(0x0095, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Halberd": ItemData(0x0096, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Heavy lance": ItemData(0x0097, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Water spear": ItemData(0x4098, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Dragon spear": ItemData(0x4099, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Vice pliers": ItemData(0x009A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Coma hit": ItemData(0x009B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Figgoru": ItemData(0x009C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Superdriver": ItemData(0x009D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Stun gun": ItemData(0x009E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Battledriver": ItemData(0x009F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Launcher": ItemData(0x00A0, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Freeze bow": ItemData(0x00A1, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cursed bow": ItemData(0x00A2, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x00A3: "Arty's bow" (uncursed) | ||||
|     # ----- ARMOR ----- | ||||
|     "Apron": ItemData(0x00A4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Dress": ItemData(0x00A5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cloth": ItemData(0x00A6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Lab-coat": ItemData(0x00A7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hide armor": ItemData(0x00A8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Frock": ItemData(0x00A9, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Robe": ItemData(0x00AA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cloth armor": ItemData(0x00AB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Coat": ItemData(0x00AC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tough hide": ItemData(0x00AD, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Light dress": ItemData(0x00AE, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Light armor": ItemData(0x00AF, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Camu armor": ItemData(0x00B0, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Baggy": ItemData(0x00B1, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tight dress": ItemData(0x00B2, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Chainmail": ItemData(0x00B3, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Holy wings": ItemData(0x00B4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ironmail": ItemData(0x00B5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Toga": ItemData(0x00B6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Chain armor": ItemData(0x00B7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Thick cloth": ItemData(0x00B8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Stone plate": ItemData(0x00B9, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Long robe": ItemData(0x00BA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plated cloth": ItemData(0x00BB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Iron plate": ItemData(0x00BC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Metal mail": ItemData(0x00BD, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silk toga": ItemData(0x00BE, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver armor": ItemData(0x00BF, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Light jacket": ItemData(0x00C0, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Metal coat": ItemData(0x00C1, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver mail": ItemData(0x00C2, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power jacket": ItemData(0x00C3, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Quilted silk": ItemData(0x00C4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Metal armor": ItemData(0x00C5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power cape": ItemData(0x00C6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Magic bikini": ItemData(0x00C7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver robe": ItemData(0x00C8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Evening gown": ItemData(0x00C9, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plate armor": ItemData(0x00CA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plati plate": ItemData(0x00CB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silk robe": ItemData(0x00CC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Revive armor": ItemData(0x00CD, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Crystal mail": ItemData(0x00CE, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Crystal robe": ItemData(0x00CF, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Heal armor": ItemData(0x00D0, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Metal jacket": ItemData(0x00D1, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Deadly armor": ItemData(0x00D2, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x00D3: "Deadly armor" (uncursed) | ||||
|     "Eron dress": ItemData(0x00D4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bright armor": ItemData(0x00D5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bright cloth": ItemData(0x00D6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power robe": ItemData(0x00D7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Magic scale": ItemData(0x00D8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x00D9: "Holy robe" | ||||
|     "Ghostclothes": ItemData(0x00DA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Royal dress": ItemData(0x00DB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Full mail": ItemData(0x00DC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Old armor": ItemData(0x00DD, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zircon plate": ItemData(0x00DE, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zircon armor": ItemData(0x00DF, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Mirak plate": ItemData(0x40E0, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Ruse armor": ItemData(0x40E1, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x00E2: "Pearl armor" | ||||
|     # ----- SHIELD ----- | ||||
|     "Chop board": ItemData(0x00E3, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Small shield": ItemData(0x00E4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hide shield": ItemData(0x00E5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Buckler": ItemData(0x00E6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mini shield": ItemData(0x00E7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Wood shield": ItemData(0x00E8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bracelet": ItemData(0x00E9, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power brace": ItemData(0x00EA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Kite shield": ItemData(0x00EB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tough gloves": ItemData(0x00EC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Brone shield": ItemData(0x00ED, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Anger brace": ItemData(0x00EE, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Block shield": ItemData(0x00EF, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tecto gloves": ItemData(0x00F0, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Round shield": ItemData(0x00F1, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Pearl brace": ItemData(0x00F2, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fayza shield": ItemData(0x00F3, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Big shield": ItemData(0x00F4, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tall shield": ItemData(0x00F5, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silvo shield": ItemData(0x00F6, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Spike shield": ItemData(0x00F7, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Slash shield": ItemData(0x00F8, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mage shield": ItemData(0x00F9, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tuff buckler": ItemData(0x00FA, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tect buckler": ItemData(0x00FB, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gold gloves": ItemData(0x00FC, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gold shield": ItemData(0x00FD, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plati gloves": ItemData(0x00FE, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Plati shield": ItemData(0x00FF, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Gauntlet": ItemData(0x0100, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Rune gloves": ItemData(0x0101, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Holy shield": ItemData(0x0102, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Zirco gloves": ItemData(0x0103, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Zirco shield": ItemData(0x0104, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Old shield": ItemData(0x0105, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Flame shield": ItemData(0x4106, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Water gaunt": ItemData(0x4107, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Bolt shield": ItemData(0x4108, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Cryst shield": ItemData(0x4109, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x010A: "Mega shield" | ||||
|     "Dark mirror": ItemData(0x410B, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x010C: "Dark mirror" (uncursed) | ||||
|     "Apron shield": ItemData(0x410D, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x010E: "Pearl shield" | ||||
|     # ----- HEADGEAR ----- | ||||
|     "Pot": ItemData(0x010F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Beret": ItemData(0x0110, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cap": ItemData(0x0111, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cloth helmet": ItemData(0x0112, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hairband": ItemData(0x0113, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Headband": ItemData(0x0114, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hide helmet": ItemData(0x0115, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Jet helm": ItemData(0x0116, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Red beret": ItemData(0x0117, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Glass cap": ItemData(0x0118, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Wood helmet": ItemData(0x0119, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Blue beret": ItemData(0x011A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Brone helmet": ItemData(0x011B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Stone helmet": ItemData(0x011C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cloche": ItemData(0x011D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fury helmet": ItemData(0x011E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Iron helmet": ItemData(0x011F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tight helmet": ItemData(0x0120, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Turban": ItemData(0x0121, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plate cap": ItemData(0x0122, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Roomy helmet": ItemData(0x0123, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Tight turban": ItemData(0x0124, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Glass cloche": ItemData(0x0125, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plate helmet": ItemData(0x0126, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rock helmet": ItemData(0x0127, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Jute helmet": ItemData(0x0128, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Shade hat": ItemData(0x0129, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Metal cloche": ItemData(0x012A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "SilverHelmet": ItemData(0x012B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fury ribbon": ItemData(0x012C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Silver hat": ItemData(0x012D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Eron hat": ItemData(0x012E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Circlet": ItemData(0x012F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Golden helm": ItemData(0x0130, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gold band": ItemData(0x0131, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Plati band": ItemData(0x0132, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Plati helm": ItemData(0x0133, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Crysto beret": ItemData(0x0134, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Crysto helm": ItemData(0x0135, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Holy cap": ItemData(0x0136, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Safety hat": ItemData(0x0137, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Zirco band": ItemData(0x0138, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Zirco helmet": ItemData(0x0139, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Old helmet": ItemData(0x013A, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Agony helm": ItemData(0x413B, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Boom turban": ItemData(0x413C, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Aqua helm": ItemData(0x413D, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Ice hairband": ItemData(0x413E, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x013F: "Legend helm" | ||||
|     "Hairpin": ItemData(0x4140, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Brill helm": ItemData(0x0141, ItemType.ENTRANCE_CHEST, ItemClassification.useful), | ||||
|     # 0x0142: "Pearl helmet" | ||||
|     # ----- RING ----- | ||||
|     "Ear jewel": ItemData(0x0143, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Glass brace": ItemData(0x0144, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Glass ring": ItemData(0x0145, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Earring": ItemData(0x4146, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Speedy ring": ItemData(0x0147, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Power ring": ItemData(0x0148, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Muscle ring": ItemData(0x0149, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Protect ring": ItemData(0x014A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mind ring": ItemData(0x014B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Witch ring": ItemData(0x014C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fire ring": ItemData(0x014D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Water ring": ItemData(0x014E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ice ring": ItemData(0x014F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Thunder ring": ItemData(0x0150, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fury ring": ItemData(0x0151, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mystery ring": ItemData(0x0152, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Sonic ring": ItemData(0x0153, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Hipower ring": ItemData(0x0154, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Trick ring": ItemData(0x0155, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fake ring": ItemData(0x0156, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x0157: "S-fire ring" | ||||
|     # 0x0158: "S-water ring" | ||||
|     # 0x0159: "S-ice ring" | ||||
|     # 0x015A: "S-thun ring" | ||||
|     "S-power ring": ItemData(0x015B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "S-mind ring": ItemData(0x015C, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "S-pro ring": ItemData(0x015D, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "S-witch ring": ItemData(0x015E, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Undead ring": ItemData(0x015F, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Rocket ring": ItemData(0x0160, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Ghost ring": ItemData(0x0161, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Angry ring": ItemData(0x0162, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "S-myst ring": ItemData(0x0163, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Dia ring": ItemData(0x4164, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Sea ring": ItemData(0x4165, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Dragon ring": ItemData(0x0166, ItemType.ENTRANCE_CHEST, ItemClassification.useful), | ||||
|     "Engage ring": ItemData(0x4167, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # 0x0168: "Egg ring" | ||||
|     # ----- ROCK ----- | ||||
|     "Horse rock": ItemData(0x0169, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Eagle rock": ItemData(0x016A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Lion fang": ItemData(0x016B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bee rock": ItemData(0x016C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Snake rock": ItemData(0x016D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Cancer rock": ItemData(0x016E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Pumkin jewel": ItemData(0x016F, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Uni jewel": ItemData(0x0170, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Mysto jewel": ItemData(0x0171, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Samu jewel": ItemData(0x0172, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Bat rock": ItemData(0x0173, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Hidora rock": ItemData(0x0174, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Flame jewel": ItemData(0x0175, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Water jewel": ItemData(0x4176, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Thundo jewel": ItemData(0x4177, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Earth jewel": ItemData(0x4178, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Twist jewel": ItemData(0x4179, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Gloom jewel": ItemData(0x417A, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Tidal jewel": ItemData(0x417B, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Magma rock": ItemData(0x017C, ItemType.ENEMY_DROP, ItemClassification.useful), | ||||
|     "Evil jewel": ItemData(0x017D, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     # 0x017E: "Evil jewel" (uncursed) | ||||
|     "Gorgon rock": ItemData(0x017F, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Song rock": ItemData(0x0180, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Kraken rock": ItemData(0x0181, ItemType.RED_CHEST_PATCH, ItemClassification.useful), | ||||
|     "Catfish jwl.": ItemData(0x4182, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Camu jewel": ItemData(0x4183, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Spido jewel": ItemData(0x4184, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Gorgan rock": ItemData(0x4185, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Light jewel": ItemData(0x0186, ItemType.ENTRANCE_CHEST, ItemClassification.useful), | ||||
|     "Black eye": ItemData(0x4187, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Silver eye": ItemData(0x4188, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     "Gold eye": ItemData(0x4189, ItemType.BLUE_CHEST, ItemClassification.useful), | ||||
|     # ----- OTHER ----- | ||||
|     # 0x018A: "1 coin" | ||||
|     # 0x018B: "10 coin set" | ||||
|     # 0x018C: "50 coin set" | ||||
|     # 0x018D: "100 coin set" | ||||
|     # 0x018E: "Flame charm" | ||||
|     # 0x018F: "Zap charm" | ||||
|     # 0x0190: "Magic lamp" | ||||
|     # 0x0191: "Statue" | ||||
|     # 0x0192: "Rage knife" | ||||
|     # 0x0193: "Fortune whip" | ||||
|     # 0x0194: "Dragon blade" | ||||
|     # 0x0195: "Bunny ring" | ||||
|     # 0x0196: "Bunny ears" | ||||
|     # 0x0197: "Bunnylady" | ||||
|     # 0x0198: "Bunny sword" | ||||
|     # 0x0199: "Bunnysuit" | ||||
|     # 0x019A: "Seethru cape" | ||||
|     # 0x019B: "Seethru silk" | ||||
|     # 0x019C: "Iris sword" | ||||
|     # 0x019D: "Iris shield" | ||||
|     # 0x019E: "Iris helmet" | ||||
|     # 0x019F: "Iris armor" | ||||
|     # 0x01A0: "Iris ring" | ||||
|     # 0x01A1: "Iris jewel" | ||||
|     # 0x01A2: "Iris staff" | ||||
|     # 0x01A3: "Iris pot" | ||||
|     # 0x01A4: "Iris tiara" | ||||
|     # 0x01A5: "Power jelly" | ||||
|     # 0x01A6: "Jewel sonar" | ||||
|     # 0x01A7: "Hook" | ||||
|     # 0x01A8: "Bomb" | ||||
|     # 0x01A9: "Arrow" | ||||
|     # 0x01AA: "Fire arrow" | ||||
|     # 0x01AB: "Hammer" | ||||
|     # 0x01AC: "Treas. sword" | ||||
|     # 0x01AD: "Door key" | ||||
|     # 0x01AE: "Shrine key" | ||||
|     # 0x01AF: "Sky key" | ||||
|     # 0x01B0: "Lake key" | ||||
|     # 0x01B1: "Ruby key" | ||||
|     "Selan": ItemData(0x01B2, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Wind key" | ||||
|     "Guy": ItemData(0x01B3, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Cloud key" | ||||
|     "Arty": ItemData(0x01B4, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Light key" | ||||
|     "Dekar": ItemData(0x01B5, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Sword key" | ||||
|     "Tia": ItemData(0x01B6, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Tree key" | ||||
|     "Lexis": ItemData(0x01B7, ItemType.PARTY_MEMBER, ItemClassification.progression),  # replaces "Flower key" | ||||
|     "JELZE": ItemData(0x01B8, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Magma key" | ||||
|     "FLASH": ItemData(0x01B9, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Heart key" | ||||
|     "GUSTO": ItemData(0x01BA, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Ghost key" | ||||
|     "ZEPPY": ItemData(0x01BB, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Trial key" | ||||
|     "DARBI": ItemData(0x01BC, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Dankirk key" | ||||
|     "SULLY": ItemData(0x01BD, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Basement key" | ||||
|     "BLAZE": ItemData(0x01BE, ItemType.CAPSULE_MONSTER, ItemClassification.progression),  # replaces "Narcysus key" | ||||
|     # 0x01BF: "Truth key" | ||||
|     # 0x01C0: "Mermaid jade" | ||||
|     # 0x01C1: "Engine" | ||||
|     # 0x01C2: "Ancient key" | ||||
|     # 0x01C3: "Pretty flwr." | ||||
|     # 0x01C4: "Glass angel" | ||||
|     # 0x01C5: "VIP card" | ||||
|     # 0x01C6: "Key26" | ||||
|     # 0x01C7: "Key27" | ||||
|     # 0x01C8: "Key28" | ||||
|     # 0x01C9: "Key29" | ||||
|     # 0x01CA: "AP item"  # replaces "Key30" | ||||
|     # 0x01CB: "Crown" | ||||
|     # 0x01CC: "Ruby apple" | ||||
|     # 0x01CD: "PURIFIA" | ||||
|     # 0x01CE: "Tag ring" | ||||
|     # 0x01CF: "Tag ring" (uncursed) | ||||
|     # 0x01D0: "RAN-RAN step" | ||||
|     # 0x01D1: "Tag candy" | ||||
|     # 0x01D2: "Last" | ||||
|     # ----- SPELL ----- | ||||
|     "Flash": ItemData(0x8000, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bolt": ItemData(0x8001, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Thunder": ItemData(0x8002, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Spark": ItemData(0x8003, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fireball": ItemData(0x8004, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Firebird": ItemData(0x8005, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Droplet": ItemData(0x8006, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Vortex": ItemData(0x8007, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Dragon": ItemData(0x8008, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Gale": ItemData(0x8009, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Blizzard": ItemData(0x800A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Ice Valk": ItemData(0x800B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Perish": ItemData(0x800C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Destroy": ItemData(0x800D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Drowsy": ItemData(0x800E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Coma": ItemData(0x800F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Dread": ItemData(0x8010, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Deflect": ItemData(0x8011, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Absorb": ItemData(0x8012, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fake": ItemData(0x8013, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Trick": ItemData(0x8014, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Confuse": ItemData(0x8015, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Bravery": ItemData(0x8016, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Courage": ItemData(0x8017, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Mirror": ItemData(0x8018, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Strong": ItemData(0x8019, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Stronger": ItemData(0x801A, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Champion": ItemData(0x801B, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Poison": ItemData(0x801C, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Rally": ItemData(0x801D, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Valor": ItemData(0x801E, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Fry": ItemData(0x801F, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Zap": ItemData(0x8020, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Shield": ItemData(0x8021, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     "Waken": ItemData(0x8022, ItemType.RED_CHEST, ItemClassification.useful), | ||||
|     # 0x8023: "Release" | ||||
|     # 0x8024: "Warp" | ||||
|     # 0x8025: "Escape" | ||||
|     # 0x8026: "Reset" | ||||
|     # 0x8027: "Light" | ||||
| } | ||||
|  | ||||
| l2ac_item_name_to_id: Dict[str, int] = {name: (start_id + data.code) for name, data in l2ac_item_table.items()} | ||||
							
								
								
									
										10
									
								
								worlds/lufia2ac/Locations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								worlds/lufia2ac/Locations.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| from typing import Dict | ||||
|  | ||||
| from BaseClasses import Location | ||||
|  | ||||
| start_id: int = 0xAC0000 | ||||
| l2ac_location_name_to_id: Dict[str, int] = {f"Blue chest {i + 1}": (start_id + i) for i in range(88)} | ||||
|  | ||||
|  | ||||
| class L2ACLocation(Location): | ||||
|     game: str = "Lufia II Ancient Cave" | ||||
							
								
								
									
										540
									
								
								worlds/lufia2ac/Options.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										540
									
								
								worlds/lufia2ac/Options.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,540 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import random | ||||
| from itertools import chain, combinations | ||||
| from typing import Any, cast, Dict, List, Optional, Set, Tuple | ||||
|  | ||||
| from Options import AssembleOptions, Choice, DeathLink, Option, Range, SpecialRange, TextChoice, Toggle | ||||
|  | ||||
|  | ||||
| class AssembleCustomizableChoices(AssembleOptions): | ||||
|     def __new__(mcs, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]) -> AssembleCustomizableChoices: | ||||
|         cls: AssembleOptions = super().__new__(mcs, name, bases, attrs) | ||||
|  | ||||
|         if "extra_options" in attrs: | ||||
|             cls.name_lookup.update(enumerate(attrs["extra_options"], start=max(cls.name_lookup) + 1)) | ||||
|         return cast(AssembleCustomizableChoices, cls) | ||||
|  | ||||
|  | ||||
| class RandomGroupsChoice(Choice, metaclass=AssembleCustomizableChoices): | ||||
|     extra_options: Optional[Set[str]] | ||||
|     random_groups: Dict[str, List[str]] | ||||
|  | ||||
|     @classmethod | ||||
|     def get_option_name(cls, value: int) -> str: | ||||
|         if value in cls.options.values(): | ||||
|             return next(k for k, v in cls.options.items() if v == value) | ||||
|         else: | ||||
|             return super().get_option_name(value) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_text(cls, text: str) -> Choice: | ||||
|         key: str = text.lower() | ||||
|         if key == "random": | ||||
|             text = random.choice([o for o in cls.options if o not in cls.random_groups]) | ||||
|         elif key in cls.random_groups: | ||||
|             text = random.choice(cls.random_groups[key]) | ||||
|         return super().from_text(text) | ||||
|  | ||||
|  | ||||
| class LevelMixin: | ||||
|     xp_coefficients: List[int] = sorted([191, 65, 50, 32, 18, 14, 6, 3, 3, 2, 2, 2, 2] * 8, reverse=True) | ||||
|  | ||||
|     @classmethod | ||||
|     def _to_xp(cls, level: int, *, capsule: bool) -> int: | ||||
|         if level == 1: | ||||
|             return 0 | ||||
|         if level == 99: | ||||
|             return 9999999 | ||||
|  | ||||
|         increment: int = 20 << 8 | ||||
|         total: int = increment | ||||
|         for lv in range(2, level): | ||||
|             increment += (increment * cls.xp_coefficients[lv]) >> 8 | ||||
|             total += increment | ||||
|             if capsule: | ||||
|                 total &= 0xFFFFFF00 | ||||
|         return (total >> 8) - 10 | ||||
|  | ||||
|  | ||||
| class BlueChestChance(Range): | ||||
|     """The chance of a chest being a blue chest. | ||||
|  | ||||
|     It is given in units of 1/256, i.e., a value of 25 corresponds to 25/256 ~ 9.77%. | ||||
|     If you increase the blue chest chance, then the chance of finding consumables is decreased in return. | ||||
|     The chance of finding red chest equipment or spells is unaffected. | ||||
|     Supported values: 5 – 75 | ||||
|     Default value: 25 (five times as much as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Blue chest chance" | ||||
|     range_start = 5 | ||||
|     range_end = 75 | ||||
|     default = 25 | ||||
|  | ||||
|  | ||||
| class BlueChestCount(Range): | ||||
|     """The number of blue chest items that will be in your item pool. | ||||
|  | ||||
|     The number of blue chests in your world that count as multiworld location checks will be equal this amount plus one | ||||
|     more for each party member or capsule monster if you have shuffle_party_members/shuffle_capsule_monsters enabled. | ||||
|     (You will still encounter blue chests in your world after all the multiworld location checks have been exhausted, | ||||
|     but these chests will then generate items for yourself only.) | ||||
|     Supported values: 10 – 75 | ||||
|     Default value: 25 | ||||
|     """ | ||||
|  | ||||
|     display_name = "Blue chest count" | ||||
|     range_start = 10 | ||||
|     range_end = 75 | ||||
|     default = 25 | ||||
|  | ||||
|  | ||||
| class Boss(RandomGroupsChoice): | ||||
|     """Which boss to fight on the final floor. | ||||
|  | ||||
|     Supported values: | ||||
|     lizard_man, big_catfish, regal_goblin, follower_x2, camu, tarantula, pierre, daniele, gades_a, mummy_x4, troll_x3, | ||||
|     gades_b, idura_a, lion_x2, idura_b, idura_c, rogue_flower, soldier_x4, gargoyle_x4, venge_ghost, white_dragon_x3, | ||||
|     fire_dragon, ghost_ship, tank, gades_c, amon, erim, daos, egg_dragon, master | ||||
|     random-low — select a random regular boss, from lizard_man to troll_x3 | ||||
|     random-middle — select a random regular boss, from idura_a to gargoyle_x4 | ||||
|     random-high — select a random regular boss, from venge_ghost to tank | ||||
|     random-sinistral — select a random Sinistral boss | ||||
|     Default value: master (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Boss" | ||||
|     option_lizard_man = 0x01 | ||||
|     option_big_catfish = 0x02 | ||||
|     # 0x03 = Goblin + Skeleton; regular monsters | ||||
|     # 0x04 = Goblin; regular monster | ||||
|     option_regal_goblin = 0x05 | ||||
|     option_follower_x2 = 0x06 | ||||
|     option_camu = 0x07 | ||||
|     option_tarantula = 0x08 | ||||
|     option_pierre = 0x09 | ||||
|     option_daniele = 0x0A | ||||
|     option_gades_a = 0x0B | ||||
|     option_mummy_x4 = 0x0C | ||||
|     option_troll_x3 = 0x0D | ||||
|     option_gades_b = 0x0E | ||||
|     option_idura_a = 0x0F | ||||
|     # 0x10 = Pierre; Maxim + Tia only | ||||
|     # 0x11 = Daniele; Guy + Selan only | ||||
|     option_lion_x2 = 0x12 | ||||
|     option_idura_b = 0x13 | ||||
|     option_idura_c = 0x14 | ||||
|     option_rogue_flower = 0x15 | ||||
|     option_soldier_x4 = 0x16 | ||||
|     option_gargoyle_x4 = 0x17 | ||||
|     option_venge_ghost = 0x18 | ||||
|     option_white_dragon_x3 = 0x19 | ||||
|     option_fire_dragon = 0x1A | ||||
|     option_ghost_ship = 0x1B | ||||
|     # 0x1C = Soldier x4; same as 0x16 | ||||
|     # 0x1D = Soldier x4; same as 0x16 | ||||
|     option_tank = 0x1E | ||||
|     option_gades_c = 0x1F | ||||
|     option_amon = 0x20 | ||||
|     # 0x21 = Gades; same as 0x1F | ||||
|     # 0x22 = Amon; same as 0x20 | ||||
|     option_erim = 0x23 | ||||
|     option_daos = 0x24 | ||||
|     option_egg_dragon = 0x25 | ||||
|     option_master = 0x26 | ||||
|     default = option_master | ||||
|  | ||||
|     random_groups = { | ||||
|         "random-low": ["lizard_man", "big_catfish", "regal_goblin", "follower_x2", "camu", "tarantula", "pierre", | ||||
|                        "daniele", "mummy_x4", "troll_x3"], | ||||
|         "random-middle": ["idura_a", "lion_x2", "idura_b", "idura_c", "rogue_flower", "soldier_x4", "gargoyle_x4"], | ||||
|         "random-high": ["venge_ghost", "white_dragon_x3", "fire_dragon", "ghost_ship", "tank"], | ||||
|         "random-sinistral": ["gades_c", "amon", "erim", "daos"], | ||||
|     } | ||||
|     extra_options = frozenset(random_groups) | ||||
|  | ||||
|     @property | ||||
|     def flag(self) -> int: | ||||
|         return 0xFE if self.value == Boss.option_master else 0xFF | ||||
|  | ||||
|  | ||||
| class CapsuleCravingsJPStyle(Toggle): | ||||
|     """Make capsule monster cravings behave as in the Japanese version. | ||||
|  | ||||
|     In the US version, the data that determines which items a capsule monster can request is a mess. | ||||
|     It allows only for a very limited selection of items to be requested, and the quality of the selected item is almost | ||||
|     always either too low or too high (compared to the capsule monsters current quality preference). This means that, | ||||
|     if fed, the requested item will either be rejected by the capsule monster or lead to an unreasonable increase of the | ||||
|     quality preference, making further feeding more difficult. | ||||
|     This setting provides a fix for the bug described above. | ||||
|     If enabled, the capsule monster feeding behavior will be changed to behave analogous to the JP (and EU) version. | ||||
|     This means that requests become more varied, while the requested item will be guaranteed to be of the same quality | ||||
|     as the capsule monsters current preference. Thus, it can no longer happen that the capsule monster dislikes eating | ||||
|     the very item it just requested. | ||||
|     Supported values: false, true | ||||
|     Default value: false (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Capsule cravings JP style" | ||||
|  | ||||
|  | ||||
| class CapsuleStartingForm(SpecialRange): | ||||
|     """The starting form of your capsule monsters. | ||||
|  | ||||
|     Supported values: 1 – 4, m | ||||
|     Default value: 1 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Capsule monster starting form" | ||||
|     range_start = 1 | ||||
|     range_end = 5 | ||||
|     default = 1 | ||||
|     special_range_cutoff = 1 | ||||
|     special_range_names = { | ||||
|         "default": 1, | ||||
|         "m": 5, | ||||
|     } | ||||
|  | ||||
|     @property | ||||
|     def unlock(self) -> int: | ||||
|         if self.value == self.special_range_names["m"]: | ||||
|             return 0x0B | ||||
|         else: | ||||
|             return self.value - 1 | ||||
|  | ||||
|  | ||||
| class CapsuleStartingLevel(LevelMixin, SpecialRange): | ||||
|     """The starting level of your capsule monsters. | ||||
|  | ||||
|     Can be set to the special value party_starting_level to make it the same value as the party_starting_level option. | ||||
|     Supported values: 1 – 99, party_starting_level | ||||
|     Default value: 1 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Capsule monster starting level" | ||||
|     range_start = 0 | ||||
|     range_end = 99 | ||||
|     default = 1 | ||||
|     special_range_cutoff = 1 | ||||
|     special_range_names = { | ||||
|         "default": 1, | ||||
|         "party_starting_level": 0, | ||||
|     } | ||||
|  | ||||
|     @property | ||||
|     def xp(self) -> int: | ||||
|         return self._to_xp(self.value, capsule=True) | ||||
|  | ||||
|  | ||||
| class CrowdedFloorChance(Range): | ||||
|     """The chance of a floor being a crowded floor. | ||||
|  | ||||
|     It is given in units of 1/256, i.e., a value of 16 corresponds to 16/256 = 6.25%. | ||||
|     A crowded floor is a floor where most of the chests are grouped in one room together with many enemies. | ||||
|     Supported values: 0 – 255 | ||||
|     Default value: 16 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Crowded floor chance" | ||||
|     range_start = 0 | ||||
|     range_end = 255 | ||||
|     default = 16 | ||||
|  | ||||
|  | ||||
| class FinalFloor(Range): | ||||
|     """The final floor, where the boss resides. | ||||
|  | ||||
|     Supported values: 2 – 99 | ||||
|     Default value: 99 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Final floor" | ||||
|     range_start = 2 | ||||
|     range_end = 99 | ||||
|     default = 99 | ||||
|  | ||||
|  | ||||
| class GearVarietyAfterB9(Toggle): | ||||
|     """Fixes a bug that prevents various gear from appearing after B9. | ||||
|  | ||||
|     Due to an overflow bug in the game, the distribution of red chest gear is impaired after B9. | ||||
|     Starting with B10, the number of items available from red chests is severely limited, meaning that red chests will | ||||
|     no longer contain any shields, headgear, rings, or jewels (and the selection of body armor is reduced as well). | ||||
|     This setting provides a fix for the bug described above. | ||||
|     If enabled, red chests beyond B9 will continue to produce shields, headgear, rings, and jewels as intended, | ||||
|     while the odds of finding body armor in red chests are decreased as a result. | ||||
|     The distributions of red chest weapons, spells, and consumables as well as blue chests are unaffected. | ||||
|     Supported values: false, true | ||||
|     Default value: false (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Increase gear variety after B9" | ||||
|  | ||||
|  | ||||
| class Goal(Choice): | ||||
|     """The objective you have to fulfill in order to complete the game. | ||||
|  | ||||
|     Supported values: | ||||
|     boss — defeat the boss on the final floor | ||||
|     iris_treasure_hunt — gather the required number of Iris treasures and leave the cave | ||||
|     boss_iris_treasure_hunt — complete both the "boss" and the "iris_treasure_hunt" objective (in any order) | ||||
|     final_floor — merely reach the final floor | ||||
|     Default value: boss | ||||
|     """ | ||||
|  | ||||
|     display_name = "Goal" | ||||
|     option_boss = 0x01 | ||||
|     option_iris_treasure_hunt = 0x02 | ||||
|     option_boss_iris_treasure_hunt = 0x03 | ||||
|     option_final_floor = 0x04 | ||||
|     default = option_boss | ||||
|  | ||||
|  | ||||
| class HealingFloorChance(Range): | ||||
|     """The chance of a floor having a healing tile hidden under a bush. | ||||
|  | ||||
|     It is given in units of 1/256, i.e., a value of 16 corresponds to 16/256 = 6.25%. | ||||
|     Supported values: 0 – 255 | ||||
|     Default value: 16 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Healing tile floor chance" | ||||
|     range_start = 0 | ||||
|     range_end = 255 | ||||
|     default = 16 | ||||
|  | ||||
|  | ||||
| class InitialFloor(Range): | ||||
|     """The initial floor, where you begin your journey. | ||||
|  | ||||
|     (If this value isn't smaller than the value of final_floor, it will automatically be set to final_floor - 1) | ||||
|     Supported values: 1 – 98 | ||||
|     Default value: 1 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Initial floor" | ||||
|     range_start = 1 | ||||
|     range_end = 98 | ||||
|     default = 1 | ||||
|  | ||||
|  | ||||
| class IrisFloorChance(Range): | ||||
|     """The chance of a floor being able to generate an Iris treasure. | ||||
|  | ||||
|     It is given in units of 1/256, i.e., a value of 5 corresponds to 5/256 ~ 1.95%. | ||||
|     The true chance of a floor holding an Iris treasure you need is usually lower than the chance specified here, e.g., | ||||
|     if you have already found 8 of 9 Iris items then the chance of generating the last one is only 1/9 of this value. | ||||
|     Supported values: 5 – 255 | ||||
|     Default value: 5 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Iris treasure floor chance" | ||||
|     range_start = 5 | ||||
|     range_end = 255 | ||||
|     default = 5 | ||||
|  | ||||
|  | ||||
| class IrisTreasuresRequired(Range): | ||||
|     """The number of Iris treasures required to complete the goal. | ||||
|  | ||||
|     This setting only has an effect if the "iris_treasure_hunt" or "boss_iris_treasure_hunt" goal is active. | ||||
|     Supported values: 1 – 9 | ||||
|     Default value: 9 | ||||
|     """ | ||||
|  | ||||
|     display_name = "Iris treasures required" | ||||
|     range_start = 1 | ||||
|     range_end = 9 | ||||
|     default = 9 | ||||
|  | ||||
|  | ||||
| class MasterHp(SpecialRange): | ||||
|     """The number of hit points of the Master | ||||
|  | ||||
|     Supported values: | ||||
|     1 – 9980, | ||||
|     scale — scales the HP depending on the value of final_floor | ||||
|     Default value: 9980 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Master HP" | ||||
|     range_start = 0 | ||||
|     range_end = 9980 | ||||
|     default = 9980 | ||||
|     special_range_cutoff = 1 | ||||
|     special_range_names = { | ||||
|         "default": 9980, | ||||
|         "scale": 0, | ||||
|     } | ||||
|  | ||||
|     @staticmethod | ||||
|     def scale(final_floor: int) -> int: | ||||
|         return final_floor * 100 + 80 | ||||
|  | ||||
|  | ||||
| class PartyStartingLevel(LevelMixin, Range): | ||||
|     """The starting level of your party members. | ||||
|  | ||||
|     Supported values: 1 – 99 | ||||
|     Default value: 1 (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Party starting level" | ||||
|     range_start = 1 | ||||
|     range_end = 99 | ||||
|     default = 1 | ||||
|  | ||||
|     @property | ||||
|     def xp(self) -> int: | ||||
|         return self._to_xp(self.value, capsule=False) | ||||
|  | ||||
|  | ||||
| class RunSpeed(Choice): | ||||
|     """Modifies the game to allow you to move faster than normal when pressing the Y button. | ||||
|  | ||||
|     Supported values: disabled, double, triple, quadruple | ||||
|     Default value: disabled (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Run speed" | ||||
|     option_disabled = 0x08 | ||||
|     option_double = 0x10 | ||||
|     option_triple = 0x16 | ||||
|     option_quadruple = 0x20 | ||||
|     default = option_disabled | ||||
|  | ||||
|  | ||||
| class ShuffleCapsuleMonsters(Toggle): | ||||
|     """Shuffle the capsule monsters into the multiworld. | ||||
|  | ||||
|     Supported values: | ||||
|     false — all 7 capsule monsters are available in the menu and can be selected right away | ||||
|     true — you start without capsule monster; 7 new "items" are added to your pool and shuffled into the multiworld; | ||||
|         when one of these items is found, the corresponding capsule monster is unlocked for you to use | ||||
|     Default value: false (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Shuffle capsule monsters" | ||||
|  | ||||
|     @property | ||||
|     def unlock(self) -> int: | ||||
|         return 0b00000000 if self.value else 0b01111111 | ||||
|  | ||||
|  | ||||
| class ShufflePartyMembers(Toggle): | ||||
|     """Shuffle the party members into the multiworld. | ||||
|  | ||||
|     Supported values: | ||||
|     false — all 6 optional party members are present in the cafe and can be recruited right away | ||||
|     true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the | ||||
|         multiworld; when one of these items is found, the corresponding party member is unlocked for you to use | ||||
|     Default value: false (same as in an unmodified game) | ||||
|     """ | ||||
|  | ||||
|     display_name = "Shuffle party members" | ||||
|  | ||||
|     @property | ||||
|     def unlock(self) -> int: | ||||
|         return 0b00000000 if self.value else 0b11111100 | ||||
|  | ||||
|  | ||||
| class StartingCapsule(Choice): | ||||
|     """The capsule monster you start the game with. | ||||
|  | ||||
|     Only has an effect if shuffle_capsule_monsters is set to false. | ||||
|     Supported values: jelze, flash, gusto, zeppy, darbi, sully, blaze | ||||
|     Default value: jelze | ||||
|     """ | ||||
|  | ||||
|     display_name = "Starting capsule monster" | ||||
|     option_jelze = 0x00 | ||||
|     option_flash = 0x01 | ||||
|     option_gusto = 0x02 | ||||
|     option_zeppy = 0x03 | ||||
|     option_darbi = 0x04 | ||||
|     option_sully = 0x05 | ||||
|     option_blaze = 0x06 | ||||
|     default = option_jelze | ||||
|  | ||||
|  | ||||
| class StartingParty(RandomGroupsChoice, TextChoice): | ||||
|     """The party you start the game with. | ||||
|  | ||||
|     Only has an effect if shuffle_party_members is set to false. | ||||
|     Supported values: | ||||
|     Can be set to any valid combination of up to 4 party member initials, e.g.: | ||||
|     M — start with Maxim | ||||
|     DGMA — start with Dekar, Guy, Maxim, and Arty | ||||
|     MSTL — start with Maxim, Selan, Tia, and Lexis | ||||
|     random-2p — a random 2-person party | ||||
|     random-3p — a random 3-person party | ||||
|     random-4p — a random 4-person party | ||||
|     Default value: M | ||||
|     """ | ||||
|  | ||||
|     display_name = "Starting party" | ||||
|     default = "M" | ||||
|  | ||||
|     random_groups = { | ||||
|         "random-2p": ["M" + "".join(p) for p in combinations("ADGLST", 1)], | ||||
|         "random-3p": ["M" + "".join(p) for p in combinations("ADGLST", 2)], | ||||
|         "random-4p": ["M" + "".join(p) for p in combinations("ADGLST", 3)], | ||||
|     } | ||||
|     vars().update({f"option_{party}": party for party in (*random_groups, "M", *chain(*random_groups.values()))}) | ||||
|     _valid_sorted_parties: List[List[str]] = [sorted(party) for party in ("M", *chain(*random_groups.values()))] | ||||
|     _members_to_bytes: bytes = bytes.maketrans(b"MSGATDL", bytes(range(7))) | ||||
|  | ||||
|     def verify(self, *args, **kwargs) -> None: | ||||
|         if str(self.value).lower() in self.random_groups: | ||||
|             return | ||||
|         if sorted(str(self.value).upper()) in self._valid_sorted_parties: | ||||
|             return | ||||
|         raise ValueError(f"Could not find option '{self.value}' for '{self.__class__.__name__}', known options are:\n" | ||||
|                          f"{', '.join(self.random_groups)}, {', '.join(('M', *chain(*self.random_groups.values())))} " | ||||
|                          "as well as all permutations of these.") | ||||
|  | ||||
|     @staticmethod | ||||
|     def _flip(i: int) -> int: | ||||
|         return {4: 5, 5: 4}.get(i, i) | ||||
|  | ||||
|     @property | ||||
|     def event_script(self) -> bytes: | ||||
|         return bytes((*(b for i in bytes(self) if i != 0 for b in (0x2B, i, 0x2E, i + 0x65, 0x1A, self._flip(i) + 1)), | ||||
|                       0x1E, 0x0B, len(self) - 1, 0x1C, 0x86, 0x03, *(0x00,) * (6 * (4 - len(self))))) | ||||
|  | ||||
|     @property | ||||
|     def roster(self) -> bytes: | ||||
|         return bytes((len(self), *bytes(self), *(0xFF,) * (4 - len(self)))) | ||||
|  | ||||
|     def __bytes__(self) -> bytes: | ||||
|         return str(self.value).upper().encode("ASCII").translate(self._members_to_bytes) | ||||
|  | ||||
|     def __len__(self) -> int: | ||||
|         return len(str(self.value)) | ||||
|  | ||||
|  | ||||
| l2ac_option_definitions: Dict[str, type(Option)] = { | ||||
|     "blue_chest_chance": BlueChestChance, | ||||
|     "blue_chest_count": BlueChestCount, | ||||
|     "boss": Boss, | ||||
|     "capsule_cravings_jp_style": CapsuleCravingsJPStyle, | ||||
|     "capsule_starting_form": CapsuleStartingForm, | ||||
|     "capsule_starting_level": CapsuleStartingLevel, | ||||
|     "crowded_floor_chance": CrowdedFloorChance, | ||||
|     "death_link": DeathLink, | ||||
|     "final_floor": FinalFloor, | ||||
|     "gear_variety_after_b9": GearVarietyAfterB9, | ||||
|     "goal": Goal, | ||||
|     "healing_floor_chance": HealingFloorChance, | ||||
|     "initial_floor": InitialFloor, | ||||
|     "iris_floor_chance": IrisFloorChance, | ||||
|     "iris_treasures_required": IrisTreasuresRequired, | ||||
|     "master_hp": MasterHp, | ||||
|     "party_starting_level": PartyStartingLevel, | ||||
|     "run_speed": RunSpeed, | ||||
|     "shuffle_capsule_monsters": ShuffleCapsuleMonsters, | ||||
|     "shuffle_party_members": ShufflePartyMembers, | ||||
|     "starting_capsule": StartingCapsule, | ||||
|     "starting_party": StartingParty, | ||||
| } | ||||
							
								
								
									
										43
									
								
								worlds/lufia2ac/Rom.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								worlds/lufia2ac/Rom.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import hashlib | ||||
| import os | ||||
| from typing import Optional | ||||
|  | ||||
| import Utils | ||||
| from Utils import OptionsType | ||||
| from worlds.Files import APDeltaPatch | ||||
|  | ||||
| L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d" | ||||
|  | ||||
|  | ||||
| class L2ACDeltaPatch(APDeltaPatch): | ||||
|     hash = L2USHASH | ||||
|     game = "Lufia II Ancient Cave" | ||||
|     patch_file_ending = ".apl2ac" | ||||
|  | ||||
|     @classmethod | ||||
|     def get_source_data(cls) -> bytes: | ||||
|         return get_base_rom_bytes() | ||||
|  | ||||
|  | ||||
| def get_base_rom_bytes(file_name: str = "") -> bytes: | ||||
|     base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) | ||||
|     if not base_rom_bytes: | ||||
|         file_name: str = get_base_rom_path(file_name) | ||||
|         base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) | ||||
|  | ||||
|         basemd5 = hashlib.md5() | ||||
|         basemd5.update(base_rom_bytes) | ||||
|         if L2USHASH != basemd5.hexdigest(): | ||||
|             raise Exception("Supplied Base Rom does not match known MD5 for US release. " | ||||
|                             "Get the correct game and version, then dump it") | ||||
|         get_base_rom_bytes.base_rom_bytes = base_rom_bytes | ||||
|     return base_rom_bytes | ||||
|  | ||||
|  | ||||
| def get_base_rom_path(file_name: str = "") -> str: | ||||
|     options: OptionsType = Utils.get_options() | ||||
|     if not file_name: | ||||
|         file_name = options["lufia2ac_options"]["rom_file"] | ||||
|     if not os.path.exists(file_name): | ||||
|         file_name = Utils.user_path(file_name) | ||||
|     return file_name | ||||
							
								
								
									
										343
									
								
								worlds/lufia2ac/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								worlds/lufia2ac/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | ||||
| import base64 | ||||
| import itertools | ||||
| import os | ||||
| from enum import IntFlag | ||||
| from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple | ||||
|  | ||||
| from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, RegionType, Tutorial | ||||
| from Main import __version__ | ||||
| from Options import AssembleOptions | ||||
| from worlds.AutoWorld import WebWorld, World | ||||
| from worlds.generic.Rules import add_rule, set_rule | ||||
| from .Client import L2ACSNIClient  # noqa: F401 | ||||
| from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id | ||||
| from .Locations import l2ac_location_name_to_id, L2ACLocation | ||||
| from .Options import Boss, CapsuleStartingForm, CapsuleStartingLevel, Goal, l2ac_option_definitions, MasterHp, \ | ||||
|     PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers, StartingParty | ||||
| from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch | ||||
| from .basepatch import apply_basepatch | ||||
|  | ||||
| CHESTS_PER_SPHERE: int = 5 | ||||
|  | ||||
|  | ||||
| class L2ACWeb(WebWorld): | ||||
|     tutorials = [Tutorial( | ||||
|         "Multiworld Setup Guide", | ||||
|         "A guide to setting up Lufia II Ancient Cave for MultiWorld.", | ||||
|         "English", | ||||
|         "setup_en.md", | ||||
|         "setup/en", | ||||
|         ["word_fcuk"] | ||||
|     )] | ||||
|     theme = "dirt" | ||||
|  | ||||
|  | ||||
| class L2ACWorld(World): | ||||
|     """ | ||||
|     The Ancient Cave is a roguelike dungeon crawling game built into | ||||
|     the RGP Lufia II. Face 99 floors of ever harder to beat monsters, | ||||
|     random items and find new companions on the way to face the Royal | ||||
|     Jelly in the end. Can you beat it? | ||||
|     """ | ||||
|     game: ClassVar[str] = "Lufia II Ancient Cave" | ||||
|     web: ClassVar[WebWorld] = L2ACWeb() | ||||
|  | ||||
|     option_definitions: ClassVar[Dict[str, AssembleOptions]] = l2ac_option_definitions | ||||
|     item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id | ||||
|     location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id | ||||
|     item_name_groups: ClassVar[Dict[str, Set[str]]] = { | ||||
|         "Blue chest items": {name for name, data in l2ac_item_table.items() if data.type is ItemType.BLUE_CHEST}, | ||||
|         "Capsule monsters": {name for name, data in l2ac_item_table.items() if data.type is ItemType.CAPSULE_MONSTER}, | ||||
|         "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, | ||||
|     } | ||||
|     data_version: ClassVar[int] = 1 | ||||
|     required_client_version: Tuple[int, int, int] = (0, 3, 6) | ||||
|  | ||||
|     # L2ACWorld specific properties | ||||
|     rom_name: Optional[bytearray] | ||||
|  | ||||
|     blue_chest_chance: Optional[int] | ||||
|     blue_chest_count: Optional[int] | ||||
|     boss: Optional[Boss] | ||||
|     capsule_cravings_jp_style: Optional[int] | ||||
|     capsule_starting_form: Optional[CapsuleStartingForm] | ||||
|     capsule_starting_level: Optional[CapsuleStartingLevel] | ||||
|     crowded_floor_chance: Optional[int] | ||||
|     death_link: Optional[int] | ||||
|     final_floor: Optional[int] | ||||
|     gear_variety_after_b9: Optional[int] | ||||
|     goal: Optional[int] | ||||
|     healing_floor_chance: Optional[int] | ||||
|     initial_floor: Optional[int] | ||||
|     iris_floor_chance: Optional[int] | ||||
|     iris_treasures_required: Optional[int] | ||||
|     master_hp: Optional[int] | ||||
|     party_starting_level: Optional[PartyStartingLevel] | ||||
|     run_speed: Optional[int] | ||||
|     shuffle_capsule_monsters: Optional[ShuffleCapsuleMonsters] | ||||
|     shuffle_party_members: Optional[ShufflePartyMembers] | ||||
|     starting_capsule: Optional[int] | ||||
|     starting_party: Optional[StartingParty] | ||||
|  | ||||
|     @classmethod | ||||
|     def stage_assert_generate(cls, _multiworld: MultiWorld) -> None: | ||||
|         rom_file: str = get_base_rom_path() | ||||
|         if not os.path.exists(rom_file): | ||||
|             raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}") | ||||
|  | ||||
|         # # uncomment this section to recreate the basepatch | ||||
|         # # (you will need to provide "asar.py" as well as an Asar library in the basepatch directory) | ||||
|         # from .basepatch import create_basepatch | ||||
|         # create_basepatch() | ||||
|  | ||||
|     def generate_early(self) -> None: | ||||
|         self.rom_name = \ | ||||
|             bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] | ||||
|         self.rom_name.extend([0] * (21 - len(self.rom_name))) | ||||
|  | ||||
|         self.blue_chest_chance = self.multiworld.blue_chest_chance[self.player].value | ||||
|         self.blue_chest_count = self.multiworld.blue_chest_count[self.player].value | ||||
|         self.boss = self.multiworld.boss[self.player] | ||||
|         self.capsule_cravings_jp_style = self.multiworld.capsule_cravings_jp_style[self.player].value | ||||
|         self.capsule_starting_form = self.multiworld.capsule_starting_form[self.player] | ||||
|         self.capsule_starting_level = self.multiworld.capsule_starting_level[self.player] | ||||
|         self.crowded_floor_chance = self.multiworld.crowded_floor_chance[self.player].value | ||||
|         self.death_link = self.multiworld.death_link[self.player].value | ||||
|         self.final_floor = self.multiworld.final_floor[self.player].value | ||||
|         self.gear_variety_after_b9 = self.multiworld.gear_variety_after_b9[self.player].value | ||||
|         self.goal = self.multiworld.goal[self.player].value | ||||
|         self.healing_floor_chance = self.multiworld.healing_floor_chance[self.player].value | ||||
|         self.initial_floor = self.multiworld.initial_floor[self.player].value | ||||
|         self.iris_floor_chance = self.multiworld.iris_floor_chance[self.player].value | ||||
|         self.iris_treasures_required = self.multiworld.iris_treasures_required[self.player].value | ||||
|         self.master_hp = self.multiworld.master_hp[self.player].value | ||||
|         self.party_starting_level = self.multiworld.party_starting_level[self.player] | ||||
|         self.run_speed = self.multiworld.run_speed[self.player].value | ||||
|         self.shuffle_capsule_monsters = self.multiworld.shuffle_capsule_monsters[self.player] | ||||
|         self.shuffle_party_members = self.multiworld.shuffle_party_members[self.player] | ||||
|         self.starting_capsule = self.multiworld.starting_capsule[self.player].value | ||||
|         self.starting_party = self.multiworld.starting_party[self.player] | ||||
|  | ||||
|         if self.capsule_starting_level.value == CapsuleStartingLevel.special_range_names["party_starting_level"]: | ||||
|             self.capsule_starting_level.value = self.party_starting_level.value | ||||
|         if self.initial_floor >= self.final_floor: | ||||
|             self.initial_floor = self.final_floor - 1 | ||||
|         if self.master_hp == MasterHp.special_range_names["scale"]: | ||||
|             self.master_hp = MasterHp.scale(self.final_floor) | ||||
|         if self.shuffle_party_members: | ||||
|             self.starting_party.value = StartingParty.default | ||||
|  | ||||
|     def create_regions(self) -> None: | ||||
|         menu = Region("Menu", RegionType.Generic, "Menu", self.player, self.multiworld) | ||||
|         menu.exits.append(Entrance(self.player, "AncientDungeonEntrance", menu)) | ||||
|         self.multiworld.regions.append(menu) | ||||
|  | ||||
|         ancient_dungeon = Region("AncientDungeon", RegionType.Generic, "Ancient Dungeon", self.player, self.multiworld) | ||||
|         ancient_dungeon.exits.append(Entrance(self.player, "FinalFloorEntrance", menu)) | ||||
|         item_count: int = self.blue_chest_count | ||||
|         if self.shuffle_capsule_monsters: | ||||
|             item_count += len(self.item_name_groups["Capsule monsters"]) | ||||
|         if self.shuffle_party_members: | ||||
|             item_count += len(self.item_name_groups["Party members"]) | ||||
|         for location_name, location_id in itertools.islice(l2ac_location_name_to_id.items(), item_count): | ||||
|             ancient_dungeon.locations.append(L2ACLocation(self.player, location_name, location_id, ancient_dungeon)) | ||||
|         prog_chest_access = L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player) | ||||
|         for i in range(CHESTS_PER_SPHERE, item_count, CHESTS_PER_SPHERE): | ||||
|             chest_access = \ | ||||
|                 L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon) | ||||
|             chest_access.place_locked_item(prog_chest_access) | ||||
|             ancient_dungeon.locations.append(chest_access) | ||||
|         treasures = L2ACLocation(self.player, "Iris Treasures", None, ancient_dungeon) | ||||
|         treasures.place_locked_item(L2ACItem("Treasures collected", ItemClassification.progression, None, self.player)) | ||||
|         ancient_dungeon.locations.append(treasures) | ||||
|         self.multiworld.regions.append(ancient_dungeon) | ||||
|  | ||||
|         final_floor = Region("FinalFloor", RegionType.Generic, "Ancient Cave Final Floor", self.player, self.multiworld) | ||||
|         ff_reached = L2ACLocation(self.player, "Final Floor reached", None, final_floor) | ||||
|         ff_reached.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player)) | ||||
|         final_floor.locations.append(ff_reached) | ||||
|         boss = L2ACLocation(self.player, "Boss", None, final_floor) | ||||
|         boss.place_locked_item(L2ACItem("Boss victory", ItemClassification.progression, None, self.player)) | ||||
|         final_floor.locations.append(boss) | ||||
|         self.multiworld.regions.append(final_floor) | ||||
|  | ||||
|         self.multiworld.get_entrance("AncientDungeonEntrance", self.player) \ | ||||
|             .connect(self.multiworld.get_region("AncientDungeon", self.player)) | ||||
|         self.multiworld.get_entrance("FinalFloorEntrance", self.player) \ | ||||
|             .connect(self.multiworld.get_region("FinalFloor", self.player)) | ||||
|  | ||||
|     def create_items(self) -> None: | ||||
|         item_pool: List[str] = \ | ||||
|             self.multiworld.random.choices(sorted(self.item_name_groups["Blue chest items"]), k=self.blue_chest_count) | ||||
|         if self.shuffle_capsule_monsters: | ||||
|             item_pool += self.item_name_groups["Capsule monsters"] | ||||
|             self.blue_chest_count += len(self.item_name_groups["Capsule monsters"]) | ||||
|         if self.shuffle_party_members: | ||||
|             item_pool += self.item_name_groups["Party members"] | ||||
|             self.blue_chest_count += len(self.item_name_groups["Party members"]) | ||||
|         for item_name in item_pool: | ||||
|             item_data: ItemData = l2ac_item_table[item_name] | ||||
|             item_id: int = items_start_id + item_data.code | ||||
|             self.multiworld.itempool.append(L2ACItem(item_name, item_data.classification, item_id, self.player)) | ||||
|  | ||||
|     def set_rules(self) -> None: | ||||
|         for i in range(1, self.blue_chest_count): | ||||
|             if i % CHESTS_PER_SPHERE == 0: | ||||
|                 set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player), | ||||
|                          lambda state, j=i: state.has("Progressive chest access", self.player, j // CHESTS_PER_SPHERE)) | ||||
|                 set_rule(self.multiworld.get_location(f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", self.player), | ||||
|                          lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player)) | ||||
|             else: | ||||
|                 set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player), | ||||
|                          lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player)) | ||||
|  | ||||
|         set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), | ||||
|                  lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) | ||||
|         set_rule(self.multiworld.get_location("Iris Treasures", self.player), | ||||
|                  lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) | ||||
|         set_rule(self.multiworld.get_location("Boss", self.player), | ||||
|                  lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) | ||||
|         if self.shuffle_capsule_monsters: | ||||
|             add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player)) | ||||
|         if self.shuffle_party_members: | ||||
|             add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("Dekar", self.player) | ||||
|                      and state.has("Guy", self.player) and state.has("Arty", self.player)) | ||||
|  | ||||
|         if self.goal == Goal.option_final_floor: | ||||
|             self.multiworld.completion_condition[self.player] = \ | ||||
|                 lambda state: state.has("Final Floor access", self.player) | ||||
|         elif self.goal == Goal.option_iris_treasure_hunt: | ||||
|             self.multiworld.completion_condition[self.player] = \ | ||||
|                 lambda state: state.has("Treasures collected", self.player) | ||||
|         elif self.goal == Goal.option_boss: | ||||
|             self.multiworld.completion_condition[self.player] = \ | ||||
|                 lambda state: state.has("Boss victory", self.player) | ||||
|         elif self.goal == Goal.option_boss_iris_treasure_hunt: | ||||
|             self.multiworld.completion_condition[self.player] = \ | ||||
|                 lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player) | ||||
|  | ||||
|     def generate_output(self, output_directory: str) -> None: | ||||
|         rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") | ||||
|  | ||||
|         try: | ||||
|             rom_bytearray = bytearray(apply_basepatch(get_base_rom_bytes())) | ||||
|             # start and stop indices are offsets in the ROM file, not LoROM mapped SNES addresses | ||||
|             rom_bytearray[0x007FC0:0x007FC0 + 21] = self.rom_name | ||||
|             rom_bytearray[0x014308:0x014308 + 1] = self.capsule_starting_level.value.to_bytes(1, "little") | ||||
|             rom_bytearray[0x01432F:0x01432F + 1] = self.capsule_starting_form.unlock.to_bytes(1, "little") | ||||
|             rom_bytearray[0x01433C:0x01433C + 1] = self.capsule_starting_form.value.to_bytes(1, "little") | ||||
|             rom_bytearray[0x0190D5:0x0190D5 + 1] = self.iris_floor_chance.to_bytes(1, "little") | ||||
|             rom_bytearray[0x019153:0x019153 + 1] = (0x63 - self.blue_chest_chance).to_bytes(1, "little") | ||||
|             rom_bytearray[0x019176] = 0x38 if self.gear_variety_after_b9 else 0x18 | ||||
|             rom_bytearray[0x019477:0x019477 + 1] = self.healing_floor_chance.to_bytes(1, "little") | ||||
|             rom_bytearray[0x0194A2:0x0194A2 + 1] = self.crowded_floor_chance.to_bytes(1, "little") | ||||
|             rom_bytearray[0x019E82:0x019E82 + 1] = self.final_floor.to_bytes(1, "little") | ||||
|             rom_bytearray[0x01FC75:0x01FC75 + 1] = self.run_speed.to_bytes(1, "little") | ||||
|             rom_bytearray[0x01FC81:0x01FC81 + 1] = self.run_speed.to_bytes(1, "little") | ||||
|             rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.starting_party.roster | ||||
|             for offset in range(0x02B395, 0x02B452, 0x1B): | ||||
|                 rom_bytearray[offset:offset + 1] = self.party_starting_level.value.to_bytes(1, "little") | ||||
|             for offset in range(0x02B39A, 0x02B457, 0x1B): | ||||
|                 rom_bytearray[offset:offset + 3] = self.party_starting_level.xp.to_bytes(3, "little") | ||||
|             rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes() | ||||
|             rom_bytearray[0x056AA3:0x056AA3 + 24] = self.starting_party.event_script | ||||
|             rom_bytearray[0x072742:0x072742 + 1] = self.boss.value.to_bytes(1, "little") | ||||
|             rom_bytearray[0x072748:0x072748 + 1] = self.boss.flag.to_bytes(1, "little") | ||||
|             rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table() | ||||
|             rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.master_hp.to_bytes(2, "little") | ||||
|             rom_bytearray[0x280010:0x280010 + 2] = self.blue_chest_count.to_bytes(2, "little") | ||||
|             rom_bytearray[0x280012:0x280012 + 3] = self.capsule_starting_level.xp.to_bytes(3, "little") | ||||
|             rom_bytearray[0x280015:0x280015 + 1] = self.initial_floor.to_bytes(1, "little") | ||||
|             rom_bytearray[0x280016:0x280016 + 1] = self.starting_capsule.to_bytes(1, "little") | ||||
|             rom_bytearray[0x280017:0x280017 + 1] = self.iris_treasures_required.to_bytes(1, "little") | ||||
|             rom_bytearray[0x280018:0x280018 + 1] = self.shuffle_party_members.unlock.to_bytes(1, "little") | ||||
|             rom_bytearray[0x280019:0x280019 + 1] = self.shuffle_capsule_monsters.unlock.to_bytes(1, "little") | ||||
|             rom_bytearray[0x280030:0x280030 + 1] = self.goal.to_bytes(1, "little") | ||||
|             rom_bytearray[0x28003D:0x28003D + 1] = self.death_link.to_bytes(1, "little") | ||||
|             rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table() | ||||
|  | ||||
|             with open(rom_path, "wb") as f: | ||||
|                 f.write(rom_bytearray) | ||||
|         except Exception as e: | ||||
|             raise e | ||||
|         else: | ||||
|             patch = L2ACDeltaPatch(os.path.splitext(rom_path)[0] + L2ACDeltaPatch.patch_file_ending, | ||||
|                                    player=self.player, player_name=self.multiworld.player_name[self.player], | ||||
|                                    patched_path=rom_path) | ||||
|             patch.write() | ||||
|         finally: | ||||
|             if os.path.exists(rom_path): | ||||
|                 os.unlink(rom_path) | ||||
|  | ||||
|     def modify_multidata(self, multidata: Dict[str, Any]) -> None: | ||||
|         b64_name: str = base64.b64encode(bytes(self.rom_name)).decode() | ||||
|         multidata["connect_names"][b64_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] | ||||
|  | ||||
|     # end of ordered Main.py calls | ||||
|  | ||||
|     def create_item(self, name: str) -> Item: | ||||
|         item_data: ItemData = l2ac_item_table.get(name) | ||||
|         return L2ACItem(name, item_data.classification, items_start_id + item_data.code, self.player) | ||||
|  | ||||
|     def get_capsule_cravings_table(self) -> bytes: | ||||
|         rom: bytes = get_base_rom_bytes() | ||||
|  | ||||
|         if self.capsule_cravings_jp_style: | ||||
|             number_of_items: int = 467 | ||||
|             items_offset: int = 0x0B4F69 | ||||
|             value_thresholds: List[int] = \ | ||||
|                 [200, 500, 600, 800, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 12000, 20000, 25000, 29000, 32000, 33000] | ||||
|             tier_list: List[List[int]] = [list() for _ in value_thresholds[:-1]] | ||||
|  | ||||
|             for item_id in range(number_of_items): | ||||
|                 pointer: int = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little") | ||||
|                 if rom[items_offset + pointer] & 0x20 == 0 and rom[items_offset + pointer + 1] & 0x40 == 0: | ||||
|                     value: int = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little") | ||||
|                     for t in range(len(tier_list)): | ||||
|                         if value_thresholds[t] <= value < value_thresholds[t + 1]: | ||||
|                             tier_list[t].append(item_id) | ||||
|                             break | ||||
|             tier_sizes: List[int] = [len(tier) for tier in tier_list] | ||||
|  | ||||
|             cravings_table: bytes = b"".join(i.to_bytes(2, "little") for i in itertools.chain( | ||||
|                 *zip(itertools.accumulate((2 * tier_size for tier_size in tier_sizes), initial=0x40), tier_sizes), | ||||
|                 (item_id for tier in tier_list for item_id in tier))) | ||||
|             assert len(cravings_table) == 470, cravings_table | ||||
|             return cravings_table | ||||
|         else: | ||||
|             return rom[0x0AFF16:0x0AFF16 + 470] | ||||
|  | ||||
|     def get_goal_text_bytes(self) -> bytes: | ||||
|         goal_text: List[str] = [] | ||||
|         iris: str = f"{self.iris_treasures_required} Iris treasure{'s' if self.iris_treasures_required > 1 else ''}" | ||||
|         if self.goal == Goal.option_boss: | ||||
|             goal_text = ["You have to defeat", f"the boss on B{self.final_floor}."] | ||||
|         elif self.goal == Goal.option_iris_treasure_hunt: | ||||
|             goal_text = ["You have to find", f"{iris}."] | ||||
|         elif self.goal == Goal.option_boss_iris_treasure_hunt: | ||||
|             goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.final_floor}."] | ||||
|         elif self.goal == Goal.option_final_floor: | ||||
|             goal_text = [f"You need to get to B{self.final_floor}."] | ||||
|         assert len(goal_text) <= 4 and all(len(line) <= 28 for line in goal_text), goal_text | ||||
|         goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00)) | ||||
|         return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes)) | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_node_connection_table() -> bytes: | ||||
|         class Connect(IntFlag): | ||||
|             TOP_LEFT = 0b00000001 | ||||
|             LEFT = 0b00000010 | ||||
|             BOTTOM_LEFT = 0b00000100 | ||||
|             TOP = 0b00001000 | ||||
|             BOTTOM = 0b00010000 | ||||
|             TOP_RIGHT = 0b00100000 | ||||
|             RIGHT = 0b01000000 | ||||
|             BOTTOM_RIGHT = 0b10000000 | ||||
|  | ||||
|         rom: bytes = get_base_rom_bytes() | ||||
|  | ||||
|         return bytes(rom[0x09D59B + ((n & ~Connect.TOP_LEFT if not n & (Connect.TOP | Connect.LEFT) else n) & | ||||
|                                      (n & ~Connect.TOP_RIGHT if not n & (Connect.TOP | Connect.RIGHT) else n) & | ||||
|                                      (n & ~Connect.BOTTOM_LEFT if not n & (Connect.BOTTOM | Connect.LEFT) else n) & | ||||
|                                      (n & ~Connect.BOTTOM_RIGHT if not n & (Connect.BOTTOM | Connect.RIGHT) else n))] | ||||
|                      for n in range(256)) | ||||
							
								
								
									
										2
									
								
								worlds/lufia2ac/basepatch/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								worlds/lufia2ac/basepatch/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| asar* | ||||
| libasar* | ||||
							
								
								
									
										47
									
								
								worlds/lufia2ac/basepatch/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								worlds/lufia2ac/basepatch/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import os | ||||
|  | ||||
| import bsdiff4 | ||||
|  | ||||
| from ..Rom import get_base_rom_bytes | ||||
|  | ||||
|  | ||||
| def apply_basepatch(base_rom_bytes: bytes) -> bytes: | ||||
|     with open(os.path.join(os.path.dirname(__file__), "basepatch.bsdiff4"), "rb") as basepatch: | ||||
|         delta: bytes = basepatch.read() | ||||
|     return bsdiff4.patch(base_rom_bytes, delta) | ||||
|  | ||||
|  | ||||
| def create_basepatch() -> None: | ||||
|     from .asar import close as asar_close, geterrors as asar_errors, getprints as asar_prints, \ | ||||
|         getwarnings as asar_warnings, init as asar_init, patch as asar_patch | ||||
|  | ||||
|     os.add_dll_directory(os.path.dirname(__file__)) | ||||
|     print("Initializing Asar library") | ||||
|     asar_init() | ||||
|  | ||||
|     print("Opening base ROM") | ||||
|     old_rom_data: bytes = get_base_rom_bytes() | ||||
|  | ||||
|     print("Patching base ROM") | ||||
|     result, new_rom_data = asar_patch(os.path.join(os.path.dirname(__file__), "basepatch.asm"), old_rom_data) | ||||
|  | ||||
|     warnings = asar_warnings() | ||||
|     print("\nWarnings: " + str(len(warnings))) | ||||
|     for w in warnings: | ||||
|         print(w) | ||||
|  | ||||
|     if result: | ||||
|         print("Success") | ||||
|         for p in asar_prints(): | ||||
|             print(p) | ||||
|         asar_close() | ||||
|         delta: bytes = bsdiff4.diff(old_rom_data, new_rom_data) | ||||
|         with open(os.path.join(os.path.dirname(__file__), "basepatch.bsdiff4"), "wb") as f: | ||||
|             f.write(delta) | ||||
|     else: | ||||
|         errors = asar_errors() | ||||
|         print("\nErrors: " + str(len(errors))) | ||||
|         for error in errors: | ||||
|             print(error) | ||||
|         asar_close() | ||||
|         raise RuntimeError("Asar errors while trying to create basepatch for Lufia II Ancient Cave.") | ||||
							
								
								
									
										6
									
								
								worlds/lufia2ac/basepatch/ap_logo/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								worlds/lufia2ac/basepatch/ap_logo/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| This work is licensed under Attribution-NonCommercial 4.0 International. | ||||
| To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ | ||||
|  | ||||
| It is based on the colored AP icon. | ||||
| That icon © 2022 by Krista Corkos and Christopher Wilson is licensed under Attribution-NonCommercial 4.0 International. | ||||
| To view a copy of that license, visit http://creativecommons.org/licenses/by-nc/4.0/ | ||||
							
								
								
									
										1
									
								
								worlds/lufia2ac/basepatch/ap_logo/ap_logo.bin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								worlds/lufia2ac/basepatch/ap_logo/ap_logo.bin
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| &?CB~<<?>~~<<<3C><>@<40> <20>8<EFBFBD>d<EFBFBD><64><EFBFBD>BB<<<3C><><EFBFBD>@<40> <20>8<EFBFBD>|<7C><>B~<<<<~~?>$<B~C'?<<~~<7E><><EFBFBD>|<7C>8<EFBFBD> <20>@<40><><$~B<><42><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | ||||
							
								
								
									
										890
									
								
								worlds/lufia2ac/basepatch/basepatch.asm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										890
									
								
								worlds/lufia2ac/basepatch/basepatch.asm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,890 @@ | ||||
| lorom | ||||
|  | ||||
|  | ||||
| org $DFFFFD  ; expand ROM to 3MB | ||||
|     DB "EOF" | ||||
| org $80FFD8  ; expand SRAM to 16KB | ||||
|     DB $04                  ; overwrites DB $03 | ||||
|  | ||||
| org $80809A  ; patch copy protection | ||||
|     CMP $704000             ; overwrites CMP $702000 | ||||
| org $8080A6  ; patch copy protection | ||||
|     CMP $704000             ; overwrites CMP $702000 | ||||
|  | ||||
|  | ||||
|  | ||||
| org $8AEAA3  ; skip gruberik intro dialogue | ||||
|     DB $1C,$86,$03          ; L2SASM JMP $8AE784+$0386 | ||||
| org $8AEC82  ; skip gruberik save dialogue | ||||
|     DB $1C,$93,$01          ; L2SASM JMP $8AEB1C+$0193 | ||||
| org $8AECFE  ; skip gruberik abandon dialogue | ||||
|     DB $1C,$32,$02          ; L2SASM JMP $8AEB1C+$0232 | ||||
| org $8AF4E1  ; skip gruberik selan dialogue | ||||
|     DB $1C,$D8,$09          ; L2SASM JMP $8AEB1C+$09D8 | ||||
| org $8AF528  ; skip gruberik guy dialogue | ||||
|     DB $1C,$1E,$0A          ; L2SASM JMP $8AEB1C+$0A1E | ||||
| org $8AF55F  ; skip gruberik arty dialogue | ||||
|     DB $1C,$67,$0A          ; L2SASM JMP $8AEB1C+$0A67 | ||||
| org $8AF5B2  ; skip gruberik tia dialogue | ||||
|     DB $1C,$C3,$0A          ; L2SASM JMP $8AEB1C+$0AC3 | ||||
| org $8AF61A  ; skip gruberik dekar dialogue | ||||
|     DB $1C,$23,$0B          ; L2SASM JMP $8AEB1C+$0B23 | ||||
| org $8AF681  ; skip gruberik lexis dialogue | ||||
|     DB $1C,$85,$0B          ; L2SASM JMP $8AEB1C+$0B85 | ||||
|  | ||||
| org $8EA349  ; skip ancient cave entrance dialogue | ||||
|     DB $1C,$B0,$01          ; L2SASM JMP $8EA1AD+$01B0 | ||||
| org $8EA384  ; skip ancient cave exit dialogue | ||||
|     DB $1C,$2B,$02          ; L2SASM JMP $8EA1AD+$022B | ||||
| org $8EA565  ; skip ancient cave leaving dialogue | ||||
|     DB $1C,$E9,$03          ; L2SASM JMP $8EA1AD+$03E9 | ||||
|  | ||||
| org $8EA653  ; skip master intro dialogue | ||||
|     DB $1C,$0F,$01          ; L2SASM JMP $8EA5FA+$010F | ||||
| org $8EA721  ; skip master fight dialogue | ||||
|     DB $1C,$45,$01          ; L2SASM JMP $8EA5FA+$0145 | ||||
| org $8EA74B  ; skip master victory dialogue | ||||
|     DB $1C,$AC,$01          ; L2SASM JMP $8EA5FA+$01AC | ||||
| org $8EA7AA  ; skip master key dialogue | ||||
|     DB $1C,$CA,$01          ; L2SASM JMP $8EA5FA+$01CA | ||||
| org $8EA7F4  ; skip master goodbye dialogue | ||||
|     DB $1C,$05,$02          ; L2SASM JMP $8EA5FA+$0205 | ||||
| org $8EA807  ; skip master not fight dialogue | ||||
|     DB $1C,$18,$02          ; L2SASM JMP $8EA5FA+$0218 | ||||
|  | ||||
| org $94AC45  ; connect ancient cave exit stairs to gruberik entrance | ||||
|     DB $67,$09,$18,$68 | ||||
| org $948DE1  ; connect gruberik west border to ancient cave entrance | ||||
|     DB $07,$08,$14,$F0 | ||||
| org $948DEA  ; connect gruberik south border to ancient cave entrance | ||||
|     DB $07,$08,$14,$F0 | ||||
| org $948DF3  ; connect gruberik north border to ancient cave entrance | ||||
|     DB $07,$08,$14,$F0 | ||||
|  | ||||
|  | ||||
|  | ||||
| ; archipelago item | ||||
| org $96F9AD  ; properties | ||||
|     DB $00,$00,$00,$E4,$00,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
| org $9EDD60  ; name | ||||
|     DB "AP item     "       ; overwrites "Key30       " | ||||
| org $9FA900  ; sprite | ||||
|     incbin "ap_logo/ap_logo.bin" | ||||
|     warnpc $9FA980 | ||||
|  | ||||
|  | ||||
| org $D08000  ; signature, start of expanded data area | ||||
|     DB "ArchipelagoLufia" | ||||
|  | ||||
|  | ||||
| org $D09800  ; start of expanded code area | ||||
|  | ||||
|  | ||||
|  | ||||
| ; initialize | ||||
| pushpc | ||||
| org $808046 | ||||
|     ; DB=$80, x=1, m=1 | ||||
|     JSL Init                ; overwrites JSL $809037 | ||||
| pullpc | ||||
|  | ||||
| Init: | ||||
| ; check signature | ||||
|     LDX.b #$0F | ||||
| -:  LDA $D08000,X | ||||
|     CMP $F02000,X | ||||
|     BNE + | ||||
|     DEX | ||||
|     BPL - | ||||
|     BRA ++ | ||||
| ; set up DMA to clear expanded SRAM | ||||
| +:  STZ $211C               ; force multiplication results (MPYx) to zero | ||||
|     REP #$10 | ||||
|     LDA.b #$80 | ||||
|     STA $4300               ; transfer B-bus to A-bus, with A-bus increment | ||||
|     LDA.b #$34 | ||||
|     STA $4301               ; B-bus source register $2134 (MPYL) | ||||
|     LDX.w #$2000 | ||||
|     STX $4302               ; A-bus destination address $F02000 (SRAM) | ||||
|     LDA.b #$F0 | ||||
|     STA $4304 | ||||
|     STX $4305               ; transfer 8kB | ||||
|     LDA.b #$01 | ||||
|     STA $420B               ; start DMA channel 1 | ||||
| ; sign expanded SRAM | ||||
|     PHB | ||||
|     LDA.b #$3F | ||||
|     LDX.w #$8000 | ||||
|     LDY.w #$2000 | ||||
|     MVN $F0,$D0             ; copy 64B from $D08000 to $F02000 | ||||
|     PLB | ||||
| ++: SEP #$30 | ||||
|     JSL $809037             ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; transmit checks | ||||
| pushpc | ||||
| org $8EC1EB | ||||
|     JML TX                  ; overwrites JSL $83F559 | ||||
| pullpc | ||||
|  | ||||
| TX: | ||||
|     JSL $83F559             ; (overwritten instruction) chest opening animation | ||||
|     REP #$20 | ||||
|     LDA $7FD4EF             ; read chest item ID | ||||
|     BIT.w #$4000            ; test for blue chest flag | ||||
|     BEQ + | ||||
|     LDA $F02040             ; load check counter | ||||
|     CMP $D08010             ; compare against max AP item number | ||||
|     BPL + | ||||
|     INC                     ; increment check counter | ||||
|     STA $F02040             ; store check counter | ||||
|     SEP #$20 | ||||
|     JML $8EC331             ; skip item get process | ||||
| +:  SEP #$20 | ||||
|     JML $8EC1EF             ; continue item get process | ||||
|  | ||||
|  | ||||
|  | ||||
| ; report event flag based goal completion | ||||
| pushpc | ||||
| org $D09000 | ||||
|     DB $00,$01,$01,$02,$01,$02,$02,$03,$01,$02,$02,$03,$02,$03,$03,$04, \ | ||||
|        $01,$02,$02,$03,$02,$03,$03,$04,$02,$03,$03,$04,$03,$04,$04,$05, \ | ||||
|        $01,$02,$02,$03,$02,$03,$03,$04,$02,$03,$03,$04,$03,$04,$04,$05, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $01,$02,$02,$03,$02,$03,$03,$04,$02,$03,$03,$04,$03,$04,$04,$05, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $03,$04,$04,$05,$04,$05,$05,$06,$04,$05,$05,$06,$05,$06,$06,$07, \ | ||||
|        $01,$02,$02,$03,$02,$03,$03,$04,$02,$03,$03,$04,$03,$04,$04,$05, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $03,$04,$04,$05,$04,$05,$05,$06,$04,$05,$05,$06,$05,$06,$06,$07, \ | ||||
|        $02,$03,$03,$04,$03,$04,$04,$05,$03,$04,$04,$05,$04,$05,$05,$06, \ | ||||
|        $03,$04,$04,$05,$04,$05,$05,$06,$04,$05,$05,$06,$05,$06,$06,$07, \ | ||||
|        $03,$04,$04,$05,$04,$05,$05,$06,$04,$05,$05,$06,$05,$06,$06,$07, \ | ||||
|        $04,$05,$05,$06,$05,$06,$06,$07,$05,$06,$06,$07,$06,$07,$07,$08 | ||||
| pullpc | ||||
|  | ||||
| Goal: | ||||
|     TDC | ||||
|     LDA $0797               ; load some event flags (iris sword, iris shield, ..., iris pot) | ||||
|     TAX | ||||
|     LDA $0798               ; load some event flags (iris tiara, boss, others...) | ||||
|     TAY | ||||
|     AND.b #$02              ; test boss victory | ||||
|     LSR | ||||
|     STA $F02031             ; report boss victory goal | ||||
|     TYA | ||||
|     AND.b #$01              ; test iris tiara | ||||
|     ADC $D09000,X           ; test remaining iris items via lookup table | ||||
|     CMP $D08017             ; compare with number of treasures required | ||||
|     BMI + | ||||
|     LDA.b #$01 | ||||
|     STA $F02032             ; report iris treasures goal | ||||
|     AND $F02031 | ||||
|     STA $F02033             ; report boss victory + iris treasures goal | ||||
| +:  RTS | ||||
|  | ||||
|  | ||||
|  | ||||
| ; receive items | ||||
| RX: | ||||
|     REP #$20 | ||||
|     LDA $F02802             ; load snes side received items processed counter | ||||
|     CMP $F02800             ; compare with client side received items counter | ||||
|     BPL + | ||||
|     INC | ||||
|     STA $F02802             ; increase received items processed counter | ||||
|     ASL | ||||
|     TAX | ||||
|     LDA $F02802,X           ; load received item ID | ||||
|     BRA ++ | ||||
| +:  LDA $F02046             ; load snes side found AP items processed counter | ||||
|     CMP $F02044             ; compare with client side found AP items counter | ||||
|     BPL + | ||||
|     LDA $F02044 | ||||
|     STA $F02046             ; increase AP items processed counter | ||||
|     LDA.w #$01CA            ; load "AP item" ID | ||||
| ++: STA $7FD4EF             ; store it as a "chest" | ||||
|     JSR SpecialItemGet | ||||
|     SEP #$20 | ||||
|     JSL $8EC1EF             ; call chest opening routine (but without chest opening animation) | ||||
| +:  SEP #$20 | ||||
|     RTS | ||||
|  | ||||
| SpecialItemGet: | ||||
|     BPL +                   ; spells have high bit set | ||||
|     JSR LearnSpell | ||||
| +:  CMP.w #$01BF            ; capsule monster items range from $01B8 to $01BE | ||||
|     BPL + | ||||
|     SBC.w #$01B1            ; party member items range from $01B2 to $01B7 | ||||
|     BMI + | ||||
|     ASL | ||||
|     TAX | ||||
|     LDA $8ED8C7,X           ; load predefined bitmask with a single bit set | ||||
|     ORA $F02018             ; set unlock bit for party member/capsule monster | ||||
|     STA $F02018 | ||||
| +:  RTS | ||||
|  | ||||
| LearnSpell: | ||||
|     STA $0A0B | ||||
|     SEP #$20 | ||||
|     LDA.b #$06 | ||||
| -:  PHA | ||||
|     JSL $82FD3D             ; teach spell in $0A0B to character determined by A | ||||
|     PLA | ||||
|     DEC | ||||
|     BPL - | ||||
|     REP #$20 | ||||
|     LDA $0A0B | ||||
|     RTS | ||||
|  | ||||
|  | ||||
|  | ||||
| ; use items | ||||
| pushpc | ||||
| org $82AE6F | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL SpecialItemUse      ; overwrites JSL $81EFDF | ||||
| org $8EFD2E  ; unused region at the end of bank $8E | ||||
|     DB $1E,$0B,$01,$2B,$01,$1A,$02,$00  ; add selan | ||||
|     DB $1E,$0B,$01,$2B,$02,$1A,$03,$00  ; add guy | ||||
|     DB $1E,$0B,$01,$2B,$03,$1A,$04,$00  ; add arty | ||||
|     DB $1E,$0B,$01,$2B,$05,$1A,$05,$00  ; add dekar | ||||
|     DB $1E,$0B,$01,$2B,$04,$1A,$06,$00  ; add tia | ||||
|     DB $1E,$0B,$01,$2B,$06,$1A,$07,$00  ; add lexis | ||||
| pullpc | ||||
|  | ||||
| SpecialItemUse: | ||||
|     JSL $81EFDF             ; (overwritten instruction) | ||||
|     REP #$20 | ||||
|     LDA $0A06               ; get ID of item being used | ||||
|     CMP.w #$01B8 | ||||
|     BPL + | ||||
|     SBC.w #$01B1            ; party member items range from $01B2 to $01B7 | ||||
|     BMI + | ||||
|     ASL | ||||
|     ASL | ||||
|     ASL | ||||
|     ADC.w #$FD2E | ||||
|     STA $09B7               ; set pointer to L2SASM join script | ||||
|     SEP #$20 | ||||
|     LDA.b #$8E | ||||
|     STA $09B9 | ||||
|     PHK | ||||
|     PEA ++ | ||||
|     PEA $8DD8 | ||||
|     JML $83BB76             ; initialize parser variables | ||||
| ++: NOP | ||||
|     JSL $809CB8             ; call L2SASM parser | ||||
|     JSL $81F034             ; consume the item | ||||
|     TSX | ||||
|     INX #13 | ||||
|     TXS | ||||
|     JML $82A45E             ; leave menu | ||||
| +:  SEP #$20 | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; main loop | ||||
| pushpc | ||||
| org $83BC16 | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL MainLoop            ; overwrites LDA $09A7 : BIT.b #$01 | ||||
|     NOP | ||||
| pullpc | ||||
|  | ||||
| MainLoop: | ||||
|     JSR RX | ||||
|     JSR Goal | ||||
|     JSR Unlocks | ||||
|     LDA $09A7               ; (overwritten instruction) | ||||
|     BIT.b #$01              ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| Unlocks: | ||||
|     LDA $F02018             ; load party member unlocks from SRAM | ||||
|     STA $0780               ; transfer to flags (WRAM) | ||||
|     LDA $F02019             ; load capsule monster unlocks from SRAM | ||||
|     TAY | ||||
|     LDX.w #$0000 | ||||
| -:  TYA | ||||
|     LSR | ||||
|     TAY | ||||
|     BCC + | ||||
|     LDA $82C33C | ||||
|     CMP $11BB,X | ||||
|     BMI +++ | ||||
|     BRA ++ | ||||
| +:  LDA.b #$00 | ||||
| ++: STA $11BB,X             ; unlock/lock capsule monster #X | ||||
| +++ INX | ||||
|     CPX.w #$0007 | ||||
|     BNE - | ||||
|     LDA $F02019 | ||||
|     TAY | ||||
|     BNE + | ||||
|     LDA.b #$FF | ||||
|     STA $0A7F               ; lock capsule menu | ||||
|     BRA ++ | ||||
| +:  LDA.b #$07 | ||||
|     STA $0A7F               ; unlock capsule menu | ||||
|     LDA $F02019 | ||||
|     BIT.b #$80              ; track whether one-time setup has been done before | ||||
|     BNE ++ | ||||
|     ORA.b #$80 | ||||
|     STA $F02019 | ||||
|     CMP.b #$FF | ||||
|     BEQ ++                  ; all capsule monsters available; don't overwrite starting capsule | ||||
|     LDX.w #$FFFF | ||||
|     TYA | ||||
| -:  LSR | ||||
|     INX | ||||
|     BCC - | ||||
|     TXA | ||||
|     STA $11A3               ; activate first unlocked capsule monster | ||||
|     STA $7FB5FB | ||||
|     STA $F02016 | ||||
|     JSL $82C2FD             ; run setup routine for capsule monsters | ||||
| ++: RTS | ||||
|  | ||||
|  | ||||
|  | ||||
| ; lock party members | ||||
| pushpc | ||||
| org $8AEC3E | ||||
|     DB $15,$C4,$A4,$01      ; L2SASM JMP $8AEB1C+$01A4 if flag $C4 set | ||||
| org $8AECC0 | ||||
|     DB $6C,$65,$00,$FA          ; (overwritten instruction) | ||||
|     DB $15,$12,$AE,$01,$2E,$66  ; remove selan if flag $12 clear | ||||
|     DB $15,$13,$B4,$01,$2E,$67  ; remove guy if flag $13 clear | ||||
|     DB $15,$14,$BA,$01,$2E,$68  ; remove arty if flag $14 clear | ||||
|     DB $15,$15,$C0,$01,$2E,$6A  ; remove dekar if flag $15 clear | ||||
|     DB $15,$16,$C6,$01,$2E,$69  ; remove tia if flag $16 clear | ||||
|     DB $15,$17,$CC,$01,$2E,$6B  ; remove lexis if flag $17 clear | ||||
|     DB $00 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; party member items (IDs $01B2 - $01B7) | ||||
| pushpc | ||||
| org $96F875  ; properties | ||||
|     DB $40,$00,$00,$E9,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $40,$00,$00,$E0,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $40,$00,$00,$EB,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $40,$00,$00,$ED,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $40,$00,$00,$E8,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $40,$00,$00,$EF,$64,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
| org $979EC6  ; descriptions | ||||
|     DB "Parcelyte commander.    "    : DB $00 | ||||
|     DB "A guy named Guy.  "          : DB $00 | ||||
|     DB "(Or was it Artea?)         " : DB $00 | ||||
|     DB "Strongest warrior.   "       : DB $00 | ||||
|     DB "Elcid shopkeeper. "          : DB $00 | ||||
|     DB "Great inventor."             : DB $00 | ||||
| org $97FDAC  ; remove from scenario item list | ||||
|     DW $0000,$0000,$0000,$0000,$0000,$0000 | ||||
| org $9EDC40  ; names | ||||
|     DB "Selan       "       ; overwrites "Wind key    " | ||||
|     DB "Guy         "       ; overwrites "Cloud key   " | ||||
|     DB "Arty        "       ; overwrites "Light key   " | ||||
|     DB "Dekar       "       ; overwrites "Sword key   " | ||||
|     DB "Tia         "       ; overwrites "Tree key    " | ||||
|     DB "Lexis       "       ; overwrites "Flower key  " | ||||
| pullpc | ||||
|  | ||||
| ; capsule monster items (IDs $01B8 - $01BE) | ||||
| pushpc | ||||
| org $96F8C3  ; properties | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
|     DB $00,$00,$00,$EE,$12,$00,$00,$00,$00,$00,$00,$00,$00 | ||||
| org $979F47  ; descriptions | ||||
|     DB "NEUTRAL        "                : DB $00 | ||||
|     DB "LIGHT             "             : DB $00 | ||||
|     DB "WIND           "                : DB $00 | ||||
|     DB "WATER                         " : DB $00 | ||||
|     DB "DARK                      "     : DB $00 | ||||
|     DB "SOIL                       "    : DB $00 | ||||
|     DB "FIRE                       "    : DB $00 | ||||
| org $9EDC88  ; names | ||||
|     DB "JELZE       "       ; overwrites "Magma key   " | ||||
|     DB "FLASH       "       ; overwrites "Heart key   " | ||||
|     DB "GUSTO       "       ; overwrites "Ghost key   " | ||||
|     DB "ZEPPY       "       ; overwrites "Trial key   " | ||||
|     DB "DARBI       "       ; overwrites "Dankirk key " | ||||
|     DB "SULLY       "       ; overwrites "Basement key" | ||||
|     DB "BLAZE       "       ; overwrites "Narcysus key" | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; receive death link | ||||
| pushpc | ||||
| org $83BC91 | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL DeathLinkRX         ; overwrites LDA $7FD0AE | ||||
| pullpc | ||||
|  | ||||
| DeathLinkRX: | ||||
|     LDA $F0203F             ; check death link trigger | ||||
|     BEQ + | ||||
|     TDC | ||||
|     STA $F0203F             ; reset death link trigger | ||||
|     LDA $F0203D             ; check death link enabled | ||||
|     BEQ + | ||||
|     LDA.b #$04 | ||||
|     STA $0BBC               ; kill maxim | ||||
|     STA $0C7A               ; kill selan | ||||
|     STA $0D38               ; kill guy | ||||
|     STA $0DF6               ; kill arty | ||||
|     STA $0EB4               ; kill tia | ||||
|     STA $0F72               ; kill dekar | ||||
|     STA $1030               ; kill lexis | ||||
|     LDA.b #$FE | ||||
|     STA $7FF8A3             ; select normal enemy battle | ||||
|     LDA.b #$82 | ||||
|     STA $7FF8A4             ; select a formation containing only demise | ||||
|     JSL $8383EB             ; force battle | ||||
| +:  LDA $7FD0AE             ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
| DeathLinkTX: | ||||
|     LDA $F0203D             ; check death link enabled | ||||
|     BEQ + | ||||
|     LDA $7FF8A4             ; load formation number | ||||
|     CMP.b #$82              ; did we die from a death link? | ||||
|     BEQ + | ||||
|     STA $004202 | ||||
|     LDA.b #$0A | ||||
|     STA $004203             ; multiply by 10 to get formation offset | ||||
|     TDC | ||||
|     NOP | ||||
|     LDA $004216 | ||||
|     TAX | ||||
|     LDA $7FF756,X           ; read first monster in formation | ||||
|     INC | ||||
|     STA $F0203E             ; send death link by monster id + 1 | ||||
| +:  RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; clear receiving counters when starting new game; force "GIFT" mode | ||||
| pushpc | ||||
| org $83AD83 | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL ClearRX             ; overwrites BIT #$02 : BEQ $83ADAB | ||||
| pullpc | ||||
|  | ||||
| ClearRX: | ||||
|     REP #$20 | ||||
|     TDC | ||||
|     STA $F02800             ; clear received count | ||||
|     STA $F02802             ; clear processed count | ||||
|     SEP #$20 | ||||
|     ; absence of the overwritten instructions automatically leads to "GIFT" mode code path | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; store receiving counters when saving game | ||||
| pushpc | ||||
| org $82EB61 | ||||
|     ; DB=$8A, x=0, m=1 | ||||
|     JSL SaveRX              ; overwrites JSL $8090C9 | ||||
| pullpc | ||||
|  | ||||
| SaveRX: | ||||
|     JSL $8090C9             ; (overwritten instruction) write save slot A to SRAM | ||||
|     SEP #$10 | ||||
|     REP #$20 | ||||
|     ASL | ||||
|     ASL | ||||
|     TAX | ||||
|     LDA $F02800             ; | ||||
|     STA $F027E0,X           ; save received count | ||||
|     LDA $F02802             ; | ||||
|     STA $F027E2,X           ; save processed count | ||||
|     SEP #$20 | ||||
|     REP #$10 | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; restore receiving counters when loading game | ||||
| pushpc | ||||
| org $82EAD5 | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL LoadRX              ; overwrites JSL $809099 | ||||
| pullpc | ||||
|  | ||||
| LoadRX: | ||||
|     JSL $809099             ; (overwritten instruction) load save slot A from SRAM | ||||
|     SEP #$10 | ||||
|     REP #$20 | ||||
|     ASL | ||||
|     ASL | ||||
|     TAX | ||||
|     LDA $F027E0,X           ; | ||||
|     STA $F02800             ; restore received count | ||||
|     LDA $F027E2,X           ; | ||||
|     STA $F02802             ; restore processed count | ||||
|     SEP #$20 | ||||
|     REP #$10 | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; keep inventory after defeat | ||||
| pushpc | ||||
| org $848B9C | ||||
|     ; DB=$7E, x=0, m=1 | ||||
|     NOP #5                  ; overwrites LDA.b #$FF : STA $7FE759 : JSR $8888 | ||||
|     JSL DeathLinkTX | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; set initial floor number | ||||
| pushpc | ||||
| org $8487A9 | ||||
|     JSL InitialFloor        ; overwrites TDC : STA $7FE696 | ||||
|     NOP | ||||
| pullpc | ||||
|  | ||||
| InitialFloor: | ||||
|     LDA $D08015             ; read initial floor number | ||||
|     STA $7FE696             ; (overwritten instruction) | ||||
|     TDC                     ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; report final floor goal completion | ||||
| pushpc | ||||
| org $839E87 | ||||
|     JSL FinalFloor          ; overwrites STA $0005B0 | ||||
| pullpc | ||||
|  | ||||
| FinalFloor: | ||||
|     STA $0005B0             ; (overwritten instruction) | ||||
|     LDA.b #$01 | ||||
|     STA $F02034             ; report final floor goal | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; start with Providence | ||||
| pushpc | ||||
| org $8488BB | ||||
|     ; DB=$84, x=0, m=0 | ||||
|     SEC                     ; {carry clear = disable this feature, carry set = enable this feature} | ||||
|     JSL Providence          ; overwrites LDX.w #$1402 : STX $0A8D | ||||
|     NOP                     ; | ||||
| pullpc | ||||
|  | ||||
| Providence: | ||||
|     LDX.w #$1402            ; (overwritten instruction) | ||||
|     STX $0A8D               ; (overwritten instruction) add Potion x10 | ||||
|     BCC + | ||||
|     LDX.w #$022D            ; | ||||
|     STX $0A8F               ; add Providence | ||||
| +:  RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; start inventory | ||||
| pushpc | ||||
| org $848901 | ||||
|     ; DB=$84, x=0, m=1 | ||||
|     JSL StartInventory      ; overwrites JSL $81ED35 | ||||
| pullpc | ||||
|  | ||||
| StartInventory: | ||||
|     JSL $81ED35             ; (overwritten instruction) | ||||
|     REP #$20 | ||||
|     LDA $F02802             ; number of items to process | ||||
|     DEC | ||||
|     BMI ++                  ; skip if empty | ||||
|     ASL | ||||
|     TAX | ||||
| -:  LDA $F02804,X           ; item ID | ||||
|     BPL +                   ; spells have high bit set | ||||
|     PHX | ||||
|     JSR LearnSpell | ||||
|     PLX | ||||
| +:  BIT.w #$C000            ; ignore blue chest items (and spells) | ||||
|     BNE + | ||||
|     PHX | ||||
|     STA $09CF               ; specify item ID | ||||
|     TDC | ||||
|     INC | ||||
|     STA $09CD               ; specify quantity as 1 | ||||
|     JSL $82E80C             ; add item to inventory | ||||
|     REP #$20 | ||||
|     PLX | ||||
| +:  DEX | ||||
|     DEX | ||||
|     BPL - | ||||
| ++: SEP #$20 | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; increase variety of red chest gear after B9 | ||||
| pushpc | ||||
| org $839176 | ||||
|     ; DB=$7F, x=0, m=1 | ||||
|     CLC                     ; {carry clear = disable this feature, carry set = enable this feature} | ||||
|     JSL RedChestGear        ; overwrites LDX.w #$1000 : LDA $60 | ||||
| org $83917D | ||||
|     ; DB=$7F, x=0, m=1 | ||||
|     JSL RunEquipmentRNG     ; overwrites LSR : JSR $9E11 | ||||
| pullpc | ||||
|  | ||||
| RedChestGear: | ||||
|     BCC + | ||||
|     REP #$20                ; support more than 127 items | ||||
| +:  LDX.w #$1000            ; (overwritten instruction) | ||||
|     LDA $60                 ; (overwritten instruction) | ||||
|     RTL | ||||
| RunEquipmentRNG: | ||||
|     BCS + | ||||
|     SEP #$20 | ||||
|     PHK | ||||
|     PEA ++ | ||||
|     PEA $8DD8 | ||||
|     LSR | ||||
|     JML $839E11 | ||||
| +:  LSR                     ; (overwritten instruction) divide by 2 (translates max item offset to max item number) | ||||
|     SEP #$20                ; (the max item number fits in 8bits since there are always fewer than 256 eligible items) | ||||
|     STA $004202             ; run RNG: fill WRMPYA multiplicand register with max item number | ||||
|     JSL $8082C7             ; run RNG: load 8bit accumulator with 1st random number from PRNG | ||||
|     STA $004203             ; run RNG: fill WRMPYB multiplier register with 1st random number and start multiplication | ||||
|     NOP | ||||
|     REP #$20 | ||||
|     LDA $004216             ; run RNG: read RDMPYL+H multiplication result | ||||
|     STA $E746,Y             ; save it for later | ||||
|     SEP #$20 | ||||
|     JSL $8082C7             ; run RNG: load 8bit accumulator with 2nd random number from PRNG | ||||
|     STA $004203             ; run RNG: fill WRMPYB multiplier register with 2nd random number and start multiplication | ||||
|     CLC | ||||
|     TDC | ||||
|     LDA $004217             ; run RNG: read RDMPYH multiplication result | ||||
|     REP #$20 | ||||
|     ADC $E746,Y | ||||
|     AND.w #$FF00 | ||||
|     XBA | ||||
|     ASL                     ; multiply by 2 (translates selected item number to selected item offset) | ||||
| ++: TAX                     ; store result in 16bit X register | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; relocate capsule cravings table | ||||
| pushpc | ||||
| org $82C55A | ||||
|     LDA $D09200,X           ; overwrites LDA $95FF16,X | ||||
| org $82C55F | ||||
|     LDA $D09202,X           ; overwrites LDA $95FF18,X | ||||
| org $82C572 | ||||
|     LDA $D09200,X           ; overwrites LDA $95FF16,X | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; set capsule monster starting xp | ||||
| pushpc | ||||
| org $82C313 | ||||
|     ; DB=$84, x=0, m=1 | ||||
|     JSL CapsuleStartingXp   ; overwrites LDX.w #$0000 : LDA.b #$00 : STA $7FF1AA,X : INX : CPX.w #$0015 : BNE $82C318 | ||||
|     NOP #11 | ||||
| pullpc | ||||
|  | ||||
| CapsuleStartingXp: | ||||
|     PHB | ||||
|     REP #$20 | ||||
|     LDA $D08012 | ||||
|     STA $7FF1AA             ; store low word of starting XP for first capsule monster | ||||
|     SEP #$20 | ||||
|     LDA $D08014 | ||||
|     STA $7FF1AC             ; store highest byte of starting XP for first capsule monster | ||||
|     TDC | ||||
|     LDA.b #$11 | ||||
|     LDX.w #$F1AA | ||||
|     LDY.w #$F1AD | ||||
|     MVN $7F,$7F             ; pattern fill the remaining six capsule monster slots | ||||
|     PLB | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; set starting capsule monster | ||||
| pushpc | ||||
| org $82C36A | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     JSL StartingCapsule     ; overwrites STZ $11A3 : LDA.b #$01 | ||||
|     NOP | ||||
| pullpc | ||||
|  | ||||
| StartingCapsule: | ||||
|     LDA $F02016             ; read starting capsule monster id | ||||
|     STA $11A3 | ||||
|     LDA.b #$01              ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; enter ancient cave as if coming from the world map | ||||
| pushpc | ||||
| org $83B773 | ||||
|     ; DB=$7E, x=0, m=1 | ||||
|     JSL CaveEntrance        ; overwrites LDA $05AC : STA $05B4 | ||||
|     NOP #2 | ||||
| pullpc | ||||
|  | ||||
| CaveEntrance: | ||||
|     LDA $05AC               ; (overwritten instruction) | ||||
|     CMP.b #$68 | ||||
|     BNE +                   ; when leaving gruberik, act as if leaving world map | ||||
|     TDC | ||||
| +:  STA $05B4               ; (overwritten instruction) | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; enable run button | ||||
| ; directional input item crash fix | ||||
| pushpc | ||||
| org $83FC6C | ||||
|     REP #$10                ; overwrites BEQ $83FC8A : LDA.b #$80 | ||||
|     LDA.b #$40 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; mid-turn death fix | ||||
| pushpc | ||||
| org $85B544 | ||||
|     JSL MidTurnDeathFix     ; overwrites JSL $85CCCE | ||||
| pullpc | ||||
|  | ||||
| MidTurnDeathFix: | ||||
|     JSL $85CCCE             ; (overwritten instruction) clear shared battle registers after attack | ||||
|     LDY.w #$000F            ; offset to status effect byte | ||||
|     LDA ($BE),Y             ; offset to stat block of attacker | ||||
|     BIT.b #$04              ; check death | ||||
|     BEQ + | ||||
|     TSX                     ; attacker died; abort script | ||||
|     INX #3 | ||||
|     TXS | ||||
|     JML $85B476 | ||||
| +:  RTL                     ; attacker still alive; continue script | ||||
|  | ||||
|  | ||||
|  | ||||
| ; poison death fix | ||||
| pushpc | ||||
| org $818959 | ||||
|     JSL PoisonDeathFix      ; overwrites JSL $859DD4 | ||||
| pullpc | ||||
|  | ||||
| PoisonDeathFix: | ||||
|     JSL $859DD4             ; (overwritten instruction) | ||||
|     JSL $8593B7 | ||||
|     RTL | ||||
|  | ||||
|  | ||||
|  | ||||
| ; single-node room fix | ||||
| pushpc | ||||
| org $839C64 | ||||
|     ; DB=$7F, x=0, m=1 | ||||
|     BNE +                   ; overwrites BNE $17 | ||||
| org $839C7B | ||||
|     ; DB=$7F, x=0, m=1 | ||||
|     JMP $9BE7               ; overwrites BRA $22 : LDX.w #$00FF | ||||
| +:  TDC | ||||
|     TAX | ||||
| org $839C99 | ||||
|     ; DB=$7F, x=0, m=1 | ||||
|     INX                     ; overwrites DEX : CPX.w #$0010 : BCS $E1 | ||||
|     CPX.w #$0100 | ||||
|     BCC $E1 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; equipment text fix | ||||
| pushpc | ||||
| org $81F2E3 | ||||
|     ; DB=$9E, x=0, m=1 | ||||
|     NOP #2                  ; overwrites BPL $81F2D6 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; music menu fix | ||||
| pushpc | ||||
| org $82BF44 | ||||
|     ; DB=$83, x=0, m=1 | ||||
|     BNE $12                 ; overwrites BNE $06 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; logo skip | ||||
| pushpc | ||||
| org $80929A | ||||
|     ; DB=$80, x=0, m=1 | ||||
|     LDA.b #$00              ; overwrites LDA.b #$80 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; intro skip | ||||
| pushpc | ||||
| org $8080CF | ||||
|     ; DB=$80, x=1, m=1 | ||||
|     JML $8383BD             ; overwrites JML $808281 | ||||
| pullpc | ||||
|  | ||||
|  | ||||
|  | ||||
| ; SRAM map | ||||
| ; $F02000   16  signature | ||||
| ; $F02010   2   blue chest count | ||||
| ; $F02012   3   capsule starting xp | ||||
| ; $F02015   1   initial floor | ||||
| ; $F02016   1   starting capsule | ||||
| ; $F02017   1   iris treasures required | ||||
| ; $F02018   1   party members available | ||||
| ; $F02019   1   capsule monsters available | ||||
| ; $F02030   1   selected goal | ||||
| ; $F02031   1   goal completion: boss | ||||
| ; $F02032   1   goal completion: iris_treasure_hunt | ||||
| ; $F02033   1   goal completion: master_iris_treasure_hunt | ||||
| ; $F02034   1   goal completion: final_floor | ||||
| ; $F0203D   1   death link enabled | ||||
| ; $F0203E   1   death link sent (monster id + 1) | ||||
| ; $F0203F   1   death link received | ||||
| ; $F02040   2   check counter (snes_items_sent) | ||||
| ; $F02042   2   check counter (client_items_sent) | ||||
| ; $F02044   2   check counter (client_ap_items_found) | ||||
| ; $F02046   2   check counter (snes_ap_items_found) | ||||
| ; $F027E0   16  saved RX counters | ||||
| ; $F02800   2   received counter | ||||
| ; $F02802   2   processed counter | ||||
| ; $F02804   inf list of received items | ||||
							
								
								
									
										
											BIN
										
									
								
								worlds/lufia2ac/basepatch/basepatch.bsdiff4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								worlds/lufia2ac/basepatch/basepatch.bsdiff4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										36
									
								
								worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # Lufia II - Rise of the Sinistrals (Ancient Cave) | ||||
|  | ||||
| ## Where is the settings page? | ||||
|  | ||||
| The [player settings page for this game](../player-settings) contains all the options you need to configure and export a | ||||
| config file. | ||||
|  | ||||
| ## What does randomization do to this game? | ||||
|  | ||||
| As you may or may not know, randomization was already a core feature of the Ancient Cave in Lufia II, basically being a | ||||
| whole game within a game. The Ancient Cave has 99 floors with increasingly hard enemies, red chests and blue chests. At | ||||
| the end of the Ancient Cave you get to fight the Royal Jelly... if you make it that far. You cannot lose the Royal | ||||
| Jelly fight as it kills itself after giving you three rounds to try and kill it (or manage to vanquish your own party,  | ||||
| whichever one you can manage). | ||||
|  | ||||
| The Randomizer allows you to set four different goals (Kill the Boss, Iris Treasure Hunt, Treasure hunt + Boss, Reach  | ||||
| final floor), and modify the game in several other ways (define where the last floor is, start with providence, choose | ||||
| custom levels/evolution stages for your capsule monsters, etc.).  | ||||
|  | ||||
| ## What items and locations get shuffled? | ||||
|  | ||||
| In general, all Items can appear in the red and blue chests, the blue chest items are items you get to keep after you | ||||
| die in or escape the Ancient Cave using Providence. Archipelago Items can also appear in said chests. Iris Treasures are | ||||
| always in your local game. | ||||
|  | ||||
| ## Which items can be in another player's world? | ||||
|  | ||||
| Any of the blue chest items from the vanilla game may be placed into another player's world. | ||||
|  | ||||
| ## What does another world's item look like in Lufia II? | ||||
|  | ||||
| Items belonging to other worlds are represented by an AP icon and are called AP items. | ||||
|  | ||||
| ## When the player receives an item, what happens? | ||||
|  | ||||
| Your Party Leader will hold up the item they received when not in a fight or in a menu.  | ||||
							
								
								
									
										144
									
								
								worlds/lufia2ac/docs/setup_en.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								worlds/lufia2ac/docs/setup_en.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| # Lufia II Ancient Cave Multiworld Setup Guide | ||||
|  | ||||
| ## Required Software | ||||
|  | ||||
| - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup` | ||||
| - Hardware or software capable of loading and playing SNES ROM files | ||||
|     - An emulator capable of connecting to SNI | ||||
|       ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), | ||||
|        [BizHawk](http://tasvideos.org/BizHawk.html), or | ||||
|        [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer). Or, | ||||
|     - An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:  | ||||
| modded SNES minis are currently not supported by SNI** | ||||
| - Your American ROM file, probably named `Lufia II - Rise of the Sinistrals (USA).sfc` | ||||
|  | ||||
| ## Installation Procedures | ||||
|  | ||||
| 1. Download and install SNIClient from the link above, making sure to install the most recent version. | ||||
|    **The installer file is located in the assets section at the bottom of the version information**. | ||||
|     - During setup, you will be asked to locate your base ROM file. This is your American Lufia II - Rise of the Sinistrals ROM file. | ||||
|  | ||||
| 2. 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? | ||||
|  | ||||
| Your config file contains a set of configuration options which provide the generator with information about how it | ||||
| should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player | ||||
| to enjoy an experience customized for their taste, and different players in the same multiworld can all have different | ||||
| options. | ||||
|  | ||||
| ### Where do I get a config file? | ||||
|  | ||||
| The [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page on the website allows you to configure | ||||
| your personal settings and export a config file from them. | ||||
|  | ||||
| ### 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](/mysterycheck) page. | ||||
|  | ||||
| ## Generating a Single-Player Game | ||||
|  | ||||
| 1. Navigate to the [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page, configure your options, | ||||
|    and click the "Generate Game" button. | ||||
| 2. You will be presented with a "Seed Info" page. | ||||
| 3. Click the "Create New Room" link. | ||||
| 4. You will be presented with a server page, from which you can download your patch file. | ||||
| 5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and | ||||
|    open your emulator for you. | ||||
| 6. Since this is a single-player game, you will no longer need the client, so feel free to close it. | ||||
|  | ||||
| ## Joining a MultiWorld Game | ||||
|  | ||||
| ### Obtain your patch file and create your ROM | ||||
|  | ||||
| When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, | ||||
| the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch | ||||
| files. Your patch file should have a `.apl2ac` 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 Multitroid | ||||
|  | ||||
| 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/x64` or `/SNI/lua/x86` depending on if the | ||||
|       emulator is 64-bit or 32-bit. | ||||
| 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. You may do this by clicking on the Tools menu in BizHawk and following these | ||||
|    menu options:   | ||||
|    `Config --> Cores --> SNES --> BSNES`   | ||||
|    Once you have changed the loaded core, you must restart BizHawk. | ||||
| 2. Load your ROM file if it hasn't already been loaded. | ||||
| 3. Click on the Tools menu and click on **Lua Console** | ||||
| 4. Click Script -> Open Script... | ||||
| 5. Select the `Connector.lua` file you downloaded above | ||||
|     - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the | ||||
|       emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. | ||||
|  | ||||
| ##### 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. | ||||
|  | ||||
|  | ||||
| 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! You can execute various commands in your client. For more information regarding | ||||
| these commands you can use `/help` for local client commands and `!help` for server commands. | ||||
							
								
								
									
										87
									
								
								worlds/lufia2ac/test/TestGoal.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								worlds/lufia2ac/test/TestGoal.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| from . import L2ACTestBase | ||||
|  | ||||
|  | ||||
| class TestDefault(L2ACTestBase): | ||||
|     options = {} | ||||
|  | ||||
|     def testEverything(self): | ||||
|         self.collect_all_but(["Boss victory"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testNothing(self): | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|  | ||||
| class TestShuffleCapsuleMonsters(L2ACTestBase): | ||||
|     options = { | ||||
|         "shuffle_capsule_monsters": True, | ||||
|     } | ||||
|  | ||||
|     def testEverything(self): | ||||
|         self.collect_all_but(["Boss victory"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testBestParty(self): | ||||
|         self.collect_by_name("DARBI") | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testNoDarbi(self): | ||||
|         self.collect_all_but(["Boss victory", "DARBI"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|  | ||||
| class TestShufflePartyMembers(L2ACTestBase): | ||||
|     options = { | ||||
|         "shuffle_party_members": True, | ||||
|     } | ||||
|  | ||||
|     def testEverything(self): | ||||
|         self.collect_all_but(["Boss victory"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testBestParty(self): | ||||
|         self.collect_by_name(["Dekar", "Guy", "Arty"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testNoDekar(self): | ||||
|         self.collect_all_but(["Boss victory", "Dekar"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|     def testNoGuy(self): | ||||
|         self.collect_all_but(["Boss victory", "Guy"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|     def testNoArty(self): | ||||
|         self.collect_all_but(["Boss victory", "Arty"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|  | ||||
| class TestShuffleBoth(L2ACTestBase): | ||||
|     options = { | ||||
|         "shuffle_capsule_monsters": True, | ||||
|         "shuffle_party_members": True, | ||||
|     } | ||||
|  | ||||
|     def testEverything(self): | ||||
|         self.collect_all_but(["Boss victory"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testBestParty(self): | ||||
|         self.collect_by_name(["Dekar", "Guy", "Arty", "DARBI"]) | ||||
|         self.assertBeatable(True) | ||||
|  | ||||
|     def testNoDekar(self): | ||||
|         self.collect_all_but(["Boss victory", "Dekar"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|     def testNoGuy(self): | ||||
|         self.collect_all_but(["Boss victory", "Guy"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|     def testNoArty(self): | ||||
|         self.collect_all_but(["Boss victory", "Arty"]) | ||||
|         self.assertBeatable(False) | ||||
|  | ||||
|     def testNoDarbi(self): | ||||
|         self.collect_all_but(["Boss victory", "DARBI"]) | ||||
|         self.assertBeatable(False) | ||||
							
								
								
									
										5
									
								
								worlds/lufia2ac/test/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								worlds/lufia2ac/test/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| from test.TestBase import WorldTestBase | ||||
|  | ||||
|  | ||||
| class L2ACTestBase(WorldTestBase): | ||||
|     game = "Lufia II Ancient Cave" | ||||
		Reference in New Issue
	
	Block a user
	 el-u
					el-u