from BaseClasses import ItemClassification, Location from .options import ItemDropRandomization, Countdown, RequiredSkirmishes, IronMaidenBehavior from .locations import cvcotm_location_info from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS from .data import iname from typing import TYPE_CHECKING, Dict, List, Iterable, Tuple, NamedTuple, Optional, TypedDict if TYPE_CHECKING: from . import CVCotMWorld class StatInfo(TypedDict): # Amount this stat increases per Max Up the player starts with. amount_per: int # The most amount of this stat the player is allowed to start with. Problems arise if the stat exceeds 9999, so we # must ensure it can't if the player raises any class to level 99 as well as collects 255 of that max up. The game # caps hearts at 999 automatically, so it doesn't matter so much for that one. max_allowed: int # The key variable in extra_stats that the stat max up affects. variable: str extra_starting_stat_info: Dict[str, StatInfo] = { iname.hp_max: {"amount_per": 10, "max_allowed": 5289, "variable": "extra health"}, iname.mp_max: {"amount_per": 10, "max_allowed": 3129, "variable": "extra magic"}, iname.heart_max: {"amount_per": 6, "max_allowed": 999, "variable": "extra hearts"}, } other_player_subtype_bytes = { 0xE4: 0x03, 0xE6: 0x14, 0xE8: 0x0A } class OtherGameAppearancesInfo(TypedDict): # What type of item to place for the other player. type: int # What item to display it as for the other player. appearance: int other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = { # NOTE: Symphony of the Night is currently an unsupported world not in main. "Symphony of the Night": {"Life Vessel": {"type": 0xE4, "appearance": 0x01}, "Heart Vessel": {"type": 0xE4, "appearance": 0x00}}, "Timespinner": {"Max HP": {"type": 0xE4, "appearance": 0x01}, "Max Aura": {"type": 0xE4, "appearance": 0x02}, "Max Sand": {"type": 0xE8, "appearance": 0x0F}} } # 0 = Holy water 22 # 1 = Axe 24 # 2 = Knife 32 # 3 = Cross 6 # 4 = Stopwatch 12 # 5 = Small heart # 6 = Big heart rom_sub_weapon_offsets = { 0xD034E: b"\x01", 0xD0462: b"\x02", 0xD064E: b"\x00", 0xD06F6: b"\x02", 0xD0882: b"\x00", 0xD0912: b"\x02", 0xD0C2A: b"\x02", 0xD0C96: b"\x01", 0xD0D92: b"\x02", 0xD0DCE: b"\x01", 0xD1332: b"\x00", 0xD13AA: b"\x01", 0xD1722: b"\x02", 0xD17A6: b"\x01", 0xD1926: b"\x01", 0xD19AA: b"\x02", 0xD1A9A: b"\x02", 0xD1AA6: b"\x00", 0xD1EBA: b"\x00", 0xD1ED2: b"\x01", 0xD2262: b"\x02", 0xD23B2: b"\x03", 0xD256E: b"\x02", 0xD2742: b"\x02", 0xD2832: b"\x04", 0xD2862: b"\x01", 0xD2A2A: b"\x01", 0xD2DBA: b"\x04", 0xD2DC6: b"\x00", 0xD2E02: b"\x02", 0xD2EFE: b"\x04", 0xD2F0A: b"\x02", 0xD302A: b"\x00", 0xD3042: b"\x01", 0xD304E: b"\x04", 0xD3066: b"\x02", 0xD322E: b"\x04", 0xD334E: b"\x04", 0xD3516: b"\x03", 0xD35CA: b"\x02", 0xD371A: b"\x01", 0xD38EE: b"\x00", 0xD3BE2: b"\x02", 0xD3D1A: b"\x01", 0xD3D56: b"\x02", 0xD3ECA: b"\x00", 0xD3EE2: b"\x02", 0xD4056: b"\x01", 0xD40E6: b"\x04", 0xD413A: b"\x04", 0xD4326: b"\x00", 0xD460E: b"\x00", 0xD48D2: b"\x00", 0xD49E6: b"\x01", 0xD4ABE: b"\x02", 0xD4B8A: b"\x01", 0xD4D0A: b"\x04", 0xD4EAE: b"\x02", 0xD4F0E: b"\x00", 0xD4F92: b"\x02", 0xD4FB6: b"\x01", 0xD503A: b"\x03", 0xD5646: b"\x01", 0xD5682: b"\x02", 0xD57C6: b"\x02", 0xD57D2: b"\x02", 0xD58F2: b"\x00", 0xD5922: b"\x01", 0xD5B9E: b"\x02", 0xD5E26: b"\x01", 0xD5E56: b"\x02", 0xD5E7A: b"\x02", 0xD5F5E: b"\x00", 0xD69EA: b"\x02", 0xD69F6: b"\x01", 0xD6A02: b"\x00", 0xD6A0E: b"\x04", 0xD6A1A: b"\x03", 0xD6BE2: b"\x00", 0xD6CBA: b"\x01", 0xD6CDE: b"\x02", 0xD6EEE: b"\x00", 0xD6F1E: b"\x02", 0xD6F42: b"\x01", 0xD6FC6: b"\x04", 0xD706E: b"\x00", 0xD716A: b"\x02", 0xD72AE: b"\x01", 0xD75BA: b"\x03", 0xD76AA: b"\x04", 0xD76B6: b"\x00", 0xD76C2: b"\x01", 0xD76CE: b"\x02", 0xD76DA: b"\x03", 0xD7D46: b"\x00", 0xD7D52: b"\x00", } LOW_ITEMS = [ 41, # Potion 42, # Meat 48, # Mind Restore 51, # Heart 46, # Antidote 47, # Cure Curse 17, # Cotton Clothes 18, # Prison Garb 12, # Cotton Robe 1, # Leather Armor 2, # Bronze Armor 3, # Gold Armor 39, # Toy Ring 40, # Bear Ring 34, # Wristband 36, # Arm Guard 37, # Magic Gauntlet 38, # Miracle Armband 35, # Gauntlet ] MID_ITEMS = [ 43, # Spiced Meat 49, # Mind High 52, # Heart High 19, # Stylish Suit 20, # Night Suit 13, # Silk Robe 14, # Rainbow Robe 4, # Chainmail 5, # Steel Armor 6, # Platinum Armor 24, # Star Bracelet 29, # Cursed Ring 25, # Strength Ring 26, # Hard Ring 27, # Intelligence Ring 28, # Luck Ring 23, # Double Grips ] HIGH_ITEMS = [ 44, # Potion High 45, # Potion Ex 50, # Mind Ex 53, # Heart Ex 54, # Heart Mega 21, # Ninja Garb 22, # Soldier Fatigues 15, # Magic Robe 16, # Sage Robe 7, # Diamond Armor 8, # Mirror Armor 9, # Needle Armor 10, # Dark Armor 30, # Strength Armband 31, # Defense Armband 32, # Sage Armband 33, # Gambler Armband ] COMMON_ITEMS = LOW_ITEMS + MID_ITEMS RARE_ITEMS = LOW_ITEMS + MID_ITEMS + HIGH_ITEMS class CVCotMEnemyData(NamedTuple): name: str hp: int attack: int defense: int exp: int type: Optional[str] = None cvcotm_enemy_info: List[CVCotMEnemyData] = [ # Name HP ATK DEF EXP CVCotMEnemyData("Medusa Head", 6, 120, 60, 2), CVCotMEnemyData("Zombie", 48, 70, 20, 2), CVCotMEnemyData("Ghoul", 100, 190, 79, 3), CVCotMEnemyData("Wight", 110, 235, 87, 4), CVCotMEnemyData("Clinking Man", 80, 135, 25, 21), CVCotMEnemyData("Zombie Thief", 120, 185, 30, 58), CVCotMEnemyData("Skeleton", 25, 65, 45, 4), CVCotMEnemyData("Skeleton Bomber", 20, 50, 40, 4), CVCotMEnemyData("Electric Skeleton", 42, 80, 50, 30), CVCotMEnemyData("Skeleton Spear", 30, 65, 46, 6), CVCotMEnemyData("Skeleton Boomerang", 60, 170, 90, 112), CVCotMEnemyData("Skeleton Soldier", 35, 90, 60, 16), CVCotMEnemyData("Skeleton Knight", 50, 140, 80, 39), CVCotMEnemyData("Bone Tower", 84, 201, 280, 160), CVCotMEnemyData("Fleaman", 60, 142, 45, 29), CVCotMEnemyData("Poltergeist", 105, 360, 380, 510), CVCotMEnemyData("Bat", 5, 50, 15, 4), CVCotMEnemyData("Spirit", 9, 55, 17, 1), CVCotMEnemyData("Ectoplasm", 12, 165, 51, 2), CVCotMEnemyData("Specter", 15, 295, 95, 3), CVCotMEnemyData("Axe Armor", 55, 120, 130, 31), CVCotMEnemyData("Flame Armor", 160, 320, 300, 280), CVCotMEnemyData("Flame Demon", 300, 315, 270, 600), CVCotMEnemyData("Ice Armor", 240, 470, 520, 1500), CVCotMEnemyData("Thunder Armor", 204, 340, 320, 800), CVCotMEnemyData("Wind Armor", 320, 500, 460, 1800), CVCotMEnemyData("Earth Armor", 130, 230, 280, 240), CVCotMEnemyData("Poison Armor", 260, 382, 310, 822), CVCotMEnemyData("Forest Armor", 370, 390, 390, 1280), CVCotMEnemyData("Stone Armor", 90, 220, 320, 222), CVCotMEnemyData("Ice Demon", 350, 492, 510, 4200), CVCotMEnemyData("Holy Armor", 350, 420, 450, 1700), CVCotMEnemyData("Thunder Demon", 180, 270, 230, 450), CVCotMEnemyData("Dark Armor", 400, 680, 560, 3300), CVCotMEnemyData("Wind Demon", 400, 540, 490, 3600), CVCotMEnemyData("Bloody Sword", 30, 220, 500, 200), CVCotMEnemyData("Golem", 650, 520, 700, 1400), CVCotMEnemyData("Earth Demon", 150, 90, 85, 25), CVCotMEnemyData("Were-wolf", 160, 265, 110, 140), CVCotMEnemyData("Man Eater", 400, 330, 233, 700), CVCotMEnemyData("Devil Tower", 10, 140, 200, 17), CVCotMEnemyData("Skeleton Athlete", 100, 100, 50, 25), CVCotMEnemyData("Harpy", 120, 275, 200, 271), CVCotMEnemyData("Siren", 160, 443, 300, 880), CVCotMEnemyData("Imp", 90, 220, 99, 103), CVCotMEnemyData("Mudman", 25, 79, 30, 2), CVCotMEnemyData("Gargoyle", 60, 160, 66, 3), CVCotMEnemyData("Slime", 40, 102, 18, 11), CVCotMEnemyData("Frozen Shade", 112, 490, 560, 1212), CVCotMEnemyData("Heat Shade", 80, 240, 200, 136), CVCotMEnemyData("Poison Worm", 120, 30, 20, 12), CVCotMEnemyData("Myconid", 50, 250, 114, 25), CVCotMEnemyData("Will O'Wisp", 11, 110, 16, 9), CVCotMEnemyData("Spearfish", 40, 360, 450, 280), CVCotMEnemyData("Merman", 60, 303, 301, 10), CVCotMEnemyData("Minotaur", 410, 520, 640, 2000), CVCotMEnemyData("Were-horse", 400, 540, 360, 1970), CVCotMEnemyData("Marionette", 80, 160, 150, 127), CVCotMEnemyData("Gremlin", 30, 80, 33, 2), CVCotMEnemyData("Hopper", 40, 87, 35, 8), CVCotMEnemyData("Evil Pillar", 20, 460, 800, 480), CVCotMEnemyData("Were-panther", 200, 300, 130, 270), CVCotMEnemyData("Were-jaguar", 270, 416, 170, 760), CVCotMEnemyData("Bone Head", 24, 60, 80, 7), CVCotMEnemyData("Fox Archer", 75, 130, 59, 53), CVCotMEnemyData("Fox Hunter", 100, 290, 140, 272), CVCotMEnemyData("Were-bear", 265, 250, 140, 227), CVCotMEnemyData("Grizzly", 600, 380, 200, 960), CVCotMEnemyData("Cerberus", 600, 150, 100, 500, "boss"), CVCotMEnemyData("Beast Demon", 150, 330, 250, 260), CVCotMEnemyData("Arch Demon", 320, 505, 400, 1000), CVCotMEnemyData("Demon Lord", 460, 660, 500, 1950), CVCotMEnemyData("Gorgon", 230, 215, 165, 219), CVCotMEnemyData("Catoblepas", 550, 500, 430, 1800), CVCotMEnemyData("Succubus", 150, 400, 350, 710), CVCotMEnemyData("Fallen Angel", 370, 770, 770, 6000), CVCotMEnemyData("Necromancer", 500, 200, 250, 2500, "boss"), CVCotMEnemyData("Hyena", 93, 140, 70, 105), CVCotMEnemyData("Fishhead", 80, 320, 504, 486), CVCotMEnemyData("Dryad", 120, 300, 360, 300), CVCotMEnemyData("Mimic Candle", 990, 600, 600, 6600, "candle"), CVCotMEnemyData("Brain Float", 20, 50, 25, 10), CVCotMEnemyData("Evil Hand", 52, 150, 120, 63), CVCotMEnemyData("Abiondarg", 88, 388, 188, 388), CVCotMEnemyData("Iron Golem", 640, 290, 450, 8000, "boss"), CVCotMEnemyData("Devil", 1080, 800, 900, 10000), CVCotMEnemyData("Witch", 144, 330, 290, 600), CVCotMEnemyData("Mummy", 100, 100, 35, 3), CVCotMEnemyData("Hipogriff", 300, 500, 210, 740), CVCotMEnemyData("Adramelech", 1800, 380, 360, 16000, "boss"), CVCotMEnemyData("Arachne", 330, 420, 288, 1300), CVCotMEnemyData("Death Mantis", 200, 318, 240, 400), CVCotMEnemyData("Alraune", 774, 490, 303, 2500), CVCotMEnemyData("King Moth", 140, 290, 160, 150), CVCotMEnemyData("Killer Bee", 8, 308, 108, 88), CVCotMEnemyData("Dragon Zombie", 1400, 390, 440, 15000, "boss"), CVCotMEnemyData("Lizardman", 100, 345, 400, 800), CVCotMEnemyData("Franken", 1200, 700, 350, 2100), CVCotMEnemyData("Legion", 420, 610, 375, 1590), CVCotMEnemyData("Dullahan", 240, 550, 440, 2200), CVCotMEnemyData("Death", 880, 600, 800, 60000, "boss"), CVCotMEnemyData("Camilla", 1500, 650, 700, 80000, "boss"), CVCotMEnemyData("Hugh", 1400, 570, 750, 120000, "boss"), CVCotMEnemyData("Dracula", 1100, 805, 850, 150000, "boss"), CVCotMEnemyData("Dracula", 3000, 1000, 1000, 0, "final boss"), CVCotMEnemyData("Skeleton Medalist", 250, 100, 100, 1500), CVCotMEnemyData("Were-jaguar", 320, 518, 260, 1200, "battle arena"), CVCotMEnemyData("Were-wolf", 340, 525, 180, 1100, "battle arena"), CVCotMEnemyData("Catoblepas", 560, 510, 435, 2000, "battle arena"), CVCotMEnemyData("Hipogriff", 500, 620, 280, 1900, "battle arena"), CVCotMEnemyData("Wind Demon", 490, 600, 540, 4000, "battle arena"), CVCotMEnemyData("Witch", 210, 480, 340, 1000, "battle arena"), CVCotMEnemyData("Stone Armor", 260, 585, 750, 3000, "battle arena"), CVCotMEnemyData("Devil Tower", 50, 560, 700, 600, "battle arena"), CVCotMEnemyData("Skeleton", 150, 400, 200, 500, "battle arena"), CVCotMEnemyData("Skeleton Bomber", 150, 400, 200, 550, "battle arena"), CVCotMEnemyData("Electric Skeleton", 150, 400, 200, 700, "battle arena"), CVCotMEnemyData("Skeleton Spear", 150, 400, 200, 580, "battle arena"), CVCotMEnemyData("Flame Demon", 680, 650, 600, 4500, "battle arena"), CVCotMEnemyData("Bone Tower", 120, 500, 650, 800, "battle arena"), CVCotMEnemyData("Fox Hunter", 160, 510, 220, 600, "battle arena"), CVCotMEnemyData("Poison Armor", 380, 680, 634, 3600, "battle arena"), CVCotMEnemyData("Bloody Sword", 55, 600, 1200, 2000, "battle arena"), CVCotMEnemyData("Abiondarg", 188, 588, 288, 588, "battle arena"), CVCotMEnemyData("Legion", 540, 760, 480, 2900, "battle arena"), CVCotMEnemyData("Marionette", 200, 420, 400, 1200, "battle arena"), CVCotMEnemyData("Minotaur", 580, 700, 715, 4100, "battle arena"), CVCotMEnemyData("Arachne", 430, 590, 348, 2400, "battle arena"), CVCotMEnemyData("Succubus", 300, 670, 630, 3100, "battle arena"), CVCotMEnemyData("Demon Lord", 590, 800, 656, 4200, "battle arena"), CVCotMEnemyData("Alraune", 1003, 640, 450, 5000, "battle arena"), CVCotMEnemyData("Hyena", 210, 408, 170, 1000, "battle arena"), CVCotMEnemyData("Devil Armor", 500, 804, 714, 6600), CVCotMEnemyData("Evil Pillar", 55, 655, 900, 1500, "battle arena"), CVCotMEnemyData("White Armor", 640, 770, 807, 7000), CVCotMEnemyData("Devil", 1530, 980, 1060, 30000, "battle arena"), CVCotMEnemyData("Scary Candle", 150, 300, 300, 900, "candle"), CVCotMEnemyData("Trick Candle", 200, 400, 400, 1400, "candle"), CVCotMEnemyData("Nightmare", 250, 550, 550, 2000), CVCotMEnemyData("Lilim", 400, 800, 800, 8000), CVCotMEnemyData("Lilith", 660, 960, 960, 20000), ] # NOTE: Coffin is omitted from the end of this, as its presence doesn't # actually impact the randomizer (all stats and drops inherited from Mummy). BOSS_IDS = [enemy_id for enemy_id in range(len(cvcotm_enemy_info)) if cvcotm_enemy_info[enemy_id].type == "boss"] ENEMY_TABLE_START = 0xCB2C4 NUMBER_ITEMS = 55 COUNTDOWN_TABLE_ADDR = 0x673400 ITEM_ID_SHINNING_ARMOR = 11 def shuffle_sub_weapons(world: "CVCotMWorld") -> Dict[int, bytes]: """Shuffles the sub-weapons amongst themselves.""" sub_bytes = list(rom_sub_weapon_offsets.values()) world.random.shuffle(sub_bytes) return dict(zip(rom_sub_weapon_offsets, sub_bytes)) def get_countdown_flags(world: "CVCotMWorld", 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 count towards a number. Which number to increase is determined by the Location's "countdown" attr in its CVCotMLocationData.""" next_pos = COUNTDOWN_TABLE_ADDR + 0x40 countdown_flags: List[List[int]] = [[] for _ in range(16)] countdown_dict = {} ptr_offset = COUNTDOWN_TABLE_ADDR # Loop over every Location. for loc in active_locations: # If the Location's Item is not Progression/Useful-classified with the "Majors" Countdown being used, or if the # Location is the Iron Maiden switch with the vanilla Iron Maiden behavior, skip adding its flag to the arrays. if (not loc.item.classification & MAJORS_CLASSIFICATIONS and world.options.countdown == Countdown.option_majors): continue countdown_index = cvcotm_location_info[loc.name].countdown # Take the Location's address if the above condition is satisfied, and get the flag value out of it. countdown_flags[countdown_index] += [loc.address & 0xFF, 0] # Write the Countdown flag arrays and array pointers correctly. Each flag list should end with a 0xFFFF to indicate # the end of an area's list. for area_flags in countdown_flags: countdown_dict[ptr_offset] = int.to_bytes(next_pos | 0x08000000, 4, "little") countdown_dict[next_pos] = bytes(area_flags + [0xFF, 0xFF]) ptr_offset += 4 next_pos += len(area_flags) + 2 return countdown_dict def get_location_data(world: "CVCotMWorld", active_locations: Iterable[Location]) -> Dict[int, bytes]: """Gets ALL the Item data to go into the ROM. Items consist of four bytes; the first two represent the object ID for the "category" of item that it belongs to, the third is the sub-value for which item within that "category" it is, and the fourth controls the appearance it takes.""" location_bytes = {} for loc in active_locations: # Figure out the item ID bytes to put in each Location's offset here. # If it's a CotM Item, always write the Item's primary type byte. if loc.item.game == "Castlevania - Circle of the Moon": type_byte = cvcotm_item_info[loc.item.name].code >> 8 # If the Item is for this player, set the subtype to actually be that Item. # Otherwise, set a dummy subtype value that is different for every item type. if loc.item.player == world.player: subtype_byte = cvcotm_item_info[loc.item.name].code & 0xFF else: subtype_byte = other_player_subtype_bytes[type_byte] # If it's a DSS Card, set the appearance based on whether it's progression or not; freeze combo cards should # all appear blue in color while the others are standard purple/yellow. Otherwise, set the appearance the # same way as the subtype for local items regardless of whether it's actually local or not. if type_byte == 0xE6: if loc.item.advancement: appearance_byte = 1 else: appearance_byte = 0 else: appearance_byte = cvcotm_item_info[loc.item.name].code & 0xFF # If it's not a CotM Item at all, always set the primary type to that of a Magic Item and the subtype to that of # a dummy item. The AP Items are all under Magic Items. else: type_byte = 0xE8 subtype_byte = 0x0A # Decide which AP Item to use to represent the other game item. if loc.item.classification & ItemClassification.progression and \ loc.item.classification & ItemClassification.useful: appearance_byte = 0x0E # Progression + Useful elif loc.item.classification & ItemClassification.progression: appearance_byte = 0x0C # Progression elif loc.item.classification & ItemClassification.useful: appearance_byte = 0x0B # Useful elif loc.item.classification & ItemClassification.trap: appearance_byte = 0x0D # Trap else: appearance_byte = 0x0A # Filler # Check if the Item's game is in the other game item appearances' dict, and if so, if the Item is under that # game's name. If it is, change the appearance accordingly. # Right now, only SotN and Timespinner stat ups are supported. other_game_name = world.multiworld.worlds[loc.item.player].game if other_game_name in other_game_item_appearances: if loc.item.name in other_game_item_appearances[other_game_name]: type_byte = other_game_item_appearances[other_game_name][loc.item.name]["type"] subtype_byte = other_player_subtype_bytes[type_byte] appearance_byte = other_game_item_appearances[other_game_name][loc.item.name]["appearance"] # Create the correct bytes object for the Item on that Location. location_bytes[cvcotm_location_info[loc.name].offset] = bytes([type_byte, 1, subtype_byte, appearance_byte]) return location_bytes def populate_enemy_drops(world: "CVCotMWorld") -> Dict[int, bytes]: """Randomizes the enemy-dropped items throughout the game within each other. There are three tiers of item drops: Low, Mid, and High. Each enemy has two item slots that can both drop its own item; a Common slot and a Rare one. On Normal item randomization, easy enemies (below 61 HP) will only have Low-tier drops in both of their stats, bosses and candle enemies will be guaranteed to have High drops in one or both of their slots respectively (bosses are made to only drop one slot 100% of the time), and everything else can have a Low or Mid-tier item in its Common drop slot and a Low, Mid, OR High-tier item in its Rare drop slot. If Item Drop Randomization is set to Tiered, the HP threshold for enemies being considered "easily" will raise to below 144, enemies in the 144-369 HP range (inclusive) will have a Low-tier item in its Common slot and a Mid-tier item in its rare slot, and enemies with more than 369 HP will have a Mid-tier in its Common slot and a High-tier in its Rare slot. Candles and bosses still have Rares in all their slots, but now the guaranteed drops that land on bosses will be exclusive to them; no other enemy in the game will have their item. This and select_drop are the most directly adapted code from upstream CotMR in this package by far. Credit where it's due to Spooky for writing the original, and Malaert64 for further refinements and updating what used to be Random Item Hardmode to instead be Tiered Item Mode. The original C code this was adapted from can be found here: https://github.com/calm-palm/cotm-randomizer/blob/master/Program/randomizer.c#L1028""" placed_low_items = [0] * len(LOW_ITEMS) placed_mid_items = [0] * len(MID_ITEMS) placed_high_items = [0] * len(HIGH_ITEMS) placed_common_items = [0] * len(COMMON_ITEMS) placed_rare_items = [0] * len(RARE_ITEMS) regular_drops = [0] * len(cvcotm_enemy_info) regular_drop_chances = [0] * len(cvcotm_enemy_info) rare_drops = [0] * len(cvcotm_enemy_info) rare_drop_chances = [0] * len(cvcotm_enemy_info) # Set boss items first to prevent boss drop duplicates. # If Tiered mode is enabled, make these items exclusive to these enemies by adding an arbitrary integer larger # than could be reached normally (e.g.the total number of enemies) and use the placed high items array instead of # the placed rare items one. if world.options.item_drop_randomization == ItemDropRandomization.option_tiered: for boss_id in BOSS_IDS: regular_drops[boss_id] = select_drop(world, HIGH_ITEMS, placed_high_items, True) else: for boss_id in BOSS_IDS: regular_drops[boss_id] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS)) # Setting drop logic for all enemies. for i in range(len(cvcotm_enemy_info)): # Give Dracula II Shining Armor occasionally as a joke. if cvcotm_enemy_info[i].type == "final boss": regular_drops[i] = rare_drops[i] = ITEM_ID_SHINNING_ARMOR regular_drop_chances[i] = rare_drop_chances[i] = 5000 # Set bosses' secondary item to none since we already set the primary item earlier. elif cvcotm_enemy_info[i].type == "boss": # Set rare drop to none. rare_drops[i] = 0 # Max out rare boss drops (normally, drops are capped to 50% and 25% for common and rare respectively, but # Fuse's patch AllowAlwaysDrop.ips allows setting the regular item drop chance to 10000 to force a drop # always) regular_drop_chances[i] = 10000 rare_drop_chances[i] = 0 # Candle enemies use a similar placement logic to the bosses, except items that land on them are NOT exclusive # to them on Tiered mode. elif cvcotm_enemy_info[i].type == "candle": if world.options.item_drop_randomization == ItemDropRandomization.option_tiered: regular_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items) rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items) else: regular_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS)) rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS)) # Set base drop chances at 20-30% for common and 15-20% for rare. regular_drop_chances[i] = 2000 + world.random.randint(0, 1000) rare_drop_chances[i] = 1500 + world.random.randint(0, 500) # On All Bosses and Battle Arena Required, the Shinning Armor at the end of Battle Arena is removed. # We compensate for this by giving the Battle Arena Devil a 100% chance to drop Shinning Armor. elif cvcotm_enemy_info[i].name == "Devil" and cvcotm_enemy_info[i].type == "battle arena" and \ world.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena: regular_drops[i] = ITEM_ID_SHINNING_ARMOR rare_drops[i] = 0 regular_drop_chances[i] = 10000 rare_drop_chances[i] = 0 # Low-tier items drop from enemies that are trivial to farm (60 HP or less) # on Normal drop logic, or enemies under 144 HP on Tiered logic. elif (world.options.item_drop_randomization == ItemDropRandomization.option_normal and cvcotm_enemy_info[i].hp <= 60) or \ (world.options.item_drop_randomization == ItemDropRandomization.option_tiered and cvcotm_enemy_info[i].hp <= 143): # Low-tier enemy drops. regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items) rare_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items) # Set base drop chances at 6-10% for common and 3-6% for rare. regular_drop_chances[i] = 600 + world.random.randint(0, 400) rare_drop_chances[i] = 300 + world.random.randint(0, 300) # Rest of Tiered logic, by Malaert64. elif world.options.item_drop_randomization == ItemDropRandomization.option_tiered: # If under 370 HP, mid-tier enemy. if cvcotm_enemy_info[i].hp <= 369: regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items) rare_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items) # Otherwise, enemy HP is 370+, thus high-tier enemy. else: regular_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items) rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items) # Set base drop chances at 6-10% for common and 3-6% for rare. regular_drop_chances[i] = 600 + world.random.randint(0, 400) rare_drop_chances[i] = 300 + world.random.randint(0, 300) # Regular enemies outside Tiered logic. else: # Select a random regular and rare drop for every enemy from their respective lists. regular_drops[i] = select_drop(world, COMMON_ITEMS, placed_common_items) rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items) # Set base drop chances at 6-10% for common and 3-6% for rare. regular_drop_chances[i] = 600 + world.random.randint(0, 400) rare_drop_chances[i] = 300 + world.random.randint(0, 300) # Return the randomized drop data as bytes with their respective offsets. enemy_address = ENEMY_TABLE_START drop_data = {} for i, enemy_info in enumerate(cvcotm_enemy_info): drop_data[enemy_address] = bytes([regular_drops[i], 0, regular_drop_chances[i] & 0xFF, regular_drop_chances[i] >> 8, rare_drops[i], 0, rare_drop_chances[i] & 0xFF, rare_drop_chances[i] >> 8]) enemy_address += 20 return drop_data def select_drop(world: "CVCotMWorld", drop_list: List[int], drops_placed: List[int], exclusive_drop: bool = False, start_index: int = 0) -> int: """Chooses a drop from a given list of drops based on another given list of how many drops from that list were selected before. In order to ensure an even number of drops are distributed, drops that were selected the least are the ones that will be picked from. Calling this with exclusive_drop param being True will force the number of the chosen item really high to ensure it will never be picked again.""" # Take the list of placed item drops beginning from the starting index. drops_from_start_index = drops_placed[start_index:] # Determine the lowest drop counts and the indices with that drop count. lowest_number = min(drops_from_start_index) indices_with_lowest_number = [index for index, placed in enumerate(drops_from_start_index) if placed == lowest_number] random_index = world.random.choice(indices_with_lowest_number) random_index += start_index # Add start_index back on # Increment the number of this item placed, unless it should be exclusive to the boss / candle, in which case # set it to an arbitrarily large number to make it exclusive. if exclusive_drop: drops_placed[random_index] += 999 else: drops_placed[random_index] += 1 # Return the in-game item ID of the chosen item. return drop_list[random_index] def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bool]: """Calculate and return the starting inventory arrays. Different items go into different arrays, so they all have to be handled accordingly.""" start_inventory_data = {} magic_items_array = [0 for _ in range(8)] cards_array = [0 for _ in range(20)] extra_stats = {"extra health": 0, "extra magic": 0, "extra hearts": 0} start_with_detonator = False # If the Iron Maiden Behavior option is set to Start Broken, consider ourselves starting with the Maiden Detonator. if world.options.iron_maiden_behavior == IronMaidenBehavior.option_start_broken: start_with_detonator = True # Always start with the Dash Boots. magic_items_array[0] = 1 for item in world.multiworld.precollected_items[world.player]: array_offset = item.code & 0xFF # If it's a Maiden Detonator we're starting with, set the boolean for it to True. if item.name == iname.ironmaidens: start_with_detonator = True # If it's a Max Up we're starting with, check if increasing the extra amount of that stat will put us over the # max amount of the stat allowed. If it will, set the current extra amount to the max. Otherwise, increase it by # the amount that it should. elif "Max Up" in item.name: info = extra_starting_stat_info[item.name] if extra_stats[info["variable"]] + info["amount_per"] > info["max_allowed"]: extra_stats[info["variable"]] = info["max_allowed"] else: extra_stats[info["variable"]] += info["amount_per"] # If it's a DSS card we're starting with, set that card's value in the cards array. elif "Card" in item.name: cards_array[array_offset] = 1 # If it's none of the above, it has to be a regular Magic Item. # Increase that Magic Item's value in the Magic Items array if it's not greater than 240. Last Keys are the only # Magic Item wherein having more than one is relevant. else: # Decrease the Magic Item array offset by 1 if it's higher than the unused Map's item value. if array_offset > 5: array_offset -= 1 if magic_items_array[array_offset] < 240: magic_items_array[array_offset] += 1 # Add the start inventory arrays to the offset data in bytes form. start_inventory_data[0x680080] = bytes(magic_items_array) start_inventory_data[0x6800A0] = bytes(cards_array) # Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max # possible Max Ups. # Vampire Killer start_inventory_data[0xE08C6] = int.to_bytes(100 + extra_stats["extra health"], 2, "little") start_inventory_data[0xE08CE] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little") start_inventory_data[0xE08D4] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") # Magician start_inventory_data[0xE090E] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") start_inventory_data[0xE0916] = int.to_bytes(400 + extra_stats["extra magic"], 2, "little") start_inventory_data[0xE091C] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") # Fighter start_inventory_data[0xE0932] = int.to_bytes(200 + extra_stats["extra health"], 2, "little") start_inventory_data[0xE093A] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little") start_inventory_data[0xE0940] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") # Shooter start_inventory_data[0xE0832] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") start_inventory_data[0xE08F2] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little") start_inventory_data[0xE08F8] = int.to_bytes(250 + extra_stats["extra hearts"], 2, "little") # Thief start_inventory_data[0xE0956] = int.to_bytes(50 + extra_stats["extra health"], 2, "little") start_inventory_data[0xE095E] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little") start_inventory_data[0xE0964] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little") return start_inventory_data, start_with_detonator