Archipelago/worlds/cv64/items.py

215 lines
12 KiB
Python
Raw Normal View History

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