654 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			654 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: (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 = {
 | |
|     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",
 | |
| 
 | |
|     0x10C831: b"\x08",  # Castle Wall
 | |
|     0x10C829: b"\x10",
 | |
|     0x10C821: b"\x20",
 | |
|     0xBFCA97: b"\x04",
 | |
| 
 | |
|     # Villa
 | |
|     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 = {
 | |
|     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!"
 | |
| }
 | |
| 
 | |
| 
 | |
| 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
 | |
|                 randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)])
 | |
|     return randomized_lighting
 | |
| 
 | |
| 
 | |
| 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))
 | |
| 
 | |
| 
 | |
| 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])
 | |
| 
 | |
|     return {0xBFCD30: bytes(music_array)}
 | |
| 
 | |
| 
 | |
| 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)]
 | |
| 
 | |
|     # Convert the price list into a data dict.
 | |
|     price_dict = {}
 | |
|     for i in range(len(shop_price_list)):
 | |
|         price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big")
 | |
| 
 | |
|     return price_dict
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
|     return {0xBFD818: bytes(countdown_list)}
 | |
| 
 | |
| 
 | |
| def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
 | |
|         -> 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.
 | |
|         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:
 | |
|                 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))
 | |
| 
 | |
|     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,
 | |
|                            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":
 | |
|             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:
 | |
|                     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
 | |
| 
 | |
| 
 | |
| 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."""
 | |
|     start_inventory_data = {}
 | |
| 
 | |
|     inventory_items_array = [0 for _ in range(35)]
 | |
|     total_money = 0
 | |
|     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:
 | |
|             start_inventory_data[0xBFD883] = bytes(sub_equip_id)
 | |
|         # Starting PowerUps
 | |
|         elif item.name == iname.powerup:
 | |
|             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])
 | |
|             # 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:
 | |
|                 total_jewels += 10
 | |
|             else:
 | |
|                 total_jewels += 5
 | |
|             # Jewels cannot be higher than 99.
 | |
|             if total_jewels > 99:
 | |
|                 total_jewels = 99
 | |
|         # Starting Ice Traps
 | |
|         else:
 | |
|             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.
 | |
|     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])
 | 
