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
|