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
 |