Lufia II Ancient Cave: implement new game (#1218)

Co-authored-by: wordfcuk <greili1985@gmail.com>
This commit is contained in:
el-u 2022-12-12 02:36:18 +01:00 committed by GitHub
parent 2c46c48ba9
commit 51c6be047f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 3111 additions and 1 deletions

2
.gitignore vendored
View File

@ -4,6 +4,7 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apl2ac
*.apm3
*.apmc
*.apz5
@ -135,6 +136,7 @@ venv/
ENV/
env.bak/
venv.bak/
.code-workspace
# Spyder project settings
.spyderproject

View File

@ -33,6 +33,7 @@ Currently, the following games are supported:
* Hylics 2
* Overcooked! 2
* Zillion
* Lufia II Ancient Cave
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -307,6 +307,9 @@ def get_default_options() -> OptionsType:
"ffr_options": {
"display_msgs": True,
},
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
}
return options

View File

@ -71,7 +71,7 @@ def create():
del file_data
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
f.write(res)
# Generate JSON files for player-settings pages

View File

@ -93,6 +93,9 @@ sni_options:
lttp_options:
# File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
lufia2ac_options:
# File name of the US rom
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
sm_options:
# File name of the v1.0 J rom
rom_file: "Super Metroid (JU).sfc"

View File

@ -57,6 +57,7 @@ Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full ho
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
@ -69,6 +70,7 @@ Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Se
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
@ -90,6 +92,7 @@ Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).s
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
@ -191,6 +194,11 @@ Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Arch
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
@ -263,6 +271,9 @@ var SMWRomFilePage: TInputFileWizardPage;
var soerom: string;
var SoERomFilePage: TInputFileWizardPage;
var l2acrom: string;
var L2ACROMFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
@ -423,6 +434,8 @@ begin
Result := not (SMWROMFilePage.Values[0] = '')
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
Result := not (SoEROMFilePage.Values[0] = '')
else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then
Result := not (L2ACROMFilePage.Values[0] = '')
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
Result := not (OoTROMFilePage.Values[0] = '')
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
@ -527,6 +540,22 @@ begin
Result := '';
end;
function GetL2ACROMPath(Param: string): string;
begin
if Length(l2acrom) > 0 then
Result := l2acrom
else if Assigned(L2ACROMFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d')
if R <> 0 then
MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := L2ACROMFilePage.Values[0]
end
else
Result := '';
end;
function GetZlROMPath(Param: string): string;
begin
if Length(zlrom) > 0 then
@ -610,6 +639,10 @@ begin
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
if Length(bluerom) = 0 then
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
if Length(l2acrom) = 0 then
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
end;
@ -624,6 +657,8 @@ begin
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac'));
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then

360
worlds/lufia2ac/Client.py Normal file
View 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
View 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()}

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

@ -0,0 +1,2 @@
asar*
libasar*

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

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

View File

@ -0,0 +1 @@
&?CB~<<?>~~<<€€@À à8ødäÂÂBB<<€€À@à ø8ä|ÂþB~<<<<~~?>$<B~C'?<<~~þþü|ø8à À@€€<$~BþÂüäøøààÀÀ€€

View 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

Binary file not shown.

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

View 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.
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
Performance)".
When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
read ROM data.
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
Other hardware may find helpful information on the usb2snes platforms
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
1. Close your emulator, which may have auto-launched.
2. Power on your device and load the ROM.
### Connect to the Archipelago Server
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
into the "Server" input field then press enter.
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
successfully joining a multiworld game! 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.

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

View File

@ -0,0 +1,5 @@
from test.TestBase import WorldTestBase
class L2ACTestBase(WorldTestBase):
game = "Lufia II Ancient Cave"