667 lines
27 KiB
Python
667 lines
27 KiB
Python
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])
|