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
 | 
