* Add the cotm package with working seed playthrough generation. * Add the proper event flag IDs for the Item codes. * Oooops. Put the world completion condition in! * Adjust the game name and abbreviations. * Implement more settings. * Account for too many start_inventory_from_pool cards with Halve DSS Cards Placed. * Working (albeit very sloooooooooooow) ROM patching. * Screw you, bsdiff! AP Procedure Patch for life! * Nuke stage_assert_generate as the ROM is no longer needed for that. * Working item writing and position adjusting. * Fix the magic item graphics in Locations wherein they can be fixed. * Enable sub-weapon shuffle * Get the seed display working. * Get the enemy item drop randomization working. Phew! * Enemy drop rando and seed display fixes. * Functional Countdown + Early Double setting * Working multiworld (yay!) * Fix item links and demo shenanigans. * Add Wii U VC hash and a docs section explaining the rereleases. * Change all client read/writes to EWRAM instead of Combined WRAM. * Custom text insertion foundations. * Working text converter and word wrap detector. * More refinements to the text wrap system. * Well and truly working sent/received messages. * Add DeathLink and Battle Arena goal options. * Add tracker stuff, unittests, all locations countdown, presets. * Add to README, CODEOWNERS, and inno_setup * Add to README, CODEOWNERS, and inno_setup * Address some suggestions/problems. * Switch the Items and Locations to using dataclasses. * Add note about the alternate classes to the Game Page. * Oooops, typo! * Touch up the Options descriptions. * Fix Battle Arena flag being detected incorrectly on connection and name the locked location/item pairs better. * Implement option groups * Swap the Lizard-man Locations into their correct Regions. * Local start inventory, better DeathLink message handling, handle receiving over 255 of an item. * Update the PopTracker pack links to no longer point to the Releases page. * Add Skip Dialogues option. * Update the presets for the accessibility rework. * Swap the choices in the accessibility preset options. * Uhhhhhhh...just see the apworld v4 changelog for this one. * Ooops, typo! * . * Bunch of small stuff * Correctly change "Fake" to "Breakable" in this comment. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make can_touch_water one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make broke_iron_maidens one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix majors countdown and make can_open_ceremonial_door one line. * Make the Trap AP Item less obvious. * Add Progression + Useful stuff, patcher handling for incompatible versions, and fix some mypy stuff. * Better option groups. * Change Early Double to Early Escape Item. * Update DeathLink description and ditch the Menu region. * Fix the Start Broken choice for Iron Maiden Behavior * Remove the forced option change with Arena goal + required All Bosses and Arena. * Update the Game Page with the removal of the forced option combination change. * Fix client potential to send packets nonstop. * More review addressing. * Fix the new select_drop code. * Fix the new select_drop code for REAL this time. * Send another LocationScout if we send Location checks without having the Location info. --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
		
			
				
	
	
		
			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
 |