212 lines
11 KiB
Python
212 lines
11 KiB
Python
|
import logging
|
||
|
|
||
|
from BaseClasses import Item, ItemClassification
|
||
|
from .data import iname
|
||
|
from .locations import BASE_ID
|
||
|
from .options import IronMaidenBehavior
|
||
|
|
||
|
from typing import TYPE_CHECKING, Dict, NamedTuple, Optional
|
||
|
from collections import Counter
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from . import CVCotMWorld
|
||
|
|
||
|
|
||
|
class CVCotMItem(Item):
|
||
|
game: str = "Castlevania - Circle of the Moon"
|
||
|
|
||
|
|
||
|
class CVCotMItemData(NamedTuple):
|
||
|
code: Optional[int]
|
||
|
text_id: Optional[bytes]
|
||
|
default_classification: ItemClassification
|
||
|
tutorial_id: Optional[bytes] = None
|
||
|
# "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.
|
||
|
# "text_id" = The textbox ID for the vanilla message for receiving the Item. Used when receiving an Item through the
|
||
|
# client that was not sent by a different player.
|
||
|
# "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 the Cleansing on the
|
||
|
# Ignore Cleansing option).
|
||
|
# "tutorial_id" = The textbox ID for the item's tutorial. Used by the client if tutorials are not skipped.
|
||
|
|
||
|
|
||
|
cvcotm_item_info: Dict[str, CVCotMItemData] = {
|
||
|
iname.heart_max: CVCotMItemData(0xE400, b"\x57\x81", ItemClassification.filler),
|
||
|
iname.hp_max: CVCotMItemData(0xE401, b"\x55\x81", ItemClassification.filler),
|
||
|
iname.mp_max: CVCotMItemData(0xE402, b"\x56\x81", ItemClassification.filler),
|
||
|
iname.salamander: CVCotMItemData(0xE600, b"\x1E\x82", ItemClassification.useful),
|
||
|
iname.serpent: CVCotMItemData(0xE601, b"\x1F\x82", ItemClassification.useful |
|
||
|
ItemClassification.progression),
|
||
|
iname.mandragora: CVCotMItemData(0xE602, b"\x20\x82", ItemClassification.useful),
|
||
|
iname.golem: CVCotMItemData(0xE603, b"\x21\x82", ItemClassification.useful),
|
||
|
iname.cockatrice: CVCotMItemData(0xE604, b"\x22\x82", ItemClassification.useful |
|
||
|
ItemClassification.progression),
|
||
|
iname.manticore: CVCotMItemData(0xE605, b"\x23\x82", ItemClassification.useful),
|
||
|
iname.griffin: CVCotMItemData(0xE606, b"\x24\x82", ItemClassification.useful),
|
||
|
iname.thunderbird: CVCotMItemData(0xE607, b"\x25\x82", ItemClassification.useful),
|
||
|
iname.unicorn: CVCotMItemData(0xE608, b"\x26\x82", ItemClassification.useful),
|
||
|
iname.black_dog: CVCotMItemData(0xE609, b"\x27\x82", ItemClassification.useful),
|
||
|
iname.mercury: CVCotMItemData(0xE60A, b"\x28\x82", ItemClassification.useful |
|
||
|
ItemClassification.progression),
|
||
|
iname.venus: CVCotMItemData(0xE60B, b"\x29\x82", ItemClassification.useful),
|
||
|
iname.jupiter: CVCotMItemData(0xE60C, b"\x2A\x82", ItemClassification.useful),
|
||
|
iname.mars: CVCotMItemData(0xE60D, b"\x2B\x82", ItemClassification.useful |
|
||
|
ItemClassification.progression),
|
||
|
iname.diana: CVCotMItemData(0xE60E, b"\x2C\x82", ItemClassification.useful),
|
||
|
iname.apollo: CVCotMItemData(0xE60F, b"\x2D\x82", ItemClassification.useful),
|
||
|
iname.neptune: CVCotMItemData(0xE610, b"\x2E\x82", ItemClassification.useful),
|
||
|
iname.saturn: CVCotMItemData(0xE611, b"\x2F\x82", ItemClassification.useful),
|
||
|
iname.uranus: CVCotMItemData(0xE612, b"\x30\x82", ItemClassification.useful),
|
||
|
iname.pluto: CVCotMItemData(0xE613, b"\x31\x82", ItemClassification.useful),
|
||
|
# Dash Boots
|
||
|
iname.double: CVCotMItemData(0xE801, b"\x59\x81", ItemClassification.useful |
|
||
|
ItemClassification.progression, b"\xF4\x84"),
|
||
|
iname.tackle: CVCotMItemData(0xE802, b"\x5A\x81", ItemClassification.progression, b"\xF5\x84"),
|
||
|
iname.kick_boots: CVCotMItemData(0xE803, b"\x5B\x81", ItemClassification.progression, b"\xF6\x84"),
|
||
|
iname.heavy_ring: CVCotMItemData(0xE804, b"\x5C\x81", ItemClassification.progression, b"\xF7\x84"),
|
||
|
# Map
|
||
|
iname.cleansing: CVCotMItemData(0xE806, b"\x5D\x81", ItemClassification.progression, b"\xF8\x84"),
|
||
|
iname.roc_wing: CVCotMItemData(0xE807, b"\x5E\x81", ItemClassification.useful |
|
||
|
ItemClassification.progression, b"\xF9\x84"),
|
||
|
iname.last_key: CVCotMItemData(0xE808, b"\x5F\x81", ItemClassification.progression_skip_balancing,
|
||
|
b"\xFA\x84"),
|
||
|
iname.ironmaidens: CVCotMItemData(0xE809, b"\xF1\x84", ItemClassification.progression),
|
||
|
iname.dracula: CVCotMItemData(None, None, ItemClassification.progression),
|
||
|
iname.shinning_armor: CVCotMItemData(None, None, ItemClassification.progression),
|
||
|
}
|
||
|
|
||
|
ACTION_CARDS = {iname.mercury, iname.venus, iname.jupiter, iname.mars, iname.diana, iname.apollo, iname.neptune,
|
||
|
iname.saturn, iname.uranus, iname.pluto}
|
||
|
|
||
|
ATTRIBUTE_CARDS = {iname.salamander, iname.serpent, iname.mandragora, iname.golem, iname.cockatrice, iname.griffin,
|
||
|
iname.manticore, iname.thunderbird, iname.unicorn, iname.black_dog}
|
||
|
|
||
|
FREEZE_ACTIONS = [iname.mercury, iname.mars]
|
||
|
FREEZE_ATTRS = [iname.serpent, iname.cockatrice]
|
||
|
|
||
|
FILLER_ITEM_NAMES = [iname.heart_max, iname.hp_max, iname.mp_max]
|
||
|
|
||
|
MAJORS_CLASSIFICATIONS = ItemClassification.progression | ItemClassification.useful
|
||
|
|
||
|
|
||
|
def get_item_names_to_ids() -> Dict[str, int]:
|
||
|
return {name: cvcotm_item_info[name].code + BASE_ID for name in cvcotm_item_info
|
||
|
if cvcotm_item_info[name].code is not None}
|
||
|
|
||
|
|
||
|
def get_item_counts(world: "CVCotMWorld") -> Dict[ItemClassification, Dict[str, int]]:
|
||
|
|
||
|
item_counts: Dict[ItemClassification, Counter[str, int]] = {
|
||
|
ItemClassification.progression: Counter(),
|
||
|
ItemClassification.progression_skip_balancing: Counter(),
|
||
|
ItemClassification.useful | ItemClassification.progression: Counter(),
|
||
|
ItemClassification.useful: Counter(),
|
||
|
ItemClassification.filler: Counter(),
|
||
|
}
|
||
|
total_items = 0
|
||
|
# Items to be skipped over in the main Item creation loop.
|
||
|
excluded_items = [iname.hp_max, iname.mp_max, iname.heart_max, iname.last_key]
|
||
|
|
||
|
# If Halve DSS Cards Placed is on, determine which cards we will exclude here.
|
||
|
if world.options.halve_dss_cards_placed:
|
||
|
excluded_cards = list(ACTION_CARDS.union(ATTRIBUTE_CARDS))
|
||
|
|
||
|
has_freeze_action = False
|
||
|
has_freeze_attr = False
|
||
|
start_card_cap = 8
|
||
|
|
||
|
# Get out all cards from start_inventory_from_pool that the player isn't starting with 0 of.
|
||
|
start_cards = [item for item in world.options.start_inventory_from_pool.value if "Card" in item]
|
||
|
|
||
|
# Check for ice/stone cards that are in the player's starting cards. Increase the starting card capacity by 1
|
||
|
# for each card type satisfied.
|
||
|
for card in start_cards:
|
||
|
if card in FREEZE_ACTIONS and not has_freeze_action:
|
||
|
has_freeze_action = True
|
||
|
start_card_cap += 1
|
||
|
if card in FREEZE_ATTRS and not has_freeze_attr:
|
||
|
has_freeze_attr = True
|
||
|
start_card_cap += 1
|
||
|
|
||
|
# If we are over our starting card capacity, some starting cards will need to be removed...
|
||
|
if len(start_cards) > start_card_cap:
|
||
|
|
||
|
# Ice/stone cards will be kept no matter what. As for the others, put them in a list of possible candidates
|
||
|
# to remove.
|
||
|
kept_start_cards = []
|
||
|
removal_candidates = []
|
||
|
for card in start_cards:
|
||
|
if card in FREEZE_ACTIONS + FREEZE_ATTRS:
|
||
|
kept_start_cards.append(card)
|
||
|
else:
|
||
|
removal_candidates.append(card)
|
||
|
|
||
|
# Add a random sample of the removal candidate cards to our kept cards list.
|
||
|
kept_start_cards += world.random.sample(removal_candidates, start_card_cap - len(kept_start_cards))
|
||
|
|
||
|
# Make a list of the cards we are not keeping.
|
||
|
removed_start_cards = [card for card in removal_candidates if card not in kept_start_cards]
|
||
|
|
||
|
# Remove the cards we're not keeping from start_inventory_from_pool.
|
||
|
for card in removed_start_cards:
|
||
|
del world.options.start_inventory_from_pool.value[card]
|
||
|
|
||
|
logging.warning(f"[{world.player_name}] Too many DSS Cards in "
|
||
|
f"Start Inventory from Pool to satisfy the Halve DSS Cards Placed option. The following "
|
||
|
f"{len(removed_start_cards)} card(s) were removed: {removed_start_cards}")
|
||
|
|
||
|
start_cards = kept_start_cards
|
||
|
|
||
|
# Remove the starting cards from the excluded cards.
|
||
|
for card in ACTION_CARDS.union(ATTRIBUTE_CARDS):
|
||
|
if card in start_cards:
|
||
|
excluded_cards.remove(card)
|
||
|
|
||
|
# Remove a valid ice/stone action and/or attribute card if the player isn't starting with one.
|
||
|
if not has_freeze_action:
|
||
|
excluded_cards.remove(world.random.choice(FREEZE_ACTIONS))
|
||
|
if not has_freeze_attr:
|
||
|
excluded_cards.remove(world.random.choice(FREEZE_ATTRS))
|
||
|
|
||
|
# Remove 10 random cards from the exclusions.
|
||
|
excluded_items += world.random.sample(excluded_cards, 10)
|
||
|
|
||
|
# Exclude the Maiden Detonator from creation if the maidens start broken.
|
||
|
if world.options.iron_maiden_behavior == IronMaidenBehavior.option_start_broken:
|
||
|
excluded_items += [iname.ironmaidens]
|
||
|
|
||
|
# Add one of each Item to the pool that is not filler or progression skip balancing.
|
||
|
for item in cvcotm_item_info:
|
||
|
classification = cvcotm_item_info[item].default_classification
|
||
|
code = cvcotm_item_info[item].code
|
||
|
|
||
|
# Skip event Items and Items that are excluded from creation.
|
||
|
if code is None or item in excluded_items:
|
||
|
continue
|
||
|
|
||
|
# Classify the Cleansing as Useful instead of Progression if Ignore Cleansing is on.
|
||
|
if item == iname.cleansing and world.options.ignore_cleansing:
|
||
|
classification = ItemClassification.useful
|
||
|
|
||
|
# Classify the Kick Boots as Progression + Useful if Nerf Roc Wing is on.
|
||
|
if item == iname.kick_boots and world.options.nerf_roc_wing:
|
||
|
classification |= ItemClassification.useful
|
||
|
|
||
|
item_counts[classification][item] = 1
|
||
|
total_items += 1
|
||
|
|
||
|
# Add the total Last Keys if no skirmishes are required (meaning they're not forced anywhere).
|
||
|
if not world.options.required_skirmishes:
|
||
|
item_counts[ItemClassification.progression_skip_balancing][iname.last_key] = \
|
||
|
world.options.available_last_keys.value
|
||
|
total_items += world.options.available_last_keys.value
|
||
|
|
||
|
# Add filler items at random until the total Items = the total Locations.
|
||
|
while total_items < len(world.multiworld.get_unfilled_locations(world.player)):
|
||
|
filler_to_add = world.random.choice(FILLER_ITEM_NAMES)
|
||
|
item_counts[ItemClassification.filler][filler_to_add] += 1
|
||
|
total_items += 1
|
||
|
|
||
|
return item_counts
|