Lufia II Ancient Cave: implement new game (#1218)
Co-authored-by: wordfcuk <greili1985@gmail.com>
This commit is contained in:
parent
2c46c48ba9
commit
51c6be047f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
Utils.py
3
Utils.py
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()}
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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))
|
|
@ -0,0 +1,2 @@
|
|||
asar*
|
||||
libasar*
|
|
@ -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.")
|
|
@ -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/
|
|
@ -0,0 +1 @@
|
|||
&?CB~<<?>~~<<€€@À à8ødäÂÂBB<<€€À@à ø8ä|ÂþB~<<<<~~?>$<B~C'?<<~~þþü|ø8à À@€€<$~BþÂüäøøààÀÀ€€
|
|
@ -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.
|
@ -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.
|
|
@ -0,0 +1,144 @@
|
|||
# Lufia II Ancient Cave Multiworld Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup`
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html), or
|
||||
[RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer). Or,
|
||||
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
|
||||
modded SNES minis are currently not supported by SNI**
|
||||
- Your American ROM file, probably named `Lufia II - Rise of the Sinistrals (USA).sfc`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Download and install SNIClient from the link above, making sure to install the most recent version.
|
||||
**The installer file is located in the assets section at the bottom of the version information**.
|
||||
- During setup, you will be asked to locate your base ROM file. This is your American Lufia II - Rise of the Sinistrals ROM file.
|
||||
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
|
||||
extracted in step one.
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
|
||||
Your config file contains a set of configuration options which provide the generator with information about how it
|
||||
should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player
|
||||
to enjoy an experience customized for their taste, and different players in the same multiworld can all have different
|
||||
options.
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page on the website allows you to configure
|
||||
your personal settings and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page, configure your options,
|
||||
and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and
|
||||
open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||
files. Your patch file should have a `.apl2ac` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||
client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the connector lua file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit.
|
||||
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
|
||||
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
|
||||
menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click Script -> Open Script...
|
||||
5. Select the `Connector.lua` file you downloaded above
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
|
||||
|
||||
##### RetroArch 1.10.3 or newer
|
||||
|
||||
You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3.
|
||||
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355.
|
||||
|
||||

|
||||
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
|
||||
Performance)".
|
||||
|
||||
When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
|
||||
read ROM data.
|
||||
|
||||
#### With hardware
|
||||
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
|
||||
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
|
||||
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
|
||||
|
||||
Other hardware may find helpful information on the usb2snes platforms
|
||||
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
|
||||
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
|
||||
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
|
||||
into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
|
||||
### Play the game
|
||||
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
|
||||
successfully joining a multiworld game! You can execute various commands in your client. For more information regarding
|
||||
these commands you can use `/help` for local client commands and `!help` for server commands.
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
|||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class L2ACTestBase(WorldTestBase):
|
||||
game = "Lufia II Ancient Cave"
|
Loading…
Reference in New Issue