Archipelago/worlds/lufia2ac/__init__.py

493 lines
28 KiB
Python

import base64
import itertools
import os
from enum import IntFlag
from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
import settings
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
from Options import PerGameCommonOptions
from Utils import __version__
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_rule, CollectionRule, 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 CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
Goal, L2ACOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
from .Utils import constrained_choices, constrained_shuffle, weighted_sample
from .basepatch import apply_basepatch
CHESTS_PER_SPHERE: int = 5
class L2ACSettings(settings.Group):
class RomFile(settings.SNESRomPath):
"""File name of the US rom"""
description = "Lufia II ROM File"
copy_to = "Lufia II - Rise of the Sinistrals (USA).sfc"
md5s = [L2ACDeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
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()
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = L2ACOptions
options: L2ACOptions
settings: ClassVar[L2ACSettings]
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},
"Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE},
"Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER},
}
required_client_version: Tuple[int, int, int] = (0, 4, 4)
# L2ACWorld specific properties
rom_name: bytearray
o: L2ACOptions
@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.o = self.options
if self.o.blue_chest_count < self.o.custom_item_pool.count:
raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is "
f"greater than blue_chest_count ({self.o.blue_chest_count}).")
if self.o.capsule_starting_level == CapsuleStartingLevel.special_range_names["party_starting_level"]:
self.o.capsule_starting_level.value = int(self.o.party_starting_level)
if self.o.initial_floor >= self.o.final_floor:
self.o.initial_floor.value = self.o.final_floor - 1
if self.o.shuffle_party_members:
self.o.default_party.value = DefaultParty.default
def create_regions(self) -> None:
menu = Region("Menu", self.player, self.multiworld)
self.multiworld.regions.append(menu)
ancient_dungeon = Region("AncientDungeon", self.player, self.multiworld, "Ancient Dungeon")
item_count: int = int(self.o.blue_chest_count)
if self.o.shuffle_capsule_monsters:
item_count += len(self.item_name_groups["Capsule monsters"])
if self.o.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))
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(
L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player))
chest_access.show_in_spoiler = False
ancient_dungeon.locations.append(chest_access)
for iris in sorted(self.item_name_groups["Iris treasures"]):
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
iris_treasure: Location = \
L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon)
iris_treasure.place_locked_item(self.create_item(iris))
ancient_dungeon.locations.append(iris_treasure)
self.multiworld.regions.append(ancient_dungeon)
final_floor = Region("FinalFloor", self.player, self.multiworld, "Ancient Cave Final Floor")
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: Location = L2ACLocation(self.player, "Boss", self.location_name_to_id["Boss"], final_floor)
boss.place_locked_item(self.create_item("Ancient key"))
final_floor.locations.append(boss)
self.multiworld.regions.append(final_floor)
menu.connect(ancient_dungeon, "AncientDungeonEntrance")
ancient_dungeon.connect(final_floor, "FinalFloorEntrance")
def create_items(self) -> None:
item_pool: List[str] = self.random.choices(sorted(self.item_name_groups["Blue chest items"]),
k=self.o.blue_chest_count - self.o.custom_item_pool.count)
item_pool += [item_name for item_name, count in self.o.custom_item_pool.items() for _ in range(count)]
if self.o.shuffle_capsule_monsters:
item_pool += self.item_name_groups["Capsule monsters"]
self.o.blue_chest_count.value += len(self.item_name_groups["Capsule monsters"])
if self.o.shuffle_party_members:
item_pool += self.item_name_groups["Party members"]
self.o.blue_chest_count.value += len(self.item_name_groups["Party members"])
for item_name in item_pool:
self.multiworld.itempool.append(self.create_item(item_name))
def set_rules(self) -> None:
max_sphere: int = (self.o.blue_chest_count - 1) // CHESTS_PER_SPHERE + 1
rule_for_sphere: Dict[int, CollectionRule] = \
{sphere: lambda state, s=sphere: state.has("Progressive chest access", self.player, s - 1)
for sphere in range(2, max_sphere + 1)}
for i in range(CHESTS_PER_SPHERE * 2, self.o.blue_chest_count, CHESTS_PER_SPHERE):
set_rule(self.multiworld.get_location(f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", self.player),
rule_for_sphere[i // CHESTS_PER_SPHERE])
for i in range(CHESTS_PER_SPHERE, self.o.blue_chest_count):
set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player),
rule_for_sphere[i // CHESTS_PER_SPHERE + 1])
set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), rule_for_sphere[max_sphere])
for i in range(9):
set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player), rule_for_sphere[max_sphere])
set_rule(self.multiworld.get_location("Boss", self.player), rule_for_sphere[max_sphere])
if self.o.shuffle_capsule_monsters:
add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player))
if self.o.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.o.goal == Goal.option_final_floor:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Final Floor access", self.player)
elif self.o.goal == Goal.option_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required))
elif self.o.goal == Goal.option_boss:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Ancient key", self.player)
elif self.o.goal == Goal.option_boss_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \
lambda state: (state.has("Ancient key", self.player) and
state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required)))
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.o.capsule_starting_level.value.to_bytes(1, "little")
rom_bytearray[0x01432F:0x01432F + 1] = self.o.capsule_starting_form.unlock.to_bytes(1, "little")
rom_bytearray[0x01433C:0x01433C + 1] = self.o.capsule_starting_form.value.to_bytes(1, "little")
rom_bytearray[0x0190D5:0x0190D5 + 1] = self.o.iris_floor_chance.value.to_bytes(1, "little")
rom_bytearray[0x019147:0x019157 + 1:4] = self.o.blue_chest_chance.chest_type_thresholds
rom_bytearray[0x019176] = 0x38 if self.o.gear_variety_after_b9 else 0x18
rom_bytearray[0x019477:0x019477 + 1] = self.o.healing_floor_chance.value.to_bytes(1, "little")
rom_bytearray[0x0194A2:0x0194A2 + 1] = self.o.crowded_floor_chance.value.to_bytes(1, "little")
rom_bytearray[0x019E82:0x019E82 + 1] = self.o.final_floor.value.to_bytes(1, "little")
rom_bytearray[0x01FC75:0x01FC75 + 1] = self.o.run_speed.value.to_bytes(1, "little")
rom_bytearray[0x01FC81:0x01FC81 + 1] = self.o.run_speed.value.to_bytes(1, "little")
rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.o.default_party.roster
for offset in range(0x02B395, 0x02B452, 0x1B):
rom_bytearray[offset:offset + 1] = self.o.party_starting_level.value.to_bytes(1, "little")
for offset in range(0x02B39A, 0x02B457, 0x1B):
rom_bytearray[offset:offset + 3] = self.o.party_starting_level.xp.to_bytes(3, "little")
rom_bytearray[0x03AE49:0x03AE49 + 1] = self.o.boss.sprite.to_bytes(1, "little")
rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes()
rom_bytearray[0x056AA3:0x056AA3 + 24] = self.o.default_party.event_script
rom_bytearray[0x072740:0x072740 + 1] = self.o.boss.music.to_bytes(1, "little")
rom_bytearray[0x072742:0x072742 + 1] = self.o.boss.value.to_bytes(1, "little")
rom_bytearray[0x072748:0x072748 + 1] = self.o.boss.flag.to_bytes(1, "little")
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
rom_bytearray[0x280016:0x280016 + 1] = self.o.default_capsule.value.to_bytes(1, "little")
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
rom_bytearray[0x28001B:0x28001B + 1] = self.o.inactive_exp_gain.value.to_bytes(1, "little")
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
(rom_bytearray[0x08A1D4:0x08A1D4 + 128],
rom_bytearray[0x0A595C:0x0A595C + 200],
rom_bytearray[0x0A5DF6:0x0A5DF6 + 192],
rom_bytearray[0x27F6B5:0x27F6B5 + 113]) = self.get_enemy_floors_sprites_and_movement_patterns()
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[name]
return L2ACItem(name, item_data.classification, items_start_id + item_data.code, self.player)
def get_filler_item_name(self) -> str:
return ["Potion", "Hi-Magic", "Miracle", "Hi-Potion", "Potion", "Ex-Potion", "Regain", "Ex-Magic", "Hi-Magic"][
(self.random.randrange(9) + self.random.randrange(9)) // 2]
# end of overridden AutoWorld.py methods
def get_capsule_cravings_table(self) -> bytes:
rom: bytes = get_base_rom_bytes()
if self.o.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_enemy_floors_sprites_and_movement_patterns(self) -> Tuple[bytes, bytes, bytes, bytes]:
rom: bytes = get_base_rom_bytes()
if self.o.enemy_floor_numbers == EnemyFloorNumbers.default \
and self.o.enemy_sprites == EnemySprites.default \
and self.o.enemy_movement_patterns == EnemyMovementPatterns.default:
return rom[0x08A1D4:0x08A1D4 + 128], rom[0x0A595C:0x0A595C + 200], \
rom[0x0A5DF6:0x0A5DF6 + 192], rom[0x27F6B5:0x27F6B5 + 113]
formations: bytes = rom[0x0A595C:0x0A595C + 200]
sprites: bytes = rom[0x0A5DF6:0x0A5DF6 + 192]
indices: bytes = rom[0x27F6B5:0x27F6B5 + 113]
pointers: List[bytes] = [rom[0x08A1D4 + 2 * index:0x08A1D4 + 2 * index + 2] for index in range(64)]
used_formations: List[int] = list(formations)
formation_set: Set[int] = set(used_formations)
used_sprites: List[int] = [sprite for formation, sprite in enumerate(sprites) if formation in formation_set]
sprite_set: Set[int] = set(used_sprites)
used_indices: List[int] = [index for sprite, index in enumerate(indices, 128) if sprite in sprite_set]
index_set: Set[int] = set(used_indices)
used_pointers: List[bytes] = [pointer for index, pointer in enumerate(pointers) if index in index_set]
d: int = 2 * 6
if self.o.enemy_floor_numbers == EnemyFloorNumbers.option_shuffle:
constrained_shuffle(used_formations, d, random=self.random)
elif self.o.enemy_floor_numbers == EnemyFloorNumbers.option_randomize:
used_formations = constrained_choices(used_formations, d, k=len(used_formations), random=self.random)
if self.o.enemy_sprites == EnemySprites.option_shuffle:
self.random.shuffle(used_sprites)
elif self.o.enemy_sprites == EnemySprites.option_randomize:
used_sprites = self.random.choices(tuple(dict.fromkeys(used_sprites)), k=len(used_sprites))
elif self.o.enemy_sprites == EnemySprites.option_singularity:
used_sprites = [self.random.choice(tuple(dict.fromkeys(used_sprites)))] * len(used_sprites)
elif self.o.enemy_sprites.sprite:
used_sprites = [self.o.enemy_sprites.sprite] * len(used_sprites)
if self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_pattern:
self.random.shuffle(used_pointers)
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_pattern:
used_pointers = self.random.choices(tuple(dict.fromkeys(used_pointers)), k=len(used_pointers))
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_sprite:
self.random.shuffle(used_indices)
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_sprite:
used_indices = self.random.choices(tuple(dict.fromkeys(used_indices)), k=len(used_indices))
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_singularity:
used_indices = [self.random.choice(tuple(dict.fromkeys(used_indices)))] * len(used_indices)
elif self.o.enemy_movement_patterns.sprite:
used_indices = [indices[self.o.enemy_movement_patterns.sprite - 128]] * len(used_indices)
sprite_iter: Iterator[int] = iter(used_sprites)
index_iter: Iterator[int] = iter(used_indices)
pointer_iter: Iterator[bytes] = iter(used_pointers)
formations = bytes(used_formations)
sprites = bytes(next(sprite_iter) if form in formation_set else sprite for form, sprite in enumerate(sprites))
indices = bytes(next(index_iter) if sprite in sprite_set else idx for sprite, idx in enumerate(indices, 128))
pointers = [next(pointer_iter) if idx in index_set else pointer for idx, pointer in enumerate(pointers)]
return b"".join(pointers), formations, sprites, indices
def get_enemy_stats(self) -> bytes:
rom: bytes = get_base_rom_bytes()
if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
return rom[0x0B05C0:0x0B05C0 + 18843]
number_of_enemies: int = 224
enemy_stats = bytearray(rom[0x0B05C0:0x0B05C0 + 18843])
for enemy_id in range(number_of_enemies):
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
return enemy_stats
def get_goal_text_bytes(self) -> bytes:
goal_text: List[str] = []
iris: str = f"{self.o.iris_treasures_required} Iris treasure{'s' if self.o.iris_treasures_required > 1 else ''}"
if self.o.goal == Goal.option_boss:
goal_text = ["You have to defeat", f"the boss on B{self.o.final_floor}."]
elif self.o.goal == Goal.option_iris_treasure_hunt:
goal_text = ["You have to find", f"{iris}."]
elif self.o.goal == Goal.option_boss_iris_treasure_hunt:
goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.o.final_floor}."]
elif self.o.goal == Goal.option_final_floor:
goal_text = [f"You need to get to B{self.o.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))
def get_shops(self) -> bytes:
rom: bytes = get_base_rom_bytes()
if not self.o.shop_interval:
return rom[0x0BEE9F:0x0BEE9F + 1948]
non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
number_of_spells: int = 35
number_of_items: int = 467
spells_offset: int = 0x0AFA5B
items_offset: int = 0x0B4F69
non_restorative_list: List[List[int]] = [list() for _ in range(99)]
restorative_list: List[List[int]] = [list() for _ in range(99)]
blue_list: List[List[int]] = [list() for _ in range(99)]
spell_list: List[List[int]] = [list() for _ in range(99)]
gear_list: List[List[int]] = [list() for _ in range(99)]
weapon_list: List[List[int]] = [list() for _ in range(99)]
custom_list: List[List[int]] = [list() for _ in range(99)]
for spell_id in range(number_of_spells):
pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
for f in range(value // 1000, 99):
spell_list[f].append(spell_id)
for item_id in range(number_of_items):
pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
buckets: List[List[List[int]]] = list()
if item_id in non_restorative_ids:
buckets.append(non_restorative_list)
if item_id in restorative_ids:
buckets.append(restorative_list)
if item_id in blue_ids:
buckets.append(blue_list)
if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
category: int = rom[items_offset + pointer + 7]
if category >= 0x02:
buckets.append(gear_list)
elif category == 0x01:
buckets.append(weapon_list)
if item_id in self.o.shop_inventory.custom:
buckets.append(custom_list)
value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
for bucket in buckets:
for f in range(value // 1000, 99):
bucket[f].append(item_id)
if not self.o.gear_variety_after_b9:
for f in range(99):
del gear_list[f][len(gear_list[f]) % 128:]
def create_shop(floor: int) -> Tuple[int, ...]:
if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
return create_spell_shop(floor)
else:
return create_item_shop(floor)
def create_spell_shop(floor: int) -> Tuple[int, ...]:
spells = self.random.sample(spell_list[floor], 3)
return 0x03, 0x20, 0x00, *spells, 0xFF
def create_item_shop(floor: int) -> Tuple[int, ...]:
population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
+ gear_list[floor] + weapon_list[floor] + custom_list[floor]
weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
[(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
(self.o.shop_inventory.restorative, restorative_list[floor]),
(self.o.shop_inventory.blue_chest, blue_list[floor]),
(self.o.shop_inventory.gear, gear_list[floor]),
(self.o.shop_inventory.weapon, weapon_list[floor])]),
(self.o.shop_inventory.custom[item] for item in custom_list[floor]))
items = weighted_sample(population, weights, 5, random=self.random)
return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
shops = [create_shop(floor)
for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
for _ in range(self.o.shop_interval)]
shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
assert len(shop_bytes) <= 1948, shop_bytes
return shop_bytes.ljust(1948, b"\x00")
@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))