215 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			215 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from BaseClasses import Item | ||
|  | from .data import iname | ||
|  | from .locations import base_id, get_location_info | ||
|  | from .options import DraculasCondition, SpareKeys | ||
|  | 
 | ||
|  | from typing import TYPE_CHECKING, Dict, Union | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from . import CV64World | ||
|  | 
 | ||
|  | import math | ||
|  | 
 | ||
|  | 
 | ||
|  | class CV64Item(Item): | ||
|  |     game: str = "Castlevania 64" | ||
|  | 
 | ||
|  | 
 | ||
|  | # # #    KEY    # # # | ||
|  | # "code" = The unique part of the Item's AP code attribute, as well as the value to call the in-game "prepare item | ||
|  | #          textbox" function with to give the Item in-game. Add this + base_id to get the actual AP code. | ||
|  | # "default classification" = The AP Item Classification that gets assigned to instances of that Item in create_item | ||
|  | #                            by default, unless I deliberately override it (as is the case for some Special1s). | ||
|  | # "inventory offset" = What offset from the start of the in-game inventory array (beginning at 0x80389C4B) stores the | ||
|  | #                      current count for that Item. Used for start inventory purposes. | ||
|  | # "pickup actor id" = The ID for the Item's in-game Item pickup actor. If it's not in the Item's data dict, it's the | ||
|  | #                     same as the Item's code. This is what gets written in the ROM to replace non-NPC/shop items. | ||
|  | # "sub equip id" = For sub-weapons specifically, this is the number to put in the game's "current sub-weapon" value to | ||
|  | #                  indicate the player currently having that weapon. Used for start inventory purposes. | ||
|  | item_info = { | ||
|  |     # White jewel | ||
|  |     iname.red_jewel_s:        {"code": 0x02,  "default classification": "filler"}, | ||
|  |     iname.red_jewel_l:        {"code": 0x03,  "default classification": "filler"}, | ||
|  |     iname.special_one:        {"code": 0x04,  "default classification": "progression_skip_balancing", | ||
|  |                                "inventory offset": 0}, | ||
|  |     iname.special_two:        {"code": 0x05,  "default classification": "progression_skip_balancing", | ||
|  |                                "inventory offset": 1}, | ||
|  |     iname.roast_chicken:      {"code": 0x06,  "default classification": "filler", "inventory offset": 2}, | ||
|  |     iname.roast_beef:         {"code": 0x07,  "default classification": "filler", "inventory offset": 3}, | ||
|  |     iname.healing_kit:        {"code": 0x08,  "default classification": "useful", "inventory offset": 4}, | ||
|  |     iname.purifying:          {"code": 0x09,  "default classification": "filler", "inventory offset": 5}, | ||
|  |     iname.cure_ampoule:       {"code": 0x0A,  "default classification": "filler", "inventory offset": 6}, | ||
|  |     # pot-pourri | ||
|  |     iname.powerup:            {"code": 0x0C,  "default classification": "filler"}, | ||
|  |     iname.permaup:            {"code": 0x10C, "default classification": "useful", "pickup actor id": 0x0C, | ||
|  |                                "inventory offset": 8}, | ||
|  |     iname.knife:              {"code": 0x0D,  "default classification": "filler", "pickup actor id": 0x10, | ||
|  |                                "sub equip id": 1}, | ||
|  |     iname.holy_water:         {"code": 0x0E,  "default classification": "filler", "pickup actor id": 0x0D, | ||
|  |                                "sub equip id": 2}, | ||
|  |     iname.cross:              {"code": 0x0F,  "default classification": "filler", "pickup actor id": 0x0E, | ||
|  |                                "sub equip id": 3}, | ||
|  |     iname.axe:                {"code": 0x10,  "default classification": "filler", "pickup actor id": 0x0F, | ||
|  |                                "sub equip id": 4}, | ||
|  |     # Wooden stake (AP item) | ||
|  |     iname.ice_trap:           {"code": 0x12,  "default classification": "trap"}, | ||
|  |     # The contract | ||
|  |     # engagement ring | ||
|  |     iname.magical_nitro:      {"code": 0x15,  "default classification": "progression", "inventory offset": 17}, | ||
|  |     iname.mandragora:         {"code": 0x16,  "default classification": "progression", "inventory offset": 18}, | ||
|  |     iname.sun_card:           {"code": 0x17,  "default classification": "filler", "inventory offset": 19}, | ||
|  |     iname.moon_card:          {"code": 0x18,  "default classification": "filler", "inventory offset": 20}, | ||
|  |     # Incandescent gaze | ||
|  |     iname.archives_key:       {"code": 0x1A,  "default classification": "progression", "pickup actor id": 0x1D, | ||
|  |                                "inventory offset": 22}, | ||
|  |     iname.left_tower_key:     {"code": 0x1B,  "default classification": "progression", "pickup actor id": 0x1E, | ||
|  |                                "inventory offset": 23}, | ||
|  |     iname.storeroom_key:      {"code": 0x1C,  "default classification": "progression", "pickup actor id": 0x1F, | ||
|  |                                "inventory offset": 24}, | ||
|  |     iname.garden_key:         {"code": 0x1D,  "default classification": "progression", "pickup actor id": 0x20, | ||
|  |                                "inventory offset": 25}, | ||
|  |     iname.copper_key:         {"code": 0x1E,  "default classification": "progression", "pickup actor id": 0x21, | ||
|  |                                "inventory offset": 26}, | ||
|  |     iname.chamber_key:        {"code": 0x1F,  "default classification": "progression", "pickup actor id": 0x22, | ||
|  |                                "inventory offset": 27}, | ||
|  |     iname.execution_key:      {"code": 0x20,  "default classification": "progression", "pickup actor id": 0x23, | ||
|  |                                "inventory offset": 28}, | ||
|  |     iname.science_key1:       {"code": 0x21,  "default classification": "progression", "pickup actor id": 0x24, | ||
|  |                                "inventory offset": 29}, | ||
|  |     iname.science_key2:       {"code": 0x22,  "default classification": "progression", "pickup actor id": 0x25, | ||
|  |                                "inventory offset": 30}, | ||
|  |     iname.science_key3:       {"code": 0x23,  "default classification": "progression", "pickup actor id": 0x26, | ||
|  |                                "inventory offset": 31}, | ||
|  |     iname.clocktower_key1:    {"code": 0x24,  "default classification": "progression", "pickup actor id": 0x27, | ||
|  |                                "inventory offset": 32}, | ||
|  |     iname.clocktower_key2:    {"code": 0x25,  "default classification": "progression", "pickup actor id": 0x28, | ||
|  |                                "inventory offset": 33}, | ||
|  |     iname.clocktower_key3:    {"code": 0x26,  "default classification": "progression", "pickup actor id": 0x29, | ||
|  |                                "inventory offset": 34}, | ||
|  |     iname.five_hundred_gold:  {"code": 0x27,  "default classification": "filler", "pickup actor id": 0x1A}, | ||
|  |     iname.three_hundred_gold: {"code": 0x28,  "default classification": "filler", "pickup actor id": 0x1B}, | ||
|  |     iname.one_hundred_gold:   {"code": 0x29,  "default classification": "filler", "pickup actor id": 0x1C}, | ||
|  |     iname.crystal:            {"default classification": "progression"}, | ||
|  |     iname.trophy:             {"default classification": "progression"}, | ||
|  |     iname.victory:            {"default classification": "progression"} | ||
|  | } | ||
|  | 
 | ||
|  | filler_item_names = [iname.red_jewel_s, iname.red_jewel_l, iname.five_hundred_gold, iname.three_hundred_gold, | ||
|  |                      iname.one_hundred_gold] | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_item_info(item: str, info: str) -> Union[str, int, None]: | ||
|  |     return item_info[item].get(info, None) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_item_names_to_ids() -> Dict[str, int]: | ||
|  |     return {name: get_item_info(name, "code")+base_id for name in item_info if get_item_info(name, "code") is not None} | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_item_counts(world: "CV64World") -> Dict[str, Dict[str, int]]: | ||
|  | 
 | ||
|  |     active_locations = world.multiworld.get_unfilled_locations(world.player) | ||
|  | 
 | ||
|  |     item_counts = { | ||
|  |         "progression": {}, | ||
|  |         "progression_skip_balancing": {}, | ||
|  |         "useful": {}, | ||
|  |         "filler": {}, | ||
|  |         "trap": {} | ||
|  |     } | ||
|  |     total_items = 0 | ||
|  |     extras_count = 0 | ||
|  | 
 | ||
|  |     # Get from each location its vanilla item and add it to the default item counts. | ||
|  |     for loc in active_locations: | ||
|  |         if loc.address is None: | ||
|  |             continue | ||
|  | 
 | ||
|  |         if world.options.hard_item_pool and get_location_info(loc.name, "hard item") is not None: | ||
|  |             item_to_add = get_location_info(loc.name, "hard item") | ||
|  |         else: | ||
|  |             item_to_add = get_location_info(loc.name, "normal item") | ||
|  | 
 | ||
|  |         classification = get_item_info(item_to_add, "default classification") | ||
|  | 
 | ||
|  |         if item_to_add not in item_counts[classification]: | ||
|  |             item_counts[classification][item_to_add] = 1 | ||
|  |         else: | ||
|  |             item_counts[classification][item_to_add] += 1 | ||
|  |         total_items += 1 | ||
|  | 
 | ||
|  |     # Replace all but 2 PowerUps with junk if Permanent PowerUps is on and mark those two PowerUps as Useful. | ||
|  |     if world.options.permanent_powerups: | ||
|  |         for i in range(item_counts["filler"][iname.powerup] - 2): | ||
|  |             item_counts["filler"][world.get_filler_item_name()] += 1 | ||
|  |         del(item_counts["filler"][iname.powerup]) | ||
|  |         item_counts["useful"][iname.permaup] = 2 | ||
|  | 
 | ||
|  |     # Add the total Special1s. | ||
|  |     item_counts["progression_skip_balancing"][iname.special_one] = world.options.total_special1s.value | ||
|  |     extras_count += world.options.total_special1s.value | ||
|  | 
 | ||
|  |     # Add the total Special2s if Dracula's Condition is Special2s. | ||
|  |     if world.options.draculas_condition == DraculasCondition.option_specials: | ||
|  |         item_counts["progression_skip_balancing"][iname.special_two] = world.options.total_special2s.value | ||
|  |         extras_count += world.options.total_special2s.value | ||
|  | 
 | ||
|  |     # Determine the extra key counts if applicable. Doing this before moving Special1s will ensure only the keys and | ||
|  |     # bomb components are affected by this. | ||
|  |     for key in item_counts["progression"]: | ||
|  |         spare_keys = 0 | ||
|  |         if world.options.spare_keys == SpareKeys.option_on: | ||
|  |             spare_keys = item_counts["progression"][key] | ||
|  |         elif world.options.spare_keys == SpareKeys.option_chance: | ||
|  |             if item_counts["progression"][key] > 0: | ||
|  |                 for i in range(item_counts["progression"][key]): | ||
|  |                     spare_keys += world.random.randint(0, 1) | ||
|  |         item_counts["progression"][key] += spare_keys | ||
|  |         extras_count += spare_keys | ||
|  | 
 | ||
|  |     # Move the total number of Special1s needed to warp everywhere to normal progression balancing if S1s per warp is | ||
|  |     # 3 or lower. | ||
|  |     if world.s1s_per_warp <= 3: | ||
|  |         item_counts["progression_skip_balancing"][iname.special_one] -= world.s1s_per_warp * 7 | ||
|  |         item_counts["progression"][iname.special_one] = world.s1s_per_warp * 7 | ||
|  | 
 | ||
|  |     # Determine the total amounts of replaceable filler and non-filler junk. | ||
|  |     total_filler_junk = 0 | ||
|  |     total_non_filler_junk = 0 | ||
|  |     for junk in item_counts["filler"]: | ||
|  |         if junk in filler_item_names: | ||
|  |             total_filler_junk += item_counts["filler"][junk] | ||
|  |         else: | ||
|  |             total_non_filler_junk += item_counts["filler"][junk] | ||
|  | 
 | ||
|  |     # Subtract from the filler counts total number of "extra" items we've added. get_filler_item_name() filler will be | ||
|  |     # subtracted from first until we run out of that, at which point we'll start subtracting from the rest. At this | ||
|  |     # moment, non-filler item name filler cannot run out no matter the settings, so I haven't bothered adding handling | ||
|  |     # for when it does yet. | ||
|  |     available_filler_junk = filler_item_names.copy() | ||
|  |     for i in range(extras_count): | ||
|  |         if total_filler_junk > 0: | ||
|  |             total_filler_junk -= 1 | ||
|  |             item_to_subtract = world.random.choice(available_filler_junk) | ||
|  |         else: | ||
|  |             total_non_filler_junk -= 1 | ||
|  |             item_to_subtract = world.random.choice(list(item_counts["filler"].keys())) | ||
|  | 
 | ||
|  |         item_counts["filler"][item_to_subtract] -= 1 | ||
|  |         if item_counts["filler"][item_to_subtract] == 0: | ||
|  |             del(item_counts["filler"][item_to_subtract]) | ||
|  |             if item_to_subtract in available_filler_junk: | ||
|  |                 available_filler_junk.remove(item_to_subtract) | ||
|  | 
 | ||
|  |     # Determine the Ice Trap count by taking a certain % of the total filler remaining at this point. | ||
|  |     item_counts["trap"][iname.ice_trap] = math.floor((total_filler_junk + total_non_filler_junk) * | ||
|  |                                                      (world.options.ice_trap_percentage.value / 100.0)) | ||
|  |     for i in range(item_counts["trap"][iname.ice_trap]): | ||
|  |         # Subtract the remaining filler after determining the ice trap count. | ||
|  |         item_to_subtract = world.random.choice(list(item_counts["filler"].keys())) | ||
|  |         item_counts["filler"][item_to_subtract] -= 1 | ||
|  |         if item_counts["filler"][item_to_subtract] == 0: | ||
|  |             del (item_counts["filler"][item_to_subtract]) | ||
|  | 
 | ||
|  |     return item_counts |