Castlevania 64: Implement New Game (#2472)
This commit is contained in:
parent
32315776ac
commit
db02e9d2aa
|
@ -62,6 +62,7 @@ Currently, the following games are supported:
|
||||||
* Kirby's Dream Land 3
|
* Kirby's Dream Land 3
|
||||||
* Celeste 64
|
* Celeste 64
|
||||||
* Zork Grand Inquisitor
|
* Zork Grand Inquisitor
|
||||||
|
* Castlevania 64
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
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
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
# Bumper Stickers
|
# Bumper Stickers
|
||||||
/worlds/bumpstik/ @FelicitusNeko
|
/worlds/bumpstik/ @FelicitusNeko
|
||||||
|
|
||||||
|
# Castlevania 64
|
||||||
|
/worlds/cv64/ @LiquidCat64
|
||||||
|
|
||||||
# Celeste 64
|
# Celeste 64
|
||||||
/worlds/celeste64/ @PoryGone
|
/worlds/celeste64/ @PoryGone
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
import os
|
||||||
|
import typing
|
||||||
|
import settings
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from BaseClasses import Item, Region, MultiWorld, Tutorial, ItemClassification
|
||||||
|
from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
|
||||||
|
from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
|
||||||
|
from .entrances import verify_entrances, get_warp_entrances
|
||||||
|
from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle
|
||||||
|
from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \
|
||||||
|
shuffle_stages, generate_warps, get_region_names
|
||||||
|
from .regions import get_region_info
|
||||||
|
from .rules import CV64Rules
|
||||||
|
from .data import iname, rname, ename
|
||||||
|
from ..AutoWorld import WebWorld, World
|
||||||
|
from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \
|
||||||
|
randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \
|
||||||
|
get_countdown_numbers
|
||||||
|
from .rom import LocalRom, patch_rom, get_base_rom_path, CV64DeltaPatch
|
||||||
|
from .client import Castlevania64Client
|
||||||
|
|
||||||
|
|
||||||
|
class CV64Settings(settings.Group):
|
||||||
|
class RomFile(settings.UserFilePath):
|
||||||
|
"""File name of the CV64 US 1.0 rom"""
|
||||||
|
copy_to = "Castlevania (USA).z64"
|
||||||
|
description = "CV64 (US 1.0) ROM File"
|
||||||
|
md5s = [CV64DeltaPatch.hash]
|
||||||
|
|
||||||
|
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||||
|
|
||||||
|
|
||||||
|
class CV64Web(WebWorld):
|
||||||
|
theme = "stone"
|
||||||
|
|
||||||
|
tutorials = [Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to setting up the Archipleago Castlevania 64 randomizer on your computer and connecting it to a "
|
||||||
|
"multiworld.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["Liquid Cat"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
class CV64World(World):
|
||||||
|
"""
|
||||||
|
Castlevania for the Nintendo 64 is the first 3D game in the franchise. As either whip-wielding Belmont descendant
|
||||||
|
Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you make your
|
||||||
|
way to Dracula's chamber and stop his rule of terror!
|
||||||
|
"""
|
||||||
|
game = "Castlevania 64"
|
||||||
|
item_name_groups = {
|
||||||
|
"Bomb": {iname.magical_nitro, iname.mandragora},
|
||||||
|
"Ingredient": {iname.magical_nitro, iname.mandragora},
|
||||||
|
}
|
||||||
|
location_name_groups = {stage: set(get_locations_from_stage(stage)) for stage in vanilla_stage_order}
|
||||||
|
options_dataclass = CV64Options
|
||||||
|
options: CV64Options
|
||||||
|
settings: typing.ClassVar[CV64Settings]
|
||||||
|
topology_present = True
|
||||||
|
data_version = 1
|
||||||
|
|
||||||
|
item_name_to_id = get_item_names_to_ids()
|
||||||
|
location_name_to_id = get_location_names_to_ids()
|
||||||
|
|
||||||
|
active_stage_exits: typing.Dict[str, typing.Dict]
|
||||||
|
active_stage_list: typing.List[str]
|
||||||
|
active_warp_list: typing.List[str]
|
||||||
|
|
||||||
|
# Default values to possibly be updated in generate_early
|
||||||
|
reinhardt_stages: bool = True
|
||||||
|
carrie_stages: bool = True
|
||||||
|
branching_stages: bool = False
|
||||||
|
starting_stage: str = rname.forest_of_silence
|
||||||
|
total_s1s: int = 7
|
||||||
|
s1s_per_warp: int = 1
|
||||||
|
total_s2s: int = 0
|
||||||
|
required_s2s: int = 0
|
||||||
|
drac_condition: int = 0
|
||||||
|
|
||||||
|
auth: bytearray
|
||||||
|
|
||||||
|
web = CV64Web()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
||||||
|
rom_file = get_base_rom_path()
|
||||||
|
if not os.path.exists(rom_file):
|
||||||
|
raise FileNotFoundError(rom_file)
|
||||||
|
|
||||||
|
def generate_early(self) -> None:
|
||||||
|
# Generate the player's unique authentication
|
||||||
|
self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16))
|
||||||
|
|
||||||
|
self.total_s1s = self.options.total_special1s.value
|
||||||
|
self.s1s_per_warp = self.options.special1s_per_warp.value
|
||||||
|
self.drac_condition = self.options.draculas_condition.value
|
||||||
|
|
||||||
|
# If there are more S1s needed to unlock the whole warp menu than there are S1s in total, drop S1s per warp to
|
||||||
|
# something manageable.
|
||||||
|
if self.s1s_per_warp * 7 > self.total_s1s:
|
||||||
|
self.s1s_per_warp = self.total_s1s // 7
|
||||||
|
logging.warning(f"[{self.multiworld.player_name[self.player]}] Too many required Special1s "
|
||||||
|
f"({self.options.special1s_per_warp.value * 7}) for Special1s Per Warp setting: "
|
||||||
|
f"{self.options.special1s_per_warp.value} with Total Special1s setting: "
|
||||||
|
f"{self.options.total_special1s.value}. Lowering Special1s Per Warp to: "
|
||||||
|
f"{self.s1s_per_warp}")
|
||||||
|
self.options.special1s_per_warp.value = self.s1s_per_warp
|
||||||
|
|
||||||
|
# Set the total and required Special2s to 1 if the drac condition is the Crystal, to the specified YAML numbers
|
||||||
|
# if it's Specials, or to 0 if it's None or Bosses. The boss totals will be figured out later.
|
||||||
|
if self.drac_condition == DraculasCondition.option_crystal:
|
||||||
|
self.total_s2s = 1
|
||||||
|
self.required_s2s = 1
|
||||||
|
elif self.drac_condition == DraculasCondition.option_specials:
|
||||||
|
self.total_s2s = self.options.total_special2s.value
|
||||||
|
self.required_s2s = int(self.options.percent_special2s_required.value / 100 * self.total_s2s)
|
||||||
|
|
||||||
|
# Enable/disable character stages and branching paths accordingly
|
||||||
|
if self.options.character_stages == CharacterStages.option_reinhardt_only:
|
||||||
|
self.carrie_stages = False
|
||||||
|
elif self.options.character_stages == CharacterStages.option_carrie_only:
|
||||||
|
self.reinhardt_stages = False
|
||||||
|
elif self.options.character_stages == CharacterStages.option_both:
|
||||||
|
self.branching_stages = True
|
||||||
|
|
||||||
|
self.active_stage_exits = get_normal_stage_exits(self)
|
||||||
|
|
||||||
|
stage_1_blacklist = []
|
||||||
|
|
||||||
|
# Prevent Clock Tower from being Stage 1 if more than 4 S1s are needed to warp out of it.
|
||||||
|
if self.s1s_per_warp > 4 and not self.options.multi_hit_breakables:
|
||||||
|
stage_1_blacklist.append(rname.clock_tower)
|
||||||
|
|
||||||
|
# Shuffle the stages if the option is on.
|
||||||
|
if self.options.stage_shuffle:
|
||||||
|
self.active_stage_exits, self.starting_stage, self.active_stage_list = \
|
||||||
|
shuffle_stages(self, stage_1_blacklist)
|
||||||
|
else:
|
||||||
|
self.active_stage_list = [stage for stage in vanilla_stage_order if stage in self.active_stage_exits]
|
||||||
|
|
||||||
|
# Create a list of warps from the active stage list. They are in a random order by default and will never
|
||||||
|
# include the starting stage.
|
||||||
|
self.active_warp_list = generate_warps(self)
|
||||||
|
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
# Add the Menu Region.
|
||||||
|
created_regions = [Region("Menu", self.player, self.multiworld)]
|
||||||
|
|
||||||
|
# Add every stage Region by checking to see if that stage is active.
|
||||||
|
created_regions.extend([Region(name, self.player, self.multiworld)
|
||||||
|
for name in get_region_names(self.active_stage_exits)])
|
||||||
|
|
||||||
|
# Add the Renon's shop Region if shopsanity is on.
|
||||||
|
if self.options.shopsanity:
|
||||||
|
created_regions.append(Region(rname.renon, self.player, self.multiworld))
|
||||||
|
|
||||||
|
# Add the Dracula's chamber (the end) Region.
|
||||||
|
created_regions.append(Region(rname.ck_drac_chamber, self.player, self.multiworld))
|
||||||
|
|
||||||
|
# Set up the Regions correctly.
|
||||||
|
self.multiworld.regions.extend(created_regions)
|
||||||
|
|
||||||
|
# Add the warp Entrances to the Menu Region (the one always at the start of the Region list).
|
||||||
|
created_regions[0].add_exits(get_warp_entrances(self.active_warp_list))
|
||||||
|
|
||||||
|
for reg in created_regions:
|
||||||
|
|
||||||
|
# Add the Entrances to all the Regions.
|
||||||
|
ent_names = get_region_info(reg.name, "entrances")
|
||||||
|
if ent_names is not None:
|
||||||
|
reg.add_exits(verify_entrances(self.options, ent_names, self.active_stage_exits))
|
||||||
|
|
||||||
|
# Add the Locations to all the Regions.
|
||||||
|
loc_names = get_region_info(reg.name, "locations")
|
||||||
|
if loc_names is None:
|
||||||
|
continue
|
||||||
|
verified_locs, events = verify_locations(self.options, loc_names)
|
||||||
|
reg.add_locations(verified_locs, CV64Location)
|
||||||
|
|
||||||
|
# Place event Items on all of their associated Locations.
|
||||||
|
for event_loc, event_item in events.items():
|
||||||
|
self.get_location(event_loc).place_locked_item(self.create_item(event_item, "progression"))
|
||||||
|
# If we're looking at a boss kill trophy, increment the total S2s and, if we're not already at the
|
||||||
|
# set number of required bosses, the total required number. This way, we can prevent gen failures
|
||||||
|
# should the player set more bosses required than there are total.
|
||||||
|
if event_item == iname.trophy:
|
||||||
|
self.total_s2s += 1
|
||||||
|
if self.required_s2s < self.options.bosses_required.value:
|
||||||
|
self.required_s2s += 1
|
||||||
|
|
||||||
|
# If Dracula's Condition is Bosses and there are less calculated required S2s than the value specified by the
|
||||||
|
# player (meaning there weren't enough bosses to reach the player's setting), throw a warning and lower the
|
||||||
|
# option value.
|
||||||
|
if self.options.draculas_condition == DraculasCondition.option_bosses and self.required_s2s < \
|
||||||
|
self.options.bosses_required.value:
|
||||||
|
logging.warning(f"[{self.multiworld.player_name[self.player]}] Not enough bosses for Bosses Required "
|
||||||
|
f"setting: {self.options.bosses_required.value}. Lowering to: {self.required_s2s}")
|
||||||
|
self.options.bosses_required.value = self.required_s2s
|
||||||
|
|
||||||
|
def create_item(self, name: str, force_classification: typing.Optional[str] = None) -> Item:
|
||||||
|
if force_classification is not None:
|
||||||
|
classification = getattr(ItemClassification, force_classification)
|
||||||
|
else:
|
||||||
|
classification = getattr(ItemClassification, get_item_info(name, "default classification"))
|
||||||
|
|
||||||
|
code = get_item_info(name, "code")
|
||||||
|
if code is not None:
|
||||||
|
code += base_id
|
||||||
|
|
||||||
|
created_item = CV64Item(name, classification, code, self.player)
|
||||||
|
|
||||||
|
return created_item
|
||||||
|
|
||||||
|
def create_items(self) -> None:
|
||||||
|
item_counts = get_item_counts(self)
|
||||||
|
|
||||||
|
# Set up the items correctly
|
||||||
|
self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
|
||||||
|
in item_counts[classification] for _ in range(item_counts[classification][item])]
|
||||||
|
|
||||||
|
def set_rules(self) -> None:
|
||||||
|
# Set all the Entrance rules properly.
|
||||||
|
CV64Rules(self).set_cv64_rules()
|
||||||
|
|
||||||
|
def pre_fill(self) -> None:
|
||||||
|
# If we need more Special1s to warp out of Sphere 1 than there are locations available, then AP's fill
|
||||||
|
# algorithm may try placing the Special1s anyway despite placing the stage's single key always being an option.
|
||||||
|
# To get around this problem in the fill algorithm, the keys will be forced early in these situations to ensure
|
||||||
|
# the algorithm will pick them over the Special1s.
|
||||||
|
if self.starting_stage == rname.tower_of_science:
|
||||||
|
if self.s1s_per_warp > 3:
|
||||||
|
self.multiworld.local_early_items[self.player][iname.science_key2] = 1
|
||||||
|
elif self.starting_stage == rname.clock_tower:
|
||||||
|
if (self.s1s_per_warp > 2 and not self.options.multi_hit_breakables) or \
|
||||||
|
(self.s1s_per_warp > 8 and self.options.multi_hit_breakables):
|
||||||
|
self.multiworld.local_early_items[self.player][iname.clocktower_key1] = 1
|
||||||
|
elif self.starting_stage == rname.castle_wall:
|
||||||
|
if self.s1s_per_warp > 5 and not self.options.hard_logic and \
|
||||||
|
not self.options.multi_hit_breakables:
|
||||||
|
self.multiworld.local_early_items[self.player][iname.left_tower_key] = 1
|
||||||
|
|
||||||
|
def generate_output(self, output_directory: str) -> None:
|
||||||
|
active_locations = self.multiworld.get_locations(self.player)
|
||||||
|
|
||||||
|
# Location data and shop names, descriptions, and colors
|
||||||
|
offset_data, shop_name_list, shop_colors_list, shop_desc_list = \
|
||||||
|
get_location_data(self, active_locations)
|
||||||
|
# Shop prices
|
||||||
|
if self.options.shop_prices:
|
||||||
|
offset_data.update(randomize_shop_prices(self))
|
||||||
|
# Map lighting
|
||||||
|
if self.options.map_lighting:
|
||||||
|
offset_data.update(randomize_lighting(self))
|
||||||
|
# Sub-weapons
|
||||||
|
if self.options.sub_weapon_shuffle == SubWeaponShuffle.option_own_pool:
|
||||||
|
offset_data.update(shuffle_sub_weapons(self))
|
||||||
|
elif self.options.sub_weapon_shuffle == SubWeaponShuffle.option_anywhere:
|
||||||
|
offset_data.update(rom_sub_weapon_flags)
|
||||||
|
# Empty breakables
|
||||||
|
if self.options.empty_breakables:
|
||||||
|
offset_data.update(rom_empty_breakables_flags)
|
||||||
|
# Music
|
||||||
|
if self.options.background_music:
|
||||||
|
offset_data.update(randomize_music(self))
|
||||||
|
# Loading zones
|
||||||
|
offset_data.update(get_loading_zone_bytes(self.options, self.starting_stage, self.active_stage_exits))
|
||||||
|
# Countdown
|
||||||
|
if self.options.countdown:
|
||||||
|
offset_data.update(get_countdown_numbers(self.options, active_locations))
|
||||||
|
# Start Inventory
|
||||||
|
offset_data.update(get_start_inventory_data(self.player, self.options,
|
||||||
|
self.multiworld.precollected_items[self.player]))
|
||||||
|
|
||||||
|
cv64_rom = LocalRom(get_base_rom_path())
|
||||||
|
|
||||||
|
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.z64")
|
||||||
|
|
||||||
|
patch_rom(self, cv64_rom, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations)
|
||||||
|
|
||||||
|
cv64_rom.write_to_file(rompath)
|
||||||
|
|
||||||
|
patch = CV64DeltaPatch(os.path.splitext(rompath)[0] + CV64DeltaPatch.patch_file_ending, player=self.player,
|
||||||
|
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
|
||||||
|
patch.write()
|
||||||
|
os.unlink(rompath)
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
return self.random.choice(filler_item_names)
|
||||||
|
|
||||||
|
def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
||||||
|
# Attach each location's stage's position to its hint information if Stage Shuffle is on.
|
||||||
|
if not self.options.stage_shuffle:
|
||||||
|
return
|
||||||
|
|
||||||
|
stage_pos_data = {}
|
||||||
|
for loc in list(self.multiworld.get_locations(self.player)):
|
||||||
|
stage = get_region_info(loc.parent_region.name, "stage")
|
||||||
|
if stage is not None and loc.address is not None:
|
||||||
|
num = str(self.active_stage_exits[stage]["position"]).zfill(2)
|
||||||
|
path = self.active_stage_exits[stage]["path"]
|
||||||
|
stage_pos_data[loc.address] = f"Stage {num}"
|
||||||
|
if path != " ":
|
||||||
|
stage_pos_data[loc.address] += path
|
||||||
|
hint_data[self.player] = stage_pos_data
|
||||||
|
|
||||||
|
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
|
||||||
|
# Put the player's unique authentication in connect_names.
|
||||||
|
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
|
||||||
|
multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||||
|
|
||||||
|
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||||
|
# Write the stage order to the spoiler log
|
||||||
|
spoiler_handle.write(f"\nCastlevania 64 stage & warp orders for {self.multiworld.player_name[self.player]}:\n")
|
||||||
|
for stage in self.active_stage_list:
|
||||||
|
num = str(self.active_stage_exits[stage]["position"]).zfill(2)
|
||||||
|
path = self.active_stage_exits[stage]["path"]
|
||||||
|
spoiler_handle.writelines(f"Stage {num}{path}:\t{stage}\n")
|
||||||
|
|
||||||
|
# Write the warp order to the spoiler log
|
||||||
|
spoiler_handle.writelines(f"\nStart :\t{self.active_stage_list[0]}\n")
|
||||||
|
for i in range(1, len(self.active_warp_list)):
|
||||||
|
spoiler_handle.writelines(f"Warp {i}:\t{self.active_warp_list[i]}\n")
|
|
@ -0,0 +1,666 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from BaseClasses import ItemClassification, Location, Item
|
||||||
|
from .data import iname, rname
|
||||||
|
from .options import CV64Options, BackgroundMusic, Countdown, IceTrapAppearance, InvisibleItems, CharacterStages
|
||||||
|
from .stages import vanilla_stage_order, get_stage_info
|
||||||
|
from .locations import get_location_info, base_id
|
||||||
|
from .regions import get_region_info
|
||||||
|
from .items import get_item_info, item_info
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict, List, Tuple, Union, Iterable
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import CV64World
|
||||||
|
|
||||||
|
rom_sub_weapon_offsets = {
|
||||||
|
0x10C6EB: (0x10, rname.forest_of_silence), # Forest
|
||||||
|
0x10C6F3: (0x0F, rname.forest_of_silence),
|
||||||
|
0x10C6FB: (0x0E, rname.forest_of_silence),
|
||||||
|
0x10C703: (0x0D, rname.forest_of_silence),
|
||||||
|
|
||||||
|
0x10C81F: (0x0F, rname.castle_wall), # Castle Wall
|
||||||
|
0x10C827: (0x10, rname.castle_wall),
|
||||||
|
0x10C82F: (0x0E, rname.castle_wall),
|
||||||
|
0x7F9A0F: (0x0D, rname.castle_wall),
|
||||||
|
|
||||||
|
0x83A5D9: (0x0E, rname.villa), # Villa
|
||||||
|
0x83A5E5: (0x0D, rname.villa),
|
||||||
|
0x83A5F1: (0x0F, rname.villa),
|
||||||
|
0xBFC903: (0x10, rname.villa),
|
||||||
|
0x10C987: (0x10, rname.villa),
|
||||||
|
0x10C98F: (0x0D, rname.villa),
|
||||||
|
0x10C997: (0x0F, rname.villa),
|
||||||
|
0x10CF73: (0x10, rname.villa),
|
||||||
|
|
||||||
|
0x10CA57: (0x0D, rname.tunnel), # Tunnel
|
||||||
|
0x10CA5F: (0x0E, rname.tunnel),
|
||||||
|
0x10CA67: (0x10, rname.tunnel),
|
||||||
|
0x10CA6F: (0x0D, rname.tunnel),
|
||||||
|
0x10CA77: (0x0F, rname.tunnel),
|
||||||
|
0x10CA7F: (0x0E, rname.tunnel),
|
||||||
|
|
||||||
|
0x10CBC7: (0x0E, rname.castle_center), # Castle Center
|
||||||
|
0x10CC0F: (0x0D, rname.castle_center),
|
||||||
|
0x10CC5B: (0x0F, rname.castle_center),
|
||||||
|
|
||||||
|
0x10CD3F: (0x0E, rname.tower_of_execution), # Character towers
|
||||||
|
0x10CD65: (0x0D, rname.tower_of_execution),
|
||||||
|
0x10CE2B: (0x0E, rname.tower_of_science),
|
||||||
|
0x10CE83: (0x10, rname.duel_tower),
|
||||||
|
|
||||||
|
0x10CF8B: (0x0F, rname.room_of_clocks), # Room of Clocks
|
||||||
|
0x10CF93: (0x0D, rname.room_of_clocks),
|
||||||
|
|
||||||
|
0x99BC5A: (0x0D, rname.clock_tower), # Clock Tower
|
||||||
|
0x10CECB: (0x10, rname.clock_tower),
|
||||||
|
0x10CED3: (0x0F, rname.clock_tower),
|
||||||
|
0x10CEDB: (0x0E, rname.clock_tower),
|
||||||
|
0x10CEE3: (0x0D, rname.clock_tower),
|
||||||
|
}
|
||||||
|
|
||||||
|
rom_sub_weapon_flags = {
|
||||||
|
0x10C6EC: 0x0200FF04, # Forest of Silence
|
||||||
|
0x10C6FC: 0x0400FF04,
|
||||||
|
0x10C6F4: 0x0800FF04,
|
||||||
|
0x10C704: 0x4000FF04,
|
||||||
|
|
||||||
|
0x10C831: 0x08, # Castle Wall
|
||||||
|
0x10C829: 0x10,
|
||||||
|
0x10C821: 0x20,
|
||||||
|
0xBFCA97: 0x04,
|
||||||
|
|
||||||
|
# Villa
|
||||||
|
0xBFC926: 0xFF04,
|
||||||
|
0xBFC93A: 0x80,
|
||||||
|
0xBFC93F: 0x01,
|
||||||
|
0xBFC943: 0x40,
|
||||||
|
0xBFC947: 0x80,
|
||||||
|
0x10C989: 0x10,
|
||||||
|
0x10C991: 0x20,
|
||||||
|
0x10C999: 0x40,
|
||||||
|
0x10CF77: 0x80,
|
||||||
|
|
||||||
|
0x10CA58: 0x4000FF0E, # Tunnel
|
||||||
|
0x10CA6B: 0x80,
|
||||||
|
0x10CA60: 0x1000FF05,
|
||||||
|
0x10CA70: 0x2000FF05,
|
||||||
|
0x10CA78: 0x4000FF05,
|
||||||
|
0x10CA80: 0x8000FF05,
|
||||||
|
|
||||||
|
0x10CBCA: 0x02, # Castle Center
|
||||||
|
0x10CC10: 0x80,
|
||||||
|
0x10CC5C: 0x40,
|
||||||
|
|
||||||
|
0x10CE86: 0x01, # Duel Tower
|
||||||
|
0x10CD43: 0x02, # Tower of Execution
|
||||||
|
0x10CE2E: 0x20, # Tower of Science
|
||||||
|
|
||||||
|
0x10CF8E: 0x04, # Room of Clocks
|
||||||
|
0x10CF96: 0x08,
|
||||||
|
|
||||||
|
0x10CECE: 0x08, # Clock Tower
|
||||||
|
0x10CED6: 0x10,
|
||||||
|
0x10CEE6: 0x20,
|
||||||
|
0x10CEDE: 0x80,
|
||||||
|
}
|
||||||
|
|
||||||
|
rom_empty_breakables_flags = {
|
||||||
|
0x10C74D: 0x40FF05, # Forest of Silence
|
||||||
|
0x10C765: 0x20FF0E,
|
||||||
|
0x10C774: 0x0800FF0E,
|
||||||
|
0x10C755: 0x80FF05,
|
||||||
|
0x10C784: 0x0100FF0E,
|
||||||
|
0x10C73C: 0x0200FF0E,
|
||||||
|
|
||||||
|
0x10C8D0: 0x0400FF0E, # Villa foyer
|
||||||
|
|
||||||
|
0x10CF9F: 0x08, # Room of Clocks flags
|
||||||
|
0x10CFA7: 0x01,
|
||||||
|
0xBFCB6F: 0x04, # Room of Clocks candle property IDs
|
||||||
|
0xBFCB73: 0x05,
|
||||||
|
}
|
||||||
|
|
||||||
|
rom_axe_cross_lower_values = {
|
||||||
|
0x6: [0x7C7F97, 0x07], # Forest
|
||||||
|
0x8: [0x7C7FA6, 0xF9],
|
||||||
|
|
||||||
|
0x30: [0x83A60A, 0x71], # Villa hallway
|
||||||
|
0x27: [0x83A617, 0x26],
|
||||||
|
0x2C: [0x83A624, 0x6E],
|
||||||
|
|
||||||
|
0x16C: [0x850FE6, 0x07], # Villa maze
|
||||||
|
|
||||||
|
0x10A: [0x8C44D3, 0x08], # CC factory floor
|
||||||
|
0x109: [0x8C44E1, 0x08],
|
||||||
|
|
||||||
|
0x74: [0x8DF77C, 0x07], # CC invention area
|
||||||
|
0x60: [0x90FD37, 0x43],
|
||||||
|
0x55: [0xBFCC2B, 0x43],
|
||||||
|
0x65: [0x90FBA1, 0x51],
|
||||||
|
0x64: [0x90FBAD, 0x50],
|
||||||
|
0x61: [0x90FE56, 0x43]
|
||||||
|
}
|
||||||
|
|
||||||
|
rom_looping_music_fade_ins = {
|
||||||
|
0x10: None,
|
||||||
|
0x11: None,
|
||||||
|
0x12: None,
|
||||||
|
0x13: None,
|
||||||
|
0x14: None,
|
||||||
|
0x15: None,
|
||||||
|
0x16: 0x17,
|
||||||
|
0x18: 0x19,
|
||||||
|
0x1A: 0x1B,
|
||||||
|
0x21: 0x75,
|
||||||
|
0x27: None,
|
||||||
|
0x2E: 0x23,
|
||||||
|
0x39: None,
|
||||||
|
0x45: 0x63,
|
||||||
|
0x56: None,
|
||||||
|
0x57: 0x58,
|
||||||
|
0x59: None,
|
||||||
|
0x5A: None,
|
||||||
|
0x5B: 0x5C,
|
||||||
|
0x5D: None,
|
||||||
|
0x5E: None,
|
||||||
|
0x5F: None,
|
||||||
|
0x60: 0x61,
|
||||||
|
0x62: None,
|
||||||
|
0x64: None,
|
||||||
|
0x65: None,
|
||||||
|
0x66: None,
|
||||||
|
0x68: None,
|
||||||
|
0x69: None,
|
||||||
|
0x6D: 0x78,
|
||||||
|
0x6E: None,
|
||||||
|
0x6F: None,
|
||||||
|
0x73: None,
|
||||||
|
0x74: None,
|
||||||
|
0x77: None,
|
||||||
|
0x79: None
|
||||||
|
}
|
||||||
|
|
||||||
|
music_sfx_ids = [0x1C, 0x4B, 0x4C, 0x4D, 0x4E, 0x55, 0x6C, 0x76]
|
||||||
|
|
||||||
|
renon_item_dialogue = {
|
||||||
|
0x02: "More Sub-weapon uses!\n"
|
||||||
|
"Just what you need!",
|
||||||
|
0x03: "Galamoth told me it's\n"
|
||||||
|
"a heart in other times.",
|
||||||
|
0x04: "Who needs Warp Rooms\n"
|
||||||
|
"when you have these?",
|
||||||
|
0x05: "I was told to safeguard\n"
|
||||||
|
"this, but I dunno why.",
|
||||||
|
0x06: "Fresh off a Behemoth!\n"
|
||||||
|
"Those cows are weird.",
|
||||||
|
0x07: "Preserved with special\n"
|
||||||
|
" wall-based methods.",
|
||||||
|
0x08: "Don't tell Geneva\n"
|
||||||
|
"about this...",
|
||||||
|
0x09: "If this existed in 1094,\n"
|
||||||
|
"that whip wouldn't...",
|
||||||
|
0x0A: "For when some lizard\n"
|
||||||
|
"brain spits on your ego.",
|
||||||
|
0x0C: "It'd be a shame if you\n"
|
||||||
|
"lost it immediately...",
|
||||||
|
0x10C: "No consequences should\n"
|
||||||
|
"you perish with this!",
|
||||||
|
0x0D: "Arthur was far better\n"
|
||||||
|
"with it than you!",
|
||||||
|
0x0E: "Night Creatures handle\n"
|
||||||
|
"with care!",
|
||||||
|
0x0F: "Some may call it a\n"
|
||||||
|
"\"Banshee Boomerang.\"",
|
||||||
|
0x10: "No weapon triangle\n"
|
||||||
|
"advantages with this.",
|
||||||
|
0x12: "It looks sus? Trust me,"
|
||||||
|
"my wares are genuine.",
|
||||||
|
0x15: "This non-volatile kind\n"
|
||||||
|
"is safe to handle.",
|
||||||
|
0x16: "If you can soul-wield,\n"
|
||||||
|
"they have a good one!",
|
||||||
|
0x17: "Calls the morning sun\n"
|
||||||
|
"to vanquish the night.",
|
||||||
|
0x18: "1 on-demand horrible\n"
|
||||||
|
"night. Devils love it!",
|
||||||
|
0x1A: "Want to study here?\n"
|
||||||
|
"It will cost you.",
|
||||||
|
0x1B: "\"Let them eat cake!\"\n"
|
||||||
|
"Said no princess ever.",
|
||||||
|
0x1C: "Why do I suspect this\n"
|
||||||
|
"was a toilet room?",
|
||||||
|
0x1D: "When you see Coller,\n"
|
||||||
|
"tell him I said hi!",
|
||||||
|
0x1E: "Atomic number is 29\n"
|
||||||
|
"and weight is 63.546.",
|
||||||
|
0x1F: "One torture per pay!\n"
|
||||||
|
"Who will it be?",
|
||||||
|
0x20: "Being here feels like\n"
|
||||||
|
"time is slowing down.",
|
||||||
|
0x21: "Only one thing beind\n"
|
||||||
|
"this. Do you dare?",
|
||||||
|
0x22: "The key 2 Science!\n"
|
||||||
|
"Both halves of it!",
|
||||||
|
0x23: "This warehouse can\n"
|
||||||
|
"be yours for a fee.",
|
||||||
|
0x24: "Long road ahead if you\n"
|
||||||
|
"don't have the others.",
|
||||||
|
0x25: "Will you get the curse\n"
|
||||||
|
"of eternal burning?",
|
||||||
|
0x26: "What's beyond time?\n"
|
||||||
|
"Find out your",
|
||||||
|
0x27: "Want to take out a\n"
|
||||||
|
"loan? By all means!",
|
||||||
|
0x28: "The bag is green,\n"
|
||||||
|
"so it must be lucky!",
|
||||||
|
0x29: "(Does this fool realize?)\n"
|
||||||
|
"Oh, sorry.",
|
||||||
|
"prog": "They will absolutely\n"
|
||||||
|
"need it in time!",
|
||||||
|
"useful": "Now, this would be\n"
|
||||||
|
"useful to send...",
|
||||||
|
"common": "Every last little bit\n"
|
||||||
|
"helps, right?",
|
||||||
|
"trap": "I'll teach this fool\n"
|
||||||
|
" a lesson for a price!",
|
||||||
|
"dlc coin": "1 coin out of... wha!?\n"
|
||||||
|
"You imp, why I oughta!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_lighting(world: "CV64World") -> Dict[int, int]:
|
||||||
|
"""Generates randomized data for the map lighting table."""
|
||||||
|
randomized_lighting = {}
|
||||||
|
for entry in range(67):
|
||||||
|
for sub_entry in range(19):
|
||||||
|
if sub_entry not in [3, 7, 11, 15] and entry != 4:
|
||||||
|
# The fourth entry in the lighting table affects the lighting on some item pickups; skip it
|
||||||
|
randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = \
|
||||||
|
world.random.randint(0, 255)
|
||||||
|
return randomized_lighting
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]:
|
||||||
|
"""Shuffles the sub-weapons amongst themselves."""
|
||||||
|
sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if
|
||||||
|
rom_sub_weapon_offsets[offset][1] in world.active_stage_exits}
|
||||||
|
|
||||||
|
# Remove the one 3HB sub-weapon in Tower of Execution if 3HBs are not shuffled.
|
||||||
|
if not world.options.multi_hit_breakables and 0x10CD65 in sub_weapon_dict:
|
||||||
|
del (sub_weapon_dict[0x10CD65])
|
||||||
|
|
||||||
|
sub_bytes = list(sub_weapon_dict.values())
|
||||||
|
world.random.shuffle(sub_bytes)
|
||||||
|
return dict(zip(sub_weapon_dict, sub_bytes))
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_music(world: "CV64World") -> Dict[int, int]:
|
||||||
|
"""Generates randomized or disabled data for all the music in the game."""
|
||||||
|
music_array = bytearray(0x7A)
|
||||||
|
for number in music_sfx_ids:
|
||||||
|
music_array[number] = number
|
||||||
|
if world.options.background_music == BackgroundMusic.option_randomized:
|
||||||
|
looping_songs = []
|
||||||
|
non_looping_songs = []
|
||||||
|
fade_in_songs = {}
|
||||||
|
# Create shuffle-able lists of all the looping, non-looping, and fade-in track IDs
|
||||||
|
for i in range(0x10, len(music_array)):
|
||||||
|
if i not in rom_looping_music_fade_ins.keys() and i not in rom_looping_music_fade_ins.values() and \
|
||||||
|
i != 0x72: # Credits song is blacklisted
|
||||||
|
non_looping_songs.append(i)
|
||||||
|
elif i in rom_looping_music_fade_ins.keys():
|
||||||
|
looping_songs.append(i)
|
||||||
|
elif i in rom_looping_music_fade_ins.values():
|
||||||
|
fade_in_songs[i] = i
|
||||||
|
# Shuffle the looping songs
|
||||||
|
rando_looping_songs = looping_songs.copy()
|
||||||
|
world.random.shuffle(rando_looping_songs)
|
||||||
|
looping_songs = dict(zip(looping_songs, rando_looping_songs))
|
||||||
|
# Shuffle the non-looping songs
|
||||||
|
rando_non_looping_songs = non_looping_songs.copy()
|
||||||
|
world.random.shuffle(rando_non_looping_songs)
|
||||||
|
non_looping_songs = dict(zip(non_looping_songs, rando_non_looping_songs))
|
||||||
|
non_looping_songs[0x72] = 0x72
|
||||||
|
# Figure out the new fade-in songs if applicable
|
||||||
|
for vanilla_song in looping_songs:
|
||||||
|
if rom_looping_music_fade_ins[vanilla_song]:
|
||||||
|
if rom_looping_music_fade_ins[looping_songs[vanilla_song]]:
|
||||||
|
fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = rom_looping_music_fade_ins[
|
||||||
|
looping_songs[vanilla_song]]
|
||||||
|
else:
|
||||||
|
fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = looping_songs[vanilla_song]
|
||||||
|
# Build the new music array
|
||||||
|
for i in range(0x10, len(music_array)):
|
||||||
|
if i in looping_songs.keys():
|
||||||
|
music_array[i] = looping_songs[i]
|
||||||
|
elif i in non_looping_songs.keys():
|
||||||
|
music_array[i] = non_looping_songs[i]
|
||||||
|
else:
|
||||||
|
music_array[i] = fade_in_songs[i]
|
||||||
|
del (music_array[0x00: 0x10])
|
||||||
|
|
||||||
|
# Convert the music array into a data dict
|
||||||
|
music_offsets = {}
|
||||||
|
for i in range(len(music_array)):
|
||||||
|
music_offsets[0xBFCD30 + i] = music_array[i]
|
||||||
|
|
||||||
|
return music_offsets
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_shop_prices(world: "CV64World") -> Dict[int, int]:
|
||||||
|
"""Randomize the shop prices based on the minimum and maximum values chosen.
|
||||||
|
The minimum price will adjust if it's higher than the max."""
|
||||||
|
min_price = world.options.minimum_gold_price.value
|
||||||
|
max_price = world.options.maximum_gold_price.value
|
||||||
|
|
||||||
|
if min_price > max_price:
|
||||||
|
min_price = world.random.randint(0, max_price)
|
||||||
|
logging.warning(f"[{world.multiworld.player_name[world.player]}] The Minimum Gold Price "
|
||||||
|
f"({world.options.minimum_gold_price.value * 100}) is higher than the "
|
||||||
|
f"Maximum Gold Price ({max_price * 100}). Lowering the minimum to: {min_price * 100}")
|
||||||
|
world.options.minimum_gold_price.value = min_price
|
||||||
|
|
||||||
|
shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)]
|
||||||
|
|
||||||
|
# Convert the price list into a data dict. Which offset it starts from depends on how many bytes it takes up.
|
||||||
|
price_dict = {}
|
||||||
|
for i in range(len(shop_price_list)):
|
||||||
|
if shop_price_list[i] <= 0xFF:
|
||||||
|
price_dict[0x103D6E + (i*12)] = 0
|
||||||
|
price_dict[0x103D6F + (i*12)] = shop_price_list[i]
|
||||||
|
elif shop_price_list[i] <= 0xFFFF:
|
||||||
|
price_dict[0x103D6E + (i*12)] = shop_price_list[i]
|
||||||
|
else:
|
||||||
|
price_dict[0x103D6D + (i*12)] = shop_price_list[i]
|
||||||
|
|
||||||
|
return price_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, int]:
|
||||||
|
"""Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
|
||||||
|
increase a number.
|
||||||
|
|
||||||
|
First, check the location's info to see if it has a countdown number override.
|
||||||
|
If not, then figure it out based on the parent region's stage's position in the vanilla stage order.
|
||||||
|
If the parent region is not part of any stage (as is the case for Renon's shop), skip the location entirely."""
|
||||||
|
countdown_list = [0 for _ in range(15)]
|
||||||
|
for loc in active_locations:
|
||||||
|
if loc.address is not None and (options.countdown == Countdown.option_all_locations or
|
||||||
|
(options.countdown == Countdown.option_majors
|
||||||
|
and loc.item.advancement)):
|
||||||
|
|
||||||
|
countdown_number = get_location_info(loc.name, "countdown")
|
||||||
|
|
||||||
|
if countdown_number is None:
|
||||||
|
stage = get_region_info(loc.parent_region.name, "stage")
|
||||||
|
if stage is not None:
|
||||||
|
countdown_number = vanilla_stage_order.index(stage)
|
||||||
|
|
||||||
|
if countdown_number is not None:
|
||||||
|
countdown_list[countdown_number] += 1
|
||||||
|
|
||||||
|
# Convert the Countdown list into a data dict
|
||||||
|
countdown_dict = {}
|
||||||
|
for i in range(len(countdown_list)):
|
||||||
|
countdown_dict[0xBFD818 + i] = countdown_list[i]
|
||||||
|
|
||||||
|
return countdown_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
|
||||||
|
-> Tuple[Dict[int, int], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
|
||||||
|
"""Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of
|
||||||
|
the item, the second determines what the item actually is when picked up. All items from other worlds will be AP
|
||||||
|
items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's
|
||||||
|
another CV64 player's item and, if so, what item it is in their game. Ice Traps can assume the form of any item that
|
||||||
|
is progression, non-progression, or either depending on the player's settings.
|
||||||
|
|
||||||
|
Appearance does not matter if it's one of the two NPC-given items (from either Vincent or Heinrich Meyer). For
|
||||||
|
Renon's shop items, a list containing the shop item names, descriptions, and colors will be returned alongside the
|
||||||
|
regular data."""
|
||||||
|
|
||||||
|
# Figure out the list of possible Ice Trap appearances to use based on the settings, first and foremost.
|
||||||
|
if world.options.ice_trap_appearance == IceTrapAppearance.option_major_only:
|
||||||
|
allowed_classifications = ["progression", "progression skip balancing"]
|
||||||
|
elif world.options.ice_trap_appearance == IceTrapAppearance.option_junk_only:
|
||||||
|
allowed_classifications = ["filler", "useful"]
|
||||||
|
else:
|
||||||
|
allowed_classifications = ["progression", "progression skip balancing", "filler", "useful"]
|
||||||
|
|
||||||
|
trap_appearances = []
|
||||||
|
for item in item_info:
|
||||||
|
if item_info[item]["default classification"] in allowed_classifications and item != "Ice Trap" and \
|
||||||
|
get_item_info(item, "code") is not None:
|
||||||
|
trap_appearances.append(item)
|
||||||
|
|
||||||
|
shop_name_list = []
|
||||||
|
shop_desc_list = []
|
||||||
|
shop_colors_list = []
|
||||||
|
|
||||||
|
location_bytes = {}
|
||||||
|
|
||||||
|
for loc in active_locations:
|
||||||
|
# If the Location is an event, skip it.
|
||||||
|
if loc.address is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
loc_type = get_location_info(loc.name, "type")
|
||||||
|
|
||||||
|
# Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's
|
||||||
|
# very own, or it belongs to an Item Link that the player is a part of.
|
||||||
|
if loc.item.player == world.player or (loc.item.player in world.multiworld.groups and
|
||||||
|
world.player in world.multiworld.groups[loc.item.player]['players']):
|
||||||
|
if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id")
|
||||||
|
else:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code")
|
||||||
|
else:
|
||||||
|
# Make the item the unused Wooden Stake - our multiworld item.
|
||||||
|
location_bytes[get_location_info(loc.name, "offset")] = 0x11
|
||||||
|
|
||||||
|
# Figure out the item's appearance. If it's a CV64 player's item, change the multiworld item's model to
|
||||||
|
# match what it is. Otherwise, change it to an Archipelago progress or not progress icon. The model "change"
|
||||||
|
# has to be applied to even local items because this is how the game knows to count it on the Countdown.
|
||||||
|
if loc.item.game == "Castlevania 64":
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = get_item_info(loc.item.name, "code")
|
||||||
|
elif loc.item.advancement:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x11 # Wooden Stakes are majors
|
||||||
|
else:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12 # Roses are minors
|
||||||
|
|
||||||
|
# If it's a PermaUp, change the item's model to a big PowerUp no matter what.
|
||||||
|
if loc.item.game == "Castlevania 64" and loc.item.code == 0x10C + base_id:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
|
||||||
|
|
||||||
|
# If it's an Ice Trap, change its model to one of the appearances we determined before.
|
||||||
|
# Unless it's an NPC item, in which case use the Ice Trap's regular ID so that it won't decrement the majors
|
||||||
|
# Countdown due to how I set up the NPC items to work.
|
||||||
|
if loc.item.game == "Castlevania 64" and loc.item.code == 0x12 + base_id:
|
||||||
|
if loc_type == "npc":
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12
|
||||||
|
else:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = \
|
||||||
|
get_item_info(world.random.choice(trap_appearances), "code")
|
||||||
|
# If we chose a PermaUp as our trap appearance, change it to its actual in-game ID of 0x0B.
|
||||||
|
if location_bytes[get_location_info(loc.name, "offset") - 1] == 0x10C:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
|
||||||
|
|
||||||
|
# Apply the invisibility variable depending on the "invisible items" setting.
|
||||||
|
if (world.options.invisible_items == InvisibleItems.option_vanilla and loc_type == "inv") or \
|
||||||
|
(world.options.invisible_items == InvisibleItems.option_hide_all and loc_type not in ["npc", "shop"]):
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
|
||||||
|
elif world.options.invisible_items == InvisibleItems.option_chance and loc_type not in ["npc", "shop"]:
|
||||||
|
invisible = world.random.randint(0, 1)
|
||||||
|
if invisible:
|
||||||
|
location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
|
||||||
|
|
||||||
|
# If it's an Axe or Cross in a higher freestanding location, lower it into grab range.
|
||||||
|
# KCEK made these spawn 3.2 units higher for some reason.
|
||||||
|
if loc.address & 0xFFF in rom_axe_cross_lower_values and loc.item.code & 0xFF in [0x0F, 0x10]:
|
||||||
|
location_bytes[rom_axe_cross_lower_values[loc.address & 0xFFF][0]] = \
|
||||||
|
rom_axe_cross_lower_values[loc.address & 0xFFF][1]
|
||||||
|
|
||||||
|
# Figure out the list of shop names, descriptions, and text colors here.
|
||||||
|
if loc.parent_region.name != rname.renon:
|
||||||
|
continue
|
||||||
|
|
||||||
|
shop_name = loc.item.name
|
||||||
|
if len(shop_name) > 18:
|
||||||
|
shop_name = shop_name[0:18]
|
||||||
|
shop_name_list.append(shop_name)
|
||||||
|
|
||||||
|
if loc.item.player == world.player:
|
||||||
|
shop_desc_list.append([get_item_info(loc.item.name, "code"), None])
|
||||||
|
elif loc.item.game == "Castlevania 64":
|
||||||
|
shop_desc_list.append([get_item_info(loc.item.name, "code"),
|
||||||
|
world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
else:
|
||||||
|
if loc.item.game == "DLCQuest" and loc.item.name in ["DLC Quest: Coin Bundle",
|
||||||
|
"Live Freemium or Die: Coin Bundle"]:
|
||||||
|
if getattr(world.multiworld.worlds[loc.item.player].options, "coinbundlequantity") == 1:
|
||||||
|
shop_desc_list.append(["dlc coin", world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
shop_colors_list.append(get_item_text_color(loc))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if loc.item.advancement:
|
||||||
|
shop_desc_list.append(["prog", world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
elif loc.item.classification == ItemClassification.useful:
|
||||||
|
shop_desc_list.append(["useful", world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
elif loc.item.classification == ItemClassification.trap:
|
||||||
|
shop_desc_list.append(["trap", world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
else:
|
||||||
|
shop_desc_list.append(["common", world.multiworld.get_player_name(loc.item.player)])
|
||||||
|
|
||||||
|
shop_colors_list.append(get_item_text_color(loc))
|
||||||
|
|
||||||
|
return location_bytes, shop_name_list, shop_colors_list, shop_desc_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
|
||||||
|
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, int]:
|
||||||
|
"""Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data.
|
||||||
|
The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map
|
||||||
|
to send the player to, and which spot within the map to spawn the player at."""
|
||||||
|
|
||||||
|
# Write the byte for the starting stage to send the player to after the intro narration.
|
||||||
|
loading_zone_bytes = {0xB73308: get_stage_info(starting_stage, "start map id")}
|
||||||
|
|
||||||
|
for stage in active_stage_exits:
|
||||||
|
|
||||||
|
# Start loading zones
|
||||||
|
# If the start zone is the start of the line, have it simply refresh the map.
|
||||||
|
if active_stage_exits[stage]["prev"] == "Menu":
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = 0xFF
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = 0x00
|
||||||
|
elif active_stage_exits[stage]["prev"]:
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["prev"], "end map id")
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["prev"], "end spawn id")
|
||||||
|
|
||||||
|
# Change CC's end-spawn ID to put you at Carrie's exit if appropriate
|
||||||
|
if active_stage_exits[stage]["prev"] == rname.castle_center:
|
||||||
|
if options.character_stages == CharacterStages.option_carrie_only or \
|
||||||
|
active_stage_exits[rname.castle_center]["alt"] == stage:
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] += 1
|
||||||
|
|
||||||
|
# End loading zones
|
||||||
|
if active_stage_exits[stage]["next"]:
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "endzone map offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["next"], "start map id")
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "endzone spawn offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["next"], "start spawn id")
|
||||||
|
|
||||||
|
# Alternate end loading zones
|
||||||
|
if active_stage_exits[stage]["alt"]:
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "altzone map offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["alt"], "start map id")
|
||||||
|
loading_zone_bytes[get_stage_info(stage, "altzone spawn offset")] = \
|
||||||
|
get_stage_info(active_stage_exits[stage]["alt"], "start spawn id")
|
||||||
|
|
||||||
|
return loading_zone_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, int]:
|
||||||
|
"""Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
|
||||||
|
has to be handled appropriately."""
|
||||||
|
start_inventory_data = {0xBFD867: 0, # Jewels
|
||||||
|
0xBFD87B: 0, # PowerUps
|
||||||
|
0xBFD883: 0, # Sub-weapon
|
||||||
|
0xBFD88B: 0} # Ice Traps
|
||||||
|
|
||||||
|
inventory_items_array = [0 for _ in range(35)]
|
||||||
|
total_money = 0
|
||||||
|
|
||||||
|
items_max = 10
|
||||||
|
|
||||||
|
# Raise the items max if Increase Item Limit is enabled.
|
||||||
|
if options.increase_item_limit:
|
||||||
|
items_max = 99
|
||||||
|
|
||||||
|
for item in precollected_items:
|
||||||
|
if item.player != player:
|
||||||
|
continue
|
||||||
|
|
||||||
|
inventory_offset = get_item_info(item.name, "inventory offset")
|
||||||
|
sub_equip_id = get_item_info(item.name, "sub equip id")
|
||||||
|
# Starting inventory items
|
||||||
|
if inventory_offset is not None:
|
||||||
|
inventory_items_array[inventory_offset] += 1
|
||||||
|
if inventory_items_array[inventory_offset] > items_max and "Special" not in item.name:
|
||||||
|
inventory_items_array[inventory_offset] = items_max
|
||||||
|
if item.name == iname.permaup:
|
||||||
|
if inventory_items_array[inventory_offset] > 2:
|
||||||
|
inventory_items_array[inventory_offset] = 2
|
||||||
|
# Starting sub-weapon
|
||||||
|
elif sub_equip_id is not None:
|
||||||
|
start_inventory_data[0xBFD883] = sub_equip_id
|
||||||
|
# Starting PowerUps
|
||||||
|
elif item.name == iname.powerup:
|
||||||
|
start_inventory_data[0xBFD87B] += 1
|
||||||
|
if start_inventory_data[0xBFD87B] > 2:
|
||||||
|
start_inventory_data[0xBFD87B] = 2
|
||||||
|
# Starting Gold
|
||||||
|
elif "GOLD" in item.name:
|
||||||
|
total_money += int(item.name[0:4])
|
||||||
|
if total_money > 99999:
|
||||||
|
total_money = 99999
|
||||||
|
# Starting Jewels
|
||||||
|
elif "jewel" in item.name:
|
||||||
|
if "L" in item.name:
|
||||||
|
start_inventory_data[0xBFD867] += 10
|
||||||
|
else:
|
||||||
|
start_inventory_data[0xBFD867] += 5
|
||||||
|
if start_inventory_data[0xBFD867] > 99:
|
||||||
|
start_inventory_data[0xBFD867] = 99
|
||||||
|
# Starting Ice Traps
|
||||||
|
else:
|
||||||
|
start_inventory_data[0xBFD88B] += 1
|
||||||
|
if start_inventory_data[0xBFD88B] > 0xFF:
|
||||||
|
start_inventory_data[0xBFD88B] = 0xFF
|
||||||
|
|
||||||
|
# Convert the inventory items into data.
|
||||||
|
for i in range(len(inventory_items_array)):
|
||||||
|
start_inventory_data[0xBFE518 + i] = inventory_items_array[i]
|
||||||
|
|
||||||
|
# Convert the starting money into data. Which offset it starts from depends on how many bytes it takes up.
|
||||||
|
if total_money <= 0xFF:
|
||||||
|
start_inventory_data[0xBFE517] = total_money
|
||||||
|
elif total_money <= 0xFFFF:
|
||||||
|
start_inventory_data[0xBFE516] = total_money
|
||||||
|
else:
|
||||||
|
start_inventory_data[0xBFE515] = total_money
|
||||||
|
|
||||||
|
return start_inventory_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_text_color(loc: Location) -> bytearray:
|
||||||
|
if loc.item.advancement:
|
||||||
|
return bytearray([0xA2, 0x0C])
|
||||||
|
elif loc.item.classification == ItemClassification.useful:
|
||||||
|
return bytearray([0xA2, 0x0A])
|
||||||
|
elif loc.item.classification == ItemClassification.trap:
|
||||||
|
return bytearray([0xA2, 0x0B])
|
||||||
|
else:
|
||||||
|
return bytearray([0xA2, 0x02])
|
|
@ -0,0 +1,207 @@
|
||||||
|
from typing import TYPE_CHECKING, Set
|
||||||
|
from .locations import base_id
|
||||||
|
from .text import cv64_text_wrap, cv64_string_to_bytearray
|
||||||
|
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
import worlds._bizhawk as bizhawk
|
||||||
|
import base64
|
||||||
|
from worlds._bizhawk.client import BizHawkClient
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from worlds._bizhawk.context import BizHawkClientContext
|
||||||
|
|
||||||
|
|
||||||
|
class Castlevania64Client(BizHawkClient):
|
||||||
|
game = "Castlevania 64"
|
||||||
|
system = "N64"
|
||||||
|
patch_suffix = ".apcv64"
|
||||||
|
self_induced_death = False
|
||||||
|
received_deathlinks = 0
|
||||||
|
death_causes = []
|
||||||
|
currently_shopping = False
|
||||||
|
local_checked_locations: Set[int]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.local_checked_locations = set()
|
||||||
|
|
||||||
|
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||||
|
from CommonClient import logger
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check ROM name/patch version
|
||||||
|
game_names = await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 0x14, "ROM"), (0xBFBFD0, 12, "ROM")])
|
||||||
|
if game_names[0].decode("ascii") != "CASTLEVANIA ":
|
||||||
|
return False
|
||||||
|
if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
|
||||||
|
logger.info("ERROR: You appear to be running an unpatched version of Castlevania 64. "
|
||||||
|
"You need to generate a patch file and use it to create a patched ROM.")
|
||||||
|
return False
|
||||||
|
if game_names[1].decode("ascii") != "ARCHIPELAGO1":
|
||||||
|
logger.info("ERROR: The patch file used to create this ROM is not compatible with "
|
||||||
|
"this client. Double check your client version against the version being "
|
||||||
|
"used by the generator.")
|
||||||
|
return False
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
except bizhawk.RequestFailedError:
|
||||||
|
return False # Should verify on the next pass
|
||||||
|
|
||||||
|
ctx.game = self.game
|
||||||
|
ctx.items_handling = 0b001
|
||||||
|
ctx.want_slot_data = False
|
||||||
|
ctx.watcher_timeout = 0.125
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(0xBFBFE0, 16, "ROM")]))[0]
|
||||||
|
ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
|
||||||
|
|
||||||
|
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
|
||||||
|
if cmd != "Bounced":
|
||||||
|
return
|
||||||
|
if "tags" not in args:
|
||||||
|
return
|
||||||
|
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||||
|
self.received_deathlinks += 1
|
||||||
|
if "cause" in args["data"]:
|
||||||
|
cause = args["data"]["cause"]
|
||||||
|
if len(cause) > 88:
|
||||||
|
cause = cause[0x00:0x89]
|
||||||
|
else:
|
||||||
|
cause = f"{args['data']['source']} killed you!"
|
||||||
|
self.death_causes.append(cause)
|
||||||
|
|
||||||
|
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
|
||||||
|
try:
|
||||||
|
read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x342084, 4, "RDRAM"),
|
||||||
|
(0x389BDE, 6, "RDRAM"),
|
||||||
|
(0x389BE4, 224, "RDRAM"),
|
||||||
|
(0x389EFB, 1, "RDRAM"),
|
||||||
|
(0x389EEF, 1, "RDRAM"),
|
||||||
|
(0xBFBFDE, 2, "ROM")])
|
||||||
|
|
||||||
|
game_state = int.from_bytes(read_state[0], "big")
|
||||||
|
save_struct = read_state[2]
|
||||||
|
written_deathlinks = int.from_bytes(bytearray(read_state[1][4:6]), "big")
|
||||||
|
deathlink_induced_death = int.from_bytes(bytearray(read_state[1][0:1]), "big")
|
||||||
|
cutscene_value = int.from_bytes(read_state[3], "big")
|
||||||
|
current_menu = int.from_bytes(read_state[4], "big")
|
||||||
|
num_received_items = int.from_bytes(bytearray(save_struct[0xDA:0xDC]), "big")
|
||||||
|
rom_flags = int.from_bytes(read_state[5], "big")
|
||||||
|
|
||||||
|
# Make sure we are in the Gameplay or Credits states before detecting sent locations and/or DeathLinks.
|
||||||
|
# If we are in any other state, such as the Game Over state, set self_induced_death to false, so we can once
|
||||||
|
# again send a DeathLink once we are back in the Gameplay state.
|
||||||
|
if game_state not in [0x00000002, 0x0000000B]:
|
||||||
|
self.self_induced_death = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Enable DeathLink if the bit for it is set in our ROM flags.
|
||||||
|
if "DeathLink" not in ctx.tags and rom_flags & 0x0100:
|
||||||
|
await ctx.update_death_link(True)
|
||||||
|
|
||||||
|
# Scout the Renon shop locations if the shopsanity flag is written in the ROM.
|
||||||
|
if rom_flags & 0x0001 and ctx.locations_info == {}:
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": [base_id + i for i in range(0x1C8, 0x1CF)],
|
||||||
|
"create_as_hint": 0
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Send a DeathLink if we died on our own independently of receiving another one.
|
||||||
|
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
|
||||||
|
deathlink_induced_death:
|
||||||
|
self.self_induced_death = True
|
||||||
|
if save_struct[0xA4] & 0x08:
|
||||||
|
# Special death message for dying while having the Vamp status.
|
||||||
|
await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
|
||||||
|
else:
|
||||||
|
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
|
||||||
|
|
||||||
|
# Write any DeathLinks received along with the corresponding death cause starting with the oldest.
|
||||||
|
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
|
||||||
|
if self.received_deathlinks and not self.self_induced_death and not written_deathlinks:
|
||||||
|
death_text, num_lines = cv64_text_wrap(self.death_causes[0], 96)
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x389BE3, [0x01], "RDRAM"),
|
||||||
|
(0x389BDF, [0x11], "RDRAM"),
|
||||||
|
(0x18BF98, bytearray([0xA2, 0x0B]) +
|
||||||
|
cv64_string_to_bytearray(death_text, False), "RDRAM"),
|
||||||
|
(0x18C097, [num_lines], "RDRAM")])
|
||||||
|
self.received_deathlinks -= 1
|
||||||
|
del self.death_causes[0]
|
||||||
|
else:
|
||||||
|
# If the game hasn't received all items yet, the received item struct doesn't contain an item, the
|
||||||
|
# current number of received items still matches what we read before, and there are no open text boxes,
|
||||||
|
# then fill it with the next item and write the "item from player" text in its buffer. The game will
|
||||||
|
# increment the number of received items on its own.
|
||||||
|
if num_received_items < len(ctx.items_received):
|
||||||
|
next_item = ctx.items_received[num_received_items]
|
||||||
|
if next_item.flags & 0b001:
|
||||||
|
text_color = bytearray([0xA2, 0x0C])
|
||||||
|
elif next_item.flags & 0b010:
|
||||||
|
text_color = bytearray([0xA2, 0x0A])
|
||||||
|
elif next_item.flags & 0b100:
|
||||||
|
text_color = bytearray([0xA2, 0x0B])
|
||||||
|
else:
|
||||||
|
text_color = bytearray([0xA2, 0x02])
|
||||||
|
received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n"
|
||||||
|
f"from {ctx.player_names[next_item.player]}", 96)
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||||
|
[(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
|
||||||
|
(0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False),
|
||||||
|
"RDRAM"),
|
||||||
|
(0x18C1A7, [num_lines], "RDRAM")],
|
||||||
|
[(0x389BE1, [0x00], "RDRAM"), # Remote item reward buffer
|
||||||
|
(0x389CBE, save_struct[0xDA:0xDC], "RDRAM"), # Received items
|
||||||
|
(0x342891, [0x02], "RDRAM")]) # Textbox state
|
||||||
|
|
||||||
|
flag_bytes = bytearray(save_struct[0x00:0x44]) + bytearray(save_struct[0x90:0x9F])
|
||||||
|
locs_to_send = set()
|
||||||
|
|
||||||
|
# Check for set location flags.
|
||||||
|
for byte_i, byte in enumerate(flag_bytes):
|
||||||
|
for i in range(8):
|
||||||
|
and_value = 0x80 >> i
|
||||||
|
if byte & and_value != 0:
|
||||||
|
flag_id = byte_i * 8 + i
|
||||||
|
|
||||||
|
location_id = flag_id + base_id
|
||||||
|
if location_id in ctx.server_locations:
|
||||||
|
locs_to_send.add(location_id)
|
||||||
|
|
||||||
|
# Send locations if there are any to send.
|
||||||
|
if locs_to_send != self.local_checked_locations:
|
||||||
|
self.local_checked_locations = locs_to_send
|
||||||
|
|
||||||
|
if locs_to_send is not None:
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationChecks",
|
||||||
|
"locations": list(locs_to_send)
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Check the menu value to see if we are in Renon's shop, and set currently_shopping to True if we are.
|
||||||
|
if current_menu == 0xA:
|
||||||
|
self.currently_shopping = True
|
||||||
|
|
||||||
|
# If we are currently shopping, and the current menu value is 0 (meaning we just left the shop), hint the
|
||||||
|
# un-bought shop locations that have progression.
|
||||||
|
if current_menu == 0 and self.currently_shopping:
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": [loc for loc, n_item in ctx.locations_info.items() if n_item.flags & 0b001],
|
||||||
|
"create_as_hint": 2
|
||||||
|
}])
|
||||||
|
self.currently_shopping = False
|
||||||
|
|
||||||
|
# Send game clear if we're in either any ending cutscene or the credits state.
|
||||||
|
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "StatusUpdate",
|
||||||
|
"status": ClientStatus.CLIENT_GOAL
|
||||||
|
}])
|
||||||
|
|
||||||
|
except bizhawk.RequestFailedError:
|
||||||
|
# Exit handler and return to main loop to reconnect.
|
||||||
|
pass
|
|
@ -0,0 +1,3 @@
|
||||||
|
The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson is licensed under Attribution-NonCommercial 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
|
||||||
|
|
||||||
|
Logo modified by Liquid Cat to fit artstyle and uses within the mod.
|
Binary file not shown.
|
@ -0,0 +1,71 @@
|
||||||
|
forest_dbridge_gate = "Descending bridge gate"
|
||||||
|
forest_werewolf_gate = "Werewolf gate"
|
||||||
|
forest_end = "Dracula's drawbridge"
|
||||||
|
|
||||||
|
cw_portcullis_c = "Central portcullis"
|
||||||
|
cw_lt_skip = "Do Left Tower Skip"
|
||||||
|
cw_lt_door = "Left Tower door"
|
||||||
|
cw_end = "End portcullis"
|
||||||
|
|
||||||
|
villa_dog_gates = "Front dog gates"
|
||||||
|
villa_snipe_dogs = "Orb snipe the dogs"
|
||||||
|
villa_to_storeroom = "To Storeroom door"
|
||||||
|
villa_to_archives = "To Archives door"
|
||||||
|
villa_renon = "Villa contract"
|
||||||
|
villa_to_maze = "To maze gate"
|
||||||
|
villa_from_storeroom = "From Storeroom door"
|
||||||
|
villa_from_maze = "From maze gate"
|
||||||
|
villa_servant_door = "Servants' door"
|
||||||
|
villa_copper_door = "Copper door"
|
||||||
|
villa_copper_skip = "Get Copper Skip"
|
||||||
|
villa_bridge_door = "From bridge door"
|
||||||
|
villa_end_r = "Villa Reinhardt (daytime) exit"
|
||||||
|
villa_end_c = "Villa Carrie (nighttime) exit"
|
||||||
|
|
||||||
|
tunnel_start_renon = "Tunnel start contract"
|
||||||
|
tunnel_gondolas = "Gondola ride"
|
||||||
|
tunnel_end_renon = "Tunnel end contract"
|
||||||
|
tunnel_end = "End Tunnel door"
|
||||||
|
|
||||||
|
uw_final_waterfall = "Final waterfall"
|
||||||
|
uw_waterfall_skip = "Do Waterfall Skip"
|
||||||
|
uw_renon = "Underground Waterway contract"
|
||||||
|
uw_end = "End Waterway door"
|
||||||
|
|
||||||
|
cc_tc_door = "Torture Chamber door"
|
||||||
|
cc_renon = "Castle Center contract"
|
||||||
|
cc_lower_wall = "Lower sealed cracked wall"
|
||||||
|
cc_upper_wall = "Upper cracked wall"
|
||||||
|
cc_elevator = "Activate crystal and ride elevator"
|
||||||
|
cc_exit_r = "Castle Center Reinhardt (Medusa Head) exit"
|
||||||
|
cc_exit_c = "Castle Center Carrie (Ghost) exit"
|
||||||
|
|
||||||
|
dt_start = "Duel Tower start passage"
|
||||||
|
dt_end = "Duel Tower end passage"
|
||||||
|
|
||||||
|
toe_start = "Tower of Execution start passage"
|
||||||
|
toe_gate = "Execution gate"
|
||||||
|
toe_gate_skip = "Just jump past the gate from above, bro!"
|
||||||
|
toe_end = "Tower of Execution end staircase"
|
||||||
|
|
||||||
|
tosci_start = "Tower of Science start passage"
|
||||||
|
tosci_key1_door = "Science Door 1"
|
||||||
|
tosci_to_key2_door = "To Science Door 2"
|
||||||
|
tosci_from_key2_door = "From Science Door 2"
|
||||||
|
tosci_key3_door = "Science Door 3"
|
||||||
|
tosci_end = "Tower of Science end passage"
|
||||||
|
|
||||||
|
tosor_start = "Tower of Sorcery start passage"
|
||||||
|
tosor_end = "Tower of Sorcery end passage"
|
||||||
|
|
||||||
|
roc_gate = "Defeat boss gate"
|
||||||
|
|
||||||
|
ct_to_door1 = "To Clocktower Door 1"
|
||||||
|
ct_from_door1 = "From Clocktower Door 1"
|
||||||
|
ct_to_door2 = "To Clocktower Door 2"
|
||||||
|
ct_from_door2 = "From Clocktower Door 2"
|
||||||
|
ct_renon = "Clock Tower contract"
|
||||||
|
ct_door_3 = "Clocktower Door 3"
|
||||||
|
|
||||||
|
ck_slope_jump = "Slope Jump to boss tower"
|
||||||
|
ck_drac_door = "Dracula's door"
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Items
|
||||||
|
white_jewel = "White jewel"
|
||||||
|
special_one = "Special1"
|
||||||
|
special_two = "Special2"
|
||||||
|
red_jewel_s = "Red jewel(S)"
|
||||||
|
red_jewel_l = "Red jewel(L)"
|
||||||
|
roast_chicken = "Roast chicken"
|
||||||
|
roast_beef = "Roast beef"
|
||||||
|
healing_kit = "Healing kit"
|
||||||
|
purifying = "Purifying"
|
||||||
|
cure_ampoule = "Cure ampoule"
|
||||||
|
pot_pourri = "pot-pourri"
|
||||||
|
powerup = "PowerUp"
|
||||||
|
permaup = "PermaUp"
|
||||||
|
holy_water = "Holy water"
|
||||||
|
cross = "Cross"
|
||||||
|
axe = "Axe"
|
||||||
|
knife = "Knife"
|
||||||
|
wooden_stake = "Wooden stake"
|
||||||
|
rose = "Rose"
|
||||||
|
ice_trap = "Ice Trap"
|
||||||
|
the_contract = "The contract"
|
||||||
|
engagement_ring = "engagement ring"
|
||||||
|
magical_nitro = "Magical Nitro"
|
||||||
|
mandragora = "Mandragora"
|
||||||
|
sun_card = "Sun card"
|
||||||
|
moon_card = "Moon card"
|
||||||
|
incandescent_gaze = "Incandescent gaze"
|
||||||
|
five_hundred_gold = "500 GOLD"
|
||||||
|
three_hundred_gold = "300 GOLD"
|
||||||
|
one_hundred_gold = "100 GOLD"
|
||||||
|
archives_key = "Archives Key"
|
||||||
|
left_tower_key = "Left Tower Key"
|
||||||
|
storeroom_key = "Storeroom Key"
|
||||||
|
garden_key = "Garden Key"
|
||||||
|
copper_key = "Copper Key"
|
||||||
|
chamber_key = "Chamber Key"
|
||||||
|
execution_key = "Execution Key"
|
||||||
|
science_key1 = "Science Key1"
|
||||||
|
science_key2 = "Science Key2"
|
||||||
|
science_key3 = "Science Key3"
|
||||||
|
clocktower_key1 = "Clocktower Key1"
|
||||||
|
clocktower_key2 = "Clocktower Key2"
|
||||||
|
clocktower_key3 = "Clocktower Key3"
|
||||||
|
|
||||||
|
trophy = "Trophy"
|
||||||
|
crystal = "Crystal"
|
||||||
|
|
||||||
|
victory = "The Count Downed"
|
|
@ -0,0 +1,479 @@
|
||||||
|
# Forest of Silence main locations
|
||||||
|
forest_pillars_right = "Forest of Silence: Grab practice pillars - Right"
|
||||||
|
forest_pillars_top = "Forest of Silence: Grab practice pillars - Top"
|
||||||
|
forest_king_skeleton = "Forest of Silence: King Skeleton's bridge"
|
||||||
|
forest_lgaz_in = "Forest of Silence: Moon gazebo inside"
|
||||||
|
forest_lgaz_top = "Forest of Silence: Moon gazebo roof"
|
||||||
|
forest_hgaz_in = "Forest of Silence: Sun gazebo inside"
|
||||||
|
forest_hgaz_top = "Forest of Silence: Sun gazebo roof"
|
||||||
|
forest_weretiger_sw = "Forest of Silence: Were-tiger switch"
|
||||||
|
forest_weretiger_gate = "Forest of Silence: Dirge maiden gate"
|
||||||
|
forest_dirge_tomb_u = "Forest of Silence: Dirge maiden crypt - Upper"
|
||||||
|
forest_dirge_plaque = "Forest of Silence: Dirge maiden pedestal plaque"
|
||||||
|
forest_corpse_save = "Forest of Silence: Tri-corpse save junction"
|
||||||
|
forest_dbridge_wall = "Forest of Silence: Descending bridge wall side"
|
||||||
|
forest_dbridge_sw = "Forest of Silence: Descending bridge switch side"
|
||||||
|
forest_dbridge_gate_r = "Forest of Silence: Tri-corpse gate - Right"
|
||||||
|
forest_dbridge_tomb_uf = "Forest of Silence: Three-crypt plaza main path crypt - Upper-front"
|
||||||
|
forest_bface_tomb_lf = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-front"
|
||||||
|
forest_bface_tomb_u = "Forest of Silence: Three-crypt plaza back-facing crypt - Upper"
|
||||||
|
forest_ibridge = "Forest of Silence: Invisible bridge platform"
|
||||||
|
forest_werewolf_tomb_r = "Forest of Silence: Werewolf crypt - Right"
|
||||||
|
forest_werewolf_plaque = "Forest of Silence: Werewolf statue plaque"
|
||||||
|
forest_werewolf_tree = "Forest of Silence: Werewolf path near tree"
|
||||||
|
forest_final_sw = "Forest of Silence: Three-crypt plaza switch"
|
||||||
|
|
||||||
|
# Forest of Silence empty breakables
|
||||||
|
forest_dirge_tomb_l = "Forest of Silence: Dirge maiden crypt - Lower"
|
||||||
|
forest_dbridge_tomb_l = "Forest of Silence: Three-crypt plaza main path crypt - Lower"
|
||||||
|
forest_dbridge_tomb_ur = "Forest of Silence: Three-crypt plaza main path crypt - Upper-rear"
|
||||||
|
forest_bface_tomb_lr = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-rear"
|
||||||
|
forest_werewolf_tomb_lf = "Forest of Silence: Werewolf crypt - Left-front"
|
||||||
|
forest_werewolf_tomb_lr = "Forest of Silence: Werewolf crypt - Left-rear"
|
||||||
|
|
||||||
|
# Forest of Silence 3-hit breakables
|
||||||
|
forest_dirge_rock1 = "Forest of Silence: Dirge maiden rock - Item 1"
|
||||||
|
forest_dirge_rock2 = "Forest of Silence: Dirge maiden rock - Item 2"
|
||||||
|
forest_dirge_rock3 = "Forest of Silence: Dirge maiden rock - Item 3"
|
||||||
|
forest_dirge_rock4 = "Forest of Silence: Dirge maiden rock - Item 4"
|
||||||
|
forest_dirge_rock5 = "Forest of Silence: Dirge maiden rock - Item 5"
|
||||||
|
forest_bridge_rock1 = "Forest of Silence: Bat archway rock - Item 1"
|
||||||
|
forest_bridge_rock2 = "Forest of Silence: Bat archway rock - Item 2"
|
||||||
|
forest_bridge_rock3 = "Forest of Silence: Bat archway rock - Item 3"
|
||||||
|
forest_bridge_rock4 = "Forest of Silence: Bat archway rock - Item 4"
|
||||||
|
|
||||||
|
# Forest of Silence sub-weapons
|
||||||
|
forest_pillars_left = "Forest of Silence: Grab practice pillars - Left"
|
||||||
|
forest_dirge_ped = "Forest of Silence: Dirge maiden pedestal"
|
||||||
|
forest_dbridge_gate_l = "Forest of Silence: Tri-corpse gate - Left"
|
||||||
|
forest_werewolf_island = "Forest of Silence: Werewolf path island switch platforms"
|
||||||
|
|
||||||
|
|
||||||
|
# Castle Wall main locations
|
||||||
|
cw_ground_middle = "Castle Wall: Ground gatehouse - Middle"
|
||||||
|
cw_rrampart = "Castle Wall: Central rampart near right tower"
|
||||||
|
cw_lrampart = "Castle Wall: Central rampart near left tower"
|
||||||
|
cw_dragon_sw = "Castle Wall: White Dragons switch door"
|
||||||
|
cw_drac_sw = "Castle Wall: Dracula cutscene switch door"
|
||||||
|
cw_shelf_visible = "Castle Wall: Sandbag shelf - Visible"
|
||||||
|
cw_shelf_sandbags = "Castle Wall: Sandbag shelf - Invisible"
|
||||||
|
|
||||||
|
# Castle Wall towers main locations
|
||||||
|
cwr_bottom = "Castle Wall: Above bottom right tower door"
|
||||||
|
cwl_bottom = "Castle Wall: Above bottom left tower door"
|
||||||
|
cwl_bridge = "Castle Wall: Left tower child ledge"
|
||||||
|
|
||||||
|
# Castle Wall 3-hit breakables
|
||||||
|
cw_save_slab1 = "Castle Wall: Central rampart savepoint slab - Item 1"
|
||||||
|
cw_save_slab2 = "Castle Wall: Central rampart savepoint slab - Item 2"
|
||||||
|
cw_save_slab3 = "Castle Wall: Central rampart savepoint slab - Item 3"
|
||||||
|
cw_save_slab4 = "Castle Wall: Central rampart savepoint slab - Item 4"
|
||||||
|
cw_save_slab5 = "Castle Wall: Central rampart savepoint slab - Item 5"
|
||||||
|
cw_drac_slab1 = "Castle Wall: Dracula cutscene switch slab - Item 1"
|
||||||
|
cw_drac_slab2 = "Castle Wall: Dracula cutscene switch slab - Item 2"
|
||||||
|
cw_drac_slab3 = "Castle Wall: Dracula cutscene switch slab - Item 3"
|
||||||
|
cw_drac_slab4 = "Castle Wall: Dracula cutscene switch slab - Item 4"
|
||||||
|
cw_drac_slab5 = "Castle Wall: Dracula cutscene switch slab - Item 5"
|
||||||
|
|
||||||
|
# Castle Wall sub-weapons
|
||||||
|
cw_ground_left = "Castle Wall: Ground gatehouse - Left"
|
||||||
|
cw_ground_right = "Castle Wall: Ground gatehouse - Right"
|
||||||
|
cw_shelf_torch = "Castle Wall: Sandbag shelf floor torch"
|
||||||
|
cw_pillar = "Castle Wall: Central rampart broken pillar"
|
||||||
|
|
||||||
|
|
||||||
|
# Villa front yard main locations
|
||||||
|
villafy_outer_gate_l = "Villa: Outer front gate - Left"
|
||||||
|
villafy_outer_gate_r = "Villa: Outer front gate - Right"
|
||||||
|
villafy_inner_gate = "Villa: Inner front gate dog food"
|
||||||
|
villafy_dog_platform = "Villa: Outer front gate platform"
|
||||||
|
villafy_gate_marker = "Villa: Front yard cross grave near gates"
|
||||||
|
villafy_villa_marker = "Villa: Front yard cross grave near porch"
|
||||||
|
villafy_tombstone = "Villa: Front yard visitor's tombstone"
|
||||||
|
villafy_fountain_fl = "Villa: Midnight fountain - Front-left"
|
||||||
|
villafy_fountain_fr = "Villa: Midnight fountain - Front-right"
|
||||||
|
villafy_fountain_ml = "Villa: Midnight fountain - Middle-left"
|
||||||
|
villafy_fountain_mr = "Villa: Midnight fountain - Middle-right"
|
||||||
|
villafy_fountain_rl = "Villa: Midnight fountain - Rear-left"
|
||||||
|
villafy_fountain_rr = "Villa: Midnight fountain - Rear-right"
|
||||||
|
|
||||||
|
# Villa foyer main locations
|
||||||
|
villafo_sofa = "Villa: Foyer sofa"
|
||||||
|
villafo_pot_r = "Villa: Foyer upper-right pot"
|
||||||
|
villafo_pot_l = "Villa: Foyer upper-left pot"
|
||||||
|
villafo_rear_r = "Villa: Foyer lower level - Rear-right"
|
||||||
|
villafo_rear_l = "Villa: Foyer lower level - Rear-left"
|
||||||
|
villafo_mid_l = "Villa: Foyer lower level - Middle-left"
|
||||||
|
villafo_front_r = "Villa: Foyer lower level - Front-right"
|
||||||
|
villafo_front_l = "Villa: Foyer lower level - Front-left"
|
||||||
|
villafo_serv_ent = "Villa: Servants' entrance"
|
||||||
|
|
||||||
|
# Villa empty breakables
|
||||||
|
villafo_mid_r = "Villa: Foyer lower level - Middle-right"
|
||||||
|
|
||||||
|
# Villa 3-hit breakables
|
||||||
|
villafo_chandelier1 = "Villa: Foyer chandelier - Item 1"
|
||||||
|
villafo_chandelier2 = "Villa: Foyer chandelier - Item 2"
|
||||||
|
villafo_chandelier3 = "Villa: Foyer chandelier - Item 3"
|
||||||
|
villafo_chandelier4 = "Villa: Foyer chandelier - Item 4"
|
||||||
|
villafo_chandelier5 = "Villa: Foyer chandelier - Item 5"
|
||||||
|
|
||||||
|
# Villa living area main locations
|
||||||
|
villala_hallway_stairs = "Villa: Rose garden staircase bottom"
|
||||||
|
villala_bedroom_chairs = "Villa: Bedroom near chairs"
|
||||||
|
villala_bedroom_bed = "Villa: Bedroom near bed"
|
||||||
|
villala_vincent = "Villa: Vincent"
|
||||||
|
villala_slivingroom_table = "Villa: Mary's room table"
|
||||||
|
villala_storeroom_l = "Villa: Storeroom - Left"
|
||||||
|
villala_storeroom_r = "Villa: Storeroom - Right"
|
||||||
|
villala_storeroom_s = "Villa: Storeroom statue"
|
||||||
|
villala_diningroom_roses = "Villa: Dining room rose vase"
|
||||||
|
villala_archives_table = "Villa: Archives table"
|
||||||
|
villala_archives_rear = "Villa: Archives rear corner"
|
||||||
|
villala_llivingroom_lion = "Villa: Living room lion head"
|
||||||
|
villala_llivingroom_pot_r = "Villa: Living room - Right pot"
|
||||||
|
villala_llivingroom_pot_l = "Villa: Living room - Left pot"
|
||||||
|
villala_llivingroom_light = "Villa: Living room ceiling light"
|
||||||
|
villala_llivingroom_painting = "Villa: Living room clawed painting"
|
||||||
|
villala_exit_knight = "Villa: Maze garden exit knight"
|
||||||
|
|
||||||
|
# Villa maze main locations
|
||||||
|
villam_malus_torch = "Villa: Front maze garden - Malus start torch"
|
||||||
|
villam_malus_bush = "Villa: Front maze garden - Malus's hiding bush"
|
||||||
|
villam_frankieturf_r = "Villa: Front maze garden - Frankie's right dead-end"
|
||||||
|
villam_frankieturf_l = "Villa: Front maze garden - Frankie's left dead-end"
|
||||||
|
villam_frankieturf_ru = "Villa: Front maze garden - Frankie's right dead-end urn"
|
||||||
|
villam_fgarden_f = "Villa: Rear maze garden - Iron Thorn Fenced area - Front"
|
||||||
|
villam_fgarden_mf = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-front"
|
||||||
|
villam_fgarden_mr = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-rear"
|
||||||
|
villam_fgarden_r = "Villa: Rear maze garden - Iron Thorn Fenced area - Rear"
|
||||||
|
villam_rplatform_de = "Villa: Rear maze garden - Viewing platform dead-end"
|
||||||
|
villam_exit_de = "Villa: Rear maze garden - Past-exit dead-end"
|
||||||
|
villam_serv_path = "Villa: Servants' path small alcove"
|
||||||
|
villam_crypt_ent = "Villa: Crypt entrance"
|
||||||
|
villam_crypt_upstream = "Villa: Crypt bridge upstream"
|
||||||
|
|
||||||
|
# Villa crypt main locations
|
||||||
|
villac_ent_l = "Villa: Crypt - Left from entrance"
|
||||||
|
villac_ent_r = "Villa: Crypt - Right from entrance"
|
||||||
|
villac_wall_l = "Villa: Crypt - Left wall"
|
||||||
|
villac_wall_r = "Villa: Crypt - Right wall"
|
||||||
|
villac_coffin_r = "Villa: Crypt - Right of coffin"
|
||||||
|
|
||||||
|
# Villa sub-weapons
|
||||||
|
villala_hallway_l = "Villa: Hallway near rose garden stairs - Left"
|
||||||
|
villala_hallway_r = "Villa: Hallway near rose garden stairs - Right"
|
||||||
|
villala_slivingroom_mirror = "Villa: Mary's room corner"
|
||||||
|
villala_archives_entrance = "Villa: Archives near entrance"
|
||||||
|
villam_fplatform = "Villa: Front maze garden - Viewing platform"
|
||||||
|
villam_rplatform = "Villa: Rear maze garden - Viewing platform"
|
||||||
|
villac_coffin_l = "Villa: Crypt - Left of coffin"
|
||||||
|
|
||||||
|
|
||||||
|
# Tunnel main locations
|
||||||
|
tunnel_landing = "Tunnel: Landing point"
|
||||||
|
tunnel_landing_rc = "Tunnel: Landing point rock crusher"
|
||||||
|
tunnel_stone_alcove_l = "Tunnel: Stepping stone alcove - Left"
|
||||||
|
tunnel_twin_arrows = "Tunnel: Twin arrow signs"
|
||||||
|
tunnel_lonesome_bucket = "Tunnel: Near lonesome bucket"
|
||||||
|
tunnel_lbucket_quag = "Tunnel: Lonesome bucket poison pit"
|
||||||
|
tunnel_lbucket_albert = "Tunnel: Lonesome bucket-Albert junction"
|
||||||
|
tunnel_albert_camp = "Tunnel: Albert's campsite"
|
||||||
|
tunnel_albert_quag = "Tunnel: Albert's poison pit"
|
||||||
|
tunnel_gondola_rc_sdoor_r = "Tunnel: Gondola rock crusher sun door - Right"
|
||||||
|
tunnel_gondola_rc_sdoor_m = "Tunnel: Gondola rock crusher sun door - Middle"
|
||||||
|
tunnel_gondola_rc = "Tunnel: Gondola rock crusher"
|
||||||
|
tunnel_rgondola_station = "Tunnel: Red gondola station"
|
||||||
|
tunnel_gondola_transfer = "Tunnel: Gondola transfer point"
|
||||||
|
tunnel_corpse_bucket_quag = "Tunnel: Corpse bucket poison pit"
|
||||||
|
tunnel_corpse_bucket_mdoor_r = "Tunnel: Corpse bucket moon door - Right"
|
||||||
|
tunnel_shovel_quag_start = "Tunnel: Shovel poison pit start"
|
||||||
|
tunnel_exit_quag_start = "Tunnel: Exit door poison pit start"
|
||||||
|
tunnel_shovel_quag_end = "Tunnel: Shovel poison pit end"
|
||||||
|
tunnel_exit_quag_end = "Tunnel: Exit door poison pit end"
|
||||||
|
tunnel_shovel = "Tunnel: Shovel"
|
||||||
|
tunnel_shovel_save = "Tunnel: Shovel zone save junction"
|
||||||
|
tunnel_shovel_mdoor_l = "Tunnel: Shovel zone moon door - Left"
|
||||||
|
tunnel_shovel_sdoor_l = "Tunnel: Shovel zone sun door - Left"
|
||||||
|
tunnel_shovel_sdoor_m = "Tunnel: Shovel zone sun door - Middle"
|
||||||
|
|
||||||
|
# Tunnel 3-hit breakables
|
||||||
|
tunnel_arrows_rock1 = "Tunnel: Twin arrow signs rock - Item 1"
|
||||||
|
tunnel_arrows_rock2 = "Tunnel: Twin arrow signs rock - Item 2"
|
||||||
|
tunnel_arrows_rock3 = "Tunnel: Twin arrow signs rock - Item 3"
|
||||||
|
tunnel_arrows_rock4 = "Tunnel: Twin arrow signs rock - Item 4"
|
||||||
|
tunnel_arrows_rock5 = "Tunnel: Twin arrow signs rock - Item 5"
|
||||||
|
tunnel_bucket_quag_rock1 = "Tunnel: Lonesome bucket poison pit rock - Item 1"
|
||||||
|
tunnel_bucket_quag_rock2 = "Tunnel: Lonesome bucket poison pit rock - Item 2"
|
||||||
|
tunnel_bucket_quag_rock3 = "Tunnel: Lonesome bucket poison pit rock - Item 3"
|
||||||
|
|
||||||
|
# Tunnel sub-weapons
|
||||||
|
tunnel_stone_alcove_r = "Tunnel: Stepping stone alcove - Right"
|
||||||
|
tunnel_lbucket_mdoor_l = "Tunnel: Lonesome bucket moon door"
|
||||||
|
tunnel_gondola_rc_sdoor_l = "Tunnel: Gondola rock crusher sun door - Left"
|
||||||
|
tunnel_corpse_bucket_mdoor_l = "Tunnel: Corpse bucket moon door - Left"
|
||||||
|
tunnel_shovel_mdoor_r = "Tunnel: Shovel zone moon door - Right"
|
||||||
|
tunnel_shovel_sdoor_r = "Tunnel: Shovel zone sun door - Right"
|
||||||
|
|
||||||
|
|
||||||
|
# Underground Waterway main locations
|
||||||
|
uw_near_ent = "Underground Waterway: Near entrance corridor"
|
||||||
|
uw_across_ent = "Underground Waterway: Across from entrance"
|
||||||
|
uw_poison_parkour = "Underground Waterway: Across poison parkour ledges"
|
||||||
|
uw_waterfall_alcove = "Underground Waterway: Waterfall alcove ledge"
|
||||||
|
uw_carrie1 = "Underground Waterway: Carrie crawlspace corridor - First left"
|
||||||
|
uw_carrie2 = "Underground Waterway: Carrie crawlspace corridor - Second left"
|
||||||
|
uw_bricks_save = "Underground Waterway: Brick platforms save corridor"
|
||||||
|
uw_above_skel_ledge = "Underground Waterway: Above skeleton crusher ledge"
|
||||||
|
|
||||||
|
# Underground Waterway 3-hit breakables
|
||||||
|
uw_first_ledge1 = "Underground Waterway: First poison parkour ledge - Item 1"
|
||||||
|
uw_first_ledge2 = "Underground Waterway: First poison parkour ledge - Item 2"
|
||||||
|
uw_first_ledge3 = "Underground Waterway: First poison parkour ledge - Item 3"
|
||||||
|
uw_first_ledge4 = "Underground Waterway: First poison parkour ledge - Item 4"
|
||||||
|
uw_first_ledge5 = "Underground Waterway: First poison parkour ledge - Item 5"
|
||||||
|
uw_first_ledge6 = "Underground Waterway: First poison parkour ledge - Item 6"
|
||||||
|
uw_in_skel_ledge1 = "Underground Waterway: Inside skeleton crusher ledge - Item 1"
|
||||||
|
uw_in_skel_ledge2 = "Underground Waterway: Inside skeleton crusher ledge - Item 2"
|
||||||
|
uw_in_skel_ledge3 = "Underground Waterway: Inside skeleton crusher ledge - Item 3"
|
||||||
|
|
||||||
|
|
||||||
|
# Castle Center basement main locations
|
||||||
|
ccb_skel_hallway_ent = "Castle Center: Entrance hallway"
|
||||||
|
ccb_skel_hallway_jun = "Castle Center: Basement hallway junction"
|
||||||
|
ccb_skel_hallway_tc = "Castle Center: Torture chamber hallway"
|
||||||
|
ccb_behemoth_l_ff = "Castle Center: Behemoth arena - Left far-front torch"
|
||||||
|
ccb_behemoth_l_mf = "Castle Center: Behemoth arena - Left mid-front torch"
|
||||||
|
ccb_behemoth_l_mr = "Castle Center: Behemoth arena - Left mid-rear torch"
|
||||||
|
ccb_behemoth_l_fr = "Castle Center: Behemoth arena - Left far-rear torch"
|
||||||
|
ccb_behemoth_r_ff = "Castle Center: Behemoth arena - Right far-front torch"
|
||||||
|
ccb_behemoth_r_mf = "Castle Center: Behemoth arena - Right mid-front torch"
|
||||||
|
ccb_behemoth_r_mr = "Castle Center: Behemoth arena - Right mid-rear torch"
|
||||||
|
ccb_behemoth_r_fr = "Castle Center: Behemoth arena - Right far-rear torch"
|
||||||
|
ccb_mandrag_shelf_l = "Castle Center: Mandragora shelf - Left"
|
||||||
|
ccb_mandrag_shelf_r = "Castle Center: Mandragora shelf - Right"
|
||||||
|
ccb_torture_rack = "Castle Center: Torture chamber instrument rack"
|
||||||
|
ccb_torture_rafters = "Castle Center: Torture chamber rafters"
|
||||||
|
|
||||||
|
# Castle Center elevator room main locations
|
||||||
|
ccelv_near_machine = "Castle Center: Near elevator room machine"
|
||||||
|
ccelv_atop_machine = "Castle Center: Atop elevator room machine"
|
||||||
|
ccelv_pipes = "Castle Center: Elevator pipe device"
|
||||||
|
ccelv_staircase = "Castle Center: Elevator room staircase"
|
||||||
|
|
||||||
|
# Castle Center factory floor main locations
|
||||||
|
ccff_redcarpet_knight = "Castle Center: Red carpet hall knight"
|
||||||
|
ccff_gears_side = "Castle Center: Gear room side"
|
||||||
|
ccff_gears_mid = "Castle Center: Gear room center"
|
||||||
|
ccff_gears_corner = "Castle Center: Gear room corner"
|
||||||
|
ccff_lizard_knight = "Castle Center: Lizard locker knight"
|
||||||
|
ccff_lizard_pit = "Castle Center: Lizard locker room near pit"
|
||||||
|
ccff_lizard_corner = "Castle Center: Lizard locker room corner"
|
||||||
|
|
||||||
|
# Castle Center lizard lab main locations
|
||||||
|
ccll_brokenstairs_floor = "Castle Center: Broken staircase floor"
|
||||||
|
ccll_brokenstairs_knight = "Castle Center: Broken staircase knight"
|
||||||
|
ccll_brokenstairs_save = "Castle Center: Above broken staircase savepoint"
|
||||||
|
ccll_glassknight_l = "Castle Center: Stained Glass Knight room - Left"
|
||||||
|
ccll_glassknight_r = "Castle Center: Stained Glass Knight room - Right"
|
||||||
|
ccll_butlers_door = "Castle Center: Butler bros. room near door"
|
||||||
|
ccll_butlers_side = "Castle Center: Butler bros. room inner"
|
||||||
|
ccll_cwhall_butlerflames_past = "Castle Center: Past butler room flamethrowers"
|
||||||
|
ccll_cwhall_flamethrower = "Castle Center: Inside cracked wall hallway flamethrower"
|
||||||
|
ccll_cwhall_cwflames = "Castle Center: Past upper cracked wall flamethrowers"
|
||||||
|
ccll_cwhall_wall = "Castle Center: Inside upper cracked wall"
|
||||||
|
ccll_heinrich = "Castle Center: Heinrich Meyer"
|
||||||
|
|
||||||
|
# Castle Center library main locations
|
||||||
|
ccl_bookcase = "Castle Center: Library bookshelf"
|
||||||
|
|
||||||
|
# Castle Center invention area main locations
|
||||||
|
ccia_nitro_crates = "Castle Center: Nitro room crates"
|
||||||
|
ccia_nitro_shelf_h = "Castle Center: Magical Nitro shelf - Heinrich side"
|
||||||
|
ccia_nitro_shelf_i = "Castle Center: Magical Nitro shelf - Invention side"
|
||||||
|
ccia_nitrohall_torch = "Castle Center: Past nitro room flamethrowers"
|
||||||
|
ccia_nitrohall_flamethrower = "Castle Center: Inside nitro hallway flamethrower"
|
||||||
|
ccia_inventions_crusher = "Castle Center: Invention room spike crusher door"
|
||||||
|
ccia_inventions_maids = "Castle Center: Invention room maid sisters door"
|
||||||
|
ccia_inventions_round = "Castle Center: Invention room round machine"
|
||||||
|
ccia_inventions_famicart = "Castle Center: Invention room giant Famicart"
|
||||||
|
ccia_inventions_zeppelin = "Castle Center: Invention room zeppelin"
|
||||||
|
ccia_maids_outer = "Castle Center: Maid sisters room outer table"
|
||||||
|
ccia_maids_inner = "Castle Center: Maid sisters room inner table"
|
||||||
|
ccia_maids_vase = "Castle Center: Maid sisters room vase"
|
||||||
|
ccia_stairs_knight = "Castle Center: Hell Knight landing corner knight"
|
||||||
|
|
||||||
|
# Castle Center sub-weapons
|
||||||
|
ccb_skel_hallway_ba = "Castle Center: Behemoth arena hallway"
|
||||||
|
ccelv_switch = "Castle Center: Near elevator switch"
|
||||||
|
ccff_lizard_near_knight = "Castle Center: Near lizard locker knight"
|
||||||
|
|
||||||
|
# Castle Center lizard lockers
|
||||||
|
ccff_lizard_locker_nfr = "Castle Center: Far-right near-side lizard locker"
|
||||||
|
ccff_lizard_locker_nmr = "Castle Center: Mid-right near-side lizard locker"
|
||||||
|
ccff_lizard_locker_nml = "Castle Center: Mid-left near-side lizard locker"
|
||||||
|
ccff_lizard_locker_nfl = "Castle Center: Far-left near-side lizard locker"
|
||||||
|
ccff_lizard_locker_fl = "Castle Center: Left far-side lizard locker"
|
||||||
|
ccff_lizard_locker_fr = "Castle Center: Right far-side lizard locker"
|
||||||
|
|
||||||
|
# Castle Center 3-hit breakables
|
||||||
|
ccb_behemoth_crate1 = "Castle Center: Behemoth arena crate - Item 1"
|
||||||
|
ccb_behemoth_crate2 = "Castle Center: Behemoth arena crate - Item 2"
|
||||||
|
ccb_behemoth_crate3 = "Castle Center: Behemoth arena crate - Item 3"
|
||||||
|
ccb_behemoth_crate4 = "Castle Center: Behemoth arena crate - Item 4"
|
||||||
|
ccb_behemoth_crate5 = "Castle Center: Behemoth arena crate - Item 5"
|
||||||
|
ccelv_stand1 = "Castle Center: Elevator room unoccupied statue stand - Item 1"
|
||||||
|
ccelv_stand2 = "Castle Center: Elevator room unoccupied statue stand - Item 2"
|
||||||
|
ccelv_stand3 = "Castle Center: Elevator room unoccupied statue stand - Item 3"
|
||||||
|
ccff_lizard_slab1 = "Castle Center: Lizard locker room slab - Item 1"
|
||||||
|
ccff_lizard_slab2 = "Castle Center: Lizard locker room slab - Item 2"
|
||||||
|
ccff_lizard_slab3 = "Castle Center: Lizard locker room slab - Item 3"
|
||||||
|
ccff_lizard_slab4 = "Castle Center: Lizard locker room slab - Item 4"
|
||||||
|
|
||||||
|
|
||||||
|
# Duel Tower main locations
|
||||||
|
dt_stones_start = "Duel Tower: Stepping stone path start"
|
||||||
|
dt_werebull_arena = "Duel Tower: Above Were-bull arena"
|
||||||
|
dt_ibridge_l = "Duel Tower: Invisible bridge balcony - Left"
|
||||||
|
dt_ibridge_r = "Duel Tower: Invisible bridge balcony - Right"
|
||||||
|
|
||||||
|
# Duel Tower sub-weapons
|
||||||
|
dt_stones_end = "Duel Tower: Stepping stone path end"
|
||||||
|
|
||||||
|
|
||||||
|
# Tower of Execution main locations
|
||||||
|
toe_midsavespikes_r = "Tower of Execution: Past mid-savepoint spikes - Right"
|
||||||
|
toe_midsavespikes_l = "Tower of Execution: Past mid-savepoint spikes - Left"
|
||||||
|
toe_elec_grate = "Tower of Execution: Electric grate ledge"
|
||||||
|
toe_ibridge = "Tower of Execution: Invisible bridge ledge"
|
||||||
|
toe_top = "Tower of Execution: Guillotine tower top level"
|
||||||
|
toe_keygate_l = "Tower of Execution: Key gate alcove - Left"
|
||||||
|
|
||||||
|
# Tower of Execution 3-hit breakables
|
||||||
|
toe_ledge1 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 1"
|
||||||
|
toe_ledge2 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 2"
|
||||||
|
toe_ledge3 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 3"
|
||||||
|
toe_ledge4 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 4"
|
||||||
|
toe_ledge5 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 5"
|
||||||
|
|
||||||
|
# Tower of Execution sub-weapons
|
||||||
|
toe_keygate_r = "Tower of Execution: Key gate alcove - Right"
|
||||||
|
|
||||||
|
|
||||||
|
# Tower of Science main locations
|
||||||
|
tosci_elevator = "Tower of Science: Elevator hallway"
|
||||||
|
tosci_plain_sr = "Tower of Science: Plain sight side room"
|
||||||
|
tosci_stairs_sr = "Tower of Science: Staircase side room"
|
||||||
|
tosci_three_door_hall = "Tower of Science: Pick-a-door hallway locked middle room"
|
||||||
|
tosci_ibridge_t = "Tower of Science: Invisible bridge platform torch"
|
||||||
|
tosci_conveyor_sr = "Tower of Science: Spiky conveyor side room"
|
||||||
|
tosci_exit = "Tower of Science: Exit hallway"
|
||||||
|
tosci_key3_r = "Tower of Science: Locked Key3 room - Right"
|
||||||
|
tosci_key3_l = "Tower of Science: Locked Key3 room - Left"
|
||||||
|
|
||||||
|
# Tower of Science 3-hit breakables
|
||||||
|
tosci_ibridge_b1 = "Tower of Science: Invisible bridge platform crate - Item 1"
|
||||||
|
tosci_ibridge_b2 = "Tower of Science: Invisible bridge platform crate - Item 2"
|
||||||
|
tosci_ibridge_b3 = "Tower of Science: Invisible bridge platform crate - Item 3"
|
||||||
|
tosci_ibridge_b4 = "Tower of Science: Invisible bridge platform crate - Item 4"
|
||||||
|
tosci_ibridge_b5 = "Tower of Science: Invisible bridge platform crate - Item 5"
|
||||||
|
tosci_ibridge_b6 = "Tower of Science: Invisible bridge platform crate - Item 6"
|
||||||
|
|
||||||
|
# Tower of Science sub-weapons
|
||||||
|
tosci_key3_m = "Tower of Science: Locked Key3 room - Middle"
|
||||||
|
|
||||||
|
|
||||||
|
# Tower of Sorcery main locations
|
||||||
|
tosor_stained_tower = "Tower of Sorcery: Stained glass tower"
|
||||||
|
tosor_savepoint = "Tower of Sorcery: Mid-savepoint platform"
|
||||||
|
tosor_trickshot = "Tower of Sorcery: Trick shot from mid-savepoint platform"
|
||||||
|
tosor_yellow_bubble = "Tower of Sorcery: Above yellow bubble"
|
||||||
|
tosor_blue_platforms = "Tower of Sorcery: Above tiny blue platforms start"
|
||||||
|
tosor_side_isle = "Tower of Sorcery: Lone red platform side island"
|
||||||
|
tosor_ibridge = "Tower of Sorcery: Invisible bridge platform"
|
||||||
|
|
||||||
|
# Room of Clocks main locations
|
||||||
|
roc_ent_l = "Room of Clocks: Left from entrance hallway"
|
||||||
|
roc_cont_r = "Room of Clocks: Right of Contract"
|
||||||
|
roc_ent_r = "Room of Clocks: Right from entrance hallway"
|
||||||
|
|
||||||
|
# Room of Clocks sub-weapons
|
||||||
|
roc_elev_l = "Room of Clocks: Left of elevator hallway"
|
||||||
|
roc_elev_r = "Room of Clocks: Right of elevator hallway"
|
||||||
|
|
||||||
|
# Room of Clocks empty breakables
|
||||||
|
roc_cont_l = "Room of Clocks: Left of Contract"
|
||||||
|
roc_exit = "Room of Clocks: Left of exit"
|
||||||
|
|
||||||
|
# Clock Tower main locations
|
||||||
|
ct_gearclimb_corner = "Clock Tower: Gear climb room corner"
|
||||||
|
ct_gearclimb_side = "Clock Tower: Gear climb room side"
|
||||||
|
ct_bp_chasm_fl = "Clock Tower: Bone Pillar chasm room - Front-left"
|
||||||
|
ct_bp_chasm_fr = "Clock Tower: Bone Pillar chasm room - Front-right"
|
||||||
|
ct_bp_chasm_k = "Clock Tower: Bone Pillar chasm room key alcove"
|
||||||
|
ct_finalroom_platform = "Clock Tower: Final room key ledge"
|
||||||
|
|
||||||
|
# Clock Tower 3-hit breakables
|
||||||
|
ct_gearclimb_battery_slab1 = "Clock Tower: Gear climb room beneath battery slab - Item 1"
|
||||||
|
ct_gearclimb_battery_slab2 = "Clock Tower: Gear climb room beneath battery slab - Item 2"
|
||||||
|
ct_gearclimb_battery_slab3 = "Clock Tower: Gear climb room beneath battery slab - Item 3"
|
||||||
|
ct_gearclimb_door_slab1 = "Clock Tower: Gear climb room beneath door slab - Item 1"
|
||||||
|
ct_gearclimb_door_slab2 = "Clock Tower: Gear climb room beneath door slab - Item 2"
|
||||||
|
ct_gearclimb_door_slab3 = "Clock Tower: Gear climb room beneath door slab - Item 3"
|
||||||
|
ct_finalroom_door_slab1 = "Clock Tower: Final room entrance slab - Item 1"
|
||||||
|
ct_finalroom_door_slab2 = "Clock Tower: Final room entrance slab - Item 2"
|
||||||
|
ct_finalroom_renon_slab1 = "Clock Tower: Renon's final offers slab - Item 1"
|
||||||
|
ct_finalroom_renon_slab2 = "Clock Tower: Renon's final offers slab - Item 2"
|
||||||
|
ct_finalroom_renon_slab3 = "Clock Tower: Renon's final offers slab - Item 3"
|
||||||
|
ct_finalroom_renon_slab4 = "Clock Tower: Renon's final offers slab - Item 4"
|
||||||
|
ct_finalroom_renon_slab5 = "Clock Tower: Renon's final offers slab - Item 5"
|
||||||
|
ct_finalroom_renon_slab6 = "Clock Tower: Renon's final offers slab - Item 6"
|
||||||
|
ct_finalroom_renon_slab7 = "Clock Tower: Renon's final offers slab - Item 7"
|
||||||
|
ct_finalroom_renon_slab8 = "Clock Tower: Renon's final offers slab - Item 8"
|
||||||
|
|
||||||
|
# Clock Tower sub-weapons
|
||||||
|
ct_bp_chasm_rl = "Clock Tower: Bone Pillar chasm room - Rear-left"
|
||||||
|
ct_finalroom_fr = "Clock Tower: Final room floor - front-right"
|
||||||
|
ct_finalroom_fl = "Clock Tower: Final room floor - front-left"
|
||||||
|
ct_finalroom_rr = "Clock Tower: Final room floor - rear-right"
|
||||||
|
ct_finalroom_rl = "Clock Tower: Final room floor - rear-left"
|
||||||
|
|
||||||
|
|
||||||
|
# Castle Keep main locations
|
||||||
|
ck_flame_l = "Castle Keep: Left Dracula door flame"
|
||||||
|
ck_flame_r = "Castle Keep: Right Dracula door flame"
|
||||||
|
ck_behind_drac = "Castle Keep: Behind Dracula's chamber"
|
||||||
|
ck_cube = "Castle Keep: Dracula's floating cube"
|
||||||
|
|
||||||
|
|
||||||
|
# Renon's shop locations
|
||||||
|
renon1 = "Renon's shop: Roast Chicken purchase"
|
||||||
|
renon2 = "Renon's shop: Roast Beef purchase"
|
||||||
|
renon3 = "Renon's shop: Healing Kit purchase"
|
||||||
|
renon4 = "Renon's shop: Purifying purchase"
|
||||||
|
renon5 = "Renon's shop: Cure Ampoule purchase"
|
||||||
|
renon6 = "Renon's shop: Sun Card purchase"
|
||||||
|
renon7 = "Renon's shop: Moon Card purchase"
|
||||||
|
|
||||||
|
|
||||||
|
# Events
|
||||||
|
forest_boss_one = "Forest of Silence: King Skeleton 1"
|
||||||
|
forest_boss_two = "Forest of Silence: Were-tiger"
|
||||||
|
forest_boss_three = "Forest of Silence: King Skeleton 2"
|
||||||
|
cw_boss = "Castle Wall: Bone Dragons"
|
||||||
|
villa_boss_one = "Villa: J. A. Oldrey"
|
||||||
|
villa_boss_two = "Villa: Undead Maiden"
|
||||||
|
uw_boss = "Underground Waterway: Lizard-man trio"
|
||||||
|
cc_boss_one = "Castle Center: Behemoth"
|
||||||
|
cc_boss_two = "Castle Center: Rosa/Camilla"
|
||||||
|
dt_boss_one = "Duel Tower: Were-jaguar"
|
||||||
|
dt_boss_two = "Duel Tower: Werewolf"
|
||||||
|
dt_boss_three = "Duel Tower: Were-bull"
|
||||||
|
dt_boss_four = "Duel Tower: Were-tiger"
|
||||||
|
roc_boss = "Room of Clocks: Death/Actrise"
|
||||||
|
ck_boss_one = "Castle Keep: Renon"
|
||||||
|
ck_boss_two = "Castle Keep: Vincent"
|
||||||
|
|
||||||
|
cc_behind_the_seal = "Castle Center: Behind the seal"
|
||||||
|
|
||||||
|
the_end = "Dracula"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,63 @@
|
||||||
|
forest_of_silence = "Forest of Silence"
|
||||||
|
forest_start = "Forest of Silence: first half"
|
||||||
|
forest_mid = "Forest of Silence: second half"
|
||||||
|
forest_end = "Forest of Silence: end area"
|
||||||
|
|
||||||
|
castle_wall = "Castle Wall"
|
||||||
|
cw_start = "Castle Wall: main area"
|
||||||
|
cw_exit = "Castle Wall: exit room"
|
||||||
|
cw_ltower = "Castle Wall: left tower"
|
||||||
|
|
||||||
|
villa = "Villa"
|
||||||
|
villa_start = "Villa: dog gates"
|
||||||
|
villa_main = "Villa: main interior"
|
||||||
|
villa_storeroom = "Villa: storeroom"
|
||||||
|
villa_archives = "Villa: archives"
|
||||||
|
villa_maze = "Villa: maze"
|
||||||
|
villa_servants = "Villa: servants entrance"
|
||||||
|
villa_crypt = "Villa: crypt"
|
||||||
|
|
||||||
|
tunnel = "Tunnel"
|
||||||
|
tunnel_start = "Tunnel: first half"
|
||||||
|
tunnel_end = "Tunnel: second half"
|
||||||
|
|
||||||
|
underground_waterway = "Underground Waterway"
|
||||||
|
uw_main = "Underground Waterway: main area"
|
||||||
|
uw_end = "Underground Waterway: end"
|
||||||
|
|
||||||
|
castle_center = "Castle Center"
|
||||||
|
cc_main = "Castle Center: main area"
|
||||||
|
cc_crystal = "Castle Center: big crystal"
|
||||||
|
cc_torture_chamber = "Castle Center: torture chamber"
|
||||||
|
cc_library = "Castle Center: library"
|
||||||
|
cc_elev_top = "Castle Center: elevator top"
|
||||||
|
|
||||||
|
duel_tower = "Duel Tower"
|
||||||
|
dt_main = "Duel Tower"
|
||||||
|
|
||||||
|
tower_of_sorcery = "Tower of Sorcery"
|
||||||
|
tosor_main = "Tower of Sorcery"
|
||||||
|
|
||||||
|
tower_of_execution = "Tower of Execution"
|
||||||
|
toe_main = "Tower of Execution: main area"
|
||||||
|
toe_ledge = "Tower of Execution: gated ledge"
|
||||||
|
|
||||||
|
tower_of_science = "Tower of Science"
|
||||||
|
tosci_start = "Tower of Science: turret lab"
|
||||||
|
tosci_three_doors = "Tower of Science: locked key1 room"
|
||||||
|
tosci_conveyors = "Tower of Science: spiky conveyors"
|
||||||
|
tosci_key3 = "Tower of Science: locked key3 room"
|
||||||
|
|
||||||
|
room_of_clocks = "Room of Clocks"
|
||||||
|
roc_main = "Room of Clocks"
|
||||||
|
|
||||||
|
clock_tower = "Clock Tower"
|
||||||
|
ct_start = "Clock Tower: start"
|
||||||
|
ct_middle = "Clock Tower: middle"
|
||||||
|
ct_end = "Clock Tower: end"
|
||||||
|
|
||||||
|
castle_keep = "Castle Keep"
|
||||||
|
ck_main = "Castle Keep: exterior"
|
||||||
|
ck_drac_chamber = "Castle Keep: Dracula's chamber"
|
||||||
|
|
||||||
|
renon = "Renon's shop"
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Castlevania 64
|
||||||
|
|
||||||
|
## 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?
|
||||||
|
|
||||||
|
All items that you would normally pick up throughout the game, be it from candles, breakables, or sitting out, have been
|
||||||
|
moved around. This includes the key items that the player would normally need to find to progress in some stages, which can
|
||||||
|
now be found outside their own stages, so returning to previously-visited stages will very likely be necessary (see: [How do
|
||||||
|
I jump to a different stage?](#how-do-i-jump-to-a-different-stage?)). The positions of the stages can optionally be randomized
|
||||||
|
too, so you may start out in Duel Tower and get Forest of Silence as your penultimate stage before Castle Keep, amongst
|
||||||
|
many other possibilities.
|
||||||
|
|
||||||
|
## How do I jump to a different stage?
|
||||||
|
|
||||||
|
Instant travel to an earlier or later stage is made possible through the Warp Menu, a major addition to the game that can
|
||||||
|
be pulled up while not in a boss fight by pressing START while holding Z and R. By finding Special1 jewels (the item that
|
||||||
|
unlocks Hard Mode in vanilla Castlevania 64), more destinations become available to be selected on this menu. The destinations
|
||||||
|
on the list are randomized per seed and the first one, which requires no Special1s to select, will always be your starting
|
||||||
|
area.
|
||||||
|
|
||||||
|
NOTE: Regardless of which option on the menu you are currently highlighting, you can hold Z or R while making your selection
|
||||||
|
to return to Villa's crypt or Castle Center's top elevator room respectively, provided you've already been to that place at
|
||||||
|
least once. This can make checking out both character stages at the start of a route divergence far less of a hassle.
|
||||||
|
|
||||||
|
## Can I do everything as one character?
|
||||||
|
|
||||||
|
Yes! The Villa end-of-level coffin has had its behavior modified so that which character stage slot it sends you to
|
||||||
|
depends on the time of day, and likewise both bridges at the top of Castle Center's elevator are intact so both exits are
|
||||||
|
reachable regardless of who you are. With these changes in game behavior, every stage can be accessed by any character
|
||||||
|
in a singular run.
|
||||||
|
|
||||||
|
NOTE: By holding L while loading into a map (this can even be done while cancelling out of the Warp Menu), you can swap to
|
||||||
|
the other character you are not playing as, and/or hold C-Up to swap to and from the characters' alternate costumes. Unless
|
||||||
|
you have Carrie Logic enabled, and you are not playing as her, switching should never be necessary.
|
||||||
|
|
||||||
|
## What is the goal of Castlevania 64 when randomized?
|
||||||
|
|
||||||
|
Make it to Castle Keep, enter Dracula's chamber, and defeat him to trigger an ending and complete your goal. Whether you
|
||||||
|
get your character's good or bad ending does **not** matter; the goal will send regardless. Options exist to force a specific
|
||||||
|
ending for those who prefer a specific one.
|
||||||
|
|
||||||
|
Dracula's chamber's entrance door is initially locked until whichever of the following objectives that was specified on your
|
||||||
|
YAML under `draculas_condition` is completed:
|
||||||
|
- `crystal`: Activate the big crystal in the basement of Castle Center. Doing this entails finding two Magical Nitros and
|
||||||
|
two Mandragoras to blow up both cracked walls (see: [How does the Nitro transport work in this?](#how-does-the-nitro-transport-work-in-this?)).
|
||||||
|
Behemoth and Rosa/Camilla do **NOT** have to be defeated.
|
||||||
|
- `bosses`: Kill bosses with visible health meters to earn Trophies. The number of Trophies required can be specified under
|
||||||
|
`bosses_required`.
|
||||||
|
- `special2s`: Find enough Special2 jewels (the item that normally unlocks alternate costumes) that are shuffled in the
|
||||||
|
regular item pool. The total amount and percent needed can be specified under `total_special2s` and `percent_special2s_required` respectively.
|
||||||
|
|
||||||
|
If `none` was specified, then there is no objective. Dracula's chamber door is unlocked from the start, and you merely have to reach it.
|
||||||
|
|
||||||
|
## What items and locations get shuffled?
|
||||||
|
|
||||||
|
Inventory items, jewels, moneybags, and PowerUps are all placed in the item pool by default. Randomizing Sub-weapons is optional,
|
||||||
|
and they can be shuffled in their own separate pool or in the main item pool. An optional hack can be enabled to make your
|
||||||
|
old sub-weapon drop behind you when you receive a different one, so you can pick it up again if you still want it. Location
|
||||||
|
checks by default include freestanding items, items from one-hit breakables, and the very few items given through NPC text. Additional
|
||||||
|
locations that can be toggled are:
|
||||||
|
- Objects that break in three hits.
|
||||||
|
- Sub-weapon locations if they have been shuffled anywhere.
|
||||||
|
- Seven items sold by the shopkeeper Renon.
|
||||||
|
- The two items beyond the crawlspace in Waterway that normally require Carrie, if Carrie Logic is on.
|
||||||
|
- The six items inside the Lizard-man generators in Castle Center that open randomly to spawn Lizard-men. These are particularly annoying!
|
||||||
|
|
||||||
|
## How does the Nitro transport work in this?
|
||||||
|
|
||||||
|
Two Magical Nitros and two Mandragoras are placed into the item pool for blowing up the cracked walls in Castle Center
|
||||||
|
and two randomized items are placed on both of their shelves. The randomized Magical Nitro will **NOT** kill you upon landing
|
||||||
|
or taking damage, so don't panic when you receive one! Hazardous Waste Dispoal bins are disabled and the basement crack with
|
||||||
|
a seal will not let you set anything at it until said seal is removed so none of the limited ingredients can be wasted.
|
||||||
|
|
||||||
|
In short, Nitro is still in, explode-y business is not! Unless you turn on explosive DeathLink, that is...
|
||||||
|
|
||||||
|
## Which items can be in another player's world?
|
||||||
|
|
||||||
|
Any of the items which can be shuffled may also be placed into another player's world. The exception is if sub-weapons
|
||||||
|
are shuffled in their own pool, in which case they will only appear in your world in sub-weapon spots.
|
||||||
|
|
||||||
|
## What does another world's item look like in Castlevania 64?
|
||||||
|
|
||||||
|
An item belonging to another world will show up as that item if it's from another Castlevania 64 world, or one of two
|
||||||
|
Archipelago logo icons if it's from a different game entirely. If the icon is big and has an orange arrow in the top-right
|
||||||
|
corner, it is a progression item for that world; definitely get these! Otherwise, if it's small and with no arrow, it is
|
||||||
|
either filler, useful, or a trap.
|
||||||
|
|
||||||
|
When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you
|
||||||
|
found and who it was for. The color of the text will tell you its classification:
|
||||||
|
- <font color="moccasin">Light brown-ish</font>: Common
|
||||||
|
- <font color="white">White</font>/<font color="yellow">Yellow</font>: Useful
|
||||||
|
- <font color="yellow">Yellow</font>/<font color="lime">Green</font>: Progression
|
||||||
|
- <font color="yellow">Yellow</font>/<font color="red">Red</font>: Trap
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
A textbox containing the name of the item and the player who sent it will appear, and they will get it.
|
||||||
|
Just like the textbox that appears when sending an item, the color of the text will tell you its classification.
|
||||||
|
|
||||||
|
NOTE: You can press B to close the item textbox instantly and get through your item queue quicker.
|
||||||
|
|
||||||
|
## What tricks and glitches should I know for Hard Logic?
|
||||||
|
|
||||||
|
The following tricks always have a chance to be required:
|
||||||
|
- Left Tower Skip in Castle Wall
|
||||||
|
- Copper Door Skip in Villa (both characters have their own methods for this)
|
||||||
|
- Waterfall Skip if you travel backwards into Underground Waterway
|
||||||
|
- Slope Jump to Room of Clocks from Castle Keep
|
||||||
|
- Jump to the gated ledge from the level above in Tower of Execution
|
||||||
|
|
||||||
|
Enabling Carrie Logic will also expect the following:
|
||||||
|
|
||||||
|
- Orb-sniping dogs through the front gates in Villa
|
||||||
|
|
||||||
|
Library Skip is **NOT** logically expected on any setting. The basement hallway crack will always logically expect two Nitros
|
||||||
|
and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing
|
||||||
|
to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room.
|
||||||
|
|
||||||
|
## What are the item name groups?
|
||||||
|
The groups you can use for Castlevania 64 are `bomb` and `ingredient`, both of which will hint randomly for either a
|
||||||
|
Magical Nitro or Mandragora.
|
||||||
|
|
||||||
|
## What are the location name groups?
|
||||||
|
In Castlevania 64, every location that is specific to a stage is part of a location group under that stage's name.
|
||||||
|
So if you want to exclude all of, say, Duel Tower, you can do so by just excluding "Duel Tower" as a whole.
|
||||||
|
|
||||||
|
## I'm stuck and/or I can't find this hinted location...is there a map tracker?
|
||||||
|
At the moment, no map tracker exists. [Here](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds/cv64/docs/obscure_checks.md)
|
||||||
|
is a list of many checks that someone could very likely miss, with instructions on how to find them. See if the check you
|
||||||
|
are missing is on there and if it isn't, or you still can't find it, reach out in the [Archipelago Discord server](https://discord.gg/archipelago)
|
||||||
|
to inquire about having the list updated if you think it should be.
|
||||||
|
|
||||||
|
If you are new to this randomizer, it is strongly recommended to play with the Countdown option enabled to at least give you a general
|
||||||
|
idea of where you should be looking if you get completely stuck. It can track the total number of unchecked locations in the
|
||||||
|
area you are currently in, or the total remaining majors.
|
||||||
|
|
||||||
|
## Why does the game stop working when I sit on the title screen for too long?
|
||||||
|
This is an issue that existed with Castlevania 64 on mupen64plus way back in 2017, and BizHawk never updated their
|
||||||
|
mupen64plus core since it was fixed way back then. This is a Castlevania 64 in general problem that happens even with the
|
||||||
|
vanilla ROM, so there's not much that can done about it besides opening an issue to them (which [has been done](https://github.com/TASEmulators/BizHawk/issues/3670))
|
||||||
|
and hoping they update their mupen64plus core one day...
|
||||||
|
|
||||||
|
## How the f*** do I set Nitro/Mandragora?
|
||||||
|
<font color="yellow">(>)</font>
|
|
@ -0,0 +1,429 @@
|
||||||
|
# Obscure locations in the AP Castlevania 64 randomizer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Forest of Silence
|
||||||
|
|
||||||
|
#### Invisible bridge platform
|
||||||
|
A square platform floating off to the side of the broken bridge between the three-crypt and Werewolf areas. There's an
|
||||||
|
invisible bridge connecting it with the middle piece on the broken bridge that you can cross over to reach it. This is
|
||||||
|
where you normally get the Special1 in vanilla that unlocks Hard Mode.
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Dirge maiden pedestal plaque
|
||||||
|
This plaque in question can be found on the statue pedestal with a torch on it in the area right after the first switch gate,
|
||||||
|
near the cliff where the breakable rock can be found. The plaque reads "A maiden sings a dirge" if you check it after you
|
||||||
|
pick up the item there, hence the name of all the locations in this area.
|
||||||
|
|
||||||
|
#### Werewolf statue plaque
|
||||||
|
The plaque on the statue pedestal in the area inhabited by the Werewolf. Reading this plaque after picking up the item
|
||||||
|
says it's "the lady who blesses and restores."
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Dirge maiden pedestal rock
|
||||||
|
This rock can be found near the cliff behind the empty above-mentioned dirge maiden pedestal. Normally has a ton of money
|
||||||
|
in vanilla, contains 5 checks in rando.
|
||||||
|
|
||||||
|
#### Bat archway rock
|
||||||
|
After the broken bridge containing the invisible pathway to the Special1 in vanilla, this rock is off to the side in front
|
||||||
|
of the gate frame with a swarm of bats that come at you, before the Werewolf's territory. Contains 4 checks. If you are new
|
||||||
|
to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guranteed spot to find a PowerUp at.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Castle Wall
|
||||||
|
#### Above bottom right/left tower door
|
||||||
|
These checks are located on top of the bottom doorways inside the both the Left and Right Tower. You have to drop from above
|
||||||
|
to reach them. In the case of the left tower, it's probably easiest to wait on the green platform directly above it until it flips.
|
||||||
|
|
||||||
|
#### Left tower child ledge
|
||||||
|
When you reach the bridge of four rotating green platforms, look towards the pillar in the center of the room (hold C-up to
|
||||||
|
enter first person view), and you'll see this. There's an invisible bridge between the rotating platforms and the tiny ledge
|
||||||
|
that you can use to get to and from it. In Legacy of Darkness, it is on this ledge where one of Henry's children is found.
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Sandbag shelf - Left
|
||||||
|
If you thought the PowerUp on this shelf in vanilla CV64 was the only thing here, then you'd be wrong! Hidden inside the
|
||||||
|
sandbags near the item is another item you can pick up before subsequent checks on this spot yield "only sand and gravel".
|
||||||
|
Legacy took this item out entirely, interestingly enough.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Upper rampart savepoint slab
|
||||||
|
After killing the two White Dragons and flipping their switch, drop down onto this platform from the top, and you'll find
|
||||||
|
it near the White Jewel. Contains 5 checks that are all normally Red Jewels in vanilla, making it an excellent place to
|
||||||
|
fuel up at if you're not doing Left Tower Skip. Just be careful of the infinitely spawning skeletons!
|
||||||
|
|
||||||
|
#### Dracula switch slab
|
||||||
|
Located behind the door that you come out of at the top of the left tower where you encounter Totally Real Dracula in a
|
||||||
|
cutscene. Contains 5 checks that are all normally money; take note of all these money spots if you're plaing vanilla and
|
||||||
|
plan on trying to trigger the Renon fight.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Villa
|
||||||
|
#### Outer front gate platform
|
||||||
|
From the start of the level, turn right, and you'll see a platform with a torch above a torch on the ground. This upper torch
|
||||||
|
is reachable via an invisible platform that you can grab and pull yourself up onto. The PAL version and onwards removed
|
||||||
|
this secret entirely, interestingly enough.
|
||||||
|
|
||||||
|
#### Front yard cross grave near gates/porch
|
||||||
|
In the Villa's front yard area are two cross-shaped grave markers that are actually 1-hit breakables just like torches.
|
||||||
|
They contain a check each.
|
||||||
|
|
||||||
|
#### Midnight fountain
|
||||||
|
At exactly midnight (0:00 on the pause screen), a pillar in the fountain's base will rise to give you access to the six
|
||||||
|
checks on the upper parts of the fountain. If you're playing with Disable Time Requirements enabled, this pillar will be
|
||||||
|
raised regardless of the current time.
|
||||||
|
|
||||||
|
#### Vincent
|
||||||
|
Vincent has a check that he will give to you by speaking to him after triggering the Rosa cutscene at 3 AM in the rose
|
||||||
|
garden. With Disable Time Requirements enabled, the Rosa cutscene will trigger at any time.
|
||||||
|
|
||||||
|
#### Living room ceiling light
|
||||||
|
In the rectangular living room with ghosts and flying skulls that come at you, there are two yellow lights on the ceiling
|
||||||
|
and one red light between them. The red light can be broken for a check; just jump directly below it and use your c-left
|
||||||
|
attack to hit it.
|
||||||
|
|
||||||
|
#### Front maze garden - Frankie's right dead-end urn
|
||||||
|
When you first enter the maze, before going to trigger the Malus cutscene, go forward, right at the one-way door, then right
|
||||||
|
at the T-junction, and you'll reach a dead-end where the Gardner is just going about his business. The urn on the left
|
||||||
|
at this dead-end can be broken for a check; it's the ONLY urn in the entire maze that can be broken like this.
|
||||||
|
|
||||||
|
#### Crypt bridge upstream
|
||||||
|
After unlocking the Copper Door, follow the stream all the way past the bridge to end up at this torch.
|
||||||
|
I see many people miss this one.
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Front yard visitor's tombstone
|
||||||
|
The tombstone closest to the Villa building itself, in the top-right corner if approaching from the gates. If you are
|
||||||
|
familiar with the puzzle here in Cornell's quest in Legacy, it's the tombstone prepared for "anybody else who drops by
|
||||||
|
to visit".
|
||||||
|
|
||||||
|
#### Foyer sofa
|
||||||
|
The first sofa in the foyer, on the upper floor to the right.
|
||||||
|
|
||||||
|
#### Mary's room table
|
||||||
|
The table closer to the mirror on the right in the small room adjacent to the bedroom, where Mary would normally be found
|
||||||
|
in Cornell's story in Legacy.
|
||||||
|
|
||||||
|
#### Dining room rose vase
|
||||||
|
The vase of roses in the dining room that a rose falls out of in the cutscene here to warn Reinhardt/Carrie of the vampire
|
||||||
|
villager.
|
||||||
|
|
||||||
|
#### Living room clawed painting
|
||||||
|
The painting with claw marks on it above the fireplace in the middle of the living room.
|
||||||
|
|
||||||
|
#### Living room lion head
|
||||||
|
The lion head on the left wall of the living room (if you entered from one of the doors to the main hallway).
|
||||||
|
|
||||||
|
#### Maze garden exit knight
|
||||||
|
The suit of armor in the stairs room before the Maze Garden, where Renon normally introduces himself.
|
||||||
|
|
||||||
|
#### Storeroom statue
|
||||||
|
The weird statue in the back of the Storeroom. If you check it again after taking its item, the game questions why would
|
||||||
|
someone make something like it.
|
||||||
|
|
||||||
|
#### Archives table
|
||||||
|
The table in the middle of the Archives. In Legacy, this is where Oldrey's diary normally sits if you are playing Cornell.
|
||||||
|
|
||||||
|
#### Malus's hiding bush
|
||||||
|
The bush that Reinhardt/Carrie find Malus hiding in at the start of the Maze Garden chase sequence.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Foyer chandelier
|
||||||
|
The big chandelier above the foyer can be broken for 5 assorted items, all of which become checks in rando with the multi
|
||||||
|
hits setting on. This is the only 3-hit breakable in the entire stage. <br>
|
||||||
|
|
||||||
|
Here's a fun fact about the chandelier: for some reason, KCEK made this thing a completely separate object from every other
|
||||||
|
3-hit breakable in the game, complete with its own distinct code that I had to modify entirely separately as I was making
|
||||||
|
this rando to make the 3-hit breakable setting feasible! What fun!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tunnel
|
||||||
|
#### Stepping stone alcove
|
||||||
|
After the first save following the initial Spider Women encounter, take the first right you see, and you'll arrive back at
|
||||||
|
the poison river where there's a second set of stepping stones similar to the one you just jumped across earlier. Jump on
|
||||||
|
these to find the secret alcove containing one or two checks depending on whether sub-weapons are randomized anywhere or not.
|
||||||
|
|
||||||
|
### Sun/Moon Doors
|
||||||
|
|
||||||
|
In total, there are six of these throughout the entire stage. One of them you are required to open in order to leave the stage,
|
||||||
|
while the other five lead to optional side rooms containing items. These are all very skippable in the vanilla game, but in a
|
||||||
|
rando context, it is obviously *very* important you learn where all of them are for when the day comes that a Hookshot
|
||||||
|
lands behind one! If it helps, two of them are before the gondolas, while all the rest are after them.
|
||||||
|
|
||||||
|
#### Lonesome bucket moon door
|
||||||
|
After you ride up on the second elevator, turn left at the first split path you see, and you will find, as I called it, the
|
||||||
|
"Lonesome bucket". Keep moving forward past this, and you will arrive at the moon door. The only thing of value, beside a shop
|
||||||
|
point, is a sub-weapon location. So if you don't have sub-weapons shuffled anywhere, you can very much skip this one.
|
||||||
|
|
||||||
|
#### Gondola rock crusher sun door
|
||||||
|
Once you get past the first poison pit that you are literally required to platform over, go forward at the next junction
|
||||||
|
instead of left (going left is progress and will take you to the rock crusher right before the gondolas). This door notably
|
||||||
|
hides two Roast Beefs normally, making it probably the most worthwhile one to visit in vanilla.
|
||||||
|
|
||||||
|
#### Corpse bucket moon door
|
||||||
|
After the poison pit immediately following the gondola ride, you will arrive at a bucket surrounded by corpses (hence the name).
|
||||||
|
Go left here, and you will arrive at this door.
|
||||||
|
|
||||||
|
#### Shovel zone moon door
|
||||||
|
On the straight path to the end-of-level sun door are two separate poison pits on the right that you can platform over.
|
||||||
|
Both of these lead to and from the same optional area, the "shovel zone" as I call it due to the random shovel you can find
|
||||||
|
here. Follow the path near the shovel that leads away from both poison pits, and you'll arrive at a junction with a save jewel.
|
||||||
|
Go straight on at this junction to arrive at this moon door. This particular one is more notable in Legacy of Darkness as it
|
||||||
|
contains one of the locations of Henry's children.
|
||||||
|
|
||||||
|
#### Shovel zone sun door
|
||||||
|
Same as the above moon door, but go left at the save jewel junction instead of straight.
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Twin arrow signs
|
||||||
|
From the save point after the stepping stones following the initial Spider Women encounter, travel forward until you reach a
|
||||||
|
T-junction with two arrow signs at it. The right-pointing sign here contains an item on its post.
|
||||||
|
|
||||||
|
#### Near lonesome bucket
|
||||||
|
After riding the first upwards elevator following turning left at the twin arrow signs, you'll arrive at the lonesome bucket
|
||||||
|
area, with said bucket being found if you turn left at the first opportunity after said elevator. The item here is not
|
||||||
|
found *in* the bucket, but rather on a completely unmarked spot some meters from it. This had to have been a mistake,
|
||||||
|
seeing as Legacy moved it to actually be in the bucket.
|
||||||
|
|
||||||
|
#### Shovel
|
||||||
|
Can be found by taking either platforming course on the right side of the straightaway to the end after the gondolas.
|
||||||
|
This entire zone is noteable for the fact that there's no reason for Reinhardt to come here in either game; it's only ever
|
||||||
|
required for Henry to rescue one of his children.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Twin arrow signs rock
|
||||||
|
Turn right at the twin arrow signs junction, and you'll find this rock at the dead-end by the river. It contains a bunch of
|
||||||
|
healing and status items that translate into 5 rando checks.
|
||||||
|
|
||||||
|
#### Lonesome bucket poison pit rock
|
||||||
|
Near the lonesome bucket is the start of a platforming course over poison water that connects near Albert's campsite...which
|
||||||
|
you could reach anyway just by traveling forward at the prior junction instead of left. So what's the point of this poison
|
||||||
|
pit, then? Look out into the middle of it, and you'll see this rock on a tiny island out in the middle of it. If you choose
|
||||||
|
to take the hard way here, your reward will be three meat checks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Underground Waterway
|
||||||
|
#### Carrie Crawlspace
|
||||||
|
This is located shortly after the corridor following the ledges that let you reach the first waterfall's source alcove.
|
||||||
|
Notably, only Carrie is able to crouch and go through this, making these the only checks in the *entire* game that are
|
||||||
|
hard impossible without being a specific character. So if you have Carrie Logic on and your character is Reinhardt, you'll
|
||||||
|
have to hold L while loading into a map to change to Carrie just for this one secret. If Carrie Logic is off, then these
|
||||||
|
locations will not be added and you can just skip them entirely.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### First poison parkour ledge
|
||||||
|
Near the start of the level is a series of ledges you can climb onto and platform across to reach a corner with a lantern
|
||||||
|
that you can normally get a Cure Ampoule from. The first of these ledges can be broken for an assortment of 6 things.
|
||||||
|
|
||||||
|
#### Inside skeleton crusher ledge
|
||||||
|
To the left of the hallway entrance leading to the third switch is a long shimmy-able ledge that you can grab onto and shimmy
|
||||||
|
for a whole eternity (I implemented a setting JUST to make shimmying this ledge faster!) to get to a couple stand on-able ledges,
|
||||||
|
one of which has a lantern above it containing a check. This ledge can be broken for 3 chickens. I'd highly suggest bringing
|
||||||
|
Holy Water for this because otherwise you're forced to break it from the other, lower ledge that's here. And this ledge
|
||||||
|
will drop endless crawling skeletons on you as long as you're on it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Castle Center
|
||||||
|
#### Atop elevator room machine
|
||||||
|
In the elevator room, right from the entrance coming in from the vampire triplets' room, is a machine that you can press
|
||||||
|
C-Right on to get dialog reading "An enormous machine." There's a torch on top of this machine that you can reach by
|
||||||
|
climbing onto the slanted part of the walls in the room.
|
||||||
|
|
||||||
|
#### Heinrich Meyer
|
||||||
|
The friendly lizard-man who normally gives you the Chamber Key in vanilla has a check for you just like Vincent.
|
||||||
|
Yes, he has a name! And you'd best not forget it!
|
||||||
|
|
||||||
|
#### Torture chamber rafters
|
||||||
|
A check can be found in the rafters in the room with the Mandragora shelf. Get onto and jump off the giant scythe or the
|
||||||
|
torture instrument shelf to make it up there. It's less annoying to do without Mandragora since having it will cause ghosts to
|
||||||
|
infinitely spawn in here.
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Red carpet hall knight
|
||||||
|
The suit of armor in the red carpet hallway after the bottom elevator room, directly next to the door leading into the
|
||||||
|
Lizard Locker Room.
|
||||||
|
|
||||||
|
#### Lizard locker knight
|
||||||
|
The suit of armor in the Lizard Locker Room itself, directly across from the door connecting to the red carpet hallway.
|
||||||
|
|
||||||
|
#### Broken staircase knight
|
||||||
|
The suit of armor in the broken staircase room following the Lizard Locker Room.
|
||||||
|
|
||||||
|
#### Inside cracked wall hallway flamethrower
|
||||||
|
In the upper cracked wall hallway, it is in the lower flamethrower that is part of the pair between the Butler Bros. Room
|
||||||
|
and the main part of the hallway.
|
||||||
|
|
||||||
|
#### Nitro room crates
|
||||||
|
The wall of crates in the Nitro room on Heinrich Meyer's side. This is notable for being one of the very rare Healing Kits
|
||||||
|
that you can get for free in vanilla.
|
||||||
|
|
||||||
|
#### Hell Knight landing corner knight
|
||||||
|
The inactive suit of armor in the corner of the room before the Maid Sisters' Room, which also contains an active knight.
|
||||||
|
|
||||||
|
#### Maid sisters room vase
|
||||||
|
The lone vase in the vampire Maid Sisters' Room, directly across from the door leading to the Hell Knight Landing.
|
||||||
|
Yes, you are actually supposed to *check* this with C-right to get its item; not break it like you did to the pots in
|
||||||
|
the Villa earlier!
|
||||||
|
|
||||||
|
#### Invention room giant Famicart
|
||||||
|
The giant square-shaped thing in one corner of the invention room that looks vaguely like a massive video game cartridge.
|
||||||
|
A Famicom cartridge, perhaps?
|
||||||
|
|
||||||
|
#### Invention room round machine
|
||||||
|
The brown circular machine in the invention room, close to the middle of the wall on the side of the Spike Crusher Room.
|
||||||
|
|
||||||
|
#### Inside nitro hallway flamethrower
|
||||||
|
The lower flamethrower in the hallway between the Nitro room from the Spike Crusher Room, near the two doors to said rooms.
|
||||||
|
|
||||||
|
#### Torture chamber instrument rack
|
||||||
|
The shelf full of torture instruments in the torture chamber, to the right of the Mandragora shelf.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Behemoth arena crate
|
||||||
|
This large crate can be found in the back-right corner of Behemoth's arena and is pretty hard to miss. Break it to get 5
|
||||||
|
moneybags-turned-checks.
|
||||||
|
|
||||||
|
#### Elevator room unoccupied statue stand
|
||||||
|
In the bottom elevator room is a statue on a stand that will cry literal Bloody Tears if you get near it. On the opposite
|
||||||
|
side of the room from this, near the enormous machine, is a stand much like the one the aforementioned statue is on only
|
||||||
|
this one is completely vacant. This stand can be broken for 3 roast beefs-turned checks.
|
||||||
|
|
||||||
|
#### Lizard locker room slab
|
||||||
|
In the Lizard Locker Room, on top of the second locker from the side of the room with the door to the red carpet hallway,
|
||||||
|
is a metallic box-like slab thingy that can be broken normally for 4 status items. This 3HB is notable for being one of two
|
||||||
|
funny ones in the game that does NOT set a flag when you break it in vanilla, meaning you can keep breaking it over and
|
||||||
|
over again for infinite Purifyings and Cure Ampoules!
|
||||||
|
|
||||||
|
### The Lizard Lockers
|
||||||
|
If you turned on the Lizard Locker Items setting, then hoo boy, you are in for a FUN =) time! Inside each one of the six Lizard
|
||||||
|
Lockers is a check, and the way you get these checks is by camping near each of the lockers as you defeat said Lizards,
|
||||||
|
praying that one will emerge from it and cause it to open to give you a chance to grab it. It is *completely* luck-based,
|
||||||
|
you have a 1-in-6 (around 16%) chance per Lizard-man that emerges, and you have to repeat this process six times for each
|
||||||
|
check inside each locker. You can open and cancel the warp menu to make things easier, but other than that, enjoy playing
|
||||||
|
with the Lizards!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Duel Tower
|
||||||
|
#### Invisible bridge balcony
|
||||||
|
Located between Werewolf and Were-bull's arenas. Use an invisible bridge to reach it; it starts at the highest platform
|
||||||
|
on the same wall as it that appears after defeating Werewolf. The balcony contains two checks and a save point that I
|
||||||
|
added specifically for this rando to make the level less frustrating.
|
||||||
|
|
||||||
|
#### Above Were-bull arena
|
||||||
|
The only check that can be permanently missed in the vanilla game depending on the order you do things, not counting any
|
||||||
|
points of no return. Between Werewolf and Were-bull's arenas is an alternate path that you can take downward and around
|
||||||
|
to avoid Were-bull completely and get on top of his still-raised arena, so you can reach this. In the rando, I set it up so
|
||||||
|
that his arena will go back to being raised if you leave the area and come back, and if you get the item later his arena flag
|
||||||
|
will be set then. If you're playing with Dracula's bosses condition, then you can only get one Special2 off of him the first
|
||||||
|
time you beat him and then none more after that.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tower of Execution
|
||||||
|
#### Invisible bridge ledge
|
||||||
|
There are two ways to reach this one; use the invisible bridge that starts at the walkway above the entrance, or jump to
|
||||||
|
it from the Execution Key gate alcove. Collecting this Special2 in vanilla unlocks Reinhardt's alternate costume.
|
||||||
|
|
||||||
|
#### Guillotine tower top level
|
||||||
|
This iron maiden is strategically placed in such a way that you will very likely miss it if you aren't looking carefully for
|
||||||
|
it, so I am including it here. When you make it to the top floor of the level, as you approach the tower for the final time,
|
||||||
|
look on the opposite side of it from where you approach it, and you will find this. The laggiest check in the whole game!
|
||||||
|
I'd dare someone to find some check in some other Archipelago game that lags harder than this.
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Pre-mid-savepoint platforms ledge
|
||||||
|
Here's a really weird one that even I never found about until well after I finished the 3HB setting and moved on to deving
|
||||||
|
other things, and I'd honestly be shocked if ANYONE knew about outside the context of this rando! This square-shaped
|
||||||
|
platform can be found right before the second set of expanding and retracting wall platforms, leading up to the mid-save
|
||||||
|
point, after going towards the central tower structure for the second time on the second floor. Breaking it will drop an
|
||||||
|
assortment of 5 items, one of which is notable for being the ONE sub-weapon that drops from a 3HB. This meant I had to really
|
||||||
|
change how things work to account for sub-weapons being in 3HBs!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tower of Science
|
||||||
|
#### Invisible bridge platform
|
||||||
|
Following the hallway with a save jewel beyond the Science Key2 door, look to your right, and you'll see this. Mind the
|
||||||
|
gap separating the invisible bridge from the solid ground of the bottom part of this section!
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Invisible bridge platform crate
|
||||||
|
Near the candle on the above-mentioned invisible bridge platform is a small metallic crate. Break it for a total of 6
|
||||||
|
checks, which in vanilla are 2 chickens, moneybags, and jewels.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tower of Sorcery
|
||||||
|
#### Trick shot from mid-savepoint platform
|
||||||
|
From the platform with the save jewel, look back towards the vanishing red platforms that you had to conquer to get up
|
||||||
|
there, and you'll see a breakable diamond floating high above a solid platform before it. An ultra-precise orb shot from
|
||||||
|
Carrie can hit it to reveal the check, so you can then go down and get it. If you are playing as Reinhardt, you'll have
|
||||||
|
to use a sub-weapon. Any sub-weapon that's not Holy Water will work. Sub-weapons and the necessary jewels can both be
|
||||||
|
farmed off the local Icemen if it really comes down to it.
|
||||||
|
|
||||||
|
#### Above yellow bubble
|
||||||
|
Directly above the yellow bubble that you break to raise the middle yellow large platform. Jump off the final red platform
|
||||||
|
in the series of red platforms right after the save point when said platform bobs all the way up, and you'll be able to
|
||||||
|
hit this diamond with good timing.
|
||||||
|
|
||||||
|
#### Above tiny blue platforms start
|
||||||
|
Above the large platform after the yellow ones from whence you can reach the first of the series of tiny blue platforms.
|
||||||
|
This diamond is low enough that you can reach it by simply jumping straight up to it.
|
||||||
|
|
||||||
|
#### Invisible bridge platform
|
||||||
|
Located at the very end of the stage, off to the side. Use the invisible bridge to reach it. The Special2 that unlocks
|
||||||
|
Carrie's costume can be normally found here (or Special3, depending on if you are playing the PAL version or not).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Clock Tower
|
||||||
|
All the normal items here and in Room of Clocks are self-explanatory, but the 3HBs in Clock Tower are a whoooooooole 'nother story. So buckle up...
|
||||||
|
|
||||||
|
### 3-Hit Breakables
|
||||||
|
#### Gear climb room battery underside slab
|
||||||
|
In the first room, on the side you initially climb up, go up until you find a battery-like object that you can stand on
|
||||||
|
as a platform. From the platform right after this, you can hit this 3HB that can be found on the underside of the structure
|
||||||
|
in the corner, above the structure before the first set of gears that you initially start the climb on. 3 chickens/checks
|
||||||
|
can be gotten out of this one.
|
||||||
|
|
||||||
|
#### Gear climb room door underside slab
|
||||||
|
Now THIS one can be very annoying to get, doubly so if you're playing as Reinhardt, triply so if you're playing Hard Mode
|
||||||
|
on top of that because you will also have to deal with invincible, bone-throwing red skeletons here! This slab can be
|
||||||
|
found on the underside of the platform in the first room with the door leading out into the second area and drops 3
|
||||||
|
beefs/checks when you break it. Carrie is small enough to crouch under the platform and shoot the thing a few times
|
||||||
|
without *too* much hassle, but the only way I know of for Reinhardt to get it is to hang him off the side of the gear
|
||||||
|
and pull him up once he gets moved under the platform. Getting him to then face the breakable and whip it without falling
|
||||||
|
off due to the gear's rotation is next-to-impossible, I find, so the safest method to breaking it as him is to just rush
|
||||||
|
it with his sword, go back up, repeat 2 more times. Pray the aftermentioned Hard Mode skellies don't decide to cause *too*
|
||||||
|
much trouble during all of this!
|
||||||
|
|
||||||
|
#### Final room entrance slab
|
||||||
|
Simply next to the entrance when you come into the third and final room from the intended way. Drops 2 moneybags-turned-checks,
|
||||||
|
which funnily normally have their item IDs shared with the other 3HB in this room's items, so I had to separate those for
|
||||||
|
the rando.
|
||||||
|
|
||||||
|
#### Renon's final offers slab
|
||||||
|
At the top of the final room, on a platform near the Renon contract that would normally be the very last in the game.
|
||||||
|
This 3HB drops a whopping 8 items, more than any other 3HB in the entire game, and 6 of those are all moneybags. They
|
||||||
|
*really* shower you in gold in preparation for the finale, huh?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Castle Keep
|
||||||
|
#### Behind Dracula's chamber/Dracula's floating cube
|
||||||
|
This game continues the CV tradition of having a hidden secret around Dracula's chamber that you can get helpful things
|
||||||
|
from before the final battle begins. Jump onto the torch ledges and use the thin walkway to reach the backside of Dracula's
|
||||||
|
chamber where these can both be found. The floating cube, in question, can be reached with an invisible bridge. The other
|
||||||
|
candle here is noteworthy for being the sole jewel candle in vanilla that doesn't set a flag, meaning you can keep going
|
||||||
|
back down and up the stairs to farm infinite sub-weapon ammo for Dracula like in old school Castlevania!
|
||||||
|
|
||||||
|
### Invisible Items
|
||||||
|
#### Left/Right Dracula door flame
|
||||||
|
Inside the torches on either side of the door to Dracula's chamber. Similar to the above-mentioned jewel torch, these do
|
||||||
|
not set flags. So you can get infinite healing kits for free by constantly going down and back up!
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Castlevania 64 Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
- A Castlevania 64 ROM of the US 1.0 version specifically. The Archipelago community cannot provide this.
|
||||||
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
|
||||||
|
|
||||||
|
### Configuring BizHawk
|
||||||
|
|
||||||
|
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||||
|
|
||||||
|
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||||
|
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||||
|
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||||
|
tabbed out of EmuHawk.
|
||||||
|
- Open a `.z64` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||||
|
`Controllers…`, load any `.z64` ROM first.
|
||||||
|
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||||
|
clear it.
|
||||||
|
- All non-Japanese versions of the N64 Castlevanias require a Controller Pak to save game data. To enable this, while
|
||||||
|
you still have the `.z64` ROM loaded, go to `N64 > Controller Settings...`, click the dropdown by `Controller 1`, and
|
||||||
|
click `Memory Card`. You must then restart EmuHawk for it to take effect.
|
||||||
|
- After enabling the `Memory Card` setting, next time you boot up your Castlevania 64 ROM, you will see the
|
||||||
|
No "CASTLEVANIA" Note Found screen. Pick `Create "CASTLEVANIA" Note Now > Yes` to create save data and enable saving at
|
||||||
|
the White Jewels.
|
||||||
|
|
||||||
|
|
||||||
|
## Generating and Patching a Game
|
||||||
|
|
||||||
|
1. Create your settings file (YAML). You can make one on the
|
||||||
|
[Castlevania 64 settings page](../../../games/Castlevania 64/player-settings).
|
||||||
|
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||||
|
This will generate an output file for you. Your patch file will have the `.apcv64` file extension.
|
||||||
|
3. Open `ArchipelagoLauncher.exe`
|
||||||
|
4. Select "Open Patch" on the left side and select your patch file.
|
||||||
|
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
|
||||||
|
6. A patched `.z64` file will be created in the same place as the patch file.
|
||||||
|
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||||
|
BizHawk install.
|
||||||
|
|
||||||
|
If you're playing a single-player seed, and you don't care about hints, you can stop here, close the client, and load
|
||||||
|
the patched ROM in any emulator or EverDrive of your choice. However, for multiworlds and other Archipelago features,
|
||||||
|
continue below using BizHawk as your emulator.
|
||||||
|
|
||||||
|
## Connecting to a Server
|
||||||
|
|
||||||
|
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||||
|
in case you have to close and reopen a window mid-game for some reason.
|
||||||
|
|
||||||
|
1. Castlevania 64 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
|
||||||
|
you can re-open it from the launcher.
|
||||||
|
2. Ensure EmuHawk is running the patched ROM.
|
||||||
|
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||||
|
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||||
|
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||||
|
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
|
||||||
|
Client window should indicate that it connected and recognized Castlevania 64.
|
||||||
|
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||||
|
top text field of the client and click Connect.
|
||||||
|
|
||||||
|
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
|
||||||
|
perfectly safe to make progress offline; everything will re-sync when you reconnect.
|
|
@ -0,0 +1,149 @@
|
||||||
|
from .data import ename, iname, rname
|
||||||
|
from .stages import get_stage_info
|
||||||
|
from .options import CV64Options
|
||||||
|
|
||||||
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "connection" = The name of the Region the Entrance connects into. If it's a Tuple[str, str], we take the stage in
|
||||||
|
# active_stage_exits given in the second string and then the stage given in that stage's slot given in
|
||||||
|
# the first string, and take the start or end Region of that stage.
|
||||||
|
# "rule" = What rule should be applied to the Entrance during set_rules, as defined in self.rules in the CV64Rules class
|
||||||
|
# definition in rules.py.
|
||||||
|
# "add conds" = A list of player options conditions that must be satisfied for the Entrance to be added. Can be of
|
||||||
|
# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
|
||||||
|
# the first element is the name of the option, the second is the option value to check for, and the third
|
||||||
|
# is a boolean for whether we are evaluating for the option value or not.
|
||||||
|
entrance_info = {
|
||||||
|
# Forest of Silence
|
||||||
|
ename.forest_dbridge_gate: {"connection": rname.forest_mid},
|
||||||
|
ename.forest_werewolf_gate: {"connection": rname.forest_end},
|
||||||
|
ename.forest_end: {"connection": ("next", rname.forest_of_silence)},
|
||||||
|
# Castle Wall
|
||||||
|
ename.cw_portcullis_c: {"connection": rname.cw_exit},
|
||||||
|
ename.cw_lt_skip: {"connection": ("next", rname.castle_wall), "add conds": ["hard"]},
|
||||||
|
ename.cw_lt_door: {"connection": rname.cw_ltower, "rule": iname.left_tower_key},
|
||||||
|
ename.cw_end: {"connection": ("next", rname.castle_wall)},
|
||||||
|
# Villa
|
||||||
|
ename.villa_dog_gates: {"connection": rname.villa_main},
|
||||||
|
ename.villa_snipe_dogs: {"connection": rname.villa_start, "add conds": ["carrie", "hard"]},
|
||||||
|
ename.villa_to_storeroom: {"connection": rname.villa_storeroom, "rule": iname.storeroom_key},
|
||||||
|
ename.villa_to_archives: {"connection": rname.villa_archives, "rule": iname.archives_key},
|
||||||
|
ename.villa_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.villa_to_maze: {"connection": rname.villa_maze, "rule": iname.garden_key},
|
||||||
|
ename.villa_from_storeroom: {"connection": rname.villa_main, "rule": iname.storeroom_key},
|
||||||
|
ename.villa_from_maze: {"connection": rname.villa_servants, "rule": iname.garden_key},
|
||||||
|
ename.villa_servant_door: {"connection": rname.villa_main},
|
||||||
|
ename.villa_copper_door: {"connection": rname.villa_crypt, "rule": iname.copper_key,
|
||||||
|
"add conds": ["not hard"]},
|
||||||
|
ename.villa_copper_skip: {"connection": rname.villa_crypt, "add conds": ["hard"]},
|
||||||
|
ename.villa_bridge_door: {"connection": rname.villa_maze},
|
||||||
|
ename.villa_end_r: {"connection": ("next", rname.villa)},
|
||||||
|
ename.villa_end_c: {"connection": ("alt", rname.villa)},
|
||||||
|
# Tunnel
|
||||||
|
ename.tunnel_start_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.tunnel_gondolas: {"connection": rname.tunnel_end},
|
||||||
|
ename.tunnel_end_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.tunnel_end: {"connection": ("next", rname.tunnel)},
|
||||||
|
# Underground Waterway
|
||||||
|
ename.uw_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.uw_final_waterfall: {"connection": rname.uw_end},
|
||||||
|
ename.uw_waterfall_skip: {"connection": rname.uw_main, "add conds": ["hard"]},
|
||||||
|
ename.uw_end: {"connection": ("next", rname.underground_waterway)},
|
||||||
|
# Castle Center
|
||||||
|
ename.cc_tc_door: {"connection": rname.cc_torture_chamber, "rule": iname.chamber_key},
|
||||||
|
ename.cc_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.cc_lower_wall: {"connection": rname.cc_crystal, "rule": "Bomb 2"},
|
||||||
|
ename.cc_upper_wall: {"connection": rname.cc_library, "rule": "Bomb 1"},
|
||||||
|
ename.cc_elevator: {"connection": rname.cc_elev_top},
|
||||||
|
ename.cc_exit_r: {"connection": ("next", rname.castle_center)},
|
||||||
|
ename.cc_exit_c: {"connection": ("alt", rname.castle_center)},
|
||||||
|
# Duel Tower
|
||||||
|
ename.dt_start: {"connection": ("prev", rname.duel_tower)},
|
||||||
|
ename.dt_end: {"connection": ("next", rname.duel_tower)},
|
||||||
|
# Tower of Execution
|
||||||
|
ename.toe_start: {"connection": ("prev", rname.tower_of_execution)},
|
||||||
|
ename.toe_gate: {"connection": rname.toe_ledge, "rule": iname.execution_key,
|
||||||
|
"add conds": ["not hard"]},
|
||||||
|
ename.toe_gate_skip: {"connection": rname.toe_ledge, "add conds": ["hard"]},
|
||||||
|
ename.toe_end: {"connection": ("next", rname.tower_of_execution)},
|
||||||
|
# Tower of Science
|
||||||
|
ename.tosci_start: {"connection": ("prev", rname.tower_of_science)},
|
||||||
|
ename.tosci_key1_door: {"connection": rname.tosci_three_doors, "rule": iname.science_key1},
|
||||||
|
ename.tosci_to_key2_door: {"connection": rname.tosci_conveyors, "rule": iname.science_key2},
|
||||||
|
ename.tosci_from_key2_door: {"connection": rname.tosci_start, "rule": iname.science_key2},
|
||||||
|
ename.tosci_key3_door: {"connection": rname.tosci_key3, "rule": iname.science_key3},
|
||||||
|
ename.tosci_end: {"connection": ("next", rname.tower_of_science)},
|
||||||
|
# Tower of Sorcery
|
||||||
|
ename.tosor_start: {"connection": ("prev", rname.tower_of_sorcery)},
|
||||||
|
ename.tosor_end: {"connection": ("next", rname.tower_of_sorcery)},
|
||||||
|
# Room of Clocks
|
||||||
|
ename.roc_gate: {"connection": ("next", rname.room_of_clocks)},
|
||||||
|
# Clock Tower
|
||||||
|
ename.ct_to_door1: {"connection": rname.ct_middle, "rule": iname.clocktower_key1},
|
||||||
|
ename.ct_from_door1: {"connection": rname.ct_start, "rule": iname.clocktower_key1},
|
||||||
|
ename.ct_to_door2: {"connection": rname.ct_end, "rule": iname.clocktower_key2},
|
||||||
|
ename.ct_from_door2: {"connection": rname.ct_middle, "rule": iname.clocktower_key2},
|
||||||
|
ename.ct_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
|
||||||
|
ename.ct_door_3: {"connection": ("next", rname.clock_tower), "rule": iname.clocktower_key3},
|
||||||
|
# Castle Keep
|
||||||
|
ename.ck_slope_jump: {"connection": rname.roc_main, "add conds": ["hard"]},
|
||||||
|
ename.ck_drac_door: {"connection": rname.ck_drac_chamber, "rule": "Dracula"}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_conds = {"carrie": ("carrie_logic", True, True),
|
||||||
|
"hard": ("hard_logic", True, True),
|
||||||
|
"not hard": ("hard_logic", False, True),
|
||||||
|
"shopsanity": ("shopsanity", True, True)}
|
||||||
|
|
||||||
|
stage_connection_types = {"prev": "end region",
|
||||||
|
"next": "start region",
|
||||||
|
"alt": "start region"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_entrance_info(entrance: str, info: str) -> Union[str, Tuple[str, str], List[str], None]:
|
||||||
|
return entrance_info[entrance].get(info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_warp_entrances(active_warp_list: List[str]) -> Dict[str, str]:
|
||||||
|
# Create the starting stage Entrance.
|
||||||
|
warp_entrances = {get_stage_info(active_warp_list[0], "start region"): "Start stage"}
|
||||||
|
|
||||||
|
# Create the warp Entrances.
|
||||||
|
for i in range(1, len(active_warp_list)):
|
||||||
|
mid_stage_region = get_stage_info(active_warp_list[i], "mid region")
|
||||||
|
warp_entrances.update({mid_stage_region: f"Warp {i}"})
|
||||||
|
|
||||||
|
return warp_entrances
|
||||||
|
|
||||||
|
|
||||||
|
def verify_entrances(options: CV64Options, entrances: List[str],
|
||||||
|
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[str, str]:
|
||||||
|
verified_entrances = {}
|
||||||
|
|
||||||
|
for ent_name in entrances:
|
||||||
|
ent_add_conds = get_entrance_info(ent_name, "add conds")
|
||||||
|
|
||||||
|
# Check any options that might be associated with the Entrance before adding it.
|
||||||
|
add_it = True
|
||||||
|
if ent_add_conds is not None:
|
||||||
|
for cond in ent_add_conds:
|
||||||
|
if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
|
||||||
|
add_it = False
|
||||||
|
|
||||||
|
if not add_it:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add the Entrance to the verified Entrances if the above check passes.
|
||||||
|
connection = get_entrance_info(ent_name, "connection")
|
||||||
|
|
||||||
|
# If the Entrance is a connection to a different stage, get the corresponding other stage Region.
|
||||||
|
if isinstance(connection, tuple):
|
||||||
|
connecting_stage = active_stage_exits[connection[1]][connection[0]]
|
||||||
|
# Stages that lead backwards at the beginning of the line will appear leading to "Menu".
|
||||||
|
if connecting_stage in ["Menu", None]:
|
||||||
|
continue
|
||||||
|
connection = get_stage_info(connecting_stage, stage_connection_types[connection[0]])
|
||||||
|
verified_entrances.update({connection: ent_name})
|
||||||
|
|
||||||
|
return verified_entrances
|
|
@ -0,0 +1,214 @@
|
||||||
|
from BaseClasses import Item
|
||||||
|
from .data import iname
|
||||||
|
from .locations import base_id, get_location_info
|
||||||
|
from .options import DraculasCondition, SpareKeys
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import CV64World
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
class CV64Item(Item):
|
||||||
|
game: str = "Castlevania 64"
|
||||||
|
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "code" = The unique part of the Item's AP code attribute, as well as the value to call the in-game "prepare item
|
||||||
|
# textbox" function with to give the Item in-game. Add this + base_id to get the actual AP code.
|
||||||
|
# "default classification" = The AP Item Classification that gets assigned to instances of that Item in create_item
|
||||||
|
# by default, unless I deliberately override it (as is the case for some Special1s).
|
||||||
|
# "inventory offset" = What offset from the start of the in-game inventory array (beginning at 0x80389C4B) stores the
|
||||||
|
# current count for that Item. Used for start inventory purposes.
|
||||||
|
# "pickup actor id" = The ID for the Item's in-game Item pickup actor. If it's not in the Item's data dict, it's the
|
||||||
|
# same as the Item's code. This is what gets written in the ROM to replace non-NPC/shop items.
|
||||||
|
# "sub equip id" = For sub-weapons specifically, this is the number to put in the game's "current sub-weapon" value to
|
||||||
|
# indicate the player currently having that weapon. Used for start inventory purposes.
|
||||||
|
item_info = {
|
||||||
|
# White jewel
|
||||||
|
iname.red_jewel_s: {"code": 0x02, "default classification": "filler"},
|
||||||
|
iname.red_jewel_l: {"code": 0x03, "default classification": "filler"},
|
||||||
|
iname.special_one: {"code": 0x04, "default classification": "progression_skip_balancing",
|
||||||
|
"inventory offset": 0},
|
||||||
|
iname.special_two: {"code": 0x05, "default classification": "progression_skip_balancing",
|
||||||
|
"inventory offset": 1},
|
||||||
|
iname.roast_chicken: {"code": 0x06, "default classification": "filler", "inventory offset": 2},
|
||||||
|
iname.roast_beef: {"code": 0x07, "default classification": "filler", "inventory offset": 3},
|
||||||
|
iname.healing_kit: {"code": 0x08, "default classification": "useful", "inventory offset": 4},
|
||||||
|
iname.purifying: {"code": 0x09, "default classification": "filler", "inventory offset": 5},
|
||||||
|
iname.cure_ampoule: {"code": 0x0A, "default classification": "filler", "inventory offset": 6},
|
||||||
|
# pot-pourri
|
||||||
|
iname.powerup: {"code": 0x0C, "default classification": "filler"},
|
||||||
|
iname.permaup: {"code": 0x10C, "default classification": "useful", "pickup actor id": 0x0C,
|
||||||
|
"inventory offset": 8},
|
||||||
|
iname.knife: {"code": 0x0D, "default classification": "filler", "pickup actor id": 0x10,
|
||||||
|
"sub equip id": 1},
|
||||||
|
iname.holy_water: {"code": 0x0E, "default classification": "filler", "pickup actor id": 0x0D,
|
||||||
|
"sub equip id": 2},
|
||||||
|
iname.cross: {"code": 0x0F, "default classification": "filler", "pickup actor id": 0x0E,
|
||||||
|
"sub equip id": 3},
|
||||||
|
iname.axe: {"code": 0x10, "default classification": "filler", "pickup actor id": 0x0F,
|
||||||
|
"sub equip id": 4},
|
||||||
|
# Wooden stake (AP item)
|
||||||
|
iname.ice_trap: {"code": 0x12, "default classification": "trap"},
|
||||||
|
# The contract
|
||||||
|
# engagement ring
|
||||||
|
iname.magical_nitro: {"code": 0x15, "default classification": "progression", "inventory offset": 17},
|
||||||
|
iname.mandragora: {"code": 0x16, "default classification": "progression", "inventory offset": 18},
|
||||||
|
iname.sun_card: {"code": 0x17, "default classification": "filler", "inventory offset": 19},
|
||||||
|
iname.moon_card: {"code": 0x18, "default classification": "filler", "inventory offset": 20},
|
||||||
|
# Incandescent gaze
|
||||||
|
iname.archives_key: {"code": 0x1A, "default classification": "progression", "pickup actor id": 0x1D,
|
||||||
|
"inventory offset": 22},
|
||||||
|
iname.left_tower_key: {"code": 0x1B, "default classification": "progression", "pickup actor id": 0x1E,
|
||||||
|
"inventory offset": 23},
|
||||||
|
iname.storeroom_key: {"code": 0x1C, "default classification": "progression", "pickup actor id": 0x1F,
|
||||||
|
"inventory offset": 24},
|
||||||
|
iname.garden_key: {"code": 0x1D, "default classification": "progression", "pickup actor id": 0x20,
|
||||||
|
"inventory offset": 25},
|
||||||
|
iname.copper_key: {"code": 0x1E, "default classification": "progression", "pickup actor id": 0x21,
|
||||||
|
"inventory offset": 26},
|
||||||
|
iname.chamber_key: {"code": 0x1F, "default classification": "progression", "pickup actor id": 0x22,
|
||||||
|
"inventory offset": 27},
|
||||||
|
iname.execution_key: {"code": 0x20, "default classification": "progression", "pickup actor id": 0x23,
|
||||||
|
"inventory offset": 28},
|
||||||
|
iname.science_key1: {"code": 0x21, "default classification": "progression", "pickup actor id": 0x24,
|
||||||
|
"inventory offset": 29},
|
||||||
|
iname.science_key2: {"code": 0x22, "default classification": "progression", "pickup actor id": 0x25,
|
||||||
|
"inventory offset": 30},
|
||||||
|
iname.science_key3: {"code": 0x23, "default classification": "progression", "pickup actor id": 0x26,
|
||||||
|
"inventory offset": 31},
|
||||||
|
iname.clocktower_key1: {"code": 0x24, "default classification": "progression", "pickup actor id": 0x27,
|
||||||
|
"inventory offset": 32},
|
||||||
|
iname.clocktower_key2: {"code": 0x25, "default classification": "progression", "pickup actor id": 0x28,
|
||||||
|
"inventory offset": 33},
|
||||||
|
iname.clocktower_key3: {"code": 0x26, "default classification": "progression", "pickup actor id": 0x29,
|
||||||
|
"inventory offset": 34},
|
||||||
|
iname.five_hundred_gold: {"code": 0x27, "default classification": "filler", "pickup actor id": 0x1A},
|
||||||
|
iname.three_hundred_gold: {"code": 0x28, "default classification": "filler", "pickup actor id": 0x1B},
|
||||||
|
iname.one_hundred_gold: {"code": 0x29, "default classification": "filler", "pickup actor id": 0x1C},
|
||||||
|
iname.crystal: {"default classification": "progression"},
|
||||||
|
iname.trophy: {"default classification": "progression"},
|
||||||
|
iname.victory: {"default classification": "progression"}
|
||||||
|
}
|
||||||
|
|
||||||
|
filler_item_names = [iname.red_jewel_s, iname.red_jewel_l, iname.five_hundred_gold, iname.three_hundred_gold,
|
||||||
|
iname.one_hundred_gold]
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_info(item: str, info: str) -> Union[str, int, None]:
|
||||||
|
return item_info[item].get(info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_names_to_ids() -> Dict[str, int]:
|
||||||
|
return {name: get_item_info(name, "code")+base_id for name in item_info if get_item_info(name, "code") is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_counts(world: "CV64World") -> Dict[str, Dict[str, int]]:
|
||||||
|
|
||||||
|
active_locations = world.multiworld.get_unfilled_locations(world.player)
|
||||||
|
|
||||||
|
item_counts = {
|
||||||
|
"progression": {},
|
||||||
|
"progression_skip_balancing": {},
|
||||||
|
"useful": {},
|
||||||
|
"filler": {},
|
||||||
|
"trap": {}
|
||||||
|
}
|
||||||
|
total_items = 0
|
||||||
|
extras_count = 0
|
||||||
|
|
||||||
|
# Get from each location its vanilla item and add it to the default item counts.
|
||||||
|
for loc in active_locations:
|
||||||
|
if loc.address is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if world.options.hard_item_pool and get_location_info(loc.name, "hard item") is not None:
|
||||||
|
item_to_add = get_location_info(loc.name, "hard item")
|
||||||
|
else:
|
||||||
|
item_to_add = get_location_info(loc.name, "normal item")
|
||||||
|
|
||||||
|
classification = get_item_info(item_to_add, "default classification")
|
||||||
|
|
||||||
|
if item_to_add not in item_counts[classification]:
|
||||||
|
item_counts[classification][item_to_add] = 1
|
||||||
|
else:
|
||||||
|
item_counts[classification][item_to_add] += 1
|
||||||
|
total_items += 1
|
||||||
|
|
||||||
|
# Replace all but 2 PowerUps with junk if Permanent PowerUps is on and mark those two PowerUps as Useful.
|
||||||
|
if world.options.permanent_powerups:
|
||||||
|
for i in range(item_counts["filler"][iname.powerup] - 2):
|
||||||
|
item_counts["filler"][world.get_filler_item_name()] += 1
|
||||||
|
del(item_counts["filler"][iname.powerup])
|
||||||
|
item_counts["useful"][iname.permaup] = 2
|
||||||
|
|
||||||
|
# Add the total Special1s.
|
||||||
|
item_counts["progression_skip_balancing"][iname.special_one] = world.options.total_special1s.value
|
||||||
|
extras_count += world.options.total_special1s.value
|
||||||
|
|
||||||
|
# Add the total Special2s if Dracula's Condition is Special2s.
|
||||||
|
if world.options.draculas_condition == DraculasCondition.option_specials:
|
||||||
|
item_counts["progression_skip_balancing"][iname.special_two] = world.options.total_special2s.value
|
||||||
|
extras_count += world.options.total_special2s.value
|
||||||
|
|
||||||
|
# Determine the extra key counts if applicable. Doing this before moving Special1s will ensure only the keys and
|
||||||
|
# bomb components are affected by this.
|
||||||
|
for key in item_counts["progression"]:
|
||||||
|
spare_keys = 0
|
||||||
|
if world.options.spare_keys == SpareKeys.option_on:
|
||||||
|
spare_keys = item_counts["progression"][key]
|
||||||
|
elif world.options.spare_keys == SpareKeys.option_chance:
|
||||||
|
if item_counts["progression"][key] > 0:
|
||||||
|
for i in range(item_counts["progression"][key]):
|
||||||
|
spare_keys += world.random.randint(0, 1)
|
||||||
|
item_counts["progression"][key] += spare_keys
|
||||||
|
extras_count += spare_keys
|
||||||
|
|
||||||
|
# Move the total number of Special1s needed to warp everywhere to normal progression balancing if S1s per warp is
|
||||||
|
# 3 or lower.
|
||||||
|
if world.s1s_per_warp <= 3:
|
||||||
|
item_counts["progression_skip_balancing"][iname.special_one] -= world.s1s_per_warp * 7
|
||||||
|
item_counts["progression"][iname.special_one] = world.s1s_per_warp * 7
|
||||||
|
|
||||||
|
# Determine the total amounts of replaceable filler and non-filler junk.
|
||||||
|
total_filler_junk = 0
|
||||||
|
total_non_filler_junk = 0
|
||||||
|
for junk in item_counts["filler"]:
|
||||||
|
if junk in filler_item_names:
|
||||||
|
total_filler_junk += item_counts["filler"][junk]
|
||||||
|
else:
|
||||||
|
total_non_filler_junk += item_counts["filler"][junk]
|
||||||
|
|
||||||
|
# Subtract from the filler counts total number of "extra" items we've added. get_filler_item_name() filler will be
|
||||||
|
# subtracted from first until we run out of that, at which point we'll start subtracting from the rest. At this
|
||||||
|
# moment, non-filler item name filler cannot run out no matter the settings, so I haven't bothered adding handling
|
||||||
|
# for when it does yet.
|
||||||
|
available_filler_junk = filler_item_names.copy()
|
||||||
|
for i in range(extras_count):
|
||||||
|
if total_filler_junk > 0:
|
||||||
|
total_filler_junk -= 1
|
||||||
|
item_to_subtract = world.random.choice(available_filler_junk)
|
||||||
|
else:
|
||||||
|
total_non_filler_junk -= 1
|
||||||
|
item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
|
||||||
|
|
||||||
|
item_counts["filler"][item_to_subtract] -= 1
|
||||||
|
if item_counts["filler"][item_to_subtract] == 0:
|
||||||
|
del(item_counts["filler"][item_to_subtract])
|
||||||
|
if item_to_subtract in available_filler_junk:
|
||||||
|
available_filler_junk.remove(item_to_subtract)
|
||||||
|
|
||||||
|
# Determine the Ice Trap count by taking a certain % of the total filler remaining at this point.
|
||||||
|
item_counts["trap"][iname.ice_trap] = math.floor((total_filler_junk + total_non_filler_junk) *
|
||||||
|
(world.options.ice_trap_percentage.value / 100.0))
|
||||||
|
for i in range(item_counts["trap"][iname.ice_trap]):
|
||||||
|
# Subtract the remaining filler after determining the ice trap count.
|
||||||
|
item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
|
||||||
|
item_counts["filler"][item_to_subtract] -= 1
|
||||||
|
if item_counts["filler"][item_to_subtract] == 0:
|
||||||
|
del (item_counts["filler"][item_to_subtract])
|
||||||
|
|
||||||
|
return item_counts
|
|
@ -0,0 +1,699 @@
|
||||||
|
from BaseClasses import Location
|
||||||
|
from .data import lname, iname
|
||||||
|
from .options import CV64Options, SubWeaponShuffle, DraculasCondition, RenonFightCondition, VincentFightCondition
|
||||||
|
|
||||||
|
from typing import Dict, Optional, Union, List, Tuple
|
||||||
|
|
||||||
|
base_id = 0xC64000
|
||||||
|
|
||||||
|
|
||||||
|
class CV64Location(Location):
|
||||||
|
game: str = "Castlevania 64"
|
||||||
|
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "code" = The unique part of the Location's AP code attribute, as well as the in-game bitflag index starting from
|
||||||
|
# 0x80389BE4 that indicates the Location has been checked. Add this + base_id to get the actual AP code.
|
||||||
|
# "offset" = The offset in the ROM to overwrite to change the Item on that Location.
|
||||||
|
# "normal item" = The Item normally there in vanilla on most difficulties in most versions of the game. Used to
|
||||||
|
# determine the World's Item counts by checking what Locations are active.
|
||||||
|
# "hard item" = The Item normally there in Hard Mode in the PAL version of CV64 specifically. Used instead of the
|
||||||
|
# normal Item when the hard Item pool is enabled if it's in the Location's data dict.
|
||||||
|
# "add conds" = A list of player options conditions that must be satisfied for the Location to be added. Can be of
|
||||||
|
# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
|
||||||
|
# the first element is the name of the option, the second is the option value to check for, and the third
|
||||||
|
# is a boolean for whether we are evaluating for the option value or not.
|
||||||
|
# "event" = What event Item to place on that Location, for Locations that are events specifically.
|
||||||
|
# "countdown" = What Countdown number in the array of Countdown numbers that Location contributes to. For the most part,
|
||||||
|
# this is figured out by taking that Location's corresponding stage's postion in the vanilla stage order,
|
||||||
|
# but there are some exceptions made for Locations in parts of Villa and Castle Center that split off into
|
||||||
|
# their own numbers.
|
||||||
|
# "type" = Anything special about this Location in-game, whether it be NPC-given, invisible, etc.
|
||||||
|
location_info = {
|
||||||
|
# Forest of Silence
|
||||||
|
lname.forest_pillars_right: {"code": 0x1C, "offset": 0x10C67B, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.forest_pillars_left: {"code": 0x46, "offset": 0x10C6EB, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.forest_pillars_top: {"code": 0x13, "offset": 0x10C71B, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.red_jewel_l},
|
||||||
|
lname.forest_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.forest_king_skeleton: {"code": 0xC, "offset": 0x10C6BB, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.forest_lgaz_in: {"code": 0x1A, "offset": 0x10C68B, "normal item": iname.moon_card},
|
||||||
|
lname.forest_lgaz_top: {"code": 0x19, "offset": 0x10C693, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.forest_hgaz_in: {"code": 0xB, "offset": 0x10C6C3, "normal item": iname.sun_card},
|
||||||
|
lname.forest_hgaz_top: {"code": 0x3, "offset": 0x10C6E3, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.forest_weretiger_sw: {"code": 0xA, "offset": 0x10C6CB, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.forest_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.forest_weretiger_gate: {"code": 0x7, "offset": 0x10C683, "normal item": iname.powerup},
|
||||||
|
lname.forest_dirge_tomb_l: {"code": 0x59, "offset": 0x10C74B, "normal item": iname.one_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_dirge_tomb_u: {"code": 0x8, "offset": 0x10C743, "normal item": iname.one_hundred_gold},
|
||||||
|
lname.forest_dirge_plaque: {"code": 0x6, "offset": 0x7C7F9D, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold, "type": "inv"},
|
||||||
|
lname.forest_dirge_ped: {"code": 0x45, "offset": 0x10C6FB, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.forest_dirge_rock1: {"code": 0x221, "offset": 0x10C791, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_dirge_rock2: {"code": 0x222, "offset": 0x10C793, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_dirge_rock3: {"code": 0x223, "offset": 0x10C795, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_dirge_rock4: {"code": 0x224, "offset": 0x10C797, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_dirge_rock5: {"code": 0x225, "offset": 0x10C799, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_corpse_save: {"code": 0xF, "offset": 0x10C6A3, "normal item": iname.red_jewel_s},
|
||||||
|
lname.forest_dbridge_wall: {"code": 0x18, "offset": 0x10C69B, "normal item": iname.red_jewel_s},
|
||||||
|
lname.forest_dbridge_sw: {"code": 0x9, "offset": 0x10C6D3, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.forest_dbridge_gate_l: {"code": 0x44, "offset": 0x10C6F3, "normal item": iname.axe, "add conds": ["sub"]},
|
||||||
|
lname.forest_dbridge_gate_r: {"code": 0xE, "offset": 0x10C6AB, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.forest_dbridge_tomb_l: {"code": 0xEA, "offset": 0x10C763, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_dbridge_tomb_ur: {"code": 0xE4, "offset": 0x10C773, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_dbridge_tomb_uf: {"code": 0x1B, "offset": 0x10C76B, "normal item": iname.red_jewel_s},
|
||||||
|
lname.forest_bface_tomb_lf: {"code": 0x10, "offset": 0x10C75B, "normal item": iname.roast_chicken},
|
||||||
|
lname.forest_bface_tomb_lr: {"code": 0x58, "offset": 0x10C753, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_bface_tomb_u: {"code": 0x1E, "offset": 0x10C77B, "normal item": iname.one_hundred_gold},
|
||||||
|
lname.forest_ibridge: {"code": 0x2, "offset": 0x10C713, "normal item": iname.one_hundred_gold},
|
||||||
|
lname.forest_bridge_rock1: {"code": 0x227, "offset": 0x10C79D, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.forest_bridge_rock2: {"code": 0x228, "offset": 0x10C79F, "normal item": iname.five_hundred_gold,
|
||||||
|
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.forest_bridge_rock3: {"code": 0x229, "offset": 0x10C7A1, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.forest_bridge_rock4: {"code": 0x22A, "offset": 0x10C7A3, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.forest_werewolf_tomb_lf: {"code": 0xE7, "offset": 0x10C783, "normal item": iname.one_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_werewolf_tomb_lr: {"code": 0xE6, "offset": 0x10C73B, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.forest_werewolf_tomb_r: {"code": 0x4, "offset": 0x10C733, "normal item": iname.sun_card},
|
||||||
|
lname.forest_werewolf_plaque: {"code": 0x1, "offset": 0xBFC8AF, "normal item": iname.roast_chicken,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.forest_werewolf_tree: {"code": 0xD, "offset": 0x10C6B3, "normal item": iname.red_jewel_s},
|
||||||
|
lname.forest_werewolf_island: {"code": 0x41, "offset": 0x10C703, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.forest_final_sw: {"code": 0x12, "offset": 0x10C72B, "normal item": iname.roast_beef},
|
||||||
|
lname.forest_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
|
||||||
|
# Castle Wall
|
||||||
|
lname.cwr_bottom: {"code": 0x1DD, "offset": 0x10C7E7, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.cw_dragon_sw: {"code": 0x153, "offset": 0x10C817, "normal item": iname.roast_chicken},
|
||||||
|
lname.cw_boss: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.cw_save_slab1: {"code": 0x22C, "offset": 0x10C84D, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.cw_save_slab2: {"code": 0x22D, "offset": 0x10C84F, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.cw_save_slab3: {"code": 0x22E, "offset": 0x10C851, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.cw_save_slab4: {"code": 0x22F, "offset": 0x10C853, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.cw_save_slab5: {"code": 0x230, "offset": 0x10C855, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.cw_rrampart: {"code": 0x156, "offset": 0x10C7FF, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.cw_lrampart: {"code": 0x155, "offset": 0x10C807, "normal item": iname.moon_card,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.cw_pillar: {"code": 0x14D, "offset": 0x7F9A0F, "normal item": iname.holy_water, "add conds": ["sub"]},
|
||||||
|
lname.cw_shelf_visible: {"code": 0x158, "offset": 0x7F99A9, "normal item": iname.powerup},
|
||||||
|
lname.cw_shelf_sandbags: {"code": 0x14E, "offset": 0x7F9A3E, "normal item": iname.five_hundred_gold, "type": "inv"},
|
||||||
|
lname.cw_shelf_torch: {"code": 0x14C, "offset": 0x10C82F, "normal item": iname.cross, "add conds": ["sub"]},
|
||||||
|
lname.cw_ground_left: {"code": 0x14B, "offset": 0x10C827, "normal item": iname.knife, "add conds": ["sub"]},
|
||||||
|
lname.cw_ground_middle: {"code": 0x159, "offset": 0x10C7F7, "normal item": iname.left_tower_key},
|
||||||
|
lname.cw_ground_right: {"code": 0x14A, "offset": 0x10C81F, "normal item": iname.axe, "add conds": ["sub"]},
|
||||||
|
lname.cwl_bottom: {"code": 0x1DE, "offset": 0x10C7DF, "normal item": iname.moon_card},
|
||||||
|
lname.cwl_bridge: {"code": 0x1DC, "offset": 0x10C7EF, "normal item": iname.roast_beef},
|
||||||
|
lname.cw_drac_sw: {"code": 0x154, "offset": 0x10C80F, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.cw_drac_slab1: {"code": 0x232, "offset": 0x10C859, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.cw_drac_slab2: {"code": 0x233, "offset": 0x10C85B, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.cw_drac_slab3: {"code": 0x234, "offset": 0x10C85D, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.cw_drac_slab4: {"code": 0x235, "offset": 0x10C85F, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.cw_drac_slab5: {"code": 0x236, "offset": 0x10C861, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
# Villa
|
||||||
|
lname.villafy_outer_gate_l: {"code": 0x133, "offset": 0x10C87F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.villafy_outer_gate_r: {"code": 0x132, "offset": 0x10C887, "normal item": iname.red_jewel_l},
|
||||||
|
lname.villafy_dog_platform: {"code": 0x134, "offset": 0x10C89F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.villafy_inner_gate: {"code": 0x138, "offset": 0xBFC8D7, "normal item": iname.roast_beef},
|
||||||
|
lname.villafy_gate_marker: {"code": 0x131, "offset": 0x10C8A7, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.villafy_villa_marker: {"code": 0x13E, "offset": 0x10C897, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.villafy_tombstone: {"code": 0x12F, "offset": 0x8099CC, "normal item": iname.moon_card,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.villafy_fountain_fl: {"code": 0x139, "offset": 0xBFC8CF, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.villafy_fountain_fr: {"code": 0x130, "offset": 0x80997D, "normal item": iname.purifying},
|
||||||
|
lname.villafy_fountain_ml: {"code": 0x13A, "offset": 0x809956, "normal item": iname.sun_card},
|
||||||
|
lname.villafy_fountain_mr: {"code": 0x13D, "offset": 0x80992D, "normal item": iname.moon_card},
|
||||||
|
lname.villafy_fountain_rl: {"code": 0x13B, "offset": 0xBFC8D3, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.villafy_fountain_rr: {"code": 0x13C, "offset": 0x80993C, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.villafo_front_r: {"code": 0x3D, "offset": 0x10C8E7, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.villafo_front_l: {"code": 0x3B, "offset": 0x10C8DF, "normal item": iname.red_jewel_s},
|
||||||
|
lname.villafo_mid_l: {"code": 0x3C, "offset": 0x10C8D7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.villafo_mid_r: {"code": 0xE5, "offset": 0x10C8CF, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.villafo_rear_r: {"code": 0x38, "offset": 0x10C8C7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.villafo_rear_l: {"code": 0x39, "offset": 0x10C8BF, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.villafo_pot_r: {"code": 0x2E, "offset": 0x10C8AF, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.villafo_pot_l: {"code": 0x2F, "offset": 0x10C8B7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.villafo_sofa: {"code": 0x2D, "offset": 0x81F07C, "normal item": iname.purifying,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.villafo_chandelier1: {"code": 0x27D, "offset": 0x10C8F5, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.villafo_chandelier2: {"code": 0x27E, "offset": 0x10C8F7, "normal item": iname.purifying,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.villafo_chandelier3: {"code": 0x27F, "offset": 0x10C8F9, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.villafo_chandelier4: {"code": 0x280, "offset": 0x10C8FB, "normal item": iname.cure_ampoule,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.villafo_chandelier5: {"code": 0x281, "offset": 0x10C8FD, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.villala_hallway_stairs: {"code": 0x34, "offset": 0x10C927, "normal item": iname.red_jewel_l},
|
||||||
|
lname.villala_hallway_l: {"code": 0x40, "offset": 0xBFC903, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.villala_hallway_r: {"code": 0x4F, "offset": 0xBFC8F7, "normal item": iname.axe,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.villala_bedroom_chairs: {"code": 0x33, "offset": 0x83A588, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.three_hundred_gold},
|
||||||
|
lname.villala_bedroom_bed: {"code": 0x32, "offset": 0xBFC95B, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.three_hundred_gold},
|
||||||
|
lname.villala_vincent: {"code": 0x23, "offset": 0xBFE42F, "normal item": iname.archives_key,
|
||||||
|
"type": "npc"},
|
||||||
|
lname.villala_slivingroom_table: {"code": 0x2B, "offset": 0xBFC96B, "normal item": iname.five_hundred_gold,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.villala_slivingroom_mirror: {"code": 0x49, "offset": 0x83A5D9, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.villala_diningroom_roses: {"code": 0x2A, "offset": 0xBFC90B, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.three_hundred_gold, "type": "inv"},
|
||||||
|
lname.villala_llivingroom_pot_r: {"code": 0x26, "offset": 0x10C90F, "normal item": iname.storeroom_key},
|
||||||
|
lname.villala_llivingroom_pot_l: {"code": 0x25, "offset": 0x10C917, "normal item": iname.roast_chicken},
|
||||||
|
lname.villala_llivingroom_painting: {"code": 0x2C, "offset": 0xBFC907, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.one_hundred_gold, "type": "inv"},
|
||||||
|
lname.villala_llivingroom_light: {"code": 0x28, "offset": 0x10C91F, "normal item": iname.purifying},
|
||||||
|
lname.villala_llivingroom_lion: {"code": 0x30, "offset": 0x83A610, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.five_hundred_gold, "type": "inv"},
|
||||||
|
lname.villala_exit_knight: {"code": 0x27, "offset": 0xBFC967, "normal item": iname.purifying,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.villala_storeroom_l: {"code": 0x36, "offset": 0xBFC95F, "normal item": iname.roast_beef},
|
||||||
|
lname.villala_storeroom_r: {"code": 0x37, "offset": 0xBFC8FF, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.villala_storeroom_s: {"code": 0x31, "offset": 0xBFC963, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.one_hundred_gold, "type": "inv"},
|
||||||
|
lname.villala_archives_entrance: {"code": 0x48, "offset": 0x83A5E5, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.villala_archives_table: {"code": 0x29, "offset": 0xBFC90F, "normal item": iname.purifying,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.villala_archives_rear: {"code": 0x24, "offset": 0x83A5B1, "normal item": iname.garden_key},
|
||||||
|
lname.villam_malus_torch: {"code": 0x173, "offset": 0x10C967, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_malus_bush: {"code": 0x16C, "offset": 0x850FEC, "normal item": iname.roast_chicken,
|
||||||
|
"type": "inv", "countdown": 13},
|
||||||
|
lname.villam_fplatform: {"code": 0x16B, "offset": 0x10C987, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"], "countdown": 13},
|
||||||
|
lname.villam_frankieturf_l: {"code": 0x177, "offset": 0x10C947, "normal item": iname.three_hundred_gold,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_frankieturf_r: {"code": 0x16A, "offset": 0x10C98F, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"], "countdown": 13},
|
||||||
|
lname.villam_frankieturf_ru: {"code": 0x16E, "offset": 0x10C9A7, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_fgarden_f: {"code": 0x172, "offset": 0x10C96F, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_fgarden_mf: {"code": 0x171, "offset": 0x10C977, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_fgarden_mr: {"code": 0x174, "offset": 0x10C95F, "normal item": iname.roast_chicken,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_fgarden_r: {"code": 0x170, "offset": 0x10C97F, "normal item": iname.red_jewel_l,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_rplatform: {"code": 0x169, "offset": 0x10C997, "normal item": iname.axe,
|
||||||
|
"add conds": ["sub"], "countdown": 13},
|
||||||
|
lname.villam_rplatform_de: {"code": 0x176, "offset": 0x10C94F, "normal item": iname.five_hundred_gold,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_exit_de: {"code": 0x175, "offset": 0x10C957, "normal item": iname.three_hundred_gold,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_serv_path: {"code": 0x17A, "offset": 0x10C92F, "normal item": iname.copper_key,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villafo_serv_ent: {"code": 0x3E, "offset": 0x10C8EF, "normal item": iname.roast_chicken},
|
||||||
|
lname.villam_crypt_ent: {"code": 0x178, "offset": 0x10C93F, "normal item": iname.purifying,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villam_crypt_upstream: {"code": 0x179, "offset": 0x10C937, "normal item": iname.roast_beef,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villac_ent_l: {"code": 0xC9, "offset": 0x10CF4B, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villac_ent_r: {"code": 0xC0, "offset": 0x10CF63, "normal item": iname.five_hundred_gold,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villac_wall_l: {"code": 0xC2, "offset": 0x10CF6B, "normal item": iname.roast_chicken,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villac_wall_r: {"code": 0xC1, "offset": 0x10CF5B, "normal item": iname.red_jewel_l,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villac_coffin_l: {"code": 0xD8, "offset": 0x10CF73, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"], "countdown": 13},
|
||||||
|
lname.villac_coffin_r: {"code": 0xC8, "offset": 0x10CF53, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 13},
|
||||||
|
lname.villa_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.villa_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
# Tunnel
|
||||||
|
lname.tunnel_landing: {"code": 0x197, "offset": 0x10C9AF, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_landing_rc: {"code": 0x196, "offset": 0x10C9B7, "normal item": iname.red_jewel_s,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_stone_alcove_r: {"code": 0xE1, "offset": 0x10CA57, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.tunnel_stone_alcove_l: {"code": 0x187, "offset": 0x10CA9F, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.roast_chicken},
|
||||||
|
lname.tunnel_twin_arrows: {"code": 0x195, "offset": 0xBFC993, "normal item": iname.cure_ampoule,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.tunnel_arrows_rock1: {"code": 0x238, "offset": 0x10CABD, "normal item": iname.purifying,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tunnel_arrows_rock2: {"code": 0x239, "offset": 0x10CABF, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_arrows_rock3: {"code": 0x23A, "offset": 0x10CAC1, "normal item": iname.cure_ampoule,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tunnel_arrows_rock4: {"code": 0x23B, "offset": 0x10CAC3, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_arrows_rock5: {"code": 0x23C, "offset": 0x10CAC5, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_lonesome_bucket: {"code": 0x189, "offset": 0xBFC99B, "normal item": iname.cure_ampoule,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.tunnel_lbucket_mdoor_l: {"code": 0x198, "offset": 0x10CA67, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.tunnel_lbucket_quag: {"code": 0x191, "offset": 0x10C9DF, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_bucket_quag_rock1: {"code": 0x23E, "offset": 0x10CAC9, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_bucket_quag_rock2: {"code": 0x23F, "offset": 0x10CACB, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_bucket_quag_rock3: {"code": 0x240, "offset": 0x10CACD, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
|
||||||
|
lname.tunnel_lbucket_albert: {"code": 0x190, "offset": 0x10C9E7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tunnel_albert_camp: {"code": 0x192, "offset": 0x10C9D7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tunnel_albert_quag: {"code": 0x193, "offset": 0x10C9CF, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_gondola_rc_sdoor_l: {"code": 0x53, "offset": 0x10CA5F, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.tunnel_gondola_rc_sdoor_m: {"code": 0x19E, "offset": 0x10CAA7, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_gondola_rc_sdoor_r: {"code": 0x188, "offset": 0x10CA27, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_gondola_rc: {"code": 0x19C, "offset": 0x10CAB7, "normal item": iname.powerup},
|
||||||
|
lname.tunnel_rgondola_station: {"code": 0x194, "offset": 0x10C9C7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tunnel_gondola_transfer: {"code": 0x186, "offset": 0x10CA2F, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.tunnel_corpse_bucket_quag: {"code": 0x18E, "offset": 0x10C9F7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tunnel_corpse_bucket_mdoor_l: {"code": 0x52, "offset": 0x10CA6F, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.tunnel_corpse_bucket_mdoor_r: {"code": 0x185, "offset": 0x10CA37, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_shovel_quag_start: {"code": 0x18D, "offset": 0x10C9FF, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_exit_quag_start: {"code": 0x18C, "offset": 0x10CA07, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_shovel_quag_end: {"code": 0x18B, "offset": 0x10CA0F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_exit_quag_end: {"code": 0x184, "offset": 0x10CA3F, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.tunnel_shovel: {"code": 0x18F, "offset": 0x86D8FC, "normal item": iname.roast_beef,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.tunnel_shovel_save: {"code": 0x18A, "offset": 0x10CA17, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tunnel_shovel_mdoor_l: {"code": 0x183, "offset": 0x10CA47, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.tunnel_shovel_mdoor_r: {"code": 0x51, "offset": 0x10CA77, "normal item": iname.axe,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.tunnel_shovel_sdoor_l: {"code": 0x182, "offset": 0x10CA4F, "normal item": iname.moon_card},
|
||||||
|
lname.tunnel_shovel_sdoor_m: {"code": 0x19D, "offset": 0x10CAAF, "normal item": iname.roast_chicken},
|
||||||
|
lname.tunnel_shovel_sdoor_r: {"code": 0x50, "offset": 0x10CA7F, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
# Underground Waterway
|
||||||
|
lname.uw_near_ent: {"code": 0x4C, "offset": 0x10CB03, "normal item": iname.three_hundred_gold},
|
||||||
|
lname.uw_across_ent: {"code": 0x4E, "offset": 0x10CAF3, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.uw_first_ledge1: {"code": 0x242, "offset": 0x10CB39, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.uw_first_ledge2: {"code": 0x243, "offset": 0x10CB3B, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.uw_first_ledge3: {"code": 0x244, "offset": 0x10CB3D, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.uw_first_ledge4: {"code": 0x245, "offset": 0x10CB3F, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.uw_first_ledge5: {"code": 0x246, "offset": 0x10CB41, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.uw_first_ledge6: {"code": 0x247, "offset": 0x10CB43, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.uw_poison_parkour: {"code": 0x4D, "offset": 0x10CAFB, "normal item": iname.cure_ampoule},
|
||||||
|
lname.uw_boss: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.uw_waterfall_alcove: {"code": 0x57, "offset": 0x10CB23, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.uw_carrie1: {"code": 0x4B, "offset": 0x10CB0B, "normal item": iname.moon_card,
|
||||||
|
"hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
|
||||||
|
lname.uw_carrie2: {"code": 0x4A, "offset": 0x10CB13, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
|
||||||
|
lname.uw_bricks_save: {"code": 0x5A, "offset": 0x10CB33, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.uw_above_skel_ledge: {"code": 0x56, "offset": 0x10CB2B, "normal item": iname.roast_chicken},
|
||||||
|
lname.uw_in_skel_ledge1: {"code": 0x249, "offset": 0x10CB45, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.uw_in_skel_ledge2: {"code": 0x24A, "offset": 0x10CB47, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.uw_in_skel_ledge3: {"code": 0x24B, "offset": 0x10CB49, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
# Castle Center
|
||||||
|
lname.ccb_skel_hallway_ent: {"code": 0x1AF, "offset": 0x10CB67, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ccb_skel_hallway_jun: {"code": 0x1A8, "offset": 0x10CBD7, "normal item": iname.powerup},
|
||||||
|
lname.ccb_skel_hallway_tc: {"code": 0x1AE, "offset": 0x10CB6F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.ccb_skel_hallway_ba: {"code": 0x1B6, "offset": 0x10CBC7, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ccb_behemoth_l_ff: {"code": 0x1AD, "offset": 0x10CB77, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ccb_behemoth_l_mf: {"code": 0x1B3, "offset": 0x10CBA7, "normal item": iname.three_hundred_gold,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccb_behemoth_l_mr: {"code": 0x1AC, "offset": 0x10CB7F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.ccb_behemoth_l_fr: {"code": 0x1B2, "offset": 0x10CBAF, "normal item": iname.three_hundred_gold,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccb_behemoth_r_ff: {"code": 0x1B1, "offset": 0x10CBB7, "normal item": iname.three_hundred_gold,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccb_behemoth_r_mf: {"code": 0x1AB, "offset": 0x10CB87, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ccb_behemoth_r_mr: {"code": 0x1B0, "offset": 0x10CBBF, "normal item": iname.three_hundred_gold,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccb_behemoth_r_fr: {"code": 0x1AA, "offset": 0x10CB8F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.ccb_behemoth_crate1: {"code": 0x24D, "offset": 0x10CBDD, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccb_behemoth_crate2: {"code": 0x24E, "offset": 0x10CBDF, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccb_behemoth_crate3: {"code": 0x24F, "offset": 0x10CBE1, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccb_behemoth_crate4: {"code": 0x250, "offset": 0x10CBE3, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccb_behemoth_crate5: {"code": 0x251, "offset": 0x10CBE5, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccelv_near_machine: {"code": 0x11A, "offset": 0x10CBF7, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ccelv_atop_machine: {"code": 0x118, "offset": 0x10CC17, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.three_hundred_gold},
|
||||||
|
lname.ccelv_stand1: {"code": 0x253, "offset": 0x10CC1D, "normal item": iname.roast_beef,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ccelv_stand2: {"code": 0x254, "offset": 0x10CC1F, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ccelv_stand3: {"code": 0x255, "offset": 0x10CC21, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ccelv_pipes: {"code": 0x11B, "offset": 0x10CC07, "normal item": iname.one_hundred_gold},
|
||||||
|
lname.ccelv_switch: {"code": 0x100, "offset": 0x10CC0F, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ccelv_staircase: {"code": 0x119, "offset": 0x10CBFF, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.ccff_redcarpet_knight: {"code": 0x10A, "offset": 0x8C44D9, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s, "type": "inv"},
|
||||||
|
lname.ccff_gears_side: {"code": 0x10F, "offset": 0x10CC33, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ccff_gears_mid: {"code": 0x10E, "offset": 0x10CC3B, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccff_gears_corner: {"code": 0x10D, "offset": 0x10CC43, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.ccff_lizard_knight: {"code": 0x109, "offset": 0x8C44E7, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.three_hundred_gold, "type": "inv"},
|
||||||
|
lname.ccff_lizard_near_knight: {"code": 0x101, "offset": 0x10CC5B, "normal item": iname.axe,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ccff_lizard_pit: {"code": 0x10C, "offset": 0x10CC4B, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.ccff_lizard_corner: {"code": 0x10B, "offset": 0x10CC53, "normal item": iname.moon_card,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.ccff_lizard_locker_nfr: {"code": 0x104, "offset": 0x8C450A, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_locker_nmr: {"code": 0x105, "offset": 0xBFC9C3, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_locker_nml: {"code": 0x106, "offset": 0xBFC9C7, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.cure_ampoule, "add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_locker_nfl: {"code": 0x107, "offset": 0xBFCA07, "normal item": iname.powerup,
|
||||||
|
"add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_locker_fl: {"code": 0x102, "offset": 0xBFCA03, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_locker_fr: {"code": 0x103, "offset": 0x8C44F5, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.three_hundred_gold, "add conds": ["liz"]},
|
||||||
|
lname.ccff_lizard_slab1: {"code": 0x257, "offset": 0x10CC61, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
|
||||||
|
lname.ccff_lizard_slab2: {"code": 0x258, "offset": 0x10CC63, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.powerup, "add conds": ["3hb"]},
|
||||||
|
lname.ccff_lizard_slab3: {"code": 0x259, "offset": 0x10CC65, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ccff_lizard_slab4: {"code": 0x25A, "offset": 0x10CC67, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ccb_mandrag_shelf_l: {"code": 0x1A0, "offset": 0xBFCBB3, "normal item": iname.mandragora},
|
||||||
|
lname.ccb_mandrag_shelf_r: {"code": 0x1A1, "offset": 0xBFCBAF, "normal item": iname.mandragora},
|
||||||
|
lname.ccb_torture_rack: {"code": 0x1A9, "offset": 0x8985E5, "normal item": iname.purifying,
|
||||||
|
"type": "inv"},
|
||||||
|
lname.ccb_torture_rafters: {"code": 0x1A2, "offset": 0x8985D6, "normal item": iname.roast_beef},
|
||||||
|
lname.cc_behind_the_seal: {"event": iname.crystal, "add conds": ["crystal"]},
|
||||||
|
lname.cc_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.cc_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.ccll_brokenstairs_floor: {"code": 0x7B, "offset": 0x10CC8F, "normal item": iname.red_jewel_l,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccll_brokenstairs_knight: {"code": 0x74, "offset": 0x8DF782, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
|
||||||
|
lname.ccll_brokenstairs_save: {"code": 0x7C, "offset": 0x10CC87, "normal item": iname.red_jewel_l,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccll_glassknight_l: {"code": 0x7A, "offset": 0x10CC97, "normal item": iname.red_jewel_s,
|
||||||
|
"hard item": iname.five_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccll_glassknight_r: {"code": 0x7E, "offset": 0x10CC77, "normal item": iname.red_jewel_s,
|
||||||
|
"hard item": iname.five_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccll_butlers_door: {"code": 0x7D, "offset": 0x10CC7F, "normal item": iname.red_jewel_s,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccll_butlers_side: {"code": 0x79, "offset": 0x10CC9F, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.one_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccll_cwhall_butlerflames_past: {"code": 0x78, "offset": 0x10CCA7, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.red_jewel_l, "countdown": 14},
|
||||||
|
lname.ccll_cwhall_flamethrower: {"code": 0x73, "offset": 0x8DF580, "normal item": iname.five_hundred_gold,
|
||||||
|
"type": "inv", "countdown": 14},
|
||||||
|
lname.ccll_cwhall_cwflames: {"code": 0x77, "offset": 0x10CCAF, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.red_jewel_l, "countdown": 14},
|
||||||
|
lname.ccll_heinrich: {"code": 0x69, "offset": 0xBFE443, "normal item": iname.chamber_key,
|
||||||
|
"type": "npc", "countdown": 14},
|
||||||
|
lname.ccia_nitro_crates: {"code": 0x66, "offset": 0x90FCE9, "normal item": iname.healing_kit,
|
||||||
|
"hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_nitro_shelf_h: {"code": 0x55, "offset": 0xBFCC03, "normal item": iname.magical_nitro,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccia_stairs_knight: {"code": 0x61, "offset": 0x90FE5C, "normal item": iname.five_hundred_gold,
|
||||||
|
"type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_maids_vase: {"code": 0x63, "offset": 0x90FF1D, "normal item": iname.red_jewel_l,
|
||||||
|
"type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_maids_outer: {"code": 0x6B, "offset": 0x10CCFF, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.three_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccia_maids_inner: {"code": 0x6A, "offset": 0x10CD07, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.three_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccia_inventions_maids: {"code": 0x6C, "offset": 0x10CCE7, "normal item": iname.moon_card,
|
||||||
|
"hard item": iname.one_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccia_inventions_crusher: {"code": 0x6E, "offset": 0x10CCDF, "normal item": iname.sun_card,
|
||||||
|
"hard item": iname.one_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccia_inventions_famicart: {"code": 0x64, "offset": 0x90FBB3, "normal item": iname.five_hundred_gold,
|
||||||
|
"type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_inventions_zeppelin: {"code": 0x6D, "offset": 0x90FBC0, "normal item": iname.roast_beef,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccia_inventions_round: {"code": 0x65, "offset": 0x90FBA7, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.five_hundred_gold, "type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_nitrohall_flamethrower: {"code": 0x62, "offset": 0x90FCDA, "normal item": iname.red_jewel_l,
|
||||||
|
"type": "inv", "countdown": 14},
|
||||||
|
lname.ccia_nitrohall_torch: {"code": 0x6F, "offset": 0x10CCD7, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.red_jewel_s, "countdown": 14},
|
||||||
|
lname.ccia_nitro_shelf_i: {"code": 0x60, "offset": 0xBFCBFF, "normal item": iname.magical_nitro,
|
||||||
|
"countdown": 14},
|
||||||
|
lname.ccll_cwhall_wall: {"code": 0x76, "offset": 0x10CCB7, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold, "countdown": 14},
|
||||||
|
lname.ccl_bookcase: {"code": 0x166, "offset": 0x8F1197, "normal item": iname.sun_card,
|
||||||
|
"countdown": 14},
|
||||||
|
# Duel Tower
|
||||||
|
lname.dt_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.dt_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.dt_ibridge_l: {"code": 0x81, "offset": 0x10CE8B, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.dt_ibridge_r: {"code": 0x80, "offset": 0x10CE93, "normal item": iname.powerup},
|
||||||
|
lname.dt_stones_start: {"code": 0x83, "offset": 0x10CE73, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.dt_stones_end: {"code": 0x97, "offset": 0x10CE83, "normal item": iname.knife, "add conds": ["sub"]},
|
||||||
|
lname.dt_werebull_arena: {"code": 0x82, "offset": 0x10CE7B, "normal item": iname.roast_beef},
|
||||||
|
lname.dt_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
lname.dt_boss_four: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
# Tower of Execution
|
||||||
|
lname.toe_ledge1: {"code": 0x25C, "offset": 0x10CD5D, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.toe_ledge2: {"code": 0x25D, "offset": 0x10CD5F, "normal item": iname.purifying,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.toe_ledge3: {"code": 0x25E, "offset": 0x10CD61, "normal item": iname.five_hundred_gold,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.toe_ledge4: {"code": 0x25F, "offset": 0x10CD63, "normal item": iname.cure_ampoule,
|
||||||
|
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.toe_ledge5: {"code": 0x260, "offset": 0x10CD65, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["3hb", "sub"]},
|
||||||
|
lname.toe_midsavespikes_r: {"code": 0x9C, "offset": 0x10CD1F, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.toe_midsavespikes_l: {"code": 0x9B, "offset": 0x10CD27, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.toe_elec_grate: {"code": 0x99, "offset": 0x10CD17, "normal item": iname.execution_key},
|
||||||
|
lname.toe_ibridge: {"code": 0x98, "offset": 0x10CD47, "normal item": iname.one_hundred_gold},
|
||||||
|
lname.toe_top: {"code": 0x9D, "offset": 0x10CD4F, "normal item": iname.red_jewel_l},
|
||||||
|
lname.toe_keygate_l: {"code": 0x9A, "offset": 0x10CD37, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.toe_keygate_r: {"code": 0x9E, "offset": 0x10CD3F, "normal item": iname.cross, "add conds": ["sub"]},
|
||||||
|
# Tower of Science
|
||||||
|
lname.tosci_elevator: {"code": 0x1FC, "offset": 0x10CE0B, "normal item": iname.three_hundred_gold},
|
||||||
|
lname.tosci_plain_sr: {"code": 0x1FF, "offset": 0x10CDF3, "normal item": iname.science_key1},
|
||||||
|
lname.tosci_stairs_sr: {"code": 0x1FB, "offset": 0x10CE13, "normal item": iname.three_hundred_gold},
|
||||||
|
lname.tosci_three_door_hall: {"code": 0x1FE, "offset": 0x10CDFB, "normal item": iname.science_key2},
|
||||||
|
lname.tosci_ibridge_t: {"code": 0x1F3, "offset": 0x10CE3B, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.red_jewel_l},
|
||||||
|
lname.tosci_ibridge_b1: {"code": 0x262, "offset": 0x10CE59, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tosci_ibridge_b2: {"code": 0x263, "offset": 0x10CE5B, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tosci_ibridge_b3: {"code": 0x264, "offset": 0x10CE5D, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tosci_ibridge_b4: {"code": 0x265, "offset": 0x10CE5F, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tosci_ibridge_b5: {"code": 0x266, "offset": 0x10CE61, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.tosci_ibridge_b6: {"code": 0x267, "offset": 0x10CE63, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.tosci_conveyor_sr: {"code": 0x1F7, "offset": 0x10CE33, "normal item": iname.red_jewel_l,
|
||||||
|
"hard item": iname.red_jewel_s},
|
||||||
|
lname.tosci_exit: {"code": 0x1FD, "offset": 0x10CE03, "normal item": iname.science_key3},
|
||||||
|
lname.tosci_key3_r: {"code": 0x1FA, "offset": 0x10CE1B, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.tosci_key3_m: {"code": 0x1F2, "offset": 0x10CE2B, "normal item": iname.cross, "add conds": ["sub"]},
|
||||||
|
lname.tosci_key3_l: {"code": 0x1F9, "offset": 0x10CE23, "normal item": iname.five_hundred_gold},
|
||||||
|
# Tower of Sorcery
|
||||||
|
lname.tosor_stained_tower: {"code": 0x96, "offset": 0x10CDB3, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tosor_savepoint: {"code": 0x95, "offset": 0x10CDBB, "normal item": iname.red_jewel_l},
|
||||||
|
lname.tosor_trickshot: {"code": 0x92, "offset": 0x10CDD3, "normal item": iname.roast_beef},
|
||||||
|
lname.tosor_yellow_bubble: {"code": 0x91, "offset": 0x10CDDB, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.tosor_blue_platforms: {"code": 0x94, "offset": 0x10CDC3, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tosor_side_isle: {"code": 0x93, "offset": 0x10CDCB, "normal item": iname.red_jewel_s},
|
||||||
|
lname.tosor_ibridge: {"code": 0x90, "offset": 0x10CDE3, "normal item": iname.three_hundred_gold},
|
||||||
|
# Room of Clocks
|
||||||
|
lname.roc_ent_l: {"code": 0xC6, "offset": 0x10CF7B, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.red_jewel_l},
|
||||||
|
lname.roc_ent_r: {"code": 0xC3, "offset": 0x10CFBB, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.five_hundred_gold},
|
||||||
|
lname.roc_elev_r: {"code": 0xD4, "offset": 0x10CF93, "normal item": iname.holy_water, "add conds": ["sub"]},
|
||||||
|
lname.roc_elev_l: {"code": 0xD5, "offset": 0x10CF8B, "normal item": iname.axe, "add conds": ["sub"]},
|
||||||
|
lname.roc_cont_r: {"code": 0xC5, "offset": 0x10CFB3, "normal item": iname.powerup,
|
||||||
|
"hard item": iname.one_hundred_gold},
|
||||||
|
lname.roc_cont_l: {"code": 0xDF, "offset": 0x10CFA3, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.roc_exit: {"code": 0xDC, "offset": 0x10CF9B, "normal item": iname.three_hundred_gold,
|
||||||
|
"add conds": ["empty"]},
|
||||||
|
lname.roc_boss: {"event": iname.trophy, "add conds": ["boss"]},
|
||||||
|
# Clock Tower
|
||||||
|
lname.ct_gearclimb_battery_slab1: {"code": 0x269, "offset": 0x10CEF9, "normal item": iname.roast_chicken,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_gearclimb_battery_slab2: {"code": 0x26A, "offset": 0x10CEFB, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.ct_gearclimb_battery_slab3: {"code": 0x26B, "offset": 0x10CEFD, "normal item": iname.roast_chicken,
|
||||||
|
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
|
||||||
|
lname.ct_gearclimb_corner: {"code": 0xA7, "offset": 0x10CEB3, "normal item": iname.red_jewel_s},
|
||||||
|
lname.ct_gearclimb_side: {"code": 0xAD, "offset": 0x10CEC3, "normal item": iname.clocktower_key1},
|
||||||
|
lname.ct_gearclimb_door_slab1: {"code": 0x26D, "offset": 0x10CF01, "normal item": iname.roast_beef,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_gearclimb_door_slab2: {"code": 0x26E, "offset": 0x10CF03, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ct_gearclimb_door_slab3: {"code": 0x26F, "offset": 0x10CF05, "normal item": iname.roast_beef,
|
||||||
|
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
|
||||||
|
lname.ct_bp_chasm_fl: {"code": 0xA5, "offset": 0x99BC4D, "normal item": iname.five_hundred_gold},
|
||||||
|
lname.ct_bp_chasm_fr: {"code": 0xA6, "offset": 0x99BC3E, "normal item": iname.red_jewel_l},
|
||||||
|
lname.ct_bp_chasm_rl: {"code": 0xA4, "offset": 0x99BC5A, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ct_bp_chasm_k: {"code": 0xAC, "offset": 0x99BC30, "normal item": iname.clocktower_key2},
|
||||||
|
lname.ct_finalroom_door_slab1: {"code": 0x271, "offset": 0x10CEF5, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_door_slab2: {"code": 0x272, "offset": 0x10CEF7, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_fl: {"code": 0xB3, "offset": 0x10CED3, "normal item": iname.axe,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ct_finalroom_fr: {"code": 0xB4, "offset": 0x10CECB, "normal item": iname.knife,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ct_finalroom_rl: {"code": 0xB2, "offset": 0x10CEE3, "normal item": iname.holy_water,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ct_finalroom_rr: {"code": 0xB0, "offset": 0x10CEDB, "normal item": iname.cross,
|
||||||
|
"add conds": ["sub"]},
|
||||||
|
lname.ct_finalroom_platform: {"code": 0xAB, "offset": 0x10CEBB, "normal item": iname.clocktower_key3},
|
||||||
|
lname.ct_finalroom_renon_slab1: {"code": 0x274, "offset": 0x10CF09, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab2: {"code": 0x275, "offset": 0x10CF0B, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab3: {"code": 0x276, "offset": 0x10CF0D, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab4: {"code": 0x277, "offset": 0x10CF0F, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab5: {"code": 0x278, "offset": 0x10CF11, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab6: {"code": 0x279, "offset": 0x10CF13, "normal item": iname.five_hundred_gold,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab7: {"code": 0x27A, "offset": 0x10CF15, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
lname.ct_finalroom_renon_slab8: {"code": 0x27B, "offset": 0x10CF17, "normal item": iname.red_jewel_l,
|
||||||
|
"add conds": ["3hb"]},
|
||||||
|
# Castle Keep
|
||||||
|
lname.ck_boss_one: {"event": iname.trophy, "add conds": ["boss", "renon"]},
|
||||||
|
lname.ck_boss_two: {"event": iname.trophy, "add conds": ["boss", "vincent"]},
|
||||||
|
lname.ck_flame_l: {"code": 0xAF, "offset": 0x9778C8, "normal item": iname.healing_kit, "type": "inv"},
|
||||||
|
lname.ck_flame_r: {"code": 0xAE, "offset": 0xBFCA67, "normal item": iname.healing_kit, "type": "inv"},
|
||||||
|
lname.ck_behind_drac: {"code": 0xBF, "offset": 0x10CE9B, "normal item": iname.red_jewel_l},
|
||||||
|
lname.ck_cube: {"code": 0xB5, "offset": 0x10CEA3, "normal item": iname.healing_kit},
|
||||||
|
lname.renon1: {"code": 0x1C8, "offset": 0xBFD8E5, "normal item": iname.roast_chicken, "type": "shop"},
|
||||||
|
lname.renon2: {"code": 0x1C9, "offset": 0xBFD8E7, "normal item": iname.roast_beef, "type": "shop"},
|
||||||
|
lname.renon3: {"code": 0x1CA, "offset": 0xBFD8E9, "normal item": iname.healing_kit, "type": "shop"},
|
||||||
|
lname.renon4: {"code": 0x1CB, "offset": 0xBFD8EB, "normal item": iname.purifying, "type": "shop"},
|
||||||
|
lname.renon5: {"code": 0x1CC, "offset": 0xBFD8ED, "normal item": iname.cure_ampoule, "type": "shop"},
|
||||||
|
lname.renon6: {"code": 0x1CD, "offset": 0xBFD907, "normal item": iname.sun_card, "type": "shop"},
|
||||||
|
lname.renon7: {"code": 0x1CE, "offset": 0xBFD909, "normal item": iname.moon_card, "type": "shop"},
|
||||||
|
lname.the_end: {"event": iname.victory},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
add_conds = {"carrie": ("carrie_logic", True, True),
|
||||||
|
"liz": ("lizard_locker_items", True, True),
|
||||||
|
"sub": ("sub_weapon_shuffle", SubWeaponShuffle.option_anywhere, True),
|
||||||
|
"3hb": ("multi_hit_breakables", True, True),
|
||||||
|
"empty": ("empty_breakables", True, True),
|
||||||
|
"shop": ("shopsanity", True, True),
|
||||||
|
"crystal": ("draculas_condition", DraculasCondition.option_crystal, True),
|
||||||
|
"boss": ("draculas_condition", DraculasCondition.option_bosses, True),
|
||||||
|
"renon": ("renon_fight_condition", RenonFightCondition.option_never, False),
|
||||||
|
"vincent": ("vincent_fight_condition", VincentFightCondition.option_never, False)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_info(location: str, info: str) -> Union[int, str, List[str], None]:
|
||||||
|
return location_info[location].get(info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_names_to_ids() -> Dict[str, int]:
|
||||||
|
return {name: get_location_info(name, "code")+base_id for name in location_info if get_location_info(name, "code")
|
||||||
|
is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def verify_locations(options: CV64Options, locations: List[str]) -> Tuple[Dict[str, Optional[int]], Dict[str, str]]:
|
||||||
|
|
||||||
|
verified_locations = {}
|
||||||
|
events = {}
|
||||||
|
|
||||||
|
for loc in locations:
|
||||||
|
loc_add_conds = get_location_info(loc, "add conds")
|
||||||
|
loc_code = get_location_info(loc, "code")
|
||||||
|
|
||||||
|
# Check any options that might be associated with the Location before adding it.
|
||||||
|
add_it = True
|
||||||
|
if isinstance(loc_add_conds, list):
|
||||||
|
for cond in loc_add_conds:
|
||||||
|
if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
|
||||||
|
add_it = False
|
||||||
|
|
||||||
|
if not add_it:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add the location to the verified Locations if the above check passes.
|
||||||
|
# If we are looking at an event Location, add its associated event Item to the events' dict.
|
||||||
|
# Otherwise, add the base_id to the Location's code.
|
||||||
|
if loc_code is None:
|
||||||
|
events[loc] = get_location_info(loc, "event")
|
||||||
|
else:
|
||||||
|
loc_code += base_id
|
||||||
|
verified_locations.update({loc: loc_code})
|
||||||
|
|
||||||
|
return verified_locations, events
|
|
@ -0,0 +1,266 @@
|
||||||
|
# **************************************************************
|
||||||
|
# * LZKN64 Compression and Decompression Utility *
|
||||||
|
# * Original repo at https://github.com/Fluvian/lzkn64, *
|
||||||
|
# * converted from C to Python with permission from Fluvian. *
|
||||||
|
# **************************************************************
|
||||||
|
|
||||||
|
TYPE_COMPRESS = 1
|
||||||
|
TYPE_DECOMPRESS = 2
|
||||||
|
|
||||||
|
MODE_NONE = 0x7F
|
||||||
|
MODE_WINDOW_COPY = 0x00
|
||||||
|
MODE_RAW_COPY = 0x80
|
||||||
|
MODE_RLE_WRITE_A = 0xC0
|
||||||
|
MODE_RLE_WRITE_B = 0xE0
|
||||||
|
MODE_RLE_WRITE_C = 0xFF
|
||||||
|
|
||||||
|
WINDOW_SIZE = 0x3FF
|
||||||
|
COPY_SIZE = 0x21
|
||||||
|
RLE_SIZE = 0x101
|
||||||
|
|
||||||
|
|
||||||
|
# Compresses the data in the buffer specified in the arguments.
|
||||||
|
def compress_buffer(file_buffer: bytearray) -> bytearray:
|
||||||
|
# Size of the buffer to compress
|
||||||
|
buffer_size = len(file_buffer) - 1
|
||||||
|
|
||||||
|
# Position of the current read location in the buffer.
|
||||||
|
buffer_position = 0
|
||||||
|
|
||||||
|
# Position of the current write location in the written buffer.
|
||||||
|
write_position = 4
|
||||||
|
|
||||||
|
# Allocate write_buffer with size of 0xFFFFFF (24-bit).
|
||||||
|
write_buffer = bytearray(0xFFFFFF)
|
||||||
|
|
||||||
|
# Position in the input buffer of the last time one of the copy modes was used.
|
||||||
|
buffer_last_copy_position = 0
|
||||||
|
|
||||||
|
while buffer_position < buffer_size:
|
||||||
|
# Calculate maximum length we are able to copy without going out of bounds.
|
||||||
|
if COPY_SIZE < (buffer_size - 1) - buffer_position:
|
||||||
|
sliding_window_maximum_length = COPY_SIZE
|
||||||
|
else:
|
||||||
|
sliding_window_maximum_length = (buffer_size - 1) - buffer_position
|
||||||
|
|
||||||
|
# Calculate how far we are able to look back without going behind the start of the uncompressed buffer.
|
||||||
|
if buffer_position - WINDOW_SIZE > 0:
|
||||||
|
sliding_window_maximum_offset = buffer_position - WINDOW_SIZE
|
||||||
|
else:
|
||||||
|
sliding_window_maximum_offset = 0
|
||||||
|
|
||||||
|
# Calculate maximum length the forwarding looking window is able to search.
|
||||||
|
if RLE_SIZE < (buffer_size - 1) - buffer_position:
|
||||||
|
forward_window_maximum_length = RLE_SIZE
|
||||||
|
else:
|
||||||
|
forward_window_maximum_length = (buffer_size - 1) - buffer_position
|
||||||
|
|
||||||
|
sliding_window_match_position = -1
|
||||||
|
sliding_window_match_size = 0
|
||||||
|
|
||||||
|
forward_window_match_value = 0
|
||||||
|
forward_window_match_size = 0
|
||||||
|
|
||||||
|
# The current mode the compression algorithm prefers. (0x7F == None)
|
||||||
|
current_mode = MODE_NONE
|
||||||
|
|
||||||
|
# The current submode the compression algorithm prefers.
|
||||||
|
current_submode = MODE_NONE
|
||||||
|
|
||||||
|
# How many bytes will have to be copied in the raw copy command.
|
||||||
|
raw_copy_size = buffer_position - buffer_last_copy_position
|
||||||
|
|
||||||
|
# How many bytes we still have to copy in RLE matches with more than 0x21 bytes.
|
||||||
|
rle_bytes_left = 0
|
||||||
|
|
||||||
|
"""Go backwards in the buffer, is there a matching value?
|
||||||
|
If yes, search forward and check for more matching values in a loop.
|
||||||
|
If no, go further back and repeat."""
|
||||||
|
for search_position in range(buffer_position - 1, sliding_window_maximum_offset - 1, -1):
|
||||||
|
matching_sequence_size = 0
|
||||||
|
|
||||||
|
while file_buffer[search_position + matching_sequence_size] == file_buffer[buffer_position +
|
||||||
|
matching_sequence_size]:
|
||||||
|
matching_sequence_size += 1
|
||||||
|
|
||||||
|
if matching_sequence_size >= sliding_window_maximum_length:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Once we find a match or a match that is bigger than the match before it, we save its position and length.
|
||||||
|
if matching_sequence_size > sliding_window_match_size:
|
||||||
|
sliding_window_match_position = search_position
|
||||||
|
sliding_window_match_size = matching_sequence_size
|
||||||
|
|
||||||
|
"""Look one step forward in the buffer, is there a matching value?
|
||||||
|
If yes, search further and check for a repeating value in a loop.
|
||||||
|
If no, continue to the rest of the function."""
|
||||||
|
matching_sequence_value = file_buffer[buffer_position]
|
||||||
|
matching_sequence_size = 0
|
||||||
|
|
||||||
|
while file_buffer[buffer_position + matching_sequence_size] == matching_sequence_value:
|
||||||
|
matching_sequence_size += 1
|
||||||
|
|
||||||
|
if matching_sequence_size >= forward_window_maximum_length:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we find a sequence of matching values, save them.
|
||||||
|
if matching_sequence_size >= 1:
|
||||||
|
forward_window_match_value = matching_sequence_value
|
||||||
|
forward_window_match_size = matching_sequence_size
|
||||||
|
|
||||||
|
# Try to pick which mode works best with the current values.
|
||||||
|
if sliding_window_match_size >= 3:
|
||||||
|
current_mode = MODE_WINDOW_COPY
|
||||||
|
elif forward_window_match_size >= 3:
|
||||||
|
current_mode = MODE_RLE_WRITE_A
|
||||||
|
|
||||||
|
if forward_window_match_value != 0x00 and forward_window_match_size <= COPY_SIZE:
|
||||||
|
current_submode = MODE_RLE_WRITE_A
|
||||||
|
elif forward_window_match_value != 0x00 and forward_window_match_size > COPY_SIZE:
|
||||||
|
current_submode = MODE_RLE_WRITE_A
|
||||||
|
rle_bytes_left = forward_window_match_size
|
||||||
|
elif forward_window_match_value == 0x00 and forward_window_match_size <= COPY_SIZE:
|
||||||
|
current_submode = MODE_RLE_WRITE_B
|
||||||
|
elif forward_window_match_value == 0x00 and forward_window_match_size > COPY_SIZE:
|
||||||
|
current_submode = MODE_RLE_WRITE_C
|
||||||
|
elif forward_window_match_size >= 2 and forward_window_match_value == 0x00:
|
||||||
|
current_mode = MODE_RLE_WRITE_A
|
||||||
|
current_submode = MODE_RLE_WRITE_B
|
||||||
|
|
||||||
|
"""Write a raw copy command when these following conditions are met:
|
||||||
|
The current mode is set and there are raw bytes available to be copied.
|
||||||
|
The raw byte length exceeds the maximum length that can be stored.
|
||||||
|
Raw bytes need to be written due to the proximity to the end of the buffer."""
|
||||||
|
if (current_mode != MODE_NONE and raw_copy_size >= 1) or raw_copy_size >= 0x1F or \
|
||||||
|
(buffer_position + 1) == buffer_size:
|
||||||
|
if buffer_position + 1 == buffer_size:
|
||||||
|
raw_copy_size = buffer_size - buffer_last_copy_position
|
||||||
|
|
||||||
|
write_buffer[write_position] = MODE_RAW_COPY | raw_copy_size & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
for written_bytes in range(raw_copy_size):
|
||||||
|
write_buffer[write_position] = file_buffer[buffer_last_copy_position]
|
||||||
|
write_position += 1
|
||||||
|
buffer_last_copy_position += 1
|
||||||
|
|
||||||
|
if current_mode == MODE_WINDOW_COPY:
|
||||||
|
write_buffer[write_position] = MODE_WINDOW_COPY | ((sliding_window_match_size - 2) & 0x1F) << 2 | \
|
||||||
|
(((buffer_position - sliding_window_match_position) & 0x300) >> 8)
|
||||||
|
write_position += 1
|
||||||
|
write_buffer[write_position] = (buffer_position - sliding_window_match_position) & 0xFF
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
buffer_position += sliding_window_match_size
|
||||||
|
buffer_last_copy_position = buffer_position
|
||||||
|
elif current_mode == MODE_RLE_WRITE_A:
|
||||||
|
if current_submode == MODE_RLE_WRITE_A:
|
||||||
|
if rle_bytes_left > 0:
|
||||||
|
while rle_bytes_left > 0:
|
||||||
|
# Dump raw bytes if we have less than two bytes left, not doing so would cause an underflow
|
||||||
|
# error.
|
||||||
|
if rle_bytes_left < 2:
|
||||||
|
write_buffer[write_position] = MODE_RAW_COPY | rle_bytes_left & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
for writtenBytes in range(rle_bytes_left):
|
||||||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
rle_bytes_left = 0
|
||||||
|
break
|
||||||
|
|
||||||
|
if rle_bytes_left < COPY_SIZE:
|
||||||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (rle_bytes_left - 2) & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
else:
|
||||||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (COPY_SIZE - 2) & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||||||
|
write_position += 1
|
||||||
|
rle_bytes_left -= COPY_SIZE
|
||||||
|
else:
|
||||||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (forward_window_match_size - 2) & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
elif current_submode == MODE_RLE_WRITE_B:
|
||||||
|
write_buffer[write_position] = MODE_RLE_WRITE_B | (forward_window_match_size - 2) & 0x1F
|
||||||
|
write_position += 1
|
||||||
|
elif current_submode == MODE_RLE_WRITE_C:
|
||||||
|
write_buffer[write_position] = MODE_RLE_WRITE_C
|
||||||
|
write_position += 1
|
||||||
|
write_buffer[write_position] = (forward_window_match_size - 2) & 0xFF
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
buffer_position += forward_window_match_size
|
||||||
|
buffer_last_copy_position = buffer_position
|
||||||
|
else:
|
||||||
|
buffer_position += 1
|
||||||
|
|
||||||
|
# Write the compressed size.
|
||||||
|
write_buffer[1] = 0x00
|
||||||
|
write_buffer[1] = write_position >> 16 & 0xFF
|
||||||
|
write_buffer[2] = write_position >> 8 & 0xFF
|
||||||
|
write_buffer[3] = write_position & 0xFF
|
||||||
|
|
||||||
|
# Return the compressed write buffer.
|
||||||
|
return write_buffer[0:write_position]
|
||||||
|
|
||||||
|
|
||||||
|
# Decompresses the data in the buffer specified in the arguments.
|
||||||
|
def decompress_buffer(file_buffer: bytearray) -> bytearray:
|
||||||
|
# Position of the current read location in the buffer.
|
||||||
|
buffer_position = 4
|
||||||
|
|
||||||
|
# Position of the current write location in the written buffer.
|
||||||
|
write_position = 0
|
||||||
|
|
||||||
|
# Get compressed size.
|
||||||
|
compressed_size = (file_buffer[1] << 16) + (file_buffer[2] << 8) + file_buffer[3] - 1
|
||||||
|
|
||||||
|
# Allocate writeBuffer with size of 0xFFFFFF (24-bit).
|
||||||
|
write_buffer = bytearray(0xFFFFFF)
|
||||||
|
|
||||||
|
while buffer_position < compressed_size:
|
||||||
|
mode_command = file_buffer[buffer_position]
|
||||||
|
buffer_position += 1
|
||||||
|
|
||||||
|
if MODE_WINDOW_COPY <= mode_command < MODE_RAW_COPY:
|
||||||
|
copy_length = (mode_command >> 2) + 2
|
||||||
|
copy_offset = file_buffer[buffer_position] + (mode_command << 8) & 0x3FF
|
||||||
|
buffer_position += 1
|
||||||
|
|
||||||
|
for current_length in range(copy_length, 0, -1):
|
||||||
|
write_buffer[write_position] = write_buffer[write_position - copy_offset]
|
||||||
|
write_position += 1
|
||||||
|
elif MODE_RAW_COPY <= mode_command < MODE_RLE_WRITE_A:
|
||||||
|
copy_length = mode_command & 0x1F
|
||||||
|
|
||||||
|
for current_length in range(copy_length, 0, -1):
|
||||||
|
write_buffer[write_position] = file_buffer[buffer_position]
|
||||||
|
write_position += 1
|
||||||
|
buffer_position += 1
|
||||||
|
elif MODE_RLE_WRITE_A <= mode_command <= MODE_RLE_WRITE_C:
|
||||||
|
write_length = 0
|
||||||
|
write_value = 0x00
|
||||||
|
|
||||||
|
if MODE_RLE_WRITE_A <= mode_command < MODE_RLE_WRITE_B:
|
||||||
|
write_length = (mode_command & 0x1F) + 2
|
||||||
|
write_value = file_buffer[buffer_position]
|
||||||
|
buffer_position += 1
|
||||||
|
elif MODE_RLE_WRITE_B <= mode_command < MODE_RLE_WRITE_C:
|
||||||
|
write_length = (mode_command & 0x1F) + 2
|
||||||
|
elif mode_command == MODE_RLE_WRITE_C:
|
||||||
|
write_length = file_buffer[buffer_position] + 2
|
||||||
|
buffer_position += 1
|
||||||
|
|
||||||
|
for current_length in range(write_length, 0, -1):
|
||||||
|
write_buffer[write_position] = write_value
|
||||||
|
write_position += 1
|
||||||
|
|
||||||
|
# Return the current position of the write buffer, essentially giving us the size of the write buffer.
|
||||||
|
while write_position % 16 != 0:
|
||||||
|
write_position += 1
|
||||||
|
return write_buffer[0:write_position]
|
|
@ -0,0 +1,490 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterStages(Choice):
|
||||||
|
"""Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end
|
||||||
|
of Villa and Castle Center."""
|
||||||
|
display_name = "Character Stages"
|
||||||
|
option_both = 0
|
||||||
|
option_branchless_both = 1
|
||||||
|
option_reinhardt_only = 2
|
||||||
|
option_carrie_only = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class StageShuffle(Toggle):
|
||||||
|
"""Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character
|
||||||
|
stage slots if Character Stages is set to Both; they can only be somewhere on the main path.
|
||||||
|
Castle Keep will always be at the end of the line."""
|
||||||
|
display_name = "Stage Shuffle"
|
||||||
|
|
||||||
|
|
||||||
|
class StartingStage(Choice):
|
||||||
|
"""Which stage to start at if Stage Shuffle is turned on."""
|
||||||
|
display_name = "Starting Stage"
|
||||||
|
option_forest_of_silence = 0
|
||||||
|
option_castle_wall = 1
|
||||||
|
option_villa = 2
|
||||||
|
option_tunnel = 3
|
||||||
|
option_underground_waterway = 4
|
||||||
|
option_castle_center = 5
|
||||||
|
option_duel_tower = 6
|
||||||
|
option_tower_of_execution = 7
|
||||||
|
option_tower_of_science = 8
|
||||||
|
option_tower_of_sorcery = 9
|
||||||
|
option_room_of_clocks = 10
|
||||||
|
option_clock_tower = 11
|
||||||
|
default = "random"
|
||||||
|
|
||||||
|
|
||||||
|
class WarpOrder(Choice):
|
||||||
|
"""Arranges the warps in the warp menu in whichever stage order chosen,
|
||||||
|
thereby changing the order they are unlocked in."""
|
||||||
|
display_name = "Warp Order"
|
||||||
|
option_seed_stage_order = 0
|
||||||
|
option_vanilla_stage_order = 1
|
||||||
|
option_randomized_order = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class SubWeaponShuffle(Choice):
|
||||||
|
"""Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool."""
|
||||||
|
display_name = "Sub-weapon Shuffle"
|
||||||
|
option_off = 0
|
||||||
|
option_own_pool = 1
|
||||||
|
option_anywhere = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class SpareKeys(Choice):
|
||||||
|
"""Puts an additional copy of every non-Special key item in the pool for every key item that there is.
|
||||||
|
Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them."""
|
||||||
|
display_name = "Spare Keys"
|
||||||
|
option_off = 0
|
||||||
|
option_on = 1
|
||||||
|
option_chance = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class HardItemPool(Toggle):
|
||||||
|
"""Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode
|
||||||
|
in the PAL version."""
|
||||||
|
display_name = "Hard Item Pool"
|
||||||
|
|
||||||
|
|
||||||
|
class Special1sPerWarp(Range):
|
||||||
|
"""Sets how many Special1 jewels are needed per warp menu option unlock."""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 10
|
||||||
|
default = 1
|
||||||
|
display_name = "Special1s Per Warp"
|
||||||
|
|
||||||
|
|
||||||
|
class TotalSpecial1s(Range):
|
||||||
|
"""Sets how many Speical1 jewels are in the pool in total.
|
||||||
|
If this is set to be less than Special1s Per Warp x 7, it will decrease by 1 until it isn't."""
|
||||||
|
range_start = 7
|
||||||
|
range_end = 70
|
||||||
|
default = 7
|
||||||
|
display_name = "Total Special1s"
|
||||||
|
|
||||||
|
|
||||||
|
class DraculasCondition(Choice):
|
||||||
|
"""Sets the requirement for unlocking and opening the door to Dracula's chamber.
|
||||||
|
None: No requirement. Door is unlocked from the start.
|
||||||
|
Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated.
|
||||||
|
Bosses: Kill a specified number of bosses with health bars and claim their Trophies.
|
||||||
|
Specials: Find a specified number of Special2 jewels shuffled in the main item pool."""
|
||||||
|
display_name = "Dracula's Condition"
|
||||||
|
option_none = 0
|
||||||
|
option_crystal = 1
|
||||||
|
option_bosses = 2
|
||||||
|
option_specials = 3
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class PercentSpecial2sRequired(Range):
|
||||||
|
"""Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s."""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 100
|
||||||
|
default = 80
|
||||||
|
display_name = "Percent Special2s Required"
|
||||||
|
|
||||||
|
|
||||||
|
class TotalSpecial2s(Range):
|
||||||
|
"""How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s."""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 70
|
||||||
|
default = 25
|
||||||
|
display_name = "Total Special2s"
|
||||||
|
|
||||||
|
|
||||||
|
class BossesRequired(Range):
|
||||||
|
"""How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses.
|
||||||
|
This will automatically adjust if there are fewer available bosses than the chosen number."""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 16
|
||||||
|
default = 14
|
||||||
|
display_name = "Bosses Required"
|
||||||
|
|
||||||
|
|
||||||
|
class CarrieLogic(Toggle):
|
||||||
|
"""Adds the 2 checks inside Underground Waterway's crawlspace to the pool.
|
||||||
|
If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this.
|
||||||
|
Can be combined with Hard Logic to include Carrie-only tricks."""
|
||||||
|
display_name = "Carrie Logic"
|
||||||
|
|
||||||
|
|
||||||
|
class HardLogic(Toggle):
|
||||||
|
"""Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include
|
||||||
|
Carrie-only tricks.
|
||||||
|
See the Game Page for a full list of tricks and glitches that may be logically required."""
|
||||||
|
display_name = "Hard Logic"
|
||||||
|
|
||||||
|
|
||||||
|
class MultiHitBreakables(Toggle):
|
||||||
|
"""Adds the items that drop from the objects that break in three hits to the pool. There are 17 of these throughout
|
||||||
|
the game, adding up to 74 checks in total with all stages.
|
||||||
|
The game will be modified to
|
||||||
|
remember exactly which of their items you've picked up instead of simply whether they were broken or not."""
|
||||||
|
display_name = "Multi-hit Breakables"
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyBreakables(Toggle):
|
||||||
|
"""Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.)
|
||||||
|
and some additional Red Jewels and/or moneybags into the item pool to compensate."""
|
||||||
|
display_name = "Empty Breakables"
|
||||||
|
|
||||||
|
|
||||||
|
class LizardLockerItems(Toggle):
|
||||||
|
"""Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool.
|
||||||
|
Picking up all of these can be a very tedious luck-based process, so they are off by default."""
|
||||||
|
display_name = "Lizard Locker Items"
|
||||||
|
|
||||||
|
|
||||||
|
class Shopsanity(Toggle):
|
||||||
|
"""Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will
|
||||||
|
revert to whatever it is in the vanilla game."""
|
||||||
|
display_name = "Shopsanity"
|
||||||
|
|
||||||
|
|
||||||
|
class ShopPrices(Choice):
|
||||||
|
"""Randomizes the amount of gold each item costs in Renon's shop.
|
||||||
|
Use the below options to control how much or little an item can cost."""
|
||||||
|
display_name = "Shop Prices"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_randomized = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MinimumGoldPrice(Range):
|
||||||
|
"""The lowest amount of gold an item can cost in Renon's shop, divided by 100."""
|
||||||
|
display_name = "Minimum Gold Price"
|
||||||
|
range_start = 1
|
||||||
|
range_end = 50
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
|
class MaximumGoldPrice(Range):
|
||||||
|
"""The highest amount of gold an item can cost in Renon's shop, divided by 100."""
|
||||||
|
display_name = "Maximum Gold Price"
|
||||||
|
range_start = 1
|
||||||
|
range_end = 50
|
||||||
|
default = 30
|
||||||
|
|
||||||
|
|
||||||
|
class PostBehemothBoss(Choice):
|
||||||
|
"""Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating
|
||||||
|
Behemoth."""
|
||||||
|
display_name = "Post-Behemoth Boss"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_inverted = 1
|
||||||
|
option_always_rosa = 2
|
||||||
|
option_always_camilla = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RoomOfClocksBoss(Choice):
|
||||||
|
"""Sets which boss is fought at Room of Clocks by which characters."""
|
||||||
|
display_name = "Room of Clocks Boss"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_inverted = 1
|
||||||
|
option_always_death = 2
|
||||||
|
option_always_actrise = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RenonFightCondition(Choice):
|
||||||
|
"""Sets the condition on which the Renon fight will trigger."""
|
||||||
|
display_name = "Renon Fight Condition"
|
||||||
|
option_never = 0
|
||||||
|
option_spend_30k = 1
|
||||||
|
option_always = 2
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class VincentFightCondition(Choice):
|
||||||
|
"""Sets the condition on which the vampire Vincent fight will trigger."""
|
||||||
|
display_name = "Vincent Fight Condition"
|
||||||
|
option_never = 0
|
||||||
|
option_wait_16_days = 1
|
||||||
|
option_always = 2
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class BadEndingCondition(Choice):
|
||||||
|
"""Sets the condition on which the currently-controlled character's Bad Ending will trigger."""
|
||||||
|
display_name = "Bad Ending Condition"
|
||||||
|
option_never = 0
|
||||||
|
option_kill_vincent = 1
|
||||||
|
option_always = 2
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class IncreaseItemLimit(DefaultOnToggle):
|
||||||
|
"""Increases the holding limit of usable items from 10 to 99 of each item."""
|
||||||
|
display_name = "Increase Item Limit"
|
||||||
|
|
||||||
|
|
||||||
|
class NerfHealingItems(Toggle):
|
||||||
|
"""Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%."""
|
||||||
|
display_name = "Nerf Healing Items"
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingZoneHeals(DefaultOnToggle):
|
||||||
|
"""Whether end-of-level loading zones restore health and cure status aliments or not.
|
||||||
|
Recommended off for those looking for more of a survival horror experience!"""
|
||||||
|
display_name = "Loading Zone Heals"
|
||||||
|
|
||||||
|
|
||||||
|
class InvisibleItems(Choice):
|
||||||
|
"""Sets which items are visible in their locations and which are invisible until picked up.
|
||||||
|
'Chance' gives each item a 50/50 chance of being visible or invisible."""
|
||||||
|
display_name = "Invisible Items"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_reveal_all = 1
|
||||||
|
option_hide_all = 2
|
||||||
|
option_chance = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class DropPreviousSubWeapon(Toggle):
|
||||||
|
"""When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired."""
|
||||||
|
display_name = "Drop Previous Sub-weapon"
|
||||||
|
|
||||||
|
|
||||||
|
class PermanentPowerUps(Toggle):
|
||||||
|
"""Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after
|
||||||
|
dying and/or continuing.
|
||||||
|
To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile."""
|
||||||
|
display_name = "Permanent PowerUps"
|
||||||
|
|
||||||
|
|
||||||
|
class IceTrapPercentage(Range):
|
||||||
|
"""Replaces a percentage of junk items with Ice Traps.
|
||||||
|
These will be visibly disguised as other items, and receiving one will freeze you
|
||||||
|
as if you were hit by Camilla's ice cloud attack."""
|
||||||
|
display_name = "Ice Trap Percentage"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 100
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class IceTrapAppearance(Choice):
|
||||||
|
"""What items Ice Traps can possibly be disguised as."""
|
||||||
|
display_name = "Ice Trap Appearance"
|
||||||
|
option_major_only = 0
|
||||||
|
option_junk_only = 1
|
||||||
|
option_anything = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class DisableTimeRestrictions(Toggle):
|
||||||
|
"""Disables the restriction on every event and door that requires the current time
|
||||||
|
to be within a specific range, so they can be triggered at any time.
|
||||||
|
This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar.
|
||||||
|
The Villa coffin is not affected by this."""
|
||||||
|
display_name = "Disable Time Requirements"
|
||||||
|
|
||||||
|
|
||||||
|
class SkipGondolas(Toggle):
|
||||||
|
"""Makes jumping on and activating a gondola in Tunnel instantly teleport you
|
||||||
|
to the other station, thereby skipping the entire three-minute ride.
|
||||||
|
The item normally at the gondola transfer point is moved to instead be
|
||||||
|
near the red gondola at its station."""
|
||||||
|
display_name = "Skip Gondolas"
|
||||||
|
|
||||||
|
|
||||||
|
class SkipWaterwayBlocks(Toggle):
|
||||||
|
"""Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating
|
||||||
|
brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically
|
||||||
|
required if Shopsanity is on."""
|
||||||
|
display_name = "Skip Waterway Blocks"
|
||||||
|
|
||||||
|
|
||||||
|
class Countdown(Choice):
|
||||||
|
"""Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items
|
||||||
|
or the total check locations remaining in the stage you are currently in."""
|
||||||
|
display_name = "Countdown"
|
||||||
|
option_none = 0
|
||||||
|
option_majors = 1
|
||||||
|
option_all_locations = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BigToss(Toggle):
|
||||||
|
"""Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
|
||||||
|
Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
|
||||||
|
Hold Z to have all incoming damage be treated as it normally would.
|
||||||
|
Any tricks that might be possible with it are NOT considered in logic on any setting."""
|
||||||
|
display_name = "Big Toss"
|
||||||
|
|
||||||
|
|
||||||
|
class PantherDash(Choice):
|
||||||
|
"""Hold C-right at any time to sprint way faster. Any tricks that might be
|
||||||
|
possible with it are NOT considered in logic on any setting and any boss
|
||||||
|
fights with boss health meters, if started, are expected to be finished
|
||||||
|
before leaving their arenas if Dracula's Condition is bosses. Jumpless will
|
||||||
|
prevent jumping while moving at the increased speed to ensure logic cannot be broken with it."""
|
||||||
|
display_name = "Panther Dash"
|
||||||
|
option_off = 0
|
||||||
|
option_on = 1
|
||||||
|
option_jumpless = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class IncreaseShimmySpeed(Toggle):
|
||||||
|
"""Increases the speed at which characters shimmy left and right while hanging on ledges."""
|
||||||
|
display_name = "Increase Shimmy Speed"
|
||||||
|
|
||||||
|
|
||||||
|
class FallGuard(Toggle):
|
||||||
|
"""Removes fall damage from landing too hard. Note that falling for too long will still result in instant death."""
|
||||||
|
display_name = "Fall Guard"
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundMusic(Choice):
|
||||||
|
"""Randomizes or disables the music heard throughout the game.
|
||||||
|
Randomized music is split into two pools: songs that loop and songs that don't.
|
||||||
|
The "lead-in" versions of some songs will be paired accordingly."""
|
||||||
|
display_name = "Background Music"
|
||||||
|
option_normal = 0
|
||||||
|
option_disabled = 1
|
||||||
|
option_randomized = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MapLighting(Choice):
|
||||||
|
"""Randomizes the lighting color RGB values on every map during every time of day to be literally anything.
|
||||||
|
The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects."""
|
||||||
|
display_name = "Map Lighting"
|
||||||
|
option_normal = 0
|
||||||
|
option_randomized = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class CinematicExperience(Toggle):
|
||||||
|
"""Enables an unused film reel effect on every cutscene in the game. Purely cosmetic."""
|
||||||
|
display_name = "Cinematic Experience"
|
||||||
|
|
||||||
|
|
||||||
|
class WindowColorR(Range):
|
||||||
|
"""The red value for the background color of the text windows during gameplay."""
|
||||||
|
display_name = "Window Color R"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 15
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class WindowColorG(Range):
|
||||||
|
"""The green value for the background color of the text windows during gameplay."""
|
||||||
|
display_name = "Window Color G"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 15
|
||||||
|
default = 5
|
||||||
|
|
||||||
|
|
||||||
|
class WindowColorB(Range):
|
||||||
|
"""The blue value for the background color of the text windows during gameplay."""
|
||||||
|
display_name = "Window Color B"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 15
|
||||||
|
default = 15
|
||||||
|
|
||||||
|
|
||||||
|
class WindowColorA(Range):
|
||||||
|
"""The alpha value for the background color of the text windows during gameplay."""
|
||||||
|
display_name = "Window Color A"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 15
|
||||||
|
default = 8
|
||||||
|
|
||||||
|
|
||||||
|
class DeathLink(Choice):
|
||||||
|
"""When you die, everyone dies. Of course the reverse is true too.
|
||||||
|
Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion
|
||||||
|
instead of the normal death animation."""
|
||||||
|
display_name = "DeathLink"
|
||||||
|
option_off = 0
|
||||||
|
alias_no = 0
|
||||||
|
alias_true = 1
|
||||||
|
alias_yes = 1
|
||||||
|
option_on = 1
|
||||||
|
option_explosive = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CV64Options(PerGameCommonOptions):
|
||||||
|
character_stages: CharacterStages
|
||||||
|
stage_shuffle: StageShuffle
|
||||||
|
starting_stage: StartingStage
|
||||||
|
warp_order: WarpOrder
|
||||||
|
sub_weapon_shuffle: SubWeaponShuffle
|
||||||
|
spare_keys: SpareKeys
|
||||||
|
hard_item_pool: HardItemPool
|
||||||
|
special1s_per_warp: Special1sPerWarp
|
||||||
|
total_special1s: TotalSpecial1s
|
||||||
|
draculas_condition: DraculasCondition
|
||||||
|
percent_special2s_required: PercentSpecial2sRequired
|
||||||
|
total_special2s: TotalSpecial2s
|
||||||
|
bosses_required: BossesRequired
|
||||||
|
carrie_logic: CarrieLogic
|
||||||
|
hard_logic: HardLogic
|
||||||
|
multi_hit_breakables: MultiHitBreakables
|
||||||
|
empty_breakables: EmptyBreakables
|
||||||
|
lizard_locker_items: LizardLockerItems
|
||||||
|
shopsanity: Shopsanity
|
||||||
|
shop_prices: ShopPrices
|
||||||
|
minimum_gold_price: MinimumGoldPrice
|
||||||
|
maximum_gold_price: MaximumGoldPrice
|
||||||
|
post_behemoth_boss: PostBehemothBoss
|
||||||
|
room_of_clocks_boss: RoomOfClocksBoss
|
||||||
|
renon_fight_condition: RenonFightCondition
|
||||||
|
vincent_fight_condition: VincentFightCondition
|
||||||
|
bad_ending_condition: BadEndingCondition
|
||||||
|
increase_item_limit: IncreaseItemLimit
|
||||||
|
nerf_healing_items: NerfHealingItems
|
||||||
|
loading_zone_heals: LoadingZoneHeals
|
||||||
|
invisible_items: InvisibleItems
|
||||||
|
drop_previous_sub_weapon: DropPreviousSubWeapon
|
||||||
|
permanent_powerups: PermanentPowerUps
|
||||||
|
ice_trap_percentage: IceTrapPercentage
|
||||||
|
ice_trap_appearance: IceTrapAppearance
|
||||||
|
disable_time_restrictions: DisableTimeRestrictions
|
||||||
|
skip_gondolas: SkipGondolas
|
||||||
|
skip_waterway_blocks: SkipWaterwayBlocks
|
||||||
|
countdown: Countdown
|
||||||
|
big_toss: BigToss
|
||||||
|
panther_dash: PantherDash
|
||||||
|
increase_shimmy_speed: IncreaseShimmySpeed
|
||||||
|
background_music: BackgroundMusic
|
||||||
|
map_lighting: MapLighting
|
||||||
|
fall_guard: FallGuard
|
||||||
|
cinematic_experience: CinematicExperience
|
||||||
|
window_color_r: WindowColorR
|
||||||
|
window_color_g: WindowColorG
|
||||||
|
window_color_b: WindowColorB
|
||||||
|
window_color_a: WindowColorA
|
||||||
|
death_link: DeathLink
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
|
@ -0,0 +1,517 @@
|
||||||
|
from .data import lname, rname, ename
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "stage" = What stage the Region is a part of. The Region and its corresponding Locations and Entrances will only be
|
||||||
|
# put in if its stage is active.
|
||||||
|
# "locations" = The Locations to add to that Region when putting in said Region (provided their add conditions pass).
|
||||||
|
# "entrances" = The Entrances to add to that Region when putting in said Region (provided their add conditions pass).
|
||||||
|
region_info = {
|
||||||
|
"Menu": {},
|
||||||
|
|
||||||
|
rname.forest_start: {"stage": rname.forest_of_silence,
|
||||||
|
"locations": [lname.forest_pillars_right,
|
||||||
|
lname.forest_pillars_left,
|
||||||
|
lname.forest_pillars_top,
|
||||||
|
lname.forest_king_skeleton,
|
||||||
|
lname.forest_boss_one,
|
||||||
|
lname.forest_lgaz_in,
|
||||||
|
lname.forest_lgaz_top,
|
||||||
|
lname.forest_hgaz_in,
|
||||||
|
lname.forest_hgaz_top,
|
||||||
|
lname.forest_weretiger_sw,
|
||||||
|
lname.forest_boss_two,
|
||||||
|
lname.forest_weretiger_gate,
|
||||||
|
lname.forest_dirge_tomb_l,
|
||||||
|
lname.forest_dirge_tomb_u,
|
||||||
|
lname.forest_dirge_plaque,
|
||||||
|
lname.forest_dirge_ped,
|
||||||
|
lname.forest_dirge_rock1,
|
||||||
|
lname.forest_dirge_rock2,
|
||||||
|
lname.forest_dirge_rock3,
|
||||||
|
lname.forest_dirge_rock4,
|
||||||
|
lname.forest_dirge_rock5,
|
||||||
|
lname.forest_corpse_save,
|
||||||
|
lname.forest_dbridge_wall,
|
||||||
|
lname.forest_dbridge_sw],
|
||||||
|
"entrances": [ename.forest_dbridge_gate]},
|
||||||
|
|
||||||
|
rname.forest_mid: {"stage": rname.forest_of_silence,
|
||||||
|
"locations": [lname.forest_dbridge_gate_l,
|
||||||
|
lname.forest_dbridge_gate_r,
|
||||||
|
lname.forest_dbridge_tomb_l,
|
||||||
|
lname.forest_dbridge_tomb_ur,
|
||||||
|
lname.forest_dbridge_tomb_uf,
|
||||||
|
lname.forest_bface_tomb_lf,
|
||||||
|
lname.forest_bface_tomb_lr,
|
||||||
|
lname.forest_bface_tomb_u,
|
||||||
|
lname.forest_ibridge,
|
||||||
|
lname.forest_bridge_rock1,
|
||||||
|
lname.forest_bridge_rock2,
|
||||||
|
lname.forest_bridge_rock3,
|
||||||
|
lname.forest_bridge_rock4,
|
||||||
|
lname.forest_werewolf_tomb_lf,
|
||||||
|
lname.forest_werewolf_tomb_lr,
|
||||||
|
lname.forest_werewolf_tomb_r,
|
||||||
|
lname.forest_werewolf_plaque,
|
||||||
|
lname.forest_werewolf_tree,
|
||||||
|
lname.forest_werewolf_island,
|
||||||
|
lname.forest_final_sw],
|
||||||
|
"entrances": [ename.forest_werewolf_gate]},
|
||||||
|
|
||||||
|
rname.forest_end: {"stage": rname.forest_of_silence,
|
||||||
|
"locations": [lname.forest_boss_three],
|
||||||
|
"entrances": [ename.forest_end]},
|
||||||
|
|
||||||
|
rname.cw_start: {"stage": rname.castle_wall,
|
||||||
|
"locations": [lname.cwr_bottom,
|
||||||
|
lname.cw_dragon_sw,
|
||||||
|
lname.cw_boss,
|
||||||
|
lname.cw_save_slab1,
|
||||||
|
lname.cw_save_slab2,
|
||||||
|
lname.cw_save_slab3,
|
||||||
|
lname.cw_save_slab4,
|
||||||
|
lname.cw_save_slab5,
|
||||||
|
lname.cw_rrampart,
|
||||||
|
lname.cw_lrampart,
|
||||||
|
lname.cw_pillar,
|
||||||
|
lname.cw_shelf_visible,
|
||||||
|
lname.cw_shelf_sandbags,
|
||||||
|
lname.cw_shelf_torch],
|
||||||
|
"entrances": [ename.cw_portcullis_c,
|
||||||
|
ename.cw_lt_skip,
|
||||||
|
ename.cw_lt_door]},
|
||||||
|
|
||||||
|
rname.cw_exit: {"stage": rname.castle_wall,
|
||||||
|
"locations": [lname.cw_ground_left,
|
||||||
|
lname.cw_ground_middle,
|
||||||
|
lname.cw_ground_right]},
|
||||||
|
|
||||||
|
rname.cw_ltower: {"stage": rname.castle_wall,
|
||||||
|
"locations": [lname.cwl_bottom,
|
||||||
|
lname.cwl_bridge,
|
||||||
|
lname.cw_drac_sw,
|
||||||
|
lname.cw_drac_slab1,
|
||||||
|
lname.cw_drac_slab2,
|
||||||
|
lname.cw_drac_slab3,
|
||||||
|
lname.cw_drac_slab4,
|
||||||
|
lname.cw_drac_slab5],
|
||||||
|
"entrances": [ename.cw_end]},
|
||||||
|
|
||||||
|
rname.villa_start: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villafy_outer_gate_l,
|
||||||
|
lname.villafy_outer_gate_r,
|
||||||
|
lname.villafy_dog_platform,
|
||||||
|
lname.villafy_inner_gate],
|
||||||
|
"entrances": [ename.villa_dog_gates]},
|
||||||
|
|
||||||
|
rname.villa_main: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villafy_gate_marker,
|
||||||
|
lname.villafy_villa_marker,
|
||||||
|
lname.villafy_tombstone,
|
||||||
|
lname.villafy_fountain_fl,
|
||||||
|
lname.villafy_fountain_fr,
|
||||||
|
lname.villafy_fountain_ml,
|
||||||
|
lname.villafy_fountain_mr,
|
||||||
|
lname.villafy_fountain_rl,
|
||||||
|
lname.villafy_fountain_rr,
|
||||||
|
lname.villafo_front_r,
|
||||||
|
lname.villafo_front_l,
|
||||||
|
lname.villafo_mid_l,
|
||||||
|
lname.villafo_mid_r,
|
||||||
|
lname.villafo_rear_r,
|
||||||
|
lname.villafo_rear_l,
|
||||||
|
lname.villafo_pot_r,
|
||||||
|
lname.villafo_pot_l,
|
||||||
|
lname.villafo_sofa,
|
||||||
|
lname.villafo_chandelier1,
|
||||||
|
lname.villafo_chandelier2,
|
||||||
|
lname.villafo_chandelier3,
|
||||||
|
lname.villafo_chandelier4,
|
||||||
|
lname.villafo_chandelier5,
|
||||||
|
lname.villala_hallway_stairs,
|
||||||
|
lname.villala_hallway_l,
|
||||||
|
lname.villala_hallway_r,
|
||||||
|
lname.villala_bedroom_chairs,
|
||||||
|
lname.villala_bedroom_bed,
|
||||||
|
lname.villala_vincent,
|
||||||
|
lname.villala_slivingroom_table,
|
||||||
|
lname.villala_slivingroom_mirror,
|
||||||
|
lname.villala_diningroom_roses,
|
||||||
|
lname.villala_llivingroom_pot_r,
|
||||||
|
lname.villala_llivingroom_pot_l,
|
||||||
|
lname.villala_llivingroom_painting,
|
||||||
|
lname.villala_llivingroom_light,
|
||||||
|
lname.villala_llivingroom_lion,
|
||||||
|
lname.villala_exit_knight],
|
||||||
|
"entrances": [ename.villa_snipe_dogs,
|
||||||
|
ename.villa_renon,
|
||||||
|
ename.villa_to_storeroom,
|
||||||
|
ename.villa_to_archives,
|
||||||
|
ename.villa_to_maze]},
|
||||||
|
|
||||||
|
rname.villa_storeroom: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villala_storeroom_l,
|
||||||
|
lname.villala_storeroom_r,
|
||||||
|
lname.villala_storeroom_s],
|
||||||
|
"entrances": [ename.villa_from_storeroom]},
|
||||||
|
|
||||||
|
rname.villa_archives: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villala_archives_entrance,
|
||||||
|
lname.villala_archives_table,
|
||||||
|
lname.villala_archives_rear]},
|
||||||
|
|
||||||
|
rname.villa_maze: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villam_malus_torch,
|
||||||
|
lname.villam_malus_bush,
|
||||||
|
lname.villam_fplatform,
|
||||||
|
lname.villam_frankieturf_l,
|
||||||
|
lname.villam_frankieturf_r,
|
||||||
|
lname.villam_frankieturf_ru,
|
||||||
|
lname.villam_fgarden_f,
|
||||||
|
lname.villam_fgarden_mf,
|
||||||
|
lname.villam_fgarden_mr,
|
||||||
|
lname.villam_fgarden_r,
|
||||||
|
lname.villam_rplatform,
|
||||||
|
lname.villam_rplatform_de,
|
||||||
|
lname.villam_exit_de,
|
||||||
|
lname.villam_serv_path],
|
||||||
|
"entrances": [ename.villa_from_maze,
|
||||||
|
ename.villa_copper_door,
|
||||||
|
ename.villa_copper_skip]},
|
||||||
|
|
||||||
|
rname.villa_servants: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villafo_serv_ent],
|
||||||
|
"entrances": [ename.villa_servant_door]},
|
||||||
|
|
||||||
|
rname.villa_crypt: {"stage": rname.villa,
|
||||||
|
"locations": [lname.villam_crypt_ent,
|
||||||
|
lname.villam_crypt_upstream,
|
||||||
|
lname.villac_ent_l,
|
||||||
|
lname.villac_ent_r,
|
||||||
|
lname.villac_wall_l,
|
||||||
|
lname.villac_wall_r,
|
||||||
|
lname.villac_coffin_l,
|
||||||
|
lname.villac_coffin_r,
|
||||||
|
lname.villa_boss_one,
|
||||||
|
lname.villa_boss_two],
|
||||||
|
"entrances": [ename.villa_bridge_door,
|
||||||
|
ename.villa_end_r,
|
||||||
|
ename.villa_end_c]},
|
||||||
|
|
||||||
|
rname.tunnel_start: {"stage": rname.tunnel,
|
||||||
|
"locations": [lname.tunnel_landing,
|
||||||
|
lname.tunnel_landing_rc,
|
||||||
|
lname.tunnel_stone_alcove_r,
|
||||||
|
lname.tunnel_stone_alcove_l,
|
||||||
|
lname.tunnel_twin_arrows,
|
||||||
|
lname.tunnel_arrows_rock1,
|
||||||
|
lname.tunnel_arrows_rock2,
|
||||||
|
lname.tunnel_arrows_rock3,
|
||||||
|
lname.tunnel_arrows_rock4,
|
||||||
|
lname.tunnel_arrows_rock5,
|
||||||
|
lname.tunnel_lonesome_bucket,
|
||||||
|
lname.tunnel_lbucket_mdoor_l,
|
||||||
|
lname.tunnel_lbucket_quag,
|
||||||
|
lname.tunnel_bucket_quag_rock1,
|
||||||
|
lname.tunnel_bucket_quag_rock2,
|
||||||
|
lname.tunnel_bucket_quag_rock3,
|
||||||
|
lname.tunnel_lbucket_albert,
|
||||||
|
lname.tunnel_albert_camp,
|
||||||
|
lname.tunnel_albert_quag,
|
||||||
|
lname.tunnel_gondola_rc_sdoor_l,
|
||||||
|
lname.tunnel_gondola_rc_sdoor_m,
|
||||||
|
lname.tunnel_gondola_rc_sdoor_r,
|
||||||
|
lname.tunnel_gondola_rc,
|
||||||
|
lname.tunnel_rgondola_station,
|
||||||
|
lname.tunnel_gondola_transfer],
|
||||||
|
"entrances": [ename.tunnel_start_renon,
|
||||||
|
ename.tunnel_gondolas]},
|
||||||
|
|
||||||
|
rname.tunnel_end: {"stage": rname.tunnel,
|
||||||
|
"locations": [lname.tunnel_corpse_bucket_quag,
|
||||||
|
lname.tunnel_corpse_bucket_mdoor_l,
|
||||||
|
lname.tunnel_corpse_bucket_mdoor_r,
|
||||||
|
lname.tunnel_shovel_quag_start,
|
||||||
|
lname.tunnel_exit_quag_start,
|
||||||
|
lname.tunnel_shovel_quag_end,
|
||||||
|
lname.tunnel_exit_quag_end,
|
||||||
|
lname.tunnel_shovel,
|
||||||
|
lname.tunnel_shovel_save,
|
||||||
|
lname.tunnel_shovel_mdoor_l,
|
||||||
|
lname.tunnel_shovel_mdoor_r,
|
||||||
|
lname.tunnel_shovel_sdoor_l,
|
||||||
|
lname.tunnel_shovel_sdoor_m,
|
||||||
|
lname.tunnel_shovel_sdoor_r],
|
||||||
|
"entrances": [ename.tunnel_end_renon,
|
||||||
|
ename.tunnel_end]},
|
||||||
|
|
||||||
|
rname.uw_main: {"stage": rname.underground_waterway,
|
||||||
|
"locations": [lname.uw_near_ent,
|
||||||
|
lname.uw_across_ent,
|
||||||
|
lname.uw_first_ledge1,
|
||||||
|
lname.uw_first_ledge2,
|
||||||
|
lname.uw_first_ledge3,
|
||||||
|
lname.uw_first_ledge4,
|
||||||
|
lname.uw_first_ledge5,
|
||||||
|
lname.uw_first_ledge6,
|
||||||
|
lname.uw_poison_parkour,
|
||||||
|
lname.uw_boss,
|
||||||
|
lname.uw_waterfall_alcove,
|
||||||
|
lname.uw_carrie1,
|
||||||
|
lname.uw_carrie2,
|
||||||
|
lname.uw_bricks_save,
|
||||||
|
lname.uw_above_skel_ledge,
|
||||||
|
lname.uw_in_skel_ledge1,
|
||||||
|
lname.uw_in_skel_ledge2,
|
||||||
|
lname.uw_in_skel_ledge3],
|
||||||
|
"entrances": [ename.uw_final_waterfall,
|
||||||
|
ename.uw_renon]},
|
||||||
|
|
||||||
|
rname.uw_end: {"stage": rname.underground_waterway,
|
||||||
|
"entrances": [ename.uw_waterfall_skip,
|
||||||
|
ename.uw_end]},
|
||||||
|
|
||||||
|
rname.cc_main: {"stage": rname.castle_center,
|
||||||
|
"locations": [lname.ccb_skel_hallway_ent,
|
||||||
|
lname.ccb_skel_hallway_jun,
|
||||||
|
lname.ccb_skel_hallway_tc,
|
||||||
|
lname.ccb_skel_hallway_ba,
|
||||||
|
lname.ccb_behemoth_l_ff,
|
||||||
|
lname.ccb_behemoth_l_mf,
|
||||||
|
lname.ccb_behemoth_l_mr,
|
||||||
|
lname.ccb_behemoth_l_fr,
|
||||||
|
lname.ccb_behemoth_r_ff,
|
||||||
|
lname.ccb_behemoth_r_mf,
|
||||||
|
lname.ccb_behemoth_r_mr,
|
||||||
|
lname.ccb_behemoth_r_fr,
|
||||||
|
lname.ccb_behemoth_crate1,
|
||||||
|
lname.ccb_behemoth_crate2,
|
||||||
|
lname.ccb_behemoth_crate3,
|
||||||
|
lname.ccb_behemoth_crate4,
|
||||||
|
lname.ccb_behemoth_crate5,
|
||||||
|
lname.ccelv_near_machine,
|
||||||
|
lname.ccelv_atop_machine,
|
||||||
|
lname.ccelv_stand1,
|
||||||
|
lname.ccelv_stand2,
|
||||||
|
lname.ccelv_stand3,
|
||||||
|
lname.ccelv_pipes,
|
||||||
|
lname.ccelv_switch,
|
||||||
|
lname.ccelv_staircase,
|
||||||
|
lname.ccff_redcarpet_knight,
|
||||||
|
lname.ccff_gears_side,
|
||||||
|
lname.ccff_gears_mid,
|
||||||
|
lname.ccff_gears_corner,
|
||||||
|
lname.ccff_lizard_knight,
|
||||||
|
lname.ccff_lizard_near_knight,
|
||||||
|
lname.ccff_lizard_pit,
|
||||||
|
lname.ccff_lizard_corner,
|
||||||
|
lname.ccff_lizard_locker_nfr,
|
||||||
|
lname.ccff_lizard_locker_nmr,
|
||||||
|
lname.ccff_lizard_locker_nml,
|
||||||
|
lname.ccff_lizard_locker_nfl,
|
||||||
|
lname.ccff_lizard_locker_fl,
|
||||||
|
lname.ccff_lizard_locker_fr,
|
||||||
|
lname.ccff_lizard_slab1,
|
||||||
|
lname.ccff_lizard_slab2,
|
||||||
|
lname.ccff_lizard_slab3,
|
||||||
|
lname.ccff_lizard_slab4,
|
||||||
|
lname.ccll_brokenstairs_floor,
|
||||||
|
lname.ccll_brokenstairs_knight,
|
||||||
|
lname.ccll_brokenstairs_save,
|
||||||
|
lname.ccll_glassknight_l,
|
||||||
|
lname.ccll_glassknight_r,
|
||||||
|
lname.ccll_butlers_door,
|
||||||
|
lname.ccll_butlers_side,
|
||||||
|
lname.ccll_cwhall_butlerflames_past,
|
||||||
|
lname.ccll_cwhall_flamethrower,
|
||||||
|
lname.ccll_cwhall_cwflames,
|
||||||
|
lname.ccll_heinrich,
|
||||||
|
lname.ccia_nitro_crates,
|
||||||
|
lname.ccia_nitro_shelf_h,
|
||||||
|
lname.ccia_stairs_knight,
|
||||||
|
lname.ccia_maids_vase,
|
||||||
|
lname.ccia_maids_outer,
|
||||||
|
lname.ccia_maids_inner,
|
||||||
|
lname.ccia_inventions_maids,
|
||||||
|
lname.ccia_inventions_crusher,
|
||||||
|
lname.ccia_inventions_famicart,
|
||||||
|
lname.ccia_inventions_zeppelin,
|
||||||
|
lname.ccia_inventions_round,
|
||||||
|
lname.ccia_nitrohall_flamethrower,
|
||||||
|
lname.ccia_nitrohall_torch,
|
||||||
|
lname.ccia_nitro_shelf_i],
|
||||||
|
"entrances": [ename.cc_tc_door,
|
||||||
|
ename.cc_lower_wall,
|
||||||
|
ename.cc_renon,
|
||||||
|
ename.cc_upper_wall]},
|
||||||
|
|
||||||
|
rname.cc_torture_chamber: {"stage": rname.castle_center,
|
||||||
|
"locations": [lname.ccb_mandrag_shelf_l,
|
||||||
|
lname.ccb_mandrag_shelf_r,
|
||||||
|
lname.ccb_torture_rack,
|
||||||
|
lname.ccb_torture_rafters]},
|
||||||
|
|
||||||
|
rname.cc_library: {"stage": rname.castle_center,
|
||||||
|
"locations": [lname.ccll_cwhall_wall,
|
||||||
|
lname.ccl_bookcase]},
|
||||||
|
|
||||||
|
rname.cc_crystal: {"stage": rname.castle_center,
|
||||||
|
"locations": [lname.cc_behind_the_seal,
|
||||||
|
lname.cc_boss_one,
|
||||||
|
lname.cc_boss_two],
|
||||||
|
"entrances": [ename.cc_elevator]},
|
||||||
|
|
||||||
|
rname.cc_elev_top: {"stage": rname.castle_center,
|
||||||
|
"entrances": [ename.cc_exit_r,
|
||||||
|
ename.cc_exit_c]},
|
||||||
|
|
||||||
|
rname.dt_main: {"stage": rname.duel_tower,
|
||||||
|
"locations": [lname.dt_boss_one,
|
||||||
|
lname.dt_boss_two,
|
||||||
|
lname.dt_ibridge_l,
|
||||||
|
lname.dt_ibridge_r,
|
||||||
|
lname.dt_stones_start,
|
||||||
|
lname.dt_stones_end,
|
||||||
|
lname.dt_werebull_arena,
|
||||||
|
lname.dt_boss_three,
|
||||||
|
lname.dt_boss_four],
|
||||||
|
"entrances": [ename.dt_start,
|
||||||
|
ename.dt_end]},
|
||||||
|
|
||||||
|
rname.toe_main: {"stage": rname.tower_of_execution,
|
||||||
|
"locations": [lname.toe_ledge1,
|
||||||
|
lname.toe_ledge2,
|
||||||
|
lname.toe_ledge3,
|
||||||
|
lname.toe_ledge4,
|
||||||
|
lname.toe_ledge5,
|
||||||
|
lname.toe_midsavespikes_r,
|
||||||
|
lname.toe_midsavespikes_l,
|
||||||
|
lname.toe_elec_grate,
|
||||||
|
lname.toe_ibridge,
|
||||||
|
lname.toe_top],
|
||||||
|
"entrances": [ename.toe_start,
|
||||||
|
ename.toe_gate,
|
||||||
|
ename.toe_gate_skip,
|
||||||
|
ename.toe_end]},
|
||||||
|
|
||||||
|
rname.toe_ledge: {"stage": rname.tower_of_execution,
|
||||||
|
"locations": [lname.toe_keygate_l,
|
||||||
|
lname.toe_keygate_r]},
|
||||||
|
|
||||||
|
rname.tosci_start: {"stage": rname.tower_of_science,
|
||||||
|
"locations": [lname.tosci_elevator,
|
||||||
|
lname.tosci_plain_sr,
|
||||||
|
lname.tosci_stairs_sr],
|
||||||
|
"entrances": [ename.tosci_start,
|
||||||
|
ename.tosci_key1_door,
|
||||||
|
ename.tosci_to_key2_door]},
|
||||||
|
|
||||||
|
rname.tosci_three_doors: {"stage": rname.tower_of_science,
|
||||||
|
"locations": [lname.tosci_three_door_hall]},
|
||||||
|
|
||||||
|
rname.tosci_conveyors: {"stage": rname.tower_of_science,
|
||||||
|
"locations": [lname.tosci_ibridge_t,
|
||||||
|
lname.tosci_ibridge_b1,
|
||||||
|
lname.tosci_ibridge_b2,
|
||||||
|
lname.tosci_ibridge_b3,
|
||||||
|
lname.tosci_ibridge_b4,
|
||||||
|
lname.tosci_ibridge_b5,
|
||||||
|
lname.tosci_ibridge_b6,
|
||||||
|
lname.tosci_conveyor_sr,
|
||||||
|
lname.tosci_exit],
|
||||||
|
"entrances": [ename.tosci_from_key2_door,
|
||||||
|
ename.tosci_key3_door,
|
||||||
|
ename.tosci_end]},
|
||||||
|
|
||||||
|
rname.tosci_key3: {"stage": rname.tower_of_science,
|
||||||
|
"locations": [lname.tosci_key3_r,
|
||||||
|
lname.tosci_key3_m,
|
||||||
|
lname.tosci_key3_l]},
|
||||||
|
|
||||||
|
rname.tosor_main: {"stage": rname.tower_of_sorcery,
|
||||||
|
"locations": [lname.tosor_stained_tower,
|
||||||
|
lname.tosor_savepoint,
|
||||||
|
lname.tosor_trickshot,
|
||||||
|
lname.tosor_yellow_bubble,
|
||||||
|
lname.tosor_blue_platforms,
|
||||||
|
lname.tosor_side_isle,
|
||||||
|
lname.tosor_ibridge],
|
||||||
|
"entrances": [ename.tosor_start,
|
||||||
|
ename.tosor_end]},
|
||||||
|
|
||||||
|
rname.roc_main: {"stage": rname.room_of_clocks,
|
||||||
|
"locations": [lname.roc_ent_l,
|
||||||
|
lname.roc_ent_r,
|
||||||
|
lname.roc_elev_r,
|
||||||
|
lname.roc_elev_l,
|
||||||
|
lname.roc_cont_r,
|
||||||
|
lname.roc_cont_l,
|
||||||
|
lname.roc_exit,
|
||||||
|
lname.roc_boss],
|
||||||
|
"entrances": [ename.roc_gate]},
|
||||||
|
|
||||||
|
rname.ct_start: {"stage": rname.clock_tower,
|
||||||
|
"locations": [lname.ct_gearclimb_battery_slab1,
|
||||||
|
lname.ct_gearclimb_battery_slab2,
|
||||||
|
lname.ct_gearclimb_battery_slab3,
|
||||||
|
lname.ct_gearclimb_side,
|
||||||
|
lname.ct_gearclimb_corner,
|
||||||
|
lname.ct_gearclimb_door_slab1,
|
||||||
|
lname.ct_gearclimb_door_slab2,
|
||||||
|
lname.ct_gearclimb_door_slab3],
|
||||||
|
"entrances": [ename.ct_to_door1]},
|
||||||
|
|
||||||
|
rname.ct_middle: {"stage": rname.clock_tower,
|
||||||
|
"locations": [lname.ct_bp_chasm_fl,
|
||||||
|
lname.ct_bp_chasm_fr,
|
||||||
|
lname.ct_bp_chasm_rl,
|
||||||
|
lname.ct_bp_chasm_k],
|
||||||
|
"entrances": [ename.ct_from_door1,
|
||||||
|
ename.ct_to_door2]},
|
||||||
|
|
||||||
|
rname.ct_end: {"stage": rname.clock_tower,
|
||||||
|
"locations": [lname.ct_finalroom_door_slab1,
|
||||||
|
lname.ct_finalroom_door_slab2,
|
||||||
|
lname.ct_finalroom_fl,
|
||||||
|
lname.ct_finalroom_fr,
|
||||||
|
lname.ct_finalroom_rl,
|
||||||
|
lname.ct_finalroom_rr,
|
||||||
|
lname.ct_finalroom_platform,
|
||||||
|
lname.ct_finalroom_renon_slab1,
|
||||||
|
lname.ct_finalroom_renon_slab2,
|
||||||
|
lname.ct_finalroom_renon_slab3,
|
||||||
|
lname.ct_finalroom_renon_slab4,
|
||||||
|
lname.ct_finalroom_renon_slab5,
|
||||||
|
lname.ct_finalroom_renon_slab6,
|
||||||
|
lname.ct_finalroom_renon_slab7,
|
||||||
|
lname.ct_finalroom_renon_slab8],
|
||||||
|
"entrances": [ename.ct_from_door2,
|
||||||
|
ename.ct_renon,
|
||||||
|
ename.ct_door_3]},
|
||||||
|
|
||||||
|
rname.ck_main: {"stage": rname.castle_keep,
|
||||||
|
"locations": [lname.ck_boss_one,
|
||||||
|
lname.ck_boss_two,
|
||||||
|
lname.ck_flame_l,
|
||||||
|
lname.ck_flame_r,
|
||||||
|
lname.ck_behind_drac,
|
||||||
|
lname.ck_cube],
|
||||||
|
"entrances": [ename.ck_slope_jump,
|
||||||
|
ename.ck_drac_door]},
|
||||||
|
|
||||||
|
rname.renon: {"locations": [lname.renon1,
|
||||||
|
lname.renon2,
|
||||||
|
lname.renon3,
|
||||||
|
lname.renon4,
|
||||||
|
lname.renon5,
|
||||||
|
lname.renon6,
|
||||||
|
lname.renon7]},
|
||||||
|
|
||||||
|
rname.ck_drac_chamber: {"locations": [lname.the_end]}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_region_info(region: str, info: str) -> Union[str, List[str], None]:
|
||||||
|
return region_info[region].get(info, None)
|
|
@ -0,0 +1,959 @@
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
from BaseClasses import Location
|
||||||
|
from worlds.Files import APDeltaPatch
|
||||||
|
from typing import List, Dict, Union, Iterable, Collection, TYPE_CHECKING
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
from . import lzkn64
|
||||||
|
from .data import patches
|
||||||
|
from .stages import get_stage_info
|
||||||
|
from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap
|
||||||
|
from .aesthetics import renon_item_dialogue, get_item_text_color
|
||||||
|
from .locations import get_location_info
|
||||||
|
from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \
|
||||||
|
BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash
|
||||||
|
from settings import get_settings
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import CV64World
|
||||||
|
|
||||||
|
CV64US10HASH = "1cc5cf3b4d29d8c3ade957648b529dc1"
|
||||||
|
ROM_PLAYER_LIMIT = 65535
|
||||||
|
|
||||||
|
warp_map_offsets = [0xADF67, 0xADF77, 0xADF87, 0xADF97, 0xADFA7, 0xADFBB, 0xADFCB, 0xADFDF]
|
||||||
|
|
||||||
|
|
||||||
|
class LocalRom:
|
||||||
|
orig_buffer: None
|
||||||
|
buffer: bytearray
|
||||||
|
|
||||||
|
def __init__(self, file: str) -> None:
|
||||||
|
self.orig_buffer = None
|
||||||
|
|
||||||
|
with open(file, "rb") as stream:
|
||||||
|
self.buffer = bytearray(stream.read())
|
||||||
|
|
||||||
|
def read_bit(self, address: int, bit_number: int) -> bool:
|
||||||
|
bitflag = (1 << bit_number)
|
||||||
|
return (self.buffer[address] & bitflag) != 0
|
||||||
|
|
||||||
|
def read_byte(self, address: int) -> int:
|
||||||
|
return self.buffer[address]
|
||||||
|
|
||||||
|
def read_bytes(self, start_address: int, length: int) -> bytearray:
|
||||||
|
return self.buffer[start_address:start_address + length]
|
||||||
|
|
||||||
|
def write_byte(self, address: int, value: int) -> None:
|
||||||
|
self.buffer[address] = value
|
||||||
|
|
||||||
|
def write_bytes(self, start_address: int, values: Collection[int]) -> None:
|
||||||
|
self.buffer[start_address:start_address + len(values)] = values
|
||||||
|
|
||||||
|
def write_int16(self, address: int, value: int) -> None:
|
||||||
|
value = value & 0xFFFF
|
||||||
|
self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF])
|
||||||
|
|
||||||
|
def write_int16s(self, start_address: int, values: List[int]) -> None:
|
||||||
|
for i, value in enumerate(values):
|
||||||
|
self.write_int16(start_address + (i * 2), value)
|
||||||
|
|
||||||
|
def write_int24(self, address: int, value: int) -> None:
|
||||||
|
value = value & 0xFFFFFF
|
||||||
|
self.write_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
|
||||||
|
|
||||||
|
def write_int24s(self, start_address: int, values: List[int]) -> None:
|
||||||
|
for i, value in enumerate(values):
|
||||||
|
self.write_int24(start_address + (i * 3), value)
|
||||||
|
|
||||||
|
def write_int32(self, address, value: int) -> None:
|
||||||
|
value = value & 0xFFFFFFFF
|
||||||
|
self.write_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
|
||||||
|
|
||||||
|
def write_int32s(self, start_address: int, values: list) -> None:
|
||||||
|
for i, value in enumerate(values):
|
||||||
|
self.write_int32(start_address + (i * 4), value)
|
||||||
|
|
||||||
|
def write_to_file(self, filepath: str) -> None:
|
||||||
|
with open(filepath, "wb") as outfile:
|
||||||
|
outfile.write(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_rom(world: "CV64World", rom: LocalRom, offset_data: Dict[int, int], shop_name_list: List[str],
|
||||||
|
shop_desc_list: List[List[Union[int, str, None]]], shop_colors_list: List[bytearray],
|
||||||
|
active_locations: Iterable[Location]) -> None:
|
||||||
|
|
||||||
|
multiworld = world.multiworld
|
||||||
|
options = world.options
|
||||||
|
player = world.player
|
||||||
|
active_stage_exits = world.active_stage_exits
|
||||||
|
s1s_per_warp = world.s1s_per_warp
|
||||||
|
active_warp_list = world.active_warp_list
|
||||||
|
required_s2s = world.required_s2s
|
||||||
|
total_s2s = world.total_s2s
|
||||||
|
|
||||||
|
# NOP out the CRC BNEs
|
||||||
|
rom.write_int32(0x66C, 0x00000000)
|
||||||
|
rom.write_int32(0x678, 0x00000000)
|
||||||
|
|
||||||
|
# Always offer Hard Mode on file creation
|
||||||
|
rom.write_int32(0xC8810, 0x240A0100) # ADDIU T2, R0, 0x0100
|
||||||
|
|
||||||
|
# Disable Easy Mode cutoff point at Castle Center elevator
|
||||||
|
rom.write_int32(0xD9E18, 0x240D0000) # ADDIU T5, R0, 0x0000
|
||||||
|
|
||||||
|
# Disable the Forest, Castle Wall, and Villa intro cutscenes and make it possible to change the starting level
|
||||||
|
rom.write_byte(0xB73308, 0x00)
|
||||||
|
rom.write_byte(0xB7331A, 0x40)
|
||||||
|
rom.write_byte(0xB7332B, 0x4C)
|
||||||
|
rom.write_byte(0xB6302B, 0x00)
|
||||||
|
rom.write_byte(0x109F8F, 0x00)
|
||||||
|
|
||||||
|
# Prevent Forest end cutscene flag from setting so it can be triggered infinitely
|
||||||
|
rom.write_byte(0xEEA51, 0x01)
|
||||||
|
|
||||||
|
# Hack to make the Forest, CW and Villa intro cutscenes play at the start of their levels no matter what map came
|
||||||
|
# before them
|
||||||
|
rom.write_int32(0x97244, 0x803FDD60)
|
||||||
|
rom.write_int32s(0xBFDD60, patches.forest_cw_villa_intro_cs_player)
|
||||||
|
|
||||||
|
# Make changing the map ID to 0xFF reset the map. Helpful to work around a bug wherein the camera gets stuck when
|
||||||
|
# entering a loading zone that doesn't change the map.
|
||||||
|
rom.write_int32s(0x197B0, [0x0C0FF7E6, # JAL 0x803FDF98
|
||||||
|
0x24840008]) # ADDIU A0, A0, 0x0008
|
||||||
|
rom.write_int32s(0xBFDF98, patches.map_id_refresher)
|
||||||
|
|
||||||
|
# Enable swapping characters when loading into a map by holding L.
|
||||||
|
rom.write_int32(0x97294, 0x803FDFC4)
|
||||||
|
rom.write_int32(0x19710, 0x080FF80E) # J 0x803FE038
|
||||||
|
rom.write_int32s(0xBFDFC4, patches.character_changer)
|
||||||
|
|
||||||
|
# Villa coffin time-of-day hack
|
||||||
|
rom.write_byte(0xD9D83, 0x74)
|
||||||
|
rom.write_int32(0xD9D84, 0x080FF14D) # J 0x803FC534
|
||||||
|
rom.write_int32s(0xBFC534, patches.coffin_time_checker)
|
||||||
|
|
||||||
|
# Fix both Castle Center elevator bridges for both characters unless enabling only one character's stages. At which
|
||||||
|
# point one bridge will be always broken and one always repaired instead.
|
||||||
|
if options.character_stages == CharacterStages.option_reinhardt_only:
|
||||||
|
rom.write_int32(0x6CEAA0, 0x240B0000) # ADDIU T3, R0, 0x0000
|
||||||
|
elif options.character_stages == CharacterStages.option_carrie_only:
|
||||||
|
rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
|
||||||
|
else:
|
||||||
|
rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
|
||||||
|
rom.write_int32(0x6CEAA4, 0x240D0001) # ADDIU T5, R0, 0x0001
|
||||||
|
|
||||||
|
# Were-bull arena flag hack
|
||||||
|
rom.write_int32(0x6E38F0, 0x0C0FF157) # JAL 0x803FC55C
|
||||||
|
rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter)
|
||||||
|
rom.write_int32(0xA949C, 0x0C0FF380) # JAL 0x803FCE00
|
||||||
|
rom.write_int32s(0xBFCE00, patches.werebull_flag_pickup_setter)
|
||||||
|
|
||||||
|
# Enable being able to carry multiple Special jewels, Nitros, and Mandragoras simultaneously
|
||||||
|
rom.write_int32(0xBF1F4, 0x3C038039) # LUI V1, 0x8039
|
||||||
|
# Special1
|
||||||
|
rom.write_int32(0xBF210, 0x80659C4B) # LB A1, 0x9C4B (V1)
|
||||||
|
rom.write_int32(0xBF214, 0x24A50001) # ADDIU A1, A1, 0x0001
|
||||||
|
rom.write_int32(0xBF21C, 0xA0659C4B) # SB A1, 0x9C4B (V1)
|
||||||
|
# Special2
|
||||||
|
rom.write_int32(0xBF230, 0x80659C4C) # LB A1, 0x9C4C (V1)
|
||||||
|
rom.write_int32(0xBF234, 0x24A50001) # ADDIU A1, A1, 0x0001
|
||||||
|
rom.write_int32(0xbf23C, 0xA0659C4C) # SB A1, 0x9C4C (V1)
|
||||||
|
# Magical Nitro
|
||||||
|
rom.write_int32(0xBF360, 0x10000004) # B 0x8013C184
|
||||||
|
rom.write_int32(0xBF378, 0x25E50001) # ADDIU A1, T7, 0x0001
|
||||||
|
rom.write_int32(0xBF37C, 0x10000003) # B 0x8013C19C
|
||||||
|
# Mandragora
|
||||||
|
rom.write_int32(0xBF3A8, 0x10000004) # B 0x8013C1CC
|
||||||
|
rom.write_int32(0xBF3C0, 0x25050001) # ADDIU A1, T0, 0x0001
|
||||||
|
rom.write_int32(0xBF3C4, 0x10000003) # B 0x8013C1E4
|
||||||
|
|
||||||
|
# Give PowerUps their Legacy of Darkness behavior when attempting to pick up more than two
|
||||||
|
rom.write_int16(0xA9624, 0x1000)
|
||||||
|
rom.write_int32(0xA9730, 0x24090000) # ADDIU T1, R0, 0x0000
|
||||||
|
rom.write_int32(0xBF2FC, 0x080FF16D) # J 0x803FC5B4
|
||||||
|
rom.write_int32(0xBF300, 0x00000000) # NOP
|
||||||
|
rom.write_int32s(0xBFC5B4, patches.give_powerup_stopper)
|
||||||
|
|
||||||
|
# Rename the Wooden Stake and Rose to "You are a FOOL!"
|
||||||
|
rom.write_bytes(0xEFE34,
|
||||||
|
bytearray([0xFF, 0xFF, 0xA2, 0x0B]) + cv64_string_to_bytearray("You are a FOOL!", append_end=False))
|
||||||
|
# Capitalize the "k" in "Archives key" to be consistent with...literally every other key name!
|
||||||
|
rom.write_byte(0xEFF21, 0x2D)
|
||||||
|
|
||||||
|
# Skip the "There is a white jewel" text so checking one saves the game instantly.
|
||||||
|
rom.write_int32s(0xEFC72, [0x00020002 for _ in range(37)])
|
||||||
|
rom.write_int32(0xA8FC0, 0x24020001) # ADDIU V0, R0, 0x0001
|
||||||
|
# Skip the yes/no prompts when activating things.
|
||||||
|
rom.write_int32s(0xBFDACC, patches.map_text_redirector)
|
||||||
|
rom.write_int32(0xA9084, 0x24020001) # ADDIU V0, R0, 0x0001
|
||||||
|
rom.write_int32(0xBEBE8, 0x0C0FF6B4) # JAL 0x803FDAD0
|
||||||
|
# Skip Vincent and Heinrich's mandatory-for-a-check dialogue
|
||||||
|
rom.write_int32(0xBED9C, 0x0C0FF6DA) # JAL 0x803FDB68
|
||||||
|
# Skip the long yes/no prompt in the CC planetarium to set the pieces.
|
||||||
|
rom.write_int32(0xB5C5DF, 0x24030001) # ADDIU V1, R0, 0x0001
|
||||||
|
# Skip the yes/no prompt to activate the CC elevator.
|
||||||
|
rom.write_int32(0xB5E3FB, 0x24020001) # ADDIU V0, R0, 0x0001
|
||||||
|
# Skip the yes/no prompts to set Nitro/Mandragora at both walls.
|
||||||
|
rom.write_int32(0xB5DF3E, 0x24030001) # ADDIU V1, R0, 0x0001
|
||||||
|
|
||||||
|
# Custom message if you try checking the downstairs CC crack before removing the seal.
|
||||||
|
rom.write_bytes(0xBFDBAC, cv64_string_to_bytearray("The Furious Nerd Curse\n"
|
||||||
|
"prevents you from setting\n"
|
||||||
|
"anything until the seal\n"
|
||||||
|
"is removed!", True))
|
||||||
|
|
||||||
|
rom.write_int32s(0xBFDD20, patches.special_descriptions_redirector)
|
||||||
|
|
||||||
|
# Change the Stage Select menu options
|
||||||
|
rom.write_int32s(0xADF64, patches.warp_menu_rewrite)
|
||||||
|
rom.write_int32s(0x10E0C8, patches.warp_pointer_table)
|
||||||
|
for i in range(len(active_warp_list)):
|
||||||
|
if i == 0:
|
||||||
|
rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "start map id"))
|
||||||
|
rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "start spawn id"))
|
||||||
|
else:
|
||||||
|
rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "mid map id"))
|
||||||
|
rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "mid spawn id"))
|
||||||
|
|
||||||
|
# Play the "teleportation" sound effect when teleporting
|
||||||
|
rom.write_int32s(0xAE088, [0x08004FAB, # J 0x80013EAC
|
||||||
|
0x2404019E]) # ADDIU A0, R0, 0x019E
|
||||||
|
|
||||||
|
# Change the Stage Select menu's text to reflect its new purpose
|
||||||
|
rom.write_bytes(0xEFAD0, cv64_string_to_bytearray(f"Where to...?\t{active_warp_list[0]}\t"
|
||||||
|
f"`{str(s1s_per_warp).zfill(2)} {active_warp_list[1]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 2).zfill(2)} {active_warp_list[2]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 3).zfill(2)} {active_warp_list[3]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 4).zfill(2)} {active_warp_list[4]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 5).zfill(2)} {active_warp_list[5]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 6).zfill(2)} {active_warp_list[6]}\t"
|
||||||
|
f"`{str(s1s_per_warp * 7).zfill(2)} {active_warp_list[7]}"))
|
||||||
|
|
||||||
|
# Lizard-man save proofing
|
||||||
|
rom.write_int32(0xA99AC, 0x080FF0B8) # J 0x803FC2E0
|
||||||
|
rom.write_int32s(0xBFC2E0, patches.boss_save_stopper)
|
||||||
|
|
||||||
|
# Disable or guarantee vampire Vincent's fight
|
||||||
|
if options.vincent_fight_condition == VincentFightCondition.option_never:
|
||||||
|
rom.write_int32(0xAACC0, 0x24010001) # ADDIU AT, R0, 0x0001
|
||||||
|
rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
|
||||||
|
elif options.vincent_fight_condition == VincentFightCondition.option_always:
|
||||||
|
rom.write_int32(0xAACE0, 0x24180010) # ADDIU T8, R0, 0x0010
|
||||||
|
else:
|
||||||
|
rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
|
||||||
|
|
||||||
|
# Disable or guarantee Renon's fight
|
||||||
|
rom.write_int32(0xAACB4, 0x080FF1A4) # J 0x803FC690
|
||||||
|
if options.renon_fight_condition == RenonFightCondition.option_never:
|
||||||
|
rom.write_byte(0xB804F0, 0x00)
|
||||||
|
rom.write_byte(0xB80632, 0x00)
|
||||||
|
rom.write_byte(0xB807E3, 0x00)
|
||||||
|
rom.write_byte(0xB80988, 0xB8)
|
||||||
|
rom.write_byte(0xB816BD, 0xB8)
|
||||||
|
rom.write_byte(0xB817CF, 0x00)
|
||||||
|
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
|
||||||
|
elif options.renon_fight_condition == RenonFightCondition.option_always:
|
||||||
|
rom.write_byte(0xB804F0, 0x0C)
|
||||||
|
rom.write_byte(0xB80632, 0x0C)
|
||||||
|
rom.write_byte(0xB807E3, 0x0C)
|
||||||
|
rom.write_byte(0xB80988, 0xC4)
|
||||||
|
rom.write_byte(0xB816BD, 0xC4)
|
||||||
|
rom.write_byte(0xB817CF, 0x0C)
|
||||||
|
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
|
||||||
|
else:
|
||||||
|
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker)
|
||||||
|
|
||||||
|
# NOP the Easy Mode check when buying a thing from Renon, so he can be triggered even on this mode.
|
||||||
|
rom.write_int32(0xBD8B4, 0x00000000)
|
||||||
|
|
||||||
|
# Disable or guarantee the Bad Ending
|
||||||
|
if options.bad_ending_condition == BadEndingCondition.option_never:
|
||||||
|
rom.write_int32(0xAEE5C6, 0x3C0A0000) # LUI T2, 0x0000
|
||||||
|
elif options.bad_ending_condition == BadEndingCondition.option_always:
|
||||||
|
rom.write_int32(0xAEE5C6, 0x3C0A0040) # LUI T2, 0x0040
|
||||||
|
|
||||||
|
# Play Castle Keep's song if teleporting in front of Dracula's door outside the escape sequence
|
||||||
|
rom.write_int32(0x6E937C, 0x080FF12E) # J 0x803FC4B8
|
||||||
|
rom.write_int32s(0xBFC4B8, patches.ck_door_music_player)
|
||||||
|
|
||||||
|
# Increase item capacity to 100 if "Increase Item Limit" is turned on
|
||||||
|
if options.increase_item_limit:
|
||||||
|
rom.write_byte(0xBF30B, 0x63) # Most items
|
||||||
|
rom.write_byte(0xBF3F7, 0x63) # Sun/Moon cards
|
||||||
|
rom.write_byte(0xBF353, 0x64) # Keys (increase regardless)
|
||||||
|
|
||||||
|
# Change the item healing values if "Nerf Healing" is turned on
|
||||||
|
if options.nerf_healing_items:
|
||||||
|
rom.write_byte(0xB56371, 0x50) # Healing kit (100 -> 80)
|
||||||
|
rom.write_byte(0xB56374, 0x32) # Roast beef ( 80 -> 50)
|
||||||
|
rom.write_byte(0xB56377, 0x19) # Roast chicken ( 50 -> 25)
|
||||||
|
|
||||||
|
# Disable loading zone healing if turned off
|
||||||
|
if not options.loading_zone_heals:
|
||||||
|
rom.write_byte(0xD99A5, 0x00) # Skip all loading zone checks
|
||||||
|
rom.write_byte(0xA9DFFB, 0x40) # Disable free heal from King Skeleton by reading the unused magic meter value
|
||||||
|
|
||||||
|
# Disable spinning on the Special1 and 2 pickup models so colorblind people can more easily identify them
|
||||||
|
rom.write_byte(0xEE4F5, 0x00) # Special1
|
||||||
|
rom.write_byte(0xEE505, 0x00) # Special2
|
||||||
|
# Make the Special2 the same size as a Red jewel(L) to further distinguish them
|
||||||
|
rom.write_int32(0xEE4FC, 0x3FA66666)
|
||||||
|
|
||||||
|
# Prevent the vanilla Magical Nitro transport's "can explode" flag from setting
|
||||||
|
rom.write_int32(0xB5D7AA, 0x00000000) # NOP
|
||||||
|
|
||||||
|
# Ensure the vampire Nitro check will always pass, so they'll never not spawn and crash the Villa cutscenes
|
||||||
|
rom.write_byte(0xA6253D, 0x03)
|
||||||
|
|
||||||
|
# Enable the Game Over's "Continue" menu starting the cursor on whichever checkpoint is most recent
|
||||||
|
rom.write_int32(0xB4DDC, 0x0C060D58) # JAL 0x80183560
|
||||||
|
rom.write_int32s(0x106750, patches.continue_cursor_start_checker)
|
||||||
|
rom.write_int32(0x1C444, 0x080FF08A) # J 0x803FC228
|
||||||
|
rom.write_int32(0x1C2A0, 0x080FF08A) # J 0x803FC228
|
||||||
|
rom.write_int32s(0xBFC228, patches.savepoint_cursor_updater)
|
||||||
|
rom.write_int32(0x1C2D0, 0x080FF094) # J 0x803FC250
|
||||||
|
rom.write_int32s(0xBFC250, patches.stage_start_cursor_updater)
|
||||||
|
rom.write_byte(0xB585C8, 0xFF)
|
||||||
|
|
||||||
|
# Make the Special1 and 2 play sounds when you reach milestones with them.
|
||||||
|
rom.write_int32s(0xBFDA50, patches.special_sound_notifs)
|
||||||
|
rom.write_int32(0xBF240, 0x080FF694) # J 0x803FDA50
|
||||||
|
rom.write_int32(0xBF220, 0x080FF69E) # J 0x803FDA78
|
||||||
|
|
||||||
|
# Add data for White Jewel #22 (the new Duel Tower savepoint) at the end of the White Jewel ID data list
|
||||||
|
rom.write_int16s(0x104AC8, [0x0000, 0x0006,
|
||||||
|
0x0013, 0x0015])
|
||||||
|
|
||||||
|
# Take the contract in Waterway off of its 00400000 bitflag.
|
||||||
|
rom.write_byte(0x87E3DA, 0x00)
|
||||||
|
|
||||||
|
# Spawn coordinates list extension
|
||||||
|
rom.write_int32(0xD5BF4, 0x080FF103) # J 0x803FC40C
|
||||||
|
rom.write_int32s(0xBFC40C, patches.spawn_coordinates_extension)
|
||||||
|
rom.write_int32s(0x108A5E, patches.waterway_end_coordinates)
|
||||||
|
|
||||||
|
# Change the File Select stage numbers to match the new stage order. Also fix a vanilla issue wherein saving in a
|
||||||
|
# character-exclusive stage as the other character would incorrectly display the name of that character's equivalent
|
||||||
|
# stage on the save file instead of the one they're actually in.
|
||||||
|
rom.write_byte(0xC9FE3, 0xD4)
|
||||||
|
rom.write_byte(0xCA055, 0x08)
|
||||||
|
rom.write_byte(0xCA066, 0x40)
|
||||||
|
rom.write_int32(0xCA068, 0x860C17D0) # LH T4, 0x17D0 (S0)
|
||||||
|
rom.write_byte(0xCA06D, 0x08)
|
||||||
|
rom.write_byte(0x104A31, 0x01)
|
||||||
|
rom.write_byte(0x104A39, 0x01)
|
||||||
|
rom.write_byte(0x104A89, 0x01)
|
||||||
|
rom.write_byte(0x104A91, 0x01)
|
||||||
|
rom.write_byte(0x104A99, 0x01)
|
||||||
|
rom.write_byte(0x104AA1, 0x01)
|
||||||
|
|
||||||
|
for stage in active_stage_exits:
|
||||||
|
for offset in get_stage_info(stage, "save number offsets"):
|
||||||
|
rom.write_byte(offset, active_stage_exits[stage]["position"])
|
||||||
|
|
||||||
|
# CC top elevator switch check
|
||||||
|
rom.write_int32(0x6CF0A0, 0x0C0FF0B0) # JAL 0x803FC2C0
|
||||||
|
rom.write_int32s(0xBFC2C0, patches.elevator_flag_checker)
|
||||||
|
|
||||||
|
# Disable time restrictions
|
||||||
|
if options.disable_time_restrictions:
|
||||||
|
# Fountain
|
||||||
|
rom.write_int32(0x6C2340, 0x00000000) # NOP
|
||||||
|
rom.write_int32(0x6C257C, 0x10000023) # B [forward 0x23]
|
||||||
|
# Rosa
|
||||||
|
rom.write_byte(0xEEAAB, 0x00)
|
||||||
|
rom.write_byte(0xEEAAD, 0x18)
|
||||||
|
# Moon doors
|
||||||
|
rom.write_int32(0xDC3E0, 0x00000000) # NOP
|
||||||
|
rom.write_int32(0xDC3E8, 0x00000000) # NOP
|
||||||
|
# Sun doors
|
||||||
|
rom.write_int32(0xDC410, 0x00000000) # NOP
|
||||||
|
rom.write_int32(0xDC418, 0x00000000) # NOP
|
||||||
|
|
||||||
|
# Custom data-loading code
|
||||||
|
rom.write_int32(0x6B5028, 0x08060D70) # J 0x801835D0
|
||||||
|
rom.write_int32s(0x1067B0, patches.custom_code_loader)
|
||||||
|
|
||||||
|
# Custom remote item rewarding and DeathLink receiving code
|
||||||
|
rom.write_int32(0x19B98, 0x080FF000) # J 0x803FC000
|
||||||
|
rom.write_int32s(0xBFC000, patches.remote_item_giver)
|
||||||
|
rom.write_int32s(0xBFE190, patches.subweapon_surface_checker)
|
||||||
|
|
||||||
|
# Make received DeathLinks blow you to smithereens instead of kill you normally.
|
||||||
|
if options.death_link == DeathLink.option_explosive:
|
||||||
|
rom.write_int32(0x27A70, 0x10000008) # B [forward 0x08]
|
||||||
|
rom.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition)
|
||||||
|
|
||||||
|
# Set the DeathLink ROM flag if it's on at all.
|
||||||
|
if options.death_link != DeathLink.option_off:
|
||||||
|
rom.write_byte(0xBFBFDE, 0x01)
|
||||||
|
|
||||||
|
# DeathLink counter decrementer code
|
||||||
|
rom.write_int32(0x1C340, 0x080FF8F0) # J 0x803FE3C0
|
||||||
|
rom.write_int32s(0xBFE3C0, patches.deathlink_counter_decrementer)
|
||||||
|
rom.write_int32(0x25B6C, 0x0080FF052) # J 0x803FC148
|
||||||
|
rom.write_int32s(0xBFC148, patches.nitro_fall_killer)
|
||||||
|
|
||||||
|
# Death flag un-setter on "Beginning of stage" state overwrite code
|
||||||
|
rom.write_int32(0x1C2B0, 0x080FF047) # J 0x803FC11C
|
||||||
|
rom.write_int32s(0xBFC11C, patches.death_flag_unsetter)
|
||||||
|
|
||||||
|
# Warp menu-opening code
|
||||||
|
rom.write_int32(0xB9BA8, 0x080FF099) # J 0x803FC264
|
||||||
|
rom.write_int32s(0xBFC264, patches.warp_menu_opener)
|
||||||
|
|
||||||
|
# NPC item textbox hack
|
||||||
|
rom.write_int32(0xBF1DC, 0x080FF904) # J 0x803FE410
|
||||||
|
rom.write_int32(0xBF1E0, 0x27BDFFE0) # ADDIU SP, SP, -0x20
|
||||||
|
rom.write_int32s(0xBFE410, patches.npc_item_hack)
|
||||||
|
|
||||||
|
# Sub-weapon check function hook
|
||||||
|
rom.write_int32(0xBF32C, 0x00000000) # NOP
|
||||||
|
rom.write_int32(0xBF330, 0x080FF05E) # J 0x803FC178
|
||||||
|
rom.write_int32s(0xBFC178, patches.give_subweapon_stopper)
|
||||||
|
|
||||||
|
# Warp menu Special1 restriction
|
||||||
|
rom.write_int32(0xADD68, 0x0C04AB12) # JAL 0x8012AC48
|
||||||
|
rom.write_int32s(0xADE28, patches.stage_select_overwrite)
|
||||||
|
rom.write_byte(0xADE47, s1s_per_warp)
|
||||||
|
|
||||||
|
# Dracula's door text pointer hijack
|
||||||
|
rom.write_int32(0xD69F0, 0x080FF141) # J 0x803FC504
|
||||||
|
rom.write_int32s(0xBFC504, patches.dracula_door_text_redirector)
|
||||||
|
|
||||||
|
# Dracula's chamber condition
|
||||||
|
rom.write_int32(0xE2FDC, 0x0804AB25) # J 0x8012AC78
|
||||||
|
rom.write_int32s(0xADE84, patches.special_goal_checker)
|
||||||
|
rom.write_bytes(0xBFCC48, [0xA0, 0x00, 0xFF, 0xFF, 0xA0, 0x01, 0xFF, 0xFF, 0xA0, 0x02, 0xFF, 0xFF, 0xA0, 0x03, 0xFF,
|
||||||
|
0xFF, 0xA0, 0x04, 0xFF, 0xFF, 0xA0, 0x05, 0xFF, 0xFF, 0xA0, 0x06, 0xFF, 0xFF, 0xA0, 0x07,
|
||||||
|
0xFF, 0xFF, 0xA0, 0x08, 0xFF, 0xFF, 0xA0, 0x09])
|
||||||
|
if options.draculas_condition == DraculasCondition.option_crystal:
|
||||||
|
rom.write_int32(0x6C8A54, 0x0C0FF0C1) # JAL 0x803FC304
|
||||||
|
rom.write_int32s(0xBFC304, patches.crystal_special2_giver)
|
||||||
|
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
|
||||||
|
f"You'll need the power\n"
|
||||||
|
f"of the basement crystal\n"
|
||||||
|
f"to undo the seal.", True))
|
||||||
|
special2_name = "Crystal "
|
||||||
|
special2_text = "The crystal is on!\n" \
|
||||||
|
"Time to teach the old man\n" \
|
||||||
|
"a lesson!"
|
||||||
|
elif options.draculas_condition == DraculasCondition.option_bosses:
|
||||||
|
rom.write_int32(0xBBD50, 0x080FF18C) # J 0x803FC630
|
||||||
|
rom.write_int32s(0xBFC630, patches.boss_special2_giver)
|
||||||
|
rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter_special2_electric_boogaloo)
|
||||||
|
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
|
||||||
|
f"You'll need to defeat\n"
|
||||||
|
f"{required_s2s} powerful monsters\n"
|
||||||
|
f"to undo the seal.", True))
|
||||||
|
special2_name = "Trophy "
|
||||||
|
special2_text = f"Proof you killed a powerful\n" \
|
||||||
|
f"Night Creature. Earn {required_s2s}/{total_s2s}\n" \
|
||||||
|
f"to battle Dracula."
|
||||||
|
elif options.draculas_condition == DraculasCondition.option_specials:
|
||||||
|
special2_name = "Special2"
|
||||||
|
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
|
||||||
|
f"You'll need to find\n"
|
||||||
|
f"{required_s2s} Special2 jewels\n"
|
||||||
|
f"to undo the seal.", True))
|
||||||
|
special2_text = f"Need {required_s2s}/{total_s2s} to kill Dracula.\n" \
|
||||||
|
f"Looking closely, you see...\n" \
|
||||||
|
f"a piece of him within?"
|
||||||
|
else:
|
||||||
|
rom.write_byte(0xADE8F, 0x00)
|
||||||
|
special2_name = "Special2"
|
||||||
|
special2_text = "If you're reading this,\n" \
|
||||||
|
"how did you get a Special2!?"
|
||||||
|
rom.write_byte(0xADE8F, required_s2s)
|
||||||
|
# Change the Special2 name depending on the setting.
|
||||||
|
rom.write_bytes(0xEFD4E, cv64_string_to_bytearray(special2_name))
|
||||||
|
# Change the Special1 and 2 menu descriptions to tell you how many you need to unlock a warp and fight Dracula
|
||||||
|
# respectively.
|
||||||
|
special_text_bytes = cv64_string_to_bytearray(f"{s1s_per_warp} per warp unlock.\n"
|
||||||
|
f"{options.total_special1s.value} exist in total.\n"
|
||||||
|
f"Z + R + START to warp.") + cv64_string_to_bytearray(special2_text)
|
||||||
|
rom.write_bytes(0xBFE53C, special_text_bytes)
|
||||||
|
|
||||||
|
# On-the-fly TLB script modifier
|
||||||
|
rom.write_int32s(0xBFC338, patches.double_component_checker)
|
||||||
|
rom.write_int32s(0xBFC3D4, patches.downstairs_seal_checker)
|
||||||
|
rom.write_int32s(0xBFE074, patches.mandragora_with_nitro_setter)
|
||||||
|
rom.write_int32s(0xBFC700, patches.overlay_modifiers)
|
||||||
|
|
||||||
|
# On-the-fly actor data modifier hook
|
||||||
|
rom.write_int32(0xEAB04, 0x080FF21E) # J 0x803FC878
|
||||||
|
rom.write_int32s(0xBFC870, patches.map_data_modifiers)
|
||||||
|
|
||||||
|
# Fix to make flags apply to freestanding invisible items properly
|
||||||
|
rom.write_int32(0xA84F8, 0x90CC0039) # LBU T4, 0x0039 (A2)
|
||||||
|
|
||||||
|
# Fix locked doors to check the key counters instead of their vanilla key locations' bitflags
|
||||||
|
# Pickup flag check modifications:
|
||||||
|
rom.write_int32(0x10B2D8, 0x00000002) # Left Tower Door
|
||||||
|
rom.write_int32(0x10B2F0, 0x00000003) # Storeroom Door
|
||||||
|
rom.write_int32(0x10B2FC, 0x00000001) # Archives Door
|
||||||
|
rom.write_int32(0x10B314, 0x00000004) # Maze Gate
|
||||||
|
rom.write_int32(0x10B350, 0x00000005) # Copper Door
|
||||||
|
rom.write_int32(0x10B3A4, 0x00000006) # Torture Chamber Door
|
||||||
|
rom.write_int32(0x10B3B0, 0x00000007) # ToE Gate
|
||||||
|
rom.write_int32(0x10B3BC, 0x00000008) # Science Door1
|
||||||
|
rom.write_int32(0x10B3C8, 0x00000009) # Science Door2
|
||||||
|
rom.write_int32(0x10B3D4, 0x0000000A) # Science Door3
|
||||||
|
rom.write_int32(0x6F0094, 0x0000000B) # CT Door 1
|
||||||
|
rom.write_int32(0x6F00A4, 0x0000000C) # CT Door 2
|
||||||
|
rom.write_int32(0x6F00B4, 0x0000000D) # CT Door 3
|
||||||
|
# Item counter decrement check modifications:
|
||||||
|
rom.write_int32(0xEDA84, 0x00000001) # Archives Door
|
||||||
|
rom.write_int32(0xEDA8C, 0x00000002) # Left Tower Door
|
||||||
|
rom.write_int32(0xEDA94, 0x00000003) # Storeroom Door
|
||||||
|
rom.write_int32(0xEDA9C, 0x00000004) # Maze Gate
|
||||||
|
rom.write_int32(0xEDAA4, 0x00000005) # Copper Door
|
||||||
|
rom.write_int32(0xEDAAC, 0x00000006) # Torture Chamber Door
|
||||||
|
rom.write_int32(0xEDAB4, 0x00000007) # ToE Gate
|
||||||
|
rom.write_int32(0xEDABC, 0x00000008) # Science Door1
|
||||||
|
rom.write_int32(0xEDAC4, 0x00000009) # Science Door2
|
||||||
|
rom.write_int32(0xEDACC, 0x0000000A) # Science Door3
|
||||||
|
rom.write_int32(0xEDAD4, 0x0000000B) # CT Door 1
|
||||||
|
rom.write_int32(0xEDADC, 0x0000000C) # CT Door 2
|
||||||
|
rom.write_int32(0xEDAE4, 0x0000000D) # CT Door 3
|
||||||
|
|
||||||
|
# Fix ToE gate's "unlocked" flag in the locked door flags table
|
||||||
|
rom.write_int16(0x10B3B6, 0x0001)
|
||||||
|
|
||||||
|
rom.write_int32(0x10AB2C, 0x8015FBD4) # Maze Gates' check code pointer adjustments
|
||||||
|
rom.write_int32(0x10AB40, 0x8015FBD4)
|
||||||
|
rom.write_int32s(0x10AB50, [0x0D0C0000,
|
||||||
|
0x8015FBD4])
|
||||||
|
rom.write_int32s(0x10AB64, [0x0D0C0000,
|
||||||
|
0x8015FBD4])
|
||||||
|
rom.write_int32s(0xE2E14, patches.normal_door_hook)
|
||||||
|
rom.write_int32s(0xBFC5D0, patches.normal_door_code)
|
||||||
|
rom.write_int32s(0x6EF298, patches.ct_door_hook)
|
||||||
|
rom.write_int32s(0xBFC608, patches.ct_door_code)
|
||||||
|
# Fix key counter not decrementing if 2 or above
|
||||||
|
rom.write_int32(0xAA0E0, 0x24020000) # ADDIU V0, R0, 0x0000
|
||||||
|
|
||||||
|
# Make the Easy-only candle drops in Room of Clocks appear on any difficulty
|
||||||
|
rom.write_byte(0x9B518F, 0x01)
|
||||||
|
|
||||||
|
# Slightly move some once-invisible freestanding items to be more visible
|
||||||
|
if options.invisible_items == InvisibleItems.option_reveal_all:
|
||||||
|
rom.write_byte(0x7C7F95, 0xEF) # Forest dirge maiden statue
|
||||||
|
rom.write_byte(0x7C7FA8, 0xAB) # Forest werewolf statue
|
||||||
|
rom.write_byte(0x8099C4, 0x8C) # Villa courtyard tombstone
|
||||||
|
rom.write_byte(0x83A626, 0xC2) # Villa living room painting
|
||||||
|
# rom.write_byte(0x83A62F, 0x64) # Villa Mary's room table
|
||||||
|
rom.write_byte(0xBFCB97, 0xF5) # CC torture instrument rack
|
||||||
|
rom.write_byte(0x8C44D5, 0x22) # CC red carpet hallway knight
|
||||||
|
rom.write_byte(0x8DF57C, 0xF1) # CC cracked wall hallway flamethrower
|
||||||
|
rom.write_byte(0x90FCD6, 0xA5) # CC nitro hallway flamethrower
|
||||||
|
rom.write_byte(0x90FB9F, 0x9A) # CC invention room round machine
|
||||||
|
rom.write_byte(0x90FBAF, 0x03) # CC invention room giant famicart
|
||||||
|
rom.write_byte(0x90FE54, 0x97) # CC staircase knight (x)
|
||||||
|
rom.write_byte(0x90FE58, 0xFB) # CC staircase knight (z)
|
||||||
|
|
||||||
|
# Change bitflag on item in upper coffin in Forest final switch gate tomb to one that's not used by something else
|
||||||
|
rom.write_int32(0x10C77C, 0x00000002)
|
||||||
|
|
||||||
|
# Make the torch directly behind Dracula's chamber that normally doesn't set a flag set bitflag 0x08 in 0x80389BFA
|
||||||
|
rom.write_byte(0x10CE9F, 0x01)
|
||||||
|
|
||||||
|
# Change the CC post-Behemoth boss depending on the option for Post-Behemoth Boss
|
||||||
|
if options.post_behemoth_boss == PostBehemothBoss.option_inverted:
|
||||||
|
rom.write_byte(0xEEDAD, 0x02)
|
||||||
|
rom.write_byte(0xEEDD9, 0x01)
|
||||||
|
elif options.post_behemoth_boss == PostBehemothBoss.option_always_rosa:
|
||||||
|
rom.write_byte(0xEEDAD, 0x00)
|
||||||
|
rom.write_byte(0xEEDD9, 0x03)
|
||||||
|
# Put both on the same flag so changing character won't trigger a rematch with the same boss.
|
||||||
|
rom.write_byte(0xEED8B, 0x40)
|
||||||
|
elif options.post_behemoth_boss == PostBehemothBoss.option_always_camilla:
|
||||||
|
rom.write_byte(0xEEDAD, 0x03)
|
||||||
|
rom.write_byte(0xEEDD9, 0x00)
|
||||||
|
rom.write_byte(0xEED8B, 0x40)
|
||||||
|
|
||||||
|
# Change the RoC boss depending on the option for Room of Clocks Boss
|
||||||
|
if options.room_of_clocks_boss == RoomOfClocksBoss.option_inverted:
|
||||||
|
rom.write_byte(0x109FB3, 0x56)
|
||||||
|
rom.write_byte(0x109FBF, 0x44)
|
||||||
|
rom.write_byte(0xD9D44, 0x14)
|
||||||
|
rom.write_byte(0xD9D4C, 0x14)
|
||||||
|
elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_death:
|
||||||
|
rom.write_byte(0x109FBF, 0x44)
|
||||||
|
rom.write_byte(0xD9D45, 0x00)
|
||||||
|
# Put both on the same flag so changing character won't trigger a rematch with the same boss.
|
||||||
|
rom.write_byte(0x109FB7, 0x90)
|
||||||
|
rom.write_byte(0x109FC3, 0x90)
|
||||||
|
elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_actrise:
|
||||||
|
rom.write_byte(0x109FB3, 0x56)
|
||||||
|
rom.write_int32(0xD9D44, 0x00000000)
|
||||||
|
rom.write_byte(0xD9D4D, 0x00)
|
||||||
|
rom.write_byte(0x109FB7, 0x90)
|
||||||
|
rom.write_byte(0x109FC3, 0x90)
|
||||||
|
|
||||||
|
# Un-nerf Actrise when playing as Reinhardt.
|
||||||
|
# This is likely a leftover TGS demo feature in which players could battle Actrise as Reinhardt.
|
||||||
|
rom.write_int32(0xB318B4, 0x240E0001) # ADDIU T6, R0, 0x0001
|
||||||
|
|
||||||
|
# Tunnel gondola skip
|
||||||
|
if options.skip_gondolas:
|
||||||
|
rom.write_int32(0x6C5F58, 0x080FF7D0) # J 0x803FDF40
|
||||||
|
rom.write_int32s(0xBFDF40, patches.gondola_skipper)
|
||||||
|
# New gondola transfer point candle coordinates
|
||||||
|
rom.write_byte(0xBFC9A3, 0x04)
|
||||||
|
rom.write_bytes(0x86D824, [0x27, 0x01, 0x10, 0xF7, 0xA0])
|
||||||
|
|
||||||
|
# Waterway brick platforms skip
|
||||||
|
if options.skip_waterway_blocks:
|
||||||
|
rom.write_int32(0x6C7E2C, 0x00000000) # NOP
|
||||||
|
|
||||||
|
# Ambience silencing fix
|
||||||
|
rom.write_int32(0xD9270, 0x080FF840) # J 0x803FE100
|
||||||
|
rom.write_int32s(0xBFE100, patches.ambience_silencer)
|
||||||
|
# Fix for the door sliding sound playing infinitely if leaving the fan meeting room before the door closes entirely.
|
||||||
|
# Hooking this in the ambience silencer code does nothing for some reason.
|
||||||
|
rom.write_int32s(0xAE10C, [0x08004FAB, # J 0x80013EAC
|
||||||
|
0x3404829B]) # ORI A0, R0, 0x829B
|
||||||
|
rom.write_int32s(0xD9E8C, [0x08004FAB, # J 0x80013EAC
|
||||||
|
0x3404829B]) # ORI A0, R0, 0x829B
|
||||||
|
# Fan meeting room ambience fix
|
||||||
|
rom.write_int32(0x109964, 0x803FE13C)
|
||||||
|
|
||||||
|
# Make the Villa coffin cutscene skippable
|
||||||
|
rom.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
|
||||||
|
rom.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
|
||||||
|
|
||||||
|
# Increase shimmy speed
|
||||||
|
if options.increase_shimmy_speed:
|
||||||
|
rom.write_byte(0xA4241, 0x5A)
|
||||||
|
|
||||||
|
# Disable landing fall damage
|
||||||
|
if options.fall_guard:
|
||||||
|
rom.write_byte(0x27B23, 0x00)
|
||||||
|
|
||||||
|
# Enable the unused film reel effect on all cutscenes
|
||||||
|
if options.cinematic_experience:
|
||||||
|
rom.write_int32(0xAA33C, 0x240A0001) # ADDIU T2, R0, 0x0001
|
||||||
|
rom.write_byte(0xAA34B, 0x0C)
|
||||||
|
rom.write_int32(0xAA4C4, 0x24090001) # ADDIU T1, R0, 0x0001
|
||||||
|
|
||||||
|
# Permanent PowerUp stuff
|
||||||
|
if options.permanent_powerups:
|
||||||
|
# Make receiving PowerUps increase the unused menu PowerUp counter instead of the one outside the save struct
|
||||||
|
rom.write_int32(0xBF2EC, 0x806B619B) # LB T3, 0x619B (V1)
|
||||||
|
rom.write_int32(0xBFC5BC, 0xA06C619B) # SB T4, 0x619B (V1)
|
||||||
|
# Make Reinhardt's whip check the menu PowerUp counter
|
||||||
|
rom.write_int32(0x69FA08, 0x80CC619B) # LB T4, 0x619B (A2)
|
||||||
|
rom.write_int32(0x69FBFC, 0x80C3619B) # LB V1, 0x619B (A2)
|
||||||
|
rom.write_int32(0x69FFE0, 0x818C9C53) # LB T4, 0x9C53 (T4)
|
||||||
|
# Make Carrie's orb check the menu PowerUp counter
|
||||||
|
rom.write_int32(0x6AC86C, 0x8105619B) # LB A1, 0x619B (T0)
|
||||||
|
rom.write_int32(0x6AC950, 0x8105619B) # LB A1, 0x619B (T0)
|
||||||
|
rom.write_int32(0x6AC99C, 0x810E619B) # LB T6, 0x619B (T0)
|
||||||
|
rom.write_int32(0x5AFA0, 0x80639C53) # LB V1, 0x9C53 (V1)
|
||||||
|
rom.write_int32(0x5B0A0, 0x81089C53) # LB T0, 0x9C53 (T0)
|
||||||
|
rom.write_byte(0x391C7, 0x00) # Prevent PowerUps from dropping from regular enemies
|
||||||
|
rom.write_byte(0xEDEDF, 0x03) # Make any vanishing PowerUps that do show up L jewels instead
|
||||||
|
# Rename the PowerUp to "PermaUp"
|
||||||
|
rom.write_bytes(0xEFDEE, cv64_string_to_bytearray("PermaUp"))
|
||||||
|
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
|
||||||
|
if not options.multi_hit_breakables:
|
||||||
|
rom.write_byte(0x10C7A1, 0x03)
|
||||||
|
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
|
||||||
|
# game PermaUps are distinguishable.
|
||||||
|
rom.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
|
||||||
|
|
||||||
|
# Write the randomized (or disabled) music ID list and its associated code
|
||||||
|
if options.background_music:
|
||||||
|
rom.write_int32(0x14588, 0x08060D60) # J 0x80183580
|
||||||
|
rom.write_int32(0x14590, 0x00000000) # NOP
|
||||||
|
rom.write_int32s(0x106770, patches.music_modifier)
|
||||||
|
rom.write_int32(0x15780, 0x0C0FF36E) # JAL 0x803FCDB8
|
||||||
|
rom.write_int32s(0xBFCDB8, patches.music_comparer_modifier)
|
||||||
|
|
||||||
|
# Enable storing item flags anywhere and changing the item model/visibility on any item instance.
|
||||||
|
rom.write_int32s(0xA857C, [0x080FF38F, # J 0x803FCE3C
|
||||||
|
0x94D90038]) # LHU T9, 0x0038 (A2)
|
||||||
|
rom.write_int32s(0xBFCE3C, patches.item_customizer)
|
||||||
|
rom.write_int32s(0xA86A0, [0x0C0FF3AF, # JAL 0x803FCEBC
|
||||||
|
0x95C40002]) # LHU A0, 0x0002 (T6)
|
||||||
|
rom.write_int32s(0xBFCEBC, patches.item_appearance_switcher)
|
||||||
|
rom.write_int32s(0xA8728, [0x0C0FF3B8, # JAL 0x803FCEE4
|
||||||
|
0x01396021]) # ADDU T4, T1, T9
|
||||||
|
rom.write_int32s(0xBFCEE4, patches.item_model_visibility_switcher)
|
||||||
|
rom.write_int32s(0xA8A04, [0x0C0FF3C2, # JAL 0x803FCF08
|
||||||
|
0x018B6021]) # ADDU T4, T4, T3
|
||||||
|
rom.write_int32s(0xBFCF08, patches.item_shine_visibility_switcher)
|
||||||
|
|
||||||
|
# Make Axes and Crosses in AP Locations drop to their correct height, and make items with changed appearances spin
|
||||||
|
# their correct speed.
|
||||||
|
rom.write_int32s(0xE649C, [0x0C0FFA03, # JAL 0x803FE80C
|
||||||
|
0x956C0002]) # LHU T4, 0x0002 (T3)
|
||||||
|
rom.write_int32s(0xA8B08, [0x080FFA0C, # J 0x803FE830
|
||||||
|
0x960A0038]) # LHU T2, 0x0038 (S0)
|
||||||
|
rom.write_int32s(0xE8584, [0x0C0FFA21, # JAL 0x803FE884
|
||||||
|
0x95D80000]) # LHU T8, 0x0000 (T6)
|
||||||
|
rom.write_int32s(0xE7AF0, [0x0C0FFA2A, # JAL 0x803FE8A8
|
||||||
|
0x958D0000]) # LHU T5, 0x0000 (T4)
|
||||||
|
rom.write_int32s(0xBFE7DC, patches.item_drop_spin_corrector)
|
||||||
|
|
||||||
|
# Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and
|
||||||
|
# setting flags instead.
|
||||||
|
if options.multi_hit_breakables:
|
||||||
|
rom.write_int32(0xE87F8, 0x00000000) # NOP
|
||||||
|
rom.write_int16(0xE836C, 0x1000)
|
||||||
|
rom.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
|
||||||
|
rom.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
|
||||||
|
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
|
||||||
|
rom.write_int32(0xE7D54, 0x00000000) # NOP
|
||||||
|
rom.write_int16(0xE7908, 0x1000)
|
||||||
|
rom.write_byte(0xE7A5C, 0x10)
|
||||||
|
rom.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
|
||||||
|
rom.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
|
||||||
|
|
||||||
|
# New flag values to put in each 3HB vanilla flag's spot
|
||||||
|
rom.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
|
||||||
|
rom.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
|
||||||
|
rom.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
|
||||||
|
rom.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
|
||||||
|
rom.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
|
||||||
|
rom.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
|
||||||
|
rom.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
|
||||||
|
rom.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
|
||||||
|
rom.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
|
||||||
|
rom.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
|
||||||
|
rom.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
|
||||||
|
rom.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
|
||||||
|
rom.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
|
||||||
|
rom.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
|
||||||
|
rom.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
|
||||||
|
rom.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||||
|
rom.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||||
|
rom.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||||
|
rom.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||||
|
|
||||||
|
# Once-per-frame gameplay checks
|
||||||
|
rom.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
||||||
|
rom.write_int32(0xBFD058, 0x0801AEB5) # J 0x8006BAD4
|
||||||
|
|
||||||
|
# Everything related to dropping the previous sub-weapon
|
||||||
|
if options.drop_previous_sub_weapon:
|
||||||
|
rom.write_int32(0xBFD034, 0x080FF3FF) # J 0x803FCFFC
|
||||||
|
rom.write_int32(0xBFC190, 0x080FF3F2) # J 0x803FCFC8
|
||||||
|
rom.write_int32s(0xBFCFC4, patches.prev_subweapon_spawn_checker)
|
||||||
|
rom.write_int32s(0xBFCFFC, patches.prev_subweapon_fall_checker)
|
||||||
|
rom.write_int32s(0xBFD060, patches.prev_subweapon_dropper)
|
||||||
|
|
||||||
|
# Everything related to the Countdown counter
|
||||||
|
if options.countdown:
|
||||||
|
rom.write_int32(0xBFD03C, 0x080FF9DC) # J 0x803FE770
|
||||||
|
rom.write_int32(0xD5D48, 0x080FF4EC) # J 0x803FD3B0
|
||||||
|
rom.write_int32s(0xBFD3B0, patches.countdown_number_displayer)
|
||||||
|
rom.write_int32s(0xBFD6DC, patches.countdown_number_manager)
|
||||||
|
rom.write_int32s(0xBFE770, patches.countdown_demo_hider)
|
||||||
|
rom.write_int32(0xBFCE2C, 0x080FF5D2) # J 0x803FD748
|
||||||
|
rom.write_int32s(0xBB168, [0x080FF5F4, # J 0x803FD7D0
|
||||||
|
0x8E020028]) # LW V0, 0x0028 (S0)
|
||||||
|
rom.write_int32s(0xBB1D0, [0x080FF5FB, # J 0x803FD7EC
|
||||||
|
0x8E020028]) # LW V0, 0x0028 (S0)
|
||||||
|
rom.write_int32(0xBC4A0, 0x080FF5E6) # J 0x803FD798
|
||||||
|
rom.write_int32(0xBC4C4, 0x080FF5E6) # J 0x803FD798
|
||||||
|
rom.write_int32(0x19844, 0x080FF602) # J 0x803FD808
|
||||||
|
# If the option is set to "all locations", count it down no matter what the item is.
|
||||||
|
if options.countdown == Countdown.option_all_locations:
|
||||||
|
rom.write_int32s(0xBFD71C, [0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101,
|
||||||
|
0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101])
|
||||||
|
else:
|
||||||
|
# If it's majors, then insert this last minute check I threw together for the weird edge case of a CV64 ice
|
||||||
|
# trap for another CV64 player taking the form of a major.
|
||||||
|
rom.write_int32s(0xBFD788, [0x080FF717, # J 0x803FDC5C
|
||||||
|
0x2529FFFF]) # ADDIU T1, T1, 0xFFFF
|
||||||
|
rom.write_int32s(0xBFDC5C, patches.countdown_extra_safety_check)
|
||||||
|
rom.write_int32(0xA9ECC, 0x00000000) # NOP the pointless overwrite of the item actor appearance custom value.
|
||||||
|
|
||||||
|
# Ice Trap stuff
|
||||||
|
rom.write_int32(0x697C60, 0x080FF06B) # J 0x803FC18C
|
||||||
|
rom.write_int32(0x6A5160, 0x080FF06B) # J 0x803FC18C
|
||||||
|
rom.write_int32s(0xBFC1AC, patches.ice_trap_initializer)
|
||||||
|
rom.write_int32s(0xBFE700, patches.the_deep_freezer)
|
||||||
|
rom.write_int32s(0xB2F354, [0x3739E4C0, # ORI T9, T9, 0xE4C0
|
||||||
|
0x03200008, # JR T9
|
||||||
|
0x00000000]) # NOP
|
||||||
|
rom.write_int32s(0xBFE4C0, patches.freeze_verifier)
|
||||||
|
|
||||||
|
# Initial Countdown numbers
|
||||||
|
rom.write_int32(0xAD6A8, 0x080FF60A) # J 0x803FD828
|
||||||
|
rom.write_int32s(0xBFD828, patches.new_game_extras)
|
||||||
|
|
||||||
|
# Everything related to shopsanity
|
||||||
|
if options.shopsanity:
|
||||||
|
rom.write_byte(0xBFBFDF, 0x01)
|
||||||
|
rom.write_bytes(0x103868, cv64_string_to_bytearray("Not obtained. "))
|
||||||
|
rom.write_int32s(0xBFD8D0, patches.shopsanity_stuff)
|
||||||
|
rom.write_int32(0xBD828, 0x0C0FF643) # JAL 0x803FD90C
|
||||||
|
rom.write_int32(0xBD5B8, 0x0C0FF651) # JAL 0x803FD944
|
||||||
|
rom.write_int32(0xB0610, 0x0C0FF665) # JAL 0x803FD994
|
||||||
|
rom.write_int32s(0xBD24C, [0x0C0FF677, # J 0x803FD9DC
|
||||||
|
0x00000000]) # NOP
|
||||||
|
rom.write_int32(0xBD618, 0x0C0FF684) # JAL 0x803FDA10
|
||||||
|
|
||||||
|
shopsanity_name_text = []
|
||||||
|
shopsanity_desc_text = []
|
||||||
|
for i in range(len(shop_name_list)):
|
||||||
|
shopsanity_name_text += bytearray([0xA0, i]) + shop_colors_list[i] + \
|
||||||
|
cv64_string_to_bytearray(cv64_text_truncate(shop_name_list[i], 74))
|
||||||
|
|
||||||
|
shopsanity_desc_text += [0xA0, i]
|
||||||
|
if shop_desc_list[i][1] is not None:
|
||||||
|
shopsanity_desc_text += cv64_string_to_bytearray("For " + shop_desc_list[i][1] + ".\n",
|
||||||
|
append_end=False)
|
||||||
|
shopsanity_desc_text += cv64_string_to_bytearray(renon_item_dialogue[shop_desc_list[i][0]])
|
||||||
|
rom.write_bytes(0x1AD00, shopsanity_name_text)
|
||||||
|
rom.write_bytes(0x1A800, shopsanity_desc_text)
|
||||||
|
|
||||||
|
# Panther Dash running
|
||||||
|
if options.panther_dash:
|
||||||
|
rom.write_int32(0x69C8C4, 0x0C0FF77E) # JAL 0x803FDDF8
|
||||||
|
rom.write_int32(0x6AA228, 0x0C0FF77E) # JAL 0x803FDDF8
|
||||||
|
rom.write_int32s(0x69C86C, [0x0C0FF78E, # JAL 0x803FDE38
|
||||||
|
0x3C01803E]) # LUI AT, 0x803E
|
||||||
|
rom.write_int32s(0x6AA1D0, [0x0C0FF78E, # JAL 0x803FDE38
|
||||||
|
0x3C01803E]) # LUI AT, 0x803E
|
||||||
|
rom.write_int32(0x69D37C, 0x0C0FF79E) # JAL 0x803FDE78
|
||||||
|
rom.write_int32(0x6AACE0, 0x0C0FF79E) # JAL 0x803FDE78
|
||||||
|
rom.write_int32s(0xBFDDF8, patches.panther_dash)
|
||||||
|
# Jump prevention
|
||||||
|
if options.panther_dash == PantherDash.option_jumpless:
|
||||||
|
rom.write_int32(0xBFDE2C, 0x080FF7BB) # J 0x803FDEEC
|
||||||
|
rom.write_int32(0xBFD044, 0x080FF7B1) # J 0x803FDEC4
|
||||||
|
rom.write_int32s(0x69B630, [0x0C0FF7C6, # JAL 0x803FDF18
|
||||||
|
0x8CCD0000]) # LW T5, 0x0000 (A2)
|
||||||
|
rom.write_int32s(0x6A8EC0, [0x0C0FF7C6, # JAL 0x803FDF18
|
||||||
|
0x8CCC0000]) # LW T4, 0x0000 (A2)
|
||||||
|
# Fun fact: KCEK put separate code to handle coyote time jumping
|
||||||
|
rom.write_int32s(0x69910C, [0x0C0FF7C6, # JAL 0x803FDF18
|
||||||
|
0x8C4E0000]) # LW T6, 0x0000 (V0)
|
||||||
|
rom.write_int32s(0x6A6718, [0x0C0FF7C6, # JAL 0x803FDF18
|
||||||
|
0x8C4E0000]) # LW T6, 0x0000 (V0)
|
||||||
|
rom.write_int32s(0xBFDEC4, patches.panther_jump_preventer)
|
||||||
|
|
||||||
|
# Everything related to Big Toss.
|
||||||
|
if options.big_toss:
|
||||||
|
rom.write_int32s(0x27E90, [0x0C0FFA38, # JAL 0x803FE8E0
|
||||||
|
0xAFB80074]) # SW T8, 0x0074 (SP)
|
||||||
|
rom.write_int32(0x26F54, 0x0C0FFA4D) # JAL 0x803FE934
|
||||||
|
rom.write_int32s(0xBFE8E0, patches.big_tosser)
|
||||||
|
|
||||||
|
# Write all the new randomized bytes.
|
||||||
|
for offset, item_id in offset_data.items():
|
||||||
|
if item_id <= 0xFF:
|
||||||
|
rom.write_byte(offset, item_id)
|
||||||
|
elif item_id <= 0xFFFF:
|
||||||
|
rom.write_int16(offset, item_id)
|
||||||
|
elif item_id <= 0xFFFFFF:
|
||||||
|
rom.write_int24(offset, item_id)
|
||||||
|
else:
|
||||||
|
rom.write_int32(offset, item_id)
|
||||||
|
|
||||||
|
# Write the secondary name the client will use to distinguish a vanilla ROM from an AP one.
|
||||||
|
rom.write_bytes(0xBFBFD0, "ARCHIPELAGO1".encode("utf-8"))
|
||||||
|
# Write the slot authentication
|
||||||
|
rom.write_bytes(0xBFBFE0, world.auth)
|
||||||
|
|
||||||
|
# Write the specified window colors
|
||||||
|
rom.write_byte(0xAEC23, options.window_color_r.value << 4)
|
||||||
|
rom.write_byte(0xAEC33, options.window_color_g.value << 4)
|
||||||
|
rom.write_byte(0xAEC47, options.window_color_b.value << 4)
|
||||||
|
rom.write_byte(0xAEC43, options.window_color_a.value << 4)
|
||||||
|
|
||||||
|
# Write the item/player names for other game items
|
||||||
|
for loc in active_locations:
|
||||||
|
if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == player:
|
||||||
|
continue
|
||||||
|
if len(loc.item.name) > 67:
|
||||||
|
item_name = loc.item.name[0x00:0x68]
|
||||||
|
else:
|
||||||
|
item_name = loc.item.name
|
||||||
|
inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF))
|
||||||
|
wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + multiworld.get_player_name(loc.item.player), 96)
|
||||||
|
rom.write_bytes(inject_address, get_item_text_color(loc) + cv64_string_to_bytearray(wrapped_name))
|
||||||
|
rom.write_byte(inject_address + 255, num_lines)
|
||||||
|
|
||||||
|
# Everything relating to loading the other game items text
|
||||||
|
rom.write_int32(0xA8D8C, 0x080FF88F) # J 0x803FE23C
|
||||||
|
rom.write_int32(0xBEA98, 0x0C0FF8B4) # JAL 0x803FE2D0
|
||||||
|
rom.write_int32(0xBEAB0, 0x0C0FF8BD) # JAL 0x803FE2F8
|
||||||
|
rom.write_int32(0xBEACC, 0x0C0FF8C5) # JAL 0x803FE314
|
||||||
|
rom.write_int32s(0xBFE23C, patches.multiworld_item_name_loader)
|
||||||
|
rom.write_bytes(0x10F188, [0x00 for _ in range(264)])
|
||||||
|
rom.write_bytes(0x10F298, [0x00 for _ in range(264)])
|
||||||
|
|
||||||
|
# When the game normally JALs to the item prepare textbox function after the player picks up an item, set the
|
||||||
|
# "no receiving" timer to ensure the item textbox doesn't freak out if you pick something up while there's a queue
|
||||||
|
# of unreceived items.
|
||||||
|
rom.write_int32(0xA8D94, 0x0C0FF9F0) # JAL 0x803FE7C0
|
||||||
|
rom.write_int32s(0xBFE7C0, [0x3C088039, # LUI T0, 0x8039
|
||||||
|
0x24090020, # ADDIU T1, R0, 0x0020
|
||||||
|
0x0804EDCE, # J 0x8013B738
|
||||||
|
0xA1099BE0]) # SB T1, 0x9BE0 (T0)
|
||||||
|
|
||||||
|
|
||||||
|
class CV64DeltaPatch(APDeltaPatch):
|
||||||
|
hash = CV64US10HASH
|
||||||
|
patch_file_ending: str = ".apcv64"
|
||||||
|
result_file_ending: str = ".z64"
|
||||||
|
|
||||||
|
game = "Castlevania 64"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_source_data(cls) -> bytes:
|
||||||
|
return get_base_rom_bytes()
|
||||||
|
|
||||||
|
def patch(self, target: str):
|
||||||
|
super().patch(target)
|
||||||
|
rom = LocalRom(target)
|
||||||
|
|
||||||
|
# Extract the item models file, decompress it, append the AP icons, compress it back, re-insert it.
|
||||||
|
items_file = lzkn64.decompress_buffer(rom.read_bytes(0x9C5310, 0x3D28))
|
||||||
|
compressed_file = lzkn64.compress_buffer(items_file[0:0x69B6] + pkgutil.get_data(__name__, "data/ap_icons.bin"))
|
||||||
|
rom.write_bytes(0xBB2D88, compressed_file)
|
||||||
|
# Update the items' Nisitenma-Ichigo table entry to point to the new file's start and end addresses in the ROM.
|
||||||
|
rom.write_int32s(0x95F04, [0x80BB2D88, 0x00BB2D88 + len(compressed_file)])
|
||||||
|
# Update the items' decompressed file size tables with the new file's decompressed file size.
|
||||||
|
rom.write_int16(0x95706, 0x7BF0)
|
||||||
|
rom.write_int16(0x104CCE, 0x7BF0)
|
||||||
|
# Update the Wooden Stake and Roses' item appearance settings table to point to the Archipelago item graphics.
|
||||||
|
rom.write_int16(0xEE5BA, 0x7B38)
|
||||||
|
rom.write_int16(0xEE5CA, 0x7280)
|
||||||
|
# Change the items' sizes. The progression one will be larger than the non-progression one.
|
||||||
|
rom.write_int32(0xEE5BC, 0x3FF00000)
|
||||||
|
rom.write_int32(0xEE5CC, 0x3FA00000)
|
||||||
|
rom.write_to_file(target)
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||||
|
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||||
|
if not base_rom_bytes:
|
||||||
|
file_name = get_base_rom_path(file_name)
|
||||||
|
base_rom_bytes = bytes(open(file_name, "rb").read())
|
||||||
|
|
||||||
|
basemd5 = hashlib.md5()
|
||||||
|
basemd5.update(base_rom_bytes)
|
||||||
|
if CV64US10HASH != basemd5.hexdigest():
|
||||||
|
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0."
|
||||||
|
"Get the correct game and version, then dump it.")
|
||||||
|
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||||
|
return base_rom_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_rom_path(file_name: str = "") -> str:
|
||||||
|
if not file_name:
|
||||||
|
file_name = get_settings()["cv64_options"]["rom_file"]
|
||||||
|
if not os.path.exists(file_name):
|
||||||
|
file_name = Utils.user_path(file_name)
|
||||||
|
return file_name
|
|
@ -0,0 +1,103 @@
|
||||||
|
from typing import Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState
|
||||||
|
from worlds.generic.Rules import allow_self_locking_items, CollectionRule
|
||||||
|
from .options import DraculasCondition
|
||||||
|
from .entrances import get_entrance_info
|
||||||
|
from .data import iname, rname
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import CV64World
|
||||||
|
|
||||||
|
|
||||||
|
class CV64Rules:
|
||||||
|
player: int
|
||||||
|
world: "CV64World"
|
||||||
|
rules: Dict[str, CollectionRule]
|
||||||
|
s1s_per_warp: int
|
||||||
|
required_s2s: int
|
||||||
|
drac_condition: int
|
||||||
|
|
||||||
|
def __init__(self, world: "CV64World") -> None:
|
||||||
|
self.player = world.player
|
||||||
|
self.world = world
|
||||||
|
self.s1s_per_warp = world.s1s_per_warp
|
||||||
|
self.required_s2s = world.required_s2s
|
||||||
|
self.drac_condition = world.drac_condition
|
||||||
|
|
||||||
|
self.rules = {
|
||||||
|
iname.left_tower_key: lambda state: state.has(iname.left_tower_key, self.player),
|
||||||
|
iname.storeroom_key: lambda state: state.has(iname.storeroom_key, self.player),
|
||||||
|
iname.archives_key: lambda state: state.has(iname.archives_key, self.player),
|
||||||
|
iname.garden_key: lambda state: state.has(iname.garden_key, self.player),
|
||||||
|
iname.copper_key: lambda state: state.has(iname.copper_key, self.player),
|
||||||
|
iname.chamber_key: lambda state: state.has(iname.chamber_key, self.player),
|
||||||
|
"Bomb 1": lambda state: state.has_all({iname.magical_nitro, iname.mandragora}, self.player),
|
||||||
|
"Bomb 2": lambda state: state.has(iname.magical_nitro, self.player, 2)
|
||||||
|
and state.has(iname.mandragora, self.player, 2),
|
||||||
|
iname.execution_key: lambda state: state.has(iname.execution_key, self.player),
|
||||||
|
iname.science_key1: lambda state: state.has(iname.science_key1, self.player),
|
||||||
|
iname.science_key2: lambda state: state.has(iname.science_key2, self.player),
|
||||||
|
iname.science_key3: lambda state: state.has(iname.science_key3, self.player),
|
||||||
|
iname.clocktower_key1: lambda state: state.has(iname.clocktower_key1, self.player),
|
||||||
|
iname.clocktower_key2: lambda state: state.has(iname.clocktower_key2, self.player),
|
||||||
|
iname.clocktower_key3: lambda state: state.has(iname.clocktower_key3, self.player),
|
||||||
|
"Dracula": self.can_enter_dracs_chamber
|
||||||
|
}
|
||||||
|
|
||||||
|
def can_enter_dracs_chamber(self, state: CollectionState) -> bool:
|
||||||
|
drac_object_name = None
|
||||||
|
if self.drac_condition == DraculasCondition.option_crystal:
|
||||||
|
drac_object_name = "Crystal"
|
||||||
|
elif self.drac_condition == DraculasCondition.option_bosses:
|
||||||
|
drac_object_name = "Trophy"
|
||||||
|
elif self.drac_condition == DraculasCondition.option_specials:
|
||||||
|
drac_object_name = "Special2"
|
||||||
|
|
||||||
|
if drac_object_name is not None:
|
||||||
|
return state.has(drac_object_name, self.player, self.required_s2s)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_cv64_rules(self) -> None:
|
||||||
|
multiworld = self.world.multiworld
|
||||||
|
|
||||||
|
for region in multiworld.get_regions(self.player):
|
||||||
|
# Set each entrance's rule if it should have one.
|
||||||
|
# Warp entrances have their own special handling.
|
||||||
|
for entrance in region.entrances:
|
||||||
|
if entrance.parent_region.name == "Menu":
|
||||||
|
if entrance.name.startswith("Warp "):
|
||||||
|
entrance.access_rule = lambda state, warp_num=int(entrance.name[5]): \
|
||||||
|
state.has(iname.special_one, self.player, self.s1s_per_warp * warp_num)
|
||||||
|
else:
|
||||||
|
ent_rule = get_entrance_info(entrance.name, "rule")
|
||||||
|
if ent_rule in self.rules:
|
||||||
|
entrance.access_rule = self.rules[ent_rule]
|
||||||
|
|
||||||
|
multiworld.completion_condition[self.player] = lambda state: state.has(iname.victory, self.player)
|
||||||
|
if self.world.options.accessibility: # not locations accessibility
|
||||||
|
self.set_self_locking_items()
|
||||||
|
|
||||||
|
def set_self_locking_items(self) -> None:
|
||||||
|
multiworld = self.world.multiworld
|
||||||
|
|
||||||
|
# Do the regions that we know for a fact always exist, and we always do no matter what.
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.villa_archives, self.player), iname.archives_key)
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.cc_torture_chamber, self.player), iname.chamber_key)
|
||||||
|
|
||||||
|
# Add this region if the world doesn't have the Villa Storeroom warp entrance.
|
||||||
|
if "Villa" not in self.world.active_warp_list[1:]:
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.villa_storeroom, self.player), iname.storeroom_key)
|
||||||
|
|
||||||
|
# Add this region if Hard Logic is on and Multi Hit Breakables are off.
|
||||||
|
if self.world.options.hard_logic and not self.world.options.multi_hit_breakables:
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.cw_ltower, self.player), iname.left_tower_key)
|
||||||
|
|
||||||
|
# Add these regions if Tower of Science is in the world.
|
||||||
|
if "Tower of Science" in self.world.active_stage_exits:
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.tosci_three_doors, self.player), iname.science_key1)
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.tosci_key3, self.player), iname.science_key3)
|
||||||
|
|
||||||
|
# Add this region if Tower of Execution is in the world and Hard Logic is not on.
|
||||||
|
if "Tower of Execution" in self.world.active_stage_exits and self.world.options.hard_logic:
|
||||||
|
allow_self_locking_items(multiworld.get_region(rname.toe_ledge, self.player), iname.execution_key)
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Written by Moisés
|
||||||
|
#include "include/game/module.h"
|
||||||
|
#include "include/game/math.h"
|
||||||
|
#include "cv64.h"
|
||||||
|
|
||||||
|
extern vec3f player_pos;
|
||||||
|
extern vec3s player_angle; // player_angle.y = Player's facing angle (yaw)
|
||||||
|
extern f32 player_height_with_respect_of_floor; // Stored negative in-game
|
||||||
|
|
||||||
|
#define SHT_MAX 32767.0f
|
||||||
|
#define SHT_MINV (1.0f / SHT_MAX)
|
||||||
|
|
||||||
|
void spawn_item_behind_player(s32 item) {
|
||||||
|
interactuablesModule* pickable_item = NULL;
|
||||||
|
const f32 spawnDistance = 8.0f;
|
||||||
|
vec3f player_backwards_dir;
|
||||||
|
|
||||||
|
pickable_item = (interactuablesModule*)module_createAndSetChild(moduleList_findFirstModuleByID(ACTOR_CREATOR), ACTOR_ITEM);
|
||||||
|
if (pickable_item != NULL) {
|
||||||
|
// Convert facing angle to a vec3f
|
||||||
|
// SHT_MINV needs to be negative here for the item to be spawned properly on the character's back
|
||||||
|
player_backwards_dir.x = coss(-player_angle.y) * -SHT_MINV;
|
||||||
|
player_backwards_dir.z = sins(-player_angle.y) * -SHT_MINV;
|
||||||
|
// Multiply facing vector with distance away from the player
|
||||||
|
vec3f_multiplyScalar(&player_backwards_dir, &player_backwards_dir, spawnDistance);
|
||||||
|
// Assign the position of the item relative to the player's current position.
|
||||||
|
vec3f_add(&pickable_item->position, &player_pos, &player_backwards_dir);
|
||||||
|
// The Y position of the item will be the same as the floor right under the player
|
||||||
|
// The player's height with respect of the flower under them is already stored negative in-game,
|
||||||
|
// so no need to substract
|
||||||
|
pickable_item->position.y = player_pos.y + 5.0f;
|
||||||
|
pickable_item->height = pickable_item->position.y;
|
||||||
|
|
||||||
|
// Assign item ID
|
||||||
|
pickable_item->item_ID = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const f32 droppingAccel = 0.05f;
|
||||||
|
const f32 maxDroppingSpeed = 1.5f;
|
||||||
|
f32 droppingSpeed = 0.0f;
|
||||||
|
f32 droppingTargetYPos = 0.0f;
|
||||||
|
u8 dropItemCalcFuncCalled = FALSE;
|
||||||
|
|
||||||
|
s32 drop_item_calc(interactuablesModule* pickable_item) {
|
||||||
|
if (dropItemCalcFuncCalled == FALSE) {
|
||||||
|
droppingTargetYPos = player_pos.y + player_height_with_respect_of_floor + 1.0f;
|
||||||
|
if (pickable_item->item_ID == CROSS || pickable_item->item_ID == AXE ||
|
||||||
|
pickable_item->item_ID == CROSS__VANISH || pickable_item->item_ID == AXE__VANISH) {
|
||||||
|
droppingTargetYPos += 3.0f;
|
||||||
|
}
|
||||||
|
dropItemCalcFuncCalled = TRUE;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (pickable_item->position.y <= droppingTargetYPos) {
|
||||||
|
droppingSpeed = 0.0f;
|
||||||
|
dropItemCalcFuncCalled = FALSE;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (droppingSpeed < maxDroppingSpeed) {
|
||||||
|
droppingSpeed += droppingAccel;
|
||||||
|
}
|
||||||
|
pickable_item->position.y -= droppingSpeed;
|
||||||
|
pickable_item->height = pickable_item->position.y;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Written by Moisés.
|
||||||
|
// NOTE: This is an earlier version to-be-replaced.
|
||||||
|
#include <memory.h>
|
||||||
|
#include <textbox.h>
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
// https://decomp.me/scratch/9H1Uy
|
||||||
|
u32 convertUTF8StringToUTF16(char* src, u16* buffer) {
|
||||||
|
u32 string_length = 0;
|
||||||
|
|
||||||
|
// If the source string starts with a null char (0), we assume the string empty.
|
||||||
|
if (*src != 0) {
|
||||||
|
// Copy the char from the source string into the bufferination.
|
||||||
|
// Then advance to the next char until we find the null char (0).
|
||||||
|
do {
|
||||||
|
*buffer = *src;
|
||||||
|
src++;
|
||||||
|
buffer++;
|
||||||
|
string_length++;
|
||||||
|
} while (*src != 0);
|
||||||
|
}
|
||||||
|
// Make sure to add the null char at the end of the bufferination string,
|
||||||
|
// and then return the length of the string.
|
||||||
|
*buffer = 0;
|
||||||
|
return string_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin printing ASCII text stored in a char*
|
||||||
|
textbox* print_text(const char* message, const s16 X_pos, const s16 Y_pos, const u8 number_of_lines, const s16 textbox_width, const u32 txtbox_flags, const void* module) {
|
||||||
|
textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
|
||||||
|
void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
|
||||||
|
void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
|
||||||
|
void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
|
||||||
|
u16* (*ptr_convertUTF16ToCustomTextFormat)(u16*) = convertUTF16ToCustomTextFormat;
|
||||||
|
void* (*ptr_malloc)(s32, u32) = malloc;
|
||||||
|
|
||||||
|
textbox* txtbox = NULL;
|
||||||
|
|
||||||
|
// Allocate memory for the text buffer
|
||||||
|
u16* text_buffer = (u16*) ptr_malloc(0, 100);
|
||||||
|
|
||||||
|
// Create the textbox data structure
|
||||||
|
if (module != NULL) {
|
||||||
|
txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txtbox != NULL && text_buffer != NULL && message != NULL) {
|
||||||
|
// Set text position and dimensions
|
||||||
|
ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
|
||||||
|
ptr_textbox_setDimensions(txtbox, number_of_lines, textbox_width, 0, 0);
|
||||||
|
|
||||||
|
// Convert the ASCII message to the CV64 custom format
|
||||||
|
convertUTF8StringToUTF16(message, text_buffer);
|
||||||
|
ptr_convertUTF16ToCustomTextFormat(text_buffer);
|
||||||
|
|
||||||
|
// Set the text buffer pointer to the textbox data structure
|
||||||
|
ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
|
||||||
|
}
|
||||||
|
// We return the textbox so that we can modify its properties once it begins printing
|
||||||
|
// (say to show, hide the text)
|
||||||
|
return txtbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin printing signed integer
|
||||||
|
textbox* print_number(const s32 number, u16* text_buffer, const s16 X_pos, const s16 Y_pos, const u8 number_of_digits, const u32 txtbox_flags, const u32 additional_text_flag, const void* module) {
|
||||||
|
textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
|
||||||
|
void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
|
||||||
|
void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
|
||||||
|
void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
|
||||||
|
void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
|
||||||
|
|
||||||
|
textbox* txtbox = NULL;
|
||||||
|
|
||||||
|
// Create the textbox data structure
|
||||||
|
if (module != NULL) {
|
||||||
|
txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txtbox != NULL && text_buffer != NULL) {
|
||||||
|
// Set text position and dimensions
|
||||||
|
ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
|
||||||
|
ptr_textbox_setDimensions(txtbox, 1, 100, 0, 0);
|
||||||
|
|
||||||
|
// Convert the number to the CV64 custom format
|
||||||
|
ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
|
||||||
|
|
||||||
|
// Set the text buffer pointer to the textbox data structure
|
||||||
|
ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
|
||||||
|
}
|
||||||
|
// We return the textbox so that we can modify its properties once it begins printing
|
||||||
|
// (say to show, hide the text)
|
||||||
|
return txtbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the value of a number that began printing after calling "print_number()"
|
||||||
|
void update_printed_number(textbox* txtbox, const s32 number, u16* text_buffer, const u8 number_of_digits, const u32 additional_text_flag) {
|
||||||
|
void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
|
||||||
|
|
||||||
|
if (text_buffer != NULL) {
|
||||||
|
ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
|
||||||
|
txtbox->flags |= 0x1000000; // Needed to make sure the number updates properly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_text(textbox* txtbox, const u8 display_textbox) {
|
||||||
|
if (txtbox != NULL) {
|
||||||
|
if (display_textbox == TRUE) {
|
||||||
|
// Show text
|
||||||
|
txtbox->flags &= ~HIDE_TEXTBOX;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Hide text
|
||||||
|
txtbox->flags |= HIDE_TEXTBOX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Written by Moisés
|
||||||
|
#include "print.h"
|
||||||
|
#include <textbox.h>
|
||||||
|
#include <memory.h>
|
||||||
|
|
||||||
|
#define counter_X_pos 30
|
||||||
|
#define counter_Y_pos 40
|
||||||
|
#define counter_number_of_digits 2
|
||||||
|
#define GOLD_JEWEL_FONT 0x14
|
||||||
|
|
||||||
|
extern u8 bytes[13];
|
||||||
|
|
||||||
|
u16* number_text_buffer = NULL;
|
||||||
|
textbox* txtbox = NULL;
|
||||||
|
|
||||||
|
void begin_print() {
|
||||||
|
// Allocate memory for the number text
|
||||||
|
number_text_buffer = (u16*) malloc(0, 12);
|
||||||
|
|
||||||
|
// Assuming that 0x80342814 = HUD Module
|
||||||
|
txtbox = print_number(0, number_text_buffer, counter_X_pos, counter_Y_pos, counter_number_of_digits, 0x08600000, GOLD_JEWEL_FONT, (void*) 0x80342814);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_print(u8 i) {
|
||||||
|
update_printed_number(txtbox, (s32) bytes[i], number_text_buffer, counter_number_of_digits, GOLD_JEWEL_FONT);
|
||||||
|
}
|
|
@ -0,0 +1,490 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .data import rname
|
||||||
|
from .regions import get_region_info
|
||||||
|
from .locations import get_location_info
|
||||||
|
from .options import WarpOrder
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import CV64World
|
||||||
|
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "start region" = The Region that the start of the stage is in. Used for connecting the previous stage's end and
|
||||||
|
# alternate end (if it exists) Entrances to the start of the next one.
|
||||||
|
# "start map id" = The map ID that the start of the stage is in.
|
||||||
|
# "start spawn id" = The player spawn location ID for the start of the stage. This and "start map id" are both written
|
||||||
|
# to the previous stage's end loading zone to make it send the player to the next stage in the
|
||||||
|
# world's determined stage order.
|
||||||
|
# "mid region" = The Region that the stage's middle warp point is in. Used for connecting the warp Entrances after the
|
||||||
|
# starting stage to where they should be connecting to.
|
||||||
|
# "mid map id" = The map ID that the stage's middle warp point is in.
|
||||||
|
# "mid spawn id" = The player spawn location ID for the stage's middle warp point. This and "mid map id" are both
|
||||||
|
# written to the warp menu code to make it send the player to where it should be sending them.
|
||||||
|
# "end region" = The Region that the end of the stage is in. Used for connecting the next stage's beginning Entrance
|
||||||
|
# (if it exists) to the end of the previous one.
|
||||||
|
# "end map id" = The map ID that the end of the stage is in.
|
||||||
|
# "end spawn id" = The player spawn location ID for the end of the stage. This and "end map id" are both written to the
|
||||||
|
# next stage's beginning loading zone (if it exists) to make it send the player to the previous stage
|
||||||
|
# in the world's determined stage order.
|
||||||
|
# startzone map offset = The offset in the ROM to overwrite to change where the start of the stage leads.
|
||||||
|
# startzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the previous map the
|
||||||
|
# start of the stage puts the player at.
|
||||||
|
# endzone map offset = The offset in the ROM to overwrite to change where the end of the stage leads.
|
||||||
|
# endzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the end of
|
||||||
|
# the stage puts the player at.
|
||||||
|
# altzone map offset = The offset in the ROM to overwrite to change where the alternate end of the stage leads
|
||||||
|
# (if it exists).
|
||||||
|
# altzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the alternate
|
||||||
|
# end of the stage puts the player at.
|
||||||
|
# character = What character that stage is exclusively meant for normally. Used in determining what stages to leave out
|
||||||
|
# depending on what character stage setting was chosen in the player options.
|
||||||
|
# save number offsets = The offsets to overwrite to change what stage number is displayed on the save file when saving
|
||||||
|
# at the stage's White Jewels.
|
||||||
|
# regions = All Regions that make up the stage. If the stage is in the world's active stages, its Regions and their
|
||||||
|
# corresponding Locations and Entrances will all be created.
|
||||||
|
stage_info = {
|
||||||
|
"Forest of Silence": {
|
||||||
|
"start region": rname.forest_start, "start map id": 0x00, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.forest_mid, "mid map id": 0x00, "mid spawn id": 0x04,
|
||||||
|
"end region": rname.forest_end, "end map id": 0x00, "end spawn id": 0x01,
|
||||||
|
"endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B,
|
||||||
|
"save number offsets": [0x1049C5, 0x1049CD, 0x1049D5],
|
||||||
|
"regions": [rname.forest_start,
|
||||||
|
rname.forest_mid,
|
||||||
|
rname.forest_end]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Castle Wall": {
|
||||||
|
"start region": rname.cw_start, "start map id": 0x02, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.cw_start, "mid map id": 0x02, "mid spawn id": 0x07,
|
||||||
|
"end region": rname.cw_exit, "end map id": 0x02, "end spawn id": 0x10,
|
||||||
|
"endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61,
|
||||||
|
"save number offsets": [0x1049DD, 0x1049E5, 0x1049ED],
|
||||||
|
"regions": [rname.cw_start,
|
||||||
|
rname.cw_exit,
|
||||||
|
rname.cw_ltower]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Villa": {
|
||||||
|
"start region": rname.villa_start, "start map id": 0x03, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.villa_storeroom, "mid map id": 0x05, "mid spawn id": 0x04,
|
||||||
|
"end region": rname.villa_crypt, "end map id": 0x1A, "end spawn id": 0x03,
|
||||||
|
"endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81,
|
||||||
|
"altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81,
|
||||||
|
"save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D],
|
||||||
|
"regions": [rname.villa_start,
|
||||||
|
rname.villa_main,
|
||||||
|
rname.villa_storeroom,
|
||||||
|
rname.villa_archives,
|
||||||
|
rname.villa_maze,
|
||||||
|
rname.villa_servants,
|
||||||
|
rname.villa_crypt]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Tunnel": {
|
||||||
|
"start region": rname.tunnel_start, "start map id": 0x07, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.tunnel_end, "mid map id": 0x07, "mid spawn id": 0x03,
|
||||||
|
"end region": rname.tunnel_end, "end map id": 0x07, "end spawn id": 0x11,
|
||||||
|
"endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt",
|
||||||
|
"save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D],
|
||||||
|
"regions": [rname.tunnel_start,
|
||||||
|
rname.tunnel_end]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Underground Waterway": {
|
||||||
|
"start region": rname.uw_main, "start map id": 0x08, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.uw_main, "mid map id": 0x08, "mid spawn id": 0x03,
|
||||||
|
"end region": rname.uw_end, "end map id": 0x08, "end spawn id": 0x01,
|
||||||
|
"endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie",
|
||||||
|
"save number offsets": [0x104A35, 0x104A3D],
|
||||||
|
"regions": [rname.uw_main,
|
||||||
|
rname.uw_end]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Castle Center": {
|
||||||
|
"start region": rname.cc_main, "start map id": 0x19, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.cc_main, "mid map id": 0x0E, "mid spawn id": 0x03,
|
||||||
|
"end region": rname.cc_elev_top, "end map id": 0x0F, "end spawn id": 0x02,
|
||||||
|
"endzone map offset": 0x109CB7, "endzone spawn offset": 0x109CB9,
|
||||||
|
"altzone map offset": 0x109CCF, "altzone spawn offset": 0x109CD1,
|
||||||
|
"save number offsets": [0x104A45, 0x104A4D, 0x104A55, 0x104A5D, 0x104A65, 0x104A6D, 0x104A75],
|
||||||
|
"regions": [rname.cc_main,
|
||||||
|
rname.cc_torture_chamber,
|
||||||
|
rname.cc_library,
|
||||||
|
rname.cc_crystal,
|
||||||
|
rname.cc_elev_top]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Duel Tower": {
|
||||||
|
"start region": rname.dt_main, "start map id": 0x13, "start spawn id": 0x00,
|
||||||
|
"startzone map offset": 0x109DA7, "startzone spawn offset": 0x109DA9,
|
||||||
|
"mid region": rname.dt_main, "mid map id": 0x13, "mid spawn id": 0x15,
|
||||||
|
"end region": rname.dt_main, "end map id": 0x13, "end spawn id": 0x01,
|
||||||
|
"endzone map offset": 0x109D8F, "endzone spawn offset": 0x109D91, "character": "Reinhardt",
|
||||||
|
"save number offsets": [0x104ACD],
|
||||||
|
"regions": [rname.dt_main]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Tower of Execution": {
|
||||||
|
"start region": rname.toe_main, "start map id": 0x10, "start spawn id": 0x00,
|
||||||
|
"startzone map offset": 0x109D17, "startzone spawn offset": 0x109D19,
|
||||||
|
"mid region": rname.toe_main, "mid map id": 0x10, "mid spawn id": 0x02,
|
||||||
|
"end region": rname.toe_main, "end map id": 0x10, "end spawn id": 0x12,
|
||||||
|
"endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt",
|
||||||
|
"save number offsets": [0x104A7D, 0x104A85],
|
||||||
|
"regions": [rname.toe_main,
|
||||||
|
rname.toe_ledge]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Tower of Science": {
|
||||||
|
"start region": rname.tosci_start, "start map id": 0x12, "start spawn id": 0x00,
|
||||||
|
"startzone map offset": 0x109D77, "startzone spawn offset": 0x109D79,
|
||||||
|
"mid region": rname.tosci_conveyors, "mid map id": 0x12, "mid spawn id": 0x03,
|
||||||
|
"end region": rname.tosci_conveyors, "end map id": 0x12, "end spawn id": 0x04,
|
||||||
|
"endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie",
|
||||||
|
"save number offsets": [0x104A95, 0x104A9D, 0x104AA5],
|
||||||
|
"regions": [rname.tosci_start,
|
||||||
|
rname.tosci_three_doors,
|
||||||
|
rname.tosci_conveyors,
|
||||||
|
rname.tosci_key3]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Tower of Sorcery": {
|
||||||
|
"start region": rname.tosor_main, "start map id": 0x11, "start spawn id": 0x00,
|
||||||
|
"startzone map offset": 0x109D47, "startzone spawn offset": 0x109D49,
|
||||||
|
"mid region": rname.tosor_main, "mid map id": 0x11, "mid spawn id": 0x01,
|
||||||
|
"end region": rname.tosor_main, "end map id": 0x11, "end spawn id": 0x13,
|
||||||
|
"endzone map offset": 0x109D2F, "endzone spawn offset": 0x109D31, "character": "Carrie",
|
||||||
|
"save number offsets": [0x104A8D],
|
||||||
|
"regions": [rname.tosor_main]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Room of Clocks": {
|
||||||
|
"start region": rname.roc_main, "start map id": 0x1B, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.roc_main, "mid map id": 0x1B, "mid spawn id": 0x02,
|
||||||
|
"end region": rname.roc_main, "end map id": 0x1B, "end spawn id": 0x14,
|
||||||
|
"endzone map offset": 0x109EAF, "endzone spawn offset": 0x109EB1,
|
||||||
|
"save number offsets": [0x104AC5],
|
||||||
|
"regions": [rname.roc_main]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Clock Tower": {
|
||||||
|
"start region": rname.ct_start, "start map id": 0x17, "start spawn id": 0x00,
|
||||||
|
"mid region": rname.ct_middle, "mid map id": 0x17, "mid spawn id": 0x02,
|
||||||
|
"end region": rname.ct_end, "end map id": 0x17, "end spawn id": 0x03,
|
||||||
|
"endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39,
|
||||||
|
"save number offsets": [0x104AB5, 0x104ABD],
|
||||||
|
"regions": [rname.ct_start,
|
||||||
|
rname.ct_middle,
|
||||||
|
rname.ct_end]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Castle Keep": {
|
||||||
|
"start region": rname.ck_main, "start map id": 0x14, "start spawn id": 0x02,
|
||||||
|
"mid region": rname.ck_main, "mid map id": 0x14, "mid spawn id": 0x03,
|
||||||
|
"end region": rname.ck_drac_chamber,
|
||||||
|
"save number offsets": [0x104AAD],
|
||||||
|
"regions": [rname.ck_main]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vanilla_stage_order = ("Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
|
||||||
|
"Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
|
||||||
|
"Clock Tower", "Castle Keep")
|
||||||
|
|
||||||
|
# # # KEY # # #
|
||||||
|
# "prev" = The previous stage in the line.
|
||||||
|
# "next" = The next stage in the line.
|
||||||
|
# "alt" = The alternate next stage in the line (if one exists).
|
||||||
|
# "position" = The stage's number in the order of stages.
|
||||||
|
# "path" = Character indicating whether the stage is on the main path or an alternate path, similar to Rondo of Blood.
|
||||||
|
# Used in writing the randomized stage order to the spoiler.
|
||||||
|
vanilla_stage_exits = {rname.forest_of_silence: {"prev": None, "next": rname.castle_wall,
|
||||||
|
"alt": None, "position": 1, "path": " "},
|
||||||
|
rname.castle_wall: {"prev": None, "next": rname.villa,
|
||||||
|
"alt": None, "position": 2, "path": " "},
|
||||||
|
rname.villa: {"prev": None, "next": rname.tunnel,
|
||||||
|
"alt": rname.underground_waterway, "position": 3, "path": " "},
|
||||||
|
rname.tunnel: {"prev": None, "next": rname.castle_center,
|
||||||
|
"alt": None, "position": 4, "path": " "},
|
||||||
|
rname.underground_waterway: {"prev": None, "next": rname.castle_center,
|
||||||
|
"alt": None, "position": 4, "path": "'"},
|
||||||
|
rname.castle_center: {"prev": None, "next": rname.duel_tower,
|
||||||
|
"alt": rname.tower_of_science, "position": 5, "path": " "},
|
||||||
|
rname.duel_tower: {"prev": rname.castle_center, "next": rname.tower_of_execution,
|
||||||
|
"alt": None, "position": 6, "path": " "},
|
||||||
|
rname.tower_of_execution: {"prev": rname.duel_tower, "next": rname.room_of_clocks,
|
||||||
|
"alt": None, "position": 7, "path": " "},
|
||||||
|
rname.tower_of_science: {"prev": rname.castle_center, "next": rname.tower_of_sorcery,
|
||||||
|
"alt": None, "position": 6, "path": "'"},
|
||||||
|
rname.tower_of_sorcery: {"prev": rname.tower_of_science, "next": rname.room_of_clocks,
|
||||||
|
"alt": None, "position": 7, "path": "'"},
|
||||||
|
rname.room_of_clocks: {"prev": None, "next": rname.clock_tower,
|
||||||
|
"alt": None, "position": 8, "path": " "},
|
||||||
|
rname.clock_tower: {"prev": None, "next": rname.castle_keep,
|
||||||
|
"alt": None, "position": 9, "path": " "},
|
||||||
|
rname.castle_keep: {"prev": None, "next": None,
|
||||||
|
"alt": None, "position": 10, "path": " "}}
|
||||||
|
|
||||||
|
|
||||||
|
def get_stage_info(stage: str, info: str) -> Union[str, int, Union[List[int], List[str]], None]:
|
||||||
|
return stage_info[stage].get(info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_locations_from_stage(stage: str) -> List[str]:
|
||||||
|
overall_locations = []
|
||||||
|
for region in get_stage_info(stage, "regions"):
|
||||||
|
stage_locations = get_region_info(region, "locations")
|
||||||
|
if stage_locations is not None:
|
||||||
|
overall_locations += stage_locations
|
||||||
|
|
||||||
|
final_locations = []
|
||||||
|
for loc in overall_locations:
|
||||||
|
if get_location_info(loc, "code") is not None:
|
||||||
|
final_locations.append(loc)
|
||||||
|
return final_locations
|
||||||
|
|
||||||
|
|
||||||
|
def verify_character_stage(world: "CV64World", stage: str) -> bool:
|
||||||
|
# Verify a character stage is in the world if the given stage is a character stage.
|
||||||
|
stage_char = get_stage_info(stage, "character")
|
||||||
|
return stage_char is None or (world.reinhardt_stages and stage_char == "Reinhardt") or \
|
||||||
|
(world.carrie_stages and stage_char == "Carrie")
|
||||||
|
|
||||||
|
|
||||||
|
def get_normal_stage_exits(world: "CV64World") -> Dict[str, dict]:
|
||||||
|
exits = {name: vanilla_stage_exits[name].copy() for name in vanilla_stage_exits}
|
||||||
|
non_branching_pos = 1
|
||||||
|
|
||||||
|
for stage in stage_info:
|
||||||
|
# Remove character stages that are not enabled.
|
||||||
|
if not verify_character_stage(world, stage):
|
||||||
|
del exits[stage]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If branching pathways are not enabled, update the exit info to converge said stages on a single path.
|
||||||
|
if world.branching_stages:
|
||||||
|
continue
|
||||||
|
if world.carrie_stages and not world.reinhardt_stages and exits[stage]["alt"] is not None:
|
||||||
|
exits[stage]["next"] = exits[stage]["alt"]
|
||||||
|
elif world.carrie_stages and world.reinhardt_stages and stage != rname.castle_keep:
|
||||||
|
exits[stage]["next"] = vanilla_stage_order[vanilla_stage_order.index(stage) + 1]
|
||||||
|
exits[stage]["alt"] = None
|
||||||
|
exits[stage]["position"] = non_branching_pos
|
||||||
|
exits[stage]["path"] = " "
|
||||||
|
non_branching_pos += 1
|
||||||
|
|
||||||
|
return exits
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_stages(world: "CV64World", stage_1_blacklist: List[str]) \
|
||||||
|
-> Tuple[Dict[str, Dict[str, Union[str, int, None]]], str, List[str]]:
|
||||||
|
"""Woah, this is a lot! I should probably summarize what's happening in here, huh?
|
||||||
|
|
||||||
|
So, in the vanilla game, all the stages are basically laid out on a linear "timeline" with some stages being
|
||||||
|
different depending on who you are playing as. The different character stages, in question, are the one following
|
||||||
|
Villa and the two following Castle Center. The ends of these two stages are considered the route divergences and, in
|
||||||
|
this rando, the game's behavior has been changed in such that both characters can access each other's exclusive
|
||||||
|
stages (thereby making the entire game playable in just one character run). With this in mind, when shuffling the
|
||||||
|
stages around, there is one particularly big rule that must be kept in mind to ensure things don't get too wacky.
|
||||||
|
That being:
|
||||||
|
|
||||||
|
Villa and Castle Center cannot appear in branching path stage slots; they can only be on "main" path slots.
|
||||||
|
|
||||||
|
So for this reason, generating a new stage layout is not as simple as just scrambling a list of stages around. It
|
||||||
|
must be done in such a way that whatever stages directly follow Villa or CC is not the other stage. The exception is
|
||||||
|
if branching stages are not a thing at all due to the player settings, in which case everything I said above does
|
||||||
|
not matter. Consider the following representation of a stage "timeline", wherein each "-" represents a main stage
|
||||||
|
and a "=" represents a pair of branching stages:
|
||||||
|
|
||||||
|
-==---=---
|
||||||
|
|
||||||
|
In the above example, CC is the first "-" and Villa is the fourth. CC and Villa can only be "-"s whereas every other
|
||||||
|
stage can be literally anywhere, including on one of the "=" dashes. Villa will always be followed by one pair of
|
||||||
|
branching stages and CC will be followed by two pairs.
|
||||||
|
|
||||||
|
This code starts by first generating a singular list of stages that fit the criteria of Castle Center not being in
|
||||||
|
the next two entries following Villa and Villa not being in the next four entries after Castle Center. Once that has
|
||||||
|
been figured out, it will then generate a dictionary of stages with the appropriate information regarding what
|
||||||
|
stages come before and after them to then be used for Entrance creation as well as what position in the list they
|
||||||
|
are in for the purposes of the spoiler log and extended hint information.
|
||||||
|
|
||||||
|
I opted to use the Rondo of Blood "'" stage notation to represent Carrie stage slots specifically. If a main stage
|
||||||
|
with a backwards connection connects backwards into a pair of branching stages, it will be the non-"'" stage
|
||||||
|
(Reinhardt's) that it connects to. The Carrie stage slot cannot be accessed this way.
|
||||||
|
|
||||||
|
If anyone has any ideas or suggestions on how to improve this, I'd love to hear them! Because it's only going to get
|
||||||
|
uglier come Legacy of Darkness and Cornell's funny side route later on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
starting_stage_value = world.options.starting_stage.value
|
||||||
|
|
||||||
|
# Verify the starting stage is valid. If it isn't, pick a stage at random.
|
||||||
|
if vanilla_stage_order[starting_stage_value] not in stage_1_blacklist and \
|
||||||
|
verify_character_stage(world, vanilla_stage_order[starting_stage_value]):
|
||||||
|
starting_stage = vanilla_stage_order[starting_stage_value]
|
||||||
|
else:
|
||||||
|
logging.warning(f"[{world.multiworld.player_name[world.player]}] {vanilla_stage_order[starting_stage_value]} "
|
||||||
|
f"cannot be the starting stage with the chosen settings. Picking a different stage instead...")
|
||||||
|
possible_stages = []
|
||||||
|
for stage in vanilla_stage_order:
|
||||||
|
if stage in world.active_stage_exits and stage != rname.castle_keep:
|
||||||
|
possible_stages.append(stage)
|
||||||
|
starting_stage = world.random.choice(possible_stages)
|
||||||
|
world.options.starting_stage.value = vanilla_stage_order.index(starting_stage)
|
||||||
|
|
||||||
|
remaining_stage_pool = [stage for stage in world.active_stage_exits]
|
||||||
|
remaining_stage_pool.remove(rname.castle_keep)
|
||||||
|
|
||||||
|
total_stages = len(remaining_stage_pool)
|
||||||
|
|
||||||
|
new_stage_order = []
|
||||||
|
villa_cc_ids = [2, 3]
|
||||||
|
alt_villa_stage = []
|
||||||
|
alt_cc_stages = []
|
||||||
|
|
||||||
|
# If there are branching stages, remove Villa and CC from the list and determine their placements first.
|
||||||
|
if world.branching_stages:
|
||||||
|
villa_cc_ids = world.random.sample(range(1, 5), 2)
|
||||||
|
remaining_stage_pool.remove(rname.villa)
|
||||||
|
remaining_stage_pool.remove(rname.castle_center)
|
||||||
|
|
||||||
|
# Remove the starting stage from the remaining pool if it's in there at this point.
|
||||||
|
if starting_stage in remaining_stage_pool:
|
||||||
|
remaining_stage_pool.remove(starting_stage)
|
||||||
|
|
||||||
|
# If Villa or CC is our starting stage, force its respective ID to be 0 and re-randomize the other.
|
||||||
|
if starting_stage == rname.villa:
|
||||||
|
villa_cc_ids[0] = 0
|
||||||
|
villa_cc_ids[1] = world.random.randint(1, 5)
|
||||||
|
elif starting_stage == rname.castle_center:
|
||||||
|
villa_cc_ids[1] = 0
|
||||||
|
villa_cc_ids[0] = world.random.randint(1, 5)
|
||||||
|
|
||||||
|
for i in range(total_stages):
|
||||||
|
# If we're on Villa or CC's ID while in branching stage mode, put the respective stage in the slot.
|
||||||
|
if world.branching_stages and i == villa_cc_ids[0] and rname.villa not in new_stage_order:
|
||||||
|
new_stage_order.append(rname.villa)
|
||||||
|
villa_cc_ids[1] += 2
|
||||||
|
elif world.branching_stages and i == villa_cc_ids[1] and rname.castle_center not in new_stage_order:
|
||||||
|
new_stage_order.append(rname.castle_center)
|
||||||
|
villa_cc_ids[0] += 4
|
||||||
|
else:
|
||||||
|
# If neither of the above are true, if we're looking at Stage 1, append the starting stage.
|
||||||
|
# Otherwise, draw a random stage from the active list and delete it from there.
|
||||||
|
if i == 0:
|
||||||
|
new_stage_order.append(starting_stage)
|
||||||
|
else:
|
||||||
|
new_stage_order.append(world.random.choice(remaining_stage_pool))
|
||||||
|
remaining_stage_pool.remove(new_stage_order[i])
|
||||||
|
|
||||||
|
# If we're looking at an alternate stage slot, put the stage in one of these lists to indicate it as such
|
||||||
|
if not world.branching_stages:
|
||||||
|
continue
|
||||||
|
if i - 2 >= 0:
|
||||||
|
if new_stage_order[i - 2] == rname.villa:
|
||||||
|
alt_villa_stage.append(new_stage_order[i])
|
||||||
|
if i - 3 >= 0:
|
||||||
|
if new_stage_order[i - 3] == rname.castle_center:
|
||||||
|
alt_cc_stages.append(new_stage_order[i])
|
||||||
|
if i - 4 >= 0:
|
||||||
|
if new_stage_order[i - 4] == rname.castle_center:
|
||||||
|
alt_cc_stages.append(new_stage_order[i])
|
||||||
|
|
||||||
|
new_stage_order.append(rname.castle_keep)
|
||||||
|
|
||||||
|
# Update the dictionary of stage exits
|
||||||
|
current_stage_number = 1
|
||||||
|
for i in range(len(new_stage_order)):
|
||||||
|
# Stage position number and alternate path indicator
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["position"] = current_stage_number
|
||||||
|
if new_stage_order[i] in alt_villa_stage + alt_cc_stages:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["path"] = "'"
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["path"] = " "
|
||||||
|
|
||||||
|
# Previous stage
|
||||||
|
if world.active_stage_exits[new_stage_order[i]]["prev"]:
|
||||||
|
if i - 1 < 0:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["prev"] = "Menu"
|
||||||
|
elif world.branching_stages:
|
||||||
|
if new_stage_order[i - 1] == alt_villa_stage[0] or new_stage_order[i] == alt_villa_stage[0]:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 2]
|
||||||
|
elif new_stage_order[i - 1] == alt_cc_stages[1] or new_stage_order[i] == alt_cc_stages[0]:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 3]
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
|
||||||
|
|
||||||
|
# Next stage
|
||||||
|
if world.active_stage_exits[new_stage_order[i]]["next"]:
|
||||||
|
if world.branching_stages:
|
||||||
|
if new_stage_order[i + 1] == alt_villa_stage[0]:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 2]
|
||||||
|
current_stage_number -= 1
|
||||||
|
elif new_stage_order[i + 1] == alt_cc_stages[0]:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 3]
|
||||||
|
current_stage_number -= 2
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
|
||||||
|
|
||||||
|
# Alternate next stage
|
||||||
|
if world.active_stage_exits[new_stage_order[i]]["alt"]:
|
||||||
|
if world.branching_stages:
|
||||||
|
if new_stage_order[i] == rname.villa:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["alt"] = alt_villa_stage[0]
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["alt"] = alt_cc_stages[0]
|
||||||
|
else:
|
||||||
|
world.active_stage_exits[new_stage_order[i]]["alt"] = None
|
||||||
|
|
||||||
|
current_stage_number += 1
|
||||||
|
|
||||||
|
return world.active_stage_exits, starting_stage, new_stage_order
|
||||||
|
|
||||||
|
|
||||||
|
def generate_warps(world: "CV64World") -> List[str]:
|
||||||
|
# Create a list of warps from the active stage list. They are in a random order by default and will never
|
||||||
|
# include the starting stage.
|
||||||
|
possible_warps = [stage for stage in world.active_stage_list]
|
||||||
|
|
||||||
|
# Remove the starting stage from the possible warps.
|
||||||
|
del (possible_warps[0])
|
||||||
|
|
||||||
|
active_warp_list = world.random.sample(possible_warps, 7)
|
||||||
|
|
||||||
|
if world.options.warp_order == WarpOrder.option_seed_stage_order:
|
||||||
|
# Arrange the warps to be in the seed's stage order
|
||||||
|
new_list = world.active_stage_list.copy()
|
||||||
|
for warp in world.active_stage_list:
|
||||||
|
if warp not in active_warp_list:
|
||||||
|
new_list.remove(warp)
|
||||||
|
active_warp_list = new_list
|
||||||
|
elif world.options.warp_order == WarpOrder.option_vanilla_stage_order:
|
||||||
|
# Arrange the warps to be in the vanilla game's stage order
|
||||||
|
new_list = list(vanilla_stage_order)
|
||||||
|
for warp in vanilla_stage_order:
|
||||||
|
if warp not in active_warp_list:
|
||||||
|
new_list.remove(warp)
|
||||||
|
active_warp_list = new_list
|
||||||
|
|
||||||
|
# Insert the starting stage at the start of the warp list
|
||||||
|
active_warp_list.insert(0, world.active_stage_list[0])
|
||||||
|
|
||||||
|
return active_warp_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_region_names(active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> List[str]:
|
||||||
|
region_names = []
|
||||||
|
for stage in active_stage_exits:
|
||||||
|
stage_regions = get_stage_info(stage, "regions")
|
||||||
|
for region in stage_regions:
|
||||||
|
region_names.append(region)
|
||||||
|
|
||||||
|
return region_names
|
|
@ -0,0 +1,6 @@
|
||||||
|
from test.bases import WorldTestBase
|
||||||
|
|
||||||
|
|
||||||
|
class CV64TestBase(WorldTestBase):
|
||||||
|
game = "Castlevania 64"
|
||||||
|
player: int = 1
|
|
@ -0,0 +1,250 @@
|
||||||
|
from . import CV64TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class WarpTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"special1s_per_warp": 3,
|
||||||
|
"total_special1s": 21
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_warps(self) -> None:
|
||||||
|
for i in range(1, 8):
|
||||||
|
self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
|
||||||
|
self.collect([self.get_item_by_name("Special1")] * 2)
|
||||||
|
self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
|
||||||
|
self.collect([self.get_item_by_name("Special1")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance(f"Warp {i}"))
|
||||||
|
|
||||||
|
|
||||||
|
class CastleWallTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance(f"Left Tower door"))
|
||||||
|
self.collect([self.get_item_by_name("Left Tower Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance(f"Left Tower door"))
|
||||||
|
|
||||||
|
|
||||||
|
class VillaTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Storeroom door"))
|
||||||
|
self.collect([self.get_item_by_name("Storeroom Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To Storeroom door"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Archives door"))
|
||||||
|
self.collect([self.get_item_by_name("Archives Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To Archives door"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("To maze gate"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Copper door"))
|
||||||
|
self.collect([self.get_item_by_name("Garden Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To maze gate"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Copper door"))
|
||||||
|
self.collect([self.get_item_by_name("Copper Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Copper door"))
|
||||||
|
|
||||||
|
|
||||||
|
class CastleCenterTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Torture Chamber door"))
|
||||||
|
self.collect([self.get_item_by_name("Chamber Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Torture Chamber door"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 7
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Execution gate"))
|
||||||
|
self.collect([self.get_item_by_name("Execution Key")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Execution gate"))
|
||||||
|
|
||||||
|
|
||||||
|
class ScienceTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 8
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Science Door 1"))
|
||||||
|
self.collect([self.get_item_by_name("Science Key1")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Science Door 1"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Science Door 2"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Science Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Science Key2")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To Science Door 2"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Science Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Science Key3")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Science Door 3"))
|
||||||
|
|
||||||
|
|
||||||
|
class ClocktowerTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 11
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_doors(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Clocktower Door 1"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Clocktower Key1")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To Clocktower Door 1"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Clocktower Key2")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("To Clocktower Door 2"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Clocktower Key3")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
|
||||||
|
|
||||||
|
class DraculaNoneTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"draculas_condition": 0,
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dracula_none_condition(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Left Tower Key"),
|
||||||
|
self.get_item_by_name("Garden Key"),
|
||||||
|
self.get_item_by_name("Copper Key"),
|
||||||
|
self.get_item_by_name("Science Key1"),
|
||||||
|
self.get_item_by_name("Science Key2"),
|
||||||
|
self.get_item_by_name("Science Key3"),
|
||||||
|
self.get_item_by_name("Clocktower Key1"),
|
||||||
|
self.get_item_by_name("Clocktower Key2"),
|
||||||
|
self.get_item_by_name("Clocktower Key3")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Special1")] * 7)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Dracula's door"))
|
||||||
|
|
||||||
|
|
||||||
|
class DraculaSpecialTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"draculas_condition": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dracula_special_condition(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
self.collect([self.get_item_by_name("Left Tower Key"),
|
||||||
|
self.get_item_by_name("Garden Key"),
|
||||||
|
self.get_item_by_name("Copper Key"),
|
||||||
|
self.get_item_by_name("Magical Nitro"),
|
||||||
|
self.get_item_by_name("Mandragora"),
|
||||||
|
self.get_item_by_name("Clocktower Key1"),
|
||||||
|
self.get_item_by_name("Clocktower Key2"),
|
||||||
|
self.get_item_by_name("Clocktower Key3")] * 2)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Special2")] * 19)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Special2")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Dracula's door"))
|
||||||
|
|
||||||
|
|
||||||
|
class DraculaCrystalTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"draculas_condition": 1,
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 5,
|
||||||
|
"hard_logic": True
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dracula_crystal_condition(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.collect([self.get_item_by_name("Left Tower Key"),
|
||||||
|
self.get_item_by_name("Garden Key"),
|
||||||
|
self.get_item_by_name("Copper Key"),
|
||||||
|
self.get_item_by_name("Science Key1"),
|
||||||
|
self.get_item_by_name("Science Key2"),
|
||||||
|
self.get_item_by_name("Science Key3"),
|
||||||
|
self.get_item_by_name("Clocktower Key1"),
|
||||||
|
self.get_item_by_name("Clocktower Key2"),
|
||||||
|
self.get_item_by_name("Clocktower Key3")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.collect([self.get_item_by_name("Special1")] * 7)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro"),
|
||||||
|
self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro"),
|
||||||
|
self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.assertTrue(self.can_reach_entrance("Dracula's door"))
|
||||||
|
|
||||||
|
|
||||||
|
class DraculaBossTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"draculas_condition": 2,
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"starting_stage": 5,
|
||||||
|
"hard_logic": True,
|
||||||
|
"bosses_required": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dracula_boss_condition(self) -> None:
|
||||||
|
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.collect([self.get_item_by_name("Left Tower Key"),
|
||||||
|
self.get_item_by_name("Garden Key"),
|
||||||
|
self.get_item_by_name("Copper Key"),
|
||||||
|
self.get_item_by_name("Science Key1"),
|
||||||
|
self.get_item_by_name("Science Key2"),
|
||||||
|
self.get_item_by_name("Science Key3"),
|
||||||
|
self.get_item_by_name("Clocktower Key1"),
|
||||||
|
self.get_item_by_name("Clocktower Key2"),
|
||||||
|
self.get_item_by_name("Clocktower Key3")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.collect([self.get_item_by_name("Special1")] * 7)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro"),
|
||||||
|
self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertFalse(self.can_reach_entrance("Dracula's door"))
|
||||||
|
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.collect([self.get_item_by_name("Magical Nitro"),
|
||||||
|
self.get_item_by_name("Mandragora")] * 1)
|
||||||
|
self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
|
||||||
|
self.assertTrue(self.can_reach_entrance("Dracula's door"))
|
||||||
|
|
||||||
|
|
||||||
|
class LizardTest(CV64TestBase):
|
||||||
|
options = {
|
||||||
|
"stage_shuffle": True,
|
||||||
|
"draculas_condition": 2,
|
||||||
|
"starting_stage": 4
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_lizard_man_trio(self) -> None:
|
||||||
|
self.assertTrue(self.can_reach_location("Underground Waterway: Lizard-man trio"))
|
|
@ -0,0 +1,95 @@
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
cv64_char_dict = {"\n": (0x01, 0), " ": (0x02, 4), "!": (0x03, 2), '"': (0x04, 5), "#": (0x05, 6), "$": (0x06, 5),
|
||||||
|
"%": (0x07, 8), "&": (0x08, 7), "'": (0x09, 4), "(": (0x0A, 3), ")": (0x0B, 3), "*": (0x0C, 4),
|
||||||
|
"+": (0x0D, 5), ",": (0x0E, 3), "-": (0x0F, 4), ".": (0x10, 3), "/": (0x11, 6), "0": (0x12, 5),
|
||||||
|
"1": (0x13, 3), "2": (0x14, 5), "3": (0x15, 4), "4": (0x16, 5), "5": (0x17, 5), "6": (0x18, 5),
|
||||||
|
"7": (0x19, 5), "8": (0x1A, 5), "9": (0x1B, 5), ":": (0x1C, 3), ";": (0x1D, 3), "<": (0x1E, 3),
|
||||||
|
"=": (0x1F, 4), ">": (0x20, 3), "?": (0x21, 5), "@": (0x22, 8), "A": (0x23, 7), "B": (0x24, 6),
|
||||||
|
"C": (0x25, 5), "D": (0x26, 7), "E": (0x27, 5), "F": (0x28, 6), "G": (0x29, 6), "H": (0x2A, 7),
|
||||||
|
"I": (0x2B, 3), "J": (0x2C, 3), "K": (0x2D, 6), "L": (0x2E, 6), "M": (0x2F, 8), "N": (0x30, 7),
|
||||||
|
"O": (0x31, 6), "P": (0x32, 6), "Q": (0x33, 8), "R": (0x34, 6), "S": (0x35, 5), "T": (0x36, 6),
|
||||||
|
"U": (0x37, 6), "V": (0x38, 7), "W": (0x39, 8), "X": (0x3A, 6), "Y": (0x3B, 7), "Z": (0x3C, 6),
|
||||||
|
"[": (0x3D, 3), "\\": (0x3E, 6), "]": (0x3F, 3), "^": (0x40, 6), "_": (0x41, 5), "a": (0x43, 5),
|
||||||
|
"b": (0x44, 6), "c": (0x45, 4), "d": (0x46, 6), "e": (0x47, 5), "f": (0x48, 5), "g": (0x49, 5),
|
||||||
|
"h": (0x4A, 6), "i": (0x4B, 3), "j": (0x4C, 3), "k": (0x4D, 6), "l": (0x4E, 3), "m": (0x4F, 8),
|
||||||
|
"n": (0x50, 6), "o": (0x51, 5), "p": (0x52, 5), "q": (0x53, 5), "r": (0x54, 4), "s": (0x55, 4),
|
||||||
|
"t": (0x56, 4), "u": (0x57, 5), "v": (0x58, 6), "w": (0x59, 8), "x": (0x5A, 5), "y": (0x5B, 5),
|
||||||
|
"z": (0x5C, 4), "{": (0x5D, 4), "|": (0x5E, 2), "}": (0x5F, 3), "`": (0x61, 4), "「": (0x62, 3),
|
||||||
|
"」": (0x63, 3), "~": (0x65, 3), "″": (0x72, 3), "°": (0x73, 3), "∞": (0x74, 8)}
|
||||||
|
# [0] = CV64's in-game ID for that text character.
|
||||||
|
# [1] = How much space towards the in-game line length limit it contributes.
|
||||||
|
|
||||||
|
|
||||||
|
def cv64_string_to_bytearray(cv64text: str, a_advance: bool = False, append_end: bool = True) -> bytearray:
|
||||||
|
"""Converts a string into a bytearray following CV64's string format."""
|
||||||
|
text_bytes = bytearray(0)
|
||||||
|
for i, char in enumerate(cv64text):
|
||||||
|
if char == "\t":
|
||||||
|
text_bytes.extend([0xFF, 0xFF])
|
||||||
|
else:
|
||||||
|
if char in cv64_char_dict:
|
||||||
|
text_bytes.extend([0x00, cv64_char_dict[char][0]])
|
||||||
|
else:
|
||||||
|
text_bytes.extend([0x00, 0x41])
|
||||||
|
|
||||||
|
if a_advance:
|
||||||
|
text_bytes.extend([0xA3, 0x00])
|
||||||
|
if append_end:
|
||||||
|
text_bytes.extend([0xFF, 0xFF])
|
||||||
|
return text_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def cv64_text_truncate(cv64text: str, textbox_len_limit: int) -> str:
|
||||||
|
"""Truncates a string at a given in-game text line length."""
|
||||||
|
line_len = 0
|
||||||
|
|
||||||
|
for i in range(len(cv64text)):
|
||||||
|
line_len += cv64_char_dict[cv64text[i]][1]
|
||||||
|
|
||||||
|
if line_len > textbox_len_limit:
|
||||||
|
return cv64text[0x00:i]
|
||||||
|
|
||||||
|
return cv64text
|
||||||
|
|
||||||
|
|
||||||
|
def cv64_text_wrap(cv64text: str, textbox_len_limit: int) -> Tuple[str, int]:
|
||||||
|
"""Rebuilds a string with some of its spaces replaced with newlines to ensure the text wraps properly in an in-game
|
||||||
|
textbox of a given length."""
|
||||||
|
words = cv64text.split(" ")
|
||||||
|
new_text = ""
|
||||||
|
line_len = 0
|
||||||
|
num_lines = 1
|
||||||
|
|
||||||
|
for i in range(len(words)):
|
||||||
|
word_len = 0
|
||||||
|
word_divider = " "
|
||||||
|
|
||||||
|
if line_len != 0:
|
||||||
|
line_len += 4
|
||||||
|
else:
|
||||||
|
word_divider = ""
|
||||||
|
|
||||||
|
for char in words[i]:
|
||||||
|
if char in cv64_char_dict:
|
||||||
|
line_len += cv64_char_dict[char][1]
|
||||||
|
word_len += cv64_char_dict[char][1]
|
||||||
|
else:
|
||||||
|
line_len += 5
|
||||||
|
word_len += 5
|
||||||
|
|
||||||
|
if word_len > textbox_len_limit or char in ["\n", "\t"]:
|
||||||
|
word_len = 0
|
||||||
|
line_len = 0
|
||||||
|
if num_lines < 4:
|
||||||
|
num_lines += 1
|
||||||
|
|
||||||
|
if line_len > textbox_len_limit:
|
||||||
|
word_divider = "\n"
|
||||||
|
line_len = word_len
|
||||||
|
if num_lines < 4:
|
||||||
|
num_lines += 1
|
||||||
|
|
||||||
|
new_text += word_divider + words[i]
|
||||||
|
|
||||||
|
return new_text, num_lines
|
Loading…
Reference in New Issue