Archipelago/worlds/cv64/aesthetics.py

654 lines
27 KiB
Python
Raw Normal View History

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 = {
2024-04-18 16:37:51 +00:00
0x10C6EB: (b"\x10", rname.forest_of_silence), # Forest
0x10C6F3: (b"\x0F", rname.forest_of_silence),
0x10C6FB: (b"\x0E", rname.forest_of_silence),
0x10C703: (b"\x0D", rname.forest_of_silence),
0x10C81F: (b"\x0F", rname.castle_wall), # Castle Wall
0x10C827: (b"\x10", rname.castle_wall),
0x10C82F: (b"\x0E", rname.castle_wall),
0x7F9A0F: (b"\x0D", rname.castle_wall),
0x83A5D9: (b"\x0E", rname.villa), # Villa
0x83A5E5: (b"\x0D", rname.villa),
0x83A5F1: (b"\x0F", rname.villa),
0xBFC903: (b"\x10", rname.villa),
0x10C987: (b"\x10", rname.villa),
0x10C98F: (b"\x0D", rname.villa),
0x10C997: (b"\x0F", rname.villa),
0x10CF73: (b"\x10", rname.villa),
0x10CA57: (b"\x0D", rname.tunnel), # Tunnel
0x10CA5F: (b"\x0E", rname.tunnel),
0x10CA67: (b"\x10", rname.tunnel),
0x10CA6F: (b"\x0D", rname.tunnel),
0x10CA77: (b"\x0F", rname.tunnel),
0x10CA7F: (b"\x0E", rname.tunnel),
0x10CBC7: (b"\x0E", rname.castle_center), # Castle Center
0x10CC0F: (b"\x0D", rname.castle_center),
0x10CC5B: (b"\x0F", rname.castle_center),
0x10CD3F: (b"\x0E", rname.tower_of_execution), # Character towers
0x10CD65: (b"\x0D", rname.tower_of_execution),
0x10CE2B: (b"\x0E", rname.tower_of_science),
0x10CE83: (b"\x10", rname.duel_tower),
0x10CF8B: (b"\x0F", rname.room_of_clocks), # Room of Clocks
0x10CF93: (b"\x0D", rname.room_of_clocks),
0x99BC5A: (b"\x0D", rname.clock_tower), # Clock Tower
0x10CECB: (b"\x10", rname.clock_tower),
0x10CED3: (b"\x0F", rname.clock_tower),
0x10CEDB: (b"\x0E", rname.clock_tower),
0x10CEE3: (b"\x0D", rname.clock_tower),
}
rom_sub_weapon_flags = {
2024-04-18 16:37:51 +00:00
0x10C6EC: b"\x02\x00\xFF\x04", # Forest of Silence
0x10C6FC: b"\x04\x00\xFF\x04",
0x10C6F4: b"\x08\x00\xFF\x04",
0x10C704: b"\x40\x00\xFF\x04",
2024-04-18 16:37:51 +00:00
0x10C831: b"\x08", # Castle Wall
0x10C829: b"\x10",
0x10C821: b"\x20",
0xBFCA97: b"\x04",
# Villa
2024-04-18 16:37:51 +00:00
0xBFC926: b"\xFF\x04",
0xBFC93A: b"\x80",
0xBFC93F: b"\x01",
0xBFC943: b"\x40",
0xBFC947: b"\x80",
0x10C989: b"\x10",
0x10C991: b"\x20",
0x10C999: b"\x40",
0x10CF77: b"\x80",
0x10CA58: b"\x40\x00\xFF\x0E", # Tunnel
0x10CA6B: b"\x80",
0x10CA60: b"\x10\x00\xFF\x05",
0x10CA70: b"\x20\x00\xFF\x05",
0x10CA78: b"\x40\x00\xFF\x05",
0x10CA80: b"\x80\x00\xFF\x05",
0x10CBCA: b"\x02", # Castle Center
0x10CC10: b"\x80",
0x10CC5C: b"\x40",
0x10CE86: b"\x01", # Duel Tower
0x10CD43: b"\x02", # Tower of Execution
0x10CE2E: b"\x20", # Tower of Science
0x10CF8E: b"\x04", # Room of Clocks
0x10CF96: b"\x08",
0x10CECE: b"\x08", # Clock Tower
0x10CED6: b"\x10",
0x10CEE6: b"\x20",
0x10CEDE: b"\x80",
}
rom_empty_breakables_flags = {
2024-04-18 16:37:51 +00:00
0x10C74D: b"\x40\xFF\x05", # Forest of Silence
0x10C765: b"\x20\xFF\x0E",
0x10C774: b"\x08\x00\xFF\x0E",
0x10C755: b"\x80\xFF\x05",
0x10C784: b"\x01\x00\xFF\x0E",
0x10C73C: b"\x02\x00\xFF\x0E",
0x10C8D0: b"\x04\x00\xFF\x0E", # Villa foyer
0x10CF9F: b"\x08", # Room of Clocks flags
0x10CFA7: b"\x01",
0xBFCB6F: b"\x04", # Room of Clocks candle property IDs
0xBFCB73: b"\x05",
}
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!"
}
2024-04-18 16:37:51 +00:00
def randomize_lighting(world: "CV64World") -> Dict[int, bytes]:
"""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
2024-04-18 16:37:51 +00:00
randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)])
return randomized_lighting
2024-04-18 16:37:51 +00:00
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, bytes]:
"""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))
2024-04-18 16:37:51 +00:00
def randomize_music(world: "CV64World") -> Dict[int, bytes]:
"""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])
2024-04-18 16:37:51 +00:00
return {0xBFCD30: bytes(music_array)}
2024-04-18 16:37:51 +00:00
def randomize_shop_prices(world: "CV64World") -> Dict[int, bytes]:
"""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)]
2024-04-18 16:37:51 +00:00
# Convert the price list into a data dict.
price_dict = {}
for i in range(len(shop_price_list)):
2024-04-18 16:37:51 +00:00
price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big")
return price_dict
2024-04-18 16:37:51 +00:00
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, bytes]:
"""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
2024-04-18 16:37:51 +00:00
return {0xBFD818: bytes(countdown_list)}
def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
2024-04-18 16:37:51 +00:00
-> Tuple[Dict[int, bytes], 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.
2024-04-18 16:37:51 +00:00
if loc.item.player == world.player:
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:
2024-04-18 16:37:51 +00:00
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") & 0xFF
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))
2024-04-18 16:37:51 +00:00
return {offset: int.to_bytes(byte, 1, "big") for offset, byte in location_bytes.items()}, shop_name_list,\
shop_colors_list, shop_desc_list
def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
2024-04-18 16:37:51 +00:00
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, bytes]:
"""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":
2024-04-18 16:37:51 +00:00
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = b"\xFF"
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x00"
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:
2024-04-18 16:37:51 +00:00
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x03"
# 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
2024-04-18 16:37:51 +00:00
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, bytes]:
"""Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
has to be handled appropriately."""
2024-04-18 16:37:51 +00:00
start_inventory_data = {}
inventory_items_array = [0 for _ in range(35)]
total_money = 0
2024-04-18 16:37:51 +00:00
total_jewels = 0
total_powerups = 0
total_ice_traps = 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:
2024-04-18 16:37:51 +00:00
start_inventory_data[0xBFD883] = bytes(sub_equip_id)
# Starting PowerUps
elif item.name == iname.powerup:
2024-04-18 16:37:51 +00:00
total_powerups += 1
# Can't have more than 2 PowerUps.
if total_powerups > 2:
total_powerups = 2
# Starting Gold
elif "GOLD" in item.name:
total_money += int(item.name[0:4])
2024-04-18 16:37:51 +00:00
# Money cannot be higher than 99999.
if total_money > 99999:
total_money = 99999
# Starting Jewels
elif "jewel" in item.name:
if "L" in item.name:
2024-04-18 16:37:51 +00:00
total_jewels += 10
else:
2024-04-18 16:37:51 +00:00
total_jewels += 5
# Jewels cannot be higher than 99.
if total_jewels > 99:
total_jewels = 99
# Starting Ice Traps
else:
2024-04-18 16:37:51 +00:00
total_ice_traps += 1
# Ice Traps cannot be higher than 255.
if total_ice_traps > 0xFF:
total_ice_traps = 0xFF
# Convert the jewels into data.
start_inventory_data[0xBFD867] = bytes([total_jewels])
# Convert the Ice Traps into data.
start_inventory_data[0xBFD88B] = bytes([total_ice_traps])
# Convert the inventory items into data.
2024-04-18 16:37:51 +00:00
start_inventory_data[0xBFE518] = bytes(inventory_items_array)
# Convert the starting money into data.
start_inventory_data[0xBFE514] = int.to_bytes(total_money, 4, "big")
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])