lufia2ac: new features, bug fixes, and more (#1549)

### New features

- ***Architect mode***
  Usually the cave is randomized by the game, meaning that each attempt will produce a different dungeon. However, with this new feature the player can, between runs, opt into keeping the same cave. If activated, they will then encounter the same floor layouts, same enemy spawns, and same red chest contents as on their previous attempt.   

- ***Custom item pool***
  Previously, the multiworld item pool consisted entirely of random blue chest items because, well, the permanent checks are blue chests and that's what one would normally get from these. While blue chest items often greatly increase your odds against regular enemies, being able to defeat the Master can be contingent on having an appropriate equipment setup of red chest items (such as Dekar blade) or even enemy drops (such as Hidora rock), most of which cannot normally be obtained from blue chests.
  With the custom item pool option, players now have the freedom to place any cave item into the multiworld itempool for their world.

- ***Enemy floor number, enemy sprite, and enemy movement pattern randomization***
  Experienced players can deduce a lot of information about the opposition they will be facing, for example: Given the current floor number, one can know in advance which of the enemy types will have a chance to spawn on that floor. And when seeing a particular enemy sprite, one can already know which enemy types one might have to face in battle if one were to come in contact with it, and also how that enemy group will move through the dungeon.
  Three new randomization options are added for players who want to spice up their game: one can shuffle which enemy types appear on which floor, one can shuffle which sprite is used by which enemy type, and one can shuffle which movement pattern is used by which sprite.

- ***EXP modifier***
  Just a simple multiplier option to allow people to level up faster. (For technical reasons, the maximum amount of EXP that can be awarded for a single enemy is limited to 65535, but even with the maximum allowed modifier of 500% there are only 6 enemy types in the cave that can reach this cap.)


### Balance change

- ***proportionally adjust chest type distribution to accommodate increased blue chest chance***
  One of the main problems that became apparent in the current version has to do with the distribution of chest contents. The game considers 6 categories, namely: consumable (mostly non-restorative), consumable (restorative), blue chest item, spell, gear, and weapon. Since only blue chests count as multiworld locations, we want to have a mechanism to customize the blue chest chance.
  Given how the chest types are detetermined in game, a naive implementation of an increased blue chest chance causes only the consumable chance to be decreased in return. In practice, this has resulted in some players of worlds with a high blue chest chance struggling (more than usual) to keep their party alive because they were always low on comsumables that restore HP and MP.
  The new algorithm tries to avoid this one-sided effect by having an increase in blue chest chance resulting in a decrease of all other types, calculated in such a way that the relative distribution of the other 5 categories stays (approximately) the same.


### Bug fixes

- ***prevent using party member items if character is already in party***
  This should have been changed at the same time that 6eb00621e39c930f5746f5f3c69a6bc19cd0e84a was made, but oh well... 

- ***fix glitched sprite when opening a chest immediately after receiving an item***
  When opening a chest right after receiving a multiworld item (such that there were two item get animations in the exact same iteration of the game main loop), the item from the chest would display an incorrect sprite in the wrong place. Fixed by cleaning up some relevant memory addresses after getting the multiworld item.

- ***fix death link***
  There was a condition in `deathlink_kill_player` that looked kinda smart (it checked the time against `last_death_link`), but actually wasn't smart at all because `deathlink_kill_player` is executed as an async task and the main thread will update `last_death_link` after creating the task, meaning that whether or not the incoming death link would actually be passed to the game seems to have been up to a race condition. Fixed by simply removing that check.


### Other

- ***add Lufia II Ancient Cave (and SMW) to the network diagram***
  These two games were missing from the SNES sector.

- ***implement get_filler_item_name***
  Place a restorative consumable instead of a completely random item. (Now the only known problem with item links in lufia2ac is... that noone has ever tested item links. But this should be an improvement at least. Anyway, now #1172 can come ;)
  And btw., if you think that the implementation of random selection in this method looks weird, that's because it is indeed weird. (It tries to recreate the algorithm that the game itself uses when it generates a replacement item for a chest that would contain a spell that the party already knows.)

- ***store all options in a dataclass***
  This is basically like using #993 (but without actual support from core). It makes the lufia2ac world code much nicer to maintain because one doesn't have to change 5 different places anymore when adding or renaming an option.

- ***remove master_hp.scale***
  I have to admit: `scale` was a mistake. Never have I seen a single option value cause so many user misconceptions. Some people assume it affects enemies other than the Master; some people assume it affects stats other than HP; and many people will just assume it is a magic option that will somehow counterbalance whatever settings combination they are currently trying to shoot themselves in the foot with.
  On top of that, the `scale` mechanism probably doesn't provide a good user experience even when used for its intended purpose (since having reached floor XY in general doesn't mean you will have the power to deplete XY% of the Masters usual HP; especially given that, due to the randomness of loot, you are never guaranteed to be able to defeat the vanilla Master even when you have cleared 100% of the floors).
  The intended target audience of the `master_hp` option are people who want to fight the Master (and know how to fight it), but also want to lessen (to a degree of their choosing) the harsh dependence on the specific equipment setups that are usually required to win this fight even when having done all 99 floors. They can achieve this by setting the `master_hp` option to a numeric value appropriate for the level of challenge they are seeking. Therefore, nothing of value should be lost by removing the special `scale` value from the `master_hp` option, while at the same time a major source of user confusion will be eliminated.

- ***typing***
  This (combined with the switch to the option dataclass) greatly reduces the typing problems in the lufia2ac world. The remaining typing errors mostly fall into 4 categories:
  1. Lambdas with defaults (which seem to be incorrectly reported as an error due to a mypy bug)
  1. Classmethods that return instances (which could probably be improved using PEP 673 "Self" types, but that would require Python 3.11 as the minimum supported version)
  1. Everything that inherits from TextChoice (which is a typing mess in core)
  1. Everything related to asar.py (which does not have proper typing and lies outside of this project)

## How was this tested?

https://discord.com/channels/731205301247803413/1080852357442707476 and others
This commit is contained in:
el-u
2023-03-20 17:04:57 +01:00
committed by GitHub
parent ff9f563d4a
commit 6d13dc4944
15 changed files with 1032 additions and 442 deletions

View File

@@ -2,19 +2,21 @@ import base64
import itertools
import os
from enum import IntFlag
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
from random import Random
from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial
from Main import __version__
from Options import AssembleOptions
from Utils import __version__
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, DefaultParty, Goal, l2ac_option_definitions, \
MasterHp, PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
ExpModifier, Goal, L2ACOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
from .Utils import constrained_choices, constrained_shuffle
from .basepatch import apply_basepatch
CHESTS_PER_SPHERE: int = 5
@@ -42,7 +44,7 @@ class L2ACWorld(World):
game: ClassVar[str] = "Lufia II Ancient Cave"
web: ClassVar[WebWorld] = L2ACWeb()
option_definitions: ClassVar[Dict[str, AssembleOptions]] = l2ac_option_definitions
option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions)
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]]] = {
@@ -54,30 +56,8 @@ class L2ACWorld(World):
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]
default_capsule: Optional[int]
default_party: Optional[DefaultParty]
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]
rom_name: bytearray
o: L2ACOptions
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
@@ -95,37 +75,17 @@ class L2ACWorld(World):
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.default_capsule = self.multiworld.default_capsule[self.player].value
self.default_party = self.multiworld.default_party[self.player]
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.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions})
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.default_party.value = DefaultParty.default
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)
@@ -134,10 +94,10 @@ class L2ACWorld(World):
ancient_dungeon = Region("AncientDungeon", self.player, self.multiworld, "Ancient Dungeon")
ancient_dungeon.exits.append(Entrance(self.player, "FinalFloorEntrance", ancient_dungeon))
item_count: int = self.blue_chest_count
if self.shuffle_capsule_monsters:
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.shuffle_party_members:
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))
@@ -167,21 +127,23 @@ class L2ACWorld(World):
.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: List[str] = self.multiworld.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.blue_chest_count += len(self.item_name_groups["Capsule monsters"])
if self.shuffle_party_members:
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.blue_chest_count += len(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:
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):
for i in range(1, self.o.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))
@@ -192,27 +154,27 @@ class L2ACWorld(World):
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))
lambda state: state.can_reach(f"Blue chest {self.o.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))
lambda state: state.can_reach(f"Blue chest {self.o.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:
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
if self.o.shuffle_capsule_monsters:
add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player))
if self.shuffle_party_members:
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.goal == Goal.option_final_floor:
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.goal == Goal.option_iris_treasure_hunt:
elif self.o.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:
elif self.o.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:
elif self.o.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)
@@ -223,39 +185,45 @@ class L2ACWorld(World):
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.default_party.roster
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.party_starting_level.value.to_bytes(1, "little")
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.party_starting_level.xp.to_bytes(3, "little")
rom_bytearray[offset:offset + 3] = self.o.party_starting_level.xp.to_bytes(3, "little")
rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes()
rom_bytearray[0x056AA3:0x056AA3 + 24] = self.default_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[0x056AA3:0x056AA3 + 24] = self.o.default_party.event_script
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[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.default_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[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
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[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:
@@ -276,13 +244,19 @@ class L2ACWorld(World):
# end of ordered Main.py calls
def create_item(self, name: str) -> Item:
item_data: ItemData = l2ac_item_table.get(name)
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.multiworld.random.randrange(9) + self.multiworld.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.capsule_cravings_jp_style:
if self.o.capsule_cravings_jp_style:
number_of_items: int = 467
items_offset: int = 0x0B4F69
value_thresholds: List[int] = \
@@ -307,17 +281,92 @@ class L2ACWorld(World):
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]
slot_random: Random = self.multiworld.per_slot_randoms[self.player]
d: int = 2 * 6
if self.o.enemy_floor_numbers == EnemyFloorNumbers.option_shuffle:
constrained_shuffle(used_formations, d, random=slot_random)
elif self.o.enemy_floor_numbers == EnemyFloorNumbers.option_randomize:
used_formations = constrained_choices(used_formations, d, k=len(used_formations), random=slot_random)
if self.o.enemy_sprites == EnemySprites.option_shuffle:
slot_random.shuffle(used_sprites)
elif self.o.enemy_sprites == EnemySprites.option_randomize:
used_sprites = slot_random.choices(tuple(dict.fromkeys(used_sprites)), k=len(used_sprites))
elif self.o.enemy_sprites == EnemySprites.option_singularity:
used_sprites = [slot_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:
slot_random.shuffle(used_pointers)
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_pattern:
used_pointers = slot_random.choices(tuple(dict.fromkeys(used_pointers)), k=len(used_pointers))
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_sprite:
slot_random.shuffle(used_indices)
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_sprite:
used_indices = slot_random.choices(tuple(dict.fromkeys(used_indices)), k=len(used_indices))
elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_singularity:
used_indices = [slot_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 == ExpModifier.default:
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])
return enemy_stats
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:
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.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}."]
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))