forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
305 lines
19 KiB
Python
305 lines
19 KiB
Python
import logging
|
|
|
|
from BaseClasses import Item, ItemClassification
|
|
from .data import item_names
|
|
from .data.misc_names import GAME_NAME
|
|
from .locations import CVLOD_LOCATIONS_INFO
|
|
from .options import DraculasCondition, SpareKeys, CastleWallState, VillaState
|
|
from .data.enums import Items, Pickups
|
|
|
|
from typing import TYPE_CHECKING, NamedTuple
|
|
|
|
if TYPE_CHECKING:
|
|
from . import CVLoDWorld
|
|
|
|
|
|
class CVLoDItem(Item):
|
|
game: str = GAME_NAME
|
|
|
|
|
|
class CVLoDItemData(NamedTuple):
|
|
item_id: int
|
|
pickup_id: int
|
|
default_classification: ItemClassification = ItemClassification.filler
|
|
# "item_id" = The in-game ID to pass to the in-game "prepare item textbox" function to grant that Item to the player
|
|
# in-game amongst other stuff, as well as its AP Item ID.
|
|
# "pickup_id" = The in-game ID for the pickup actor version of the item. Sometimes this is the same as the regular
|
|
# item ID used everywhere else, other times it differs. Namely, the sub-weapon pickups are in a different
|
|
# order from their items, and the gold bag pickups are before the keys instead of after.
|
|
# "default_classification" = The AP Item Classification that gets assigned to instances of that Item in create_item
|
|
# by default, unless deliberately overridden.
|
|
|
|
|
|
ALL_CVLOD_ITEMS = {
|
|
item_names.jewel_rs: CVLoDItemData(Items.RED_JEWEL_S.value, Pickups.RED_JEWEL_S),
|
|
item_names.jewel_rl: CVLoDItemData(Items.RED_JEWEL_L.value, Pickups.RED_JEWEL_L),
|
|
item_names.special1: CVLoDItemData(Items.SPECIAL1.value, Pickups.SPECIAL1,
|
|
ItemClassification.progression_deprioritized_skip_balancing),
|
|
item_names.special2: CVLoDItemData(Items.SPECIAL2.value, Pickups.SPECIAL2,
|
|
ItemClassification.progression_deprioritized_skip_balancing),
|
|
item_names.use_chicken: CVLoDItemData(Items.ROAST_CHICKEN.value, Pickups.ROAST_CHICKEN),
|
|
item_names.use_beef: CVLoDItemData(Items.ROAST_BEEF.value, Pickups.ROAST_BEEF),
|
|
item_names.use_kit: CVLoDItemData(Items.HEALING_KIT.value, Pickups.HEALING_KIT,
|
|
ItemClassification.useful),
|
|
item_names.use_purifying: CVLoDItemData(Items.PURIFYING.value, Pickups.PURIFYING),
|
|
item_names.use_ampoule: CVLoDItemData(Items.CURE_AMPOULE.value, Pickups.CURE_AMPOULE),
|
|
item_names.powerup: CVLoDItemData(Items.POWERUP.value, Pickups.POWERUP),
|
|
item_names.permaup: CVLoDItemData(Items.POWERUP.value | 0x100, Pickups.POWERUP,
|
|
ItemClassification.useful),
|
|
item_names.sub_knife: CVLoDItemData(Items.KNIFE.value, Pickups.KNIFE),
|
|
item_names.sub_holy: CVLoDItemData(Items.HOLY_WATER.value, Pickups.HOLY_WATER),
|
|
item_names.sub_cross: CVLoDItemData(Items.CROSS.value, Pickups.CROSS),
|
|
item_names.sub_axe: CVLoDItemData(Items.AXE.value, Pickups.AXE),
|
|
item_names.quest_nitro: CVLoDItemData(Items.MAGICAL_NITRO.value, Pickups.MAGICAL_NITRO,
|
|
ItemClassification.progression),
|
|
item_names.quest_mandragora: CVLoDItemData(Items.MANDRAGORA.value, Pickups.MANDRAGORA,
|
|
ItemClassification.progression),
|
|
item_names.use_card_s: CVLoDItemData(Items.SUN_CARD.value, Pickups.SUN_CARD),
|
|
item_names.use_card_m: CVLoDItemData(Items.MOON_CARD.value, Pickups.MOON_CARD),
|
|
item_names.quest_winch: CVLoDItemData(Items.WINCH_LEVER.value, Pickups.WINCH_LEVER,
|
|
ItemClassification.progression),
|
|
item_names.quest_diary: CVLoDItemData(Items.OLDREYS_DIARY.value, Pickups.OLDREYS_DIARY,
|
|
ItemClassification.progression),
|
|
item_names.quest_crest_a: CVLoDItemData(Items.CREST_HALF_A.value, Pickups.CREST_HALF_A,
|
|
ItemClassification.progression),
|
|
item_names.quest_crest_b: CVLoDItemData(Items.CREST_HALF_B.value, Pickups.CREST_HALF_B,
|
|
ItemClassification.progression),
|
|
item_names.quest_brooch: CVLoDItemData(Items.ROSE_BROOCH.value, Pickups.ROSE_BROOCH,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_arch: CVLoDItemData(Items.ARCHIVES_KEY.value, Pickups.ARCHIVES_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_left: CVLoDItemData(Items.LEFT_TOWER_KEY.value, Pickups.LEFT_TOWER_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_store: CVLoDItemData(Items.STOREROOM_KEY.value, Pickups.STOREROOM_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_grdn: CVLoDItemData(Items.GARDEN_KEY.value, Pickups.GARDEN_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_cppr: CVLoDItemData(Items.COPPER_KEY.value, Pickups.COPPER_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_chbr: CVLoDItemData(Items.CHAMBER_KEY.value, Pickups.CHAMBER_KEY,
|
|
ItemClassification.progression),
|
|
#item_names.trap_ice: CVLoDItemData(Items.EXECUTION_KEY.value, Pickups.EXECUTION_KEY,
|
|
# ItemClassification.trap),
|
|
item_names.quest_key_deck: CVLoDItemData(Items.DECK_KEY.value, Pickups.DECK_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_rose: CVLoDItemData(Items.ROSE_GARDEN_KEY.value, Pickups.ROSE_GARDEN_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_thorn: CVLoDItemData(Items.THORN_KEY.value, Pickups.THORN_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_clock_c: CVLoDItemData(Items.CLOCKTOWER_KEY_C.value, Pickups.CLOCKTOWER_KEY_C,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_clock_d: CVLoDItemData(Items.CLOCKTOWER_KEY_D.value, Pickups.CLOCKTOWER_KEY_D,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_art_1: CVLoDItemData(Items.ART_TOWER_KEY_1.value, Pickups.ART_TOWER_KEY_1,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_art_2: CVLoDItemData(Items.ART_TOWER_KEY_2.value, Pickups.ART_TOWER_KEY_2,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_ctrl: CVLoDItemData(Items.CONTROL_ROOM_KEY.value, Pickups.CONTROL_ROOM_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_wall: CVLoDItemData(Items.WALL_KEY.value, Pickups.WALL_KEY,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_clock_e: CVLoDItemData(Items.CLOCKTOWER_KEY_E.value, Pickups.CLOCKTOWER_KEY_E,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_clock_a: CVLoDItemData(Items.CLOCKTOWER_KEY_A.value, Pickups.CLOCKTOWER_KEY_A,
|
|
ItemClassification.progression),
|
|
item_names.quest_key_clock_b: CVLoDItemData(Items.CLOCKTOWER_KEY_B.value, Pickups.CLOCKTOWER_KEY_B,
|
|
ItemClassification.progression),
|
|
item_names.gold_500: CVLoDItemData(Items.FIVE_HUNDRED_GOLD.value, Pickups.FIVE_HUNDRED_GOLD),
|
|
item_names.gold_300: CVLoDItemData(Items.THREE_HUNDRED_GOLD.value, Pickups.THREE_HUNDRED_GOLD),
|
|
item_names.gold_100: CVLoDItemData(Items.ONE_HUNDRED_GOLD.value, Pickups.ONE_HUNDRED_GOLD)
|
|
}
|
|
|
|
SUB_WEAPON_IDS: dict[str, int] = {item_names.sub_knife: 1,
|
|
item_names.sub_holy: 2,
|
|
item_names.sub_cross: 3,
|
|
item_names.sub_axe: 4}
|
|
|
|
POSSIBLE_EXTRA_FILLER = [item_names.jewel_rs, item_names.jewel_rl,
|
|
item_names.gold_500, item_names.gold_300, item_names.gold_100]
|
|
|
|
# These Item pickups spawn 3.2 units higher than the other pickups and therefore must be lowered by that amount for
|
|
# a few Locations wherein they can spawn barely out of reach.
|
|
HIGHER_SPAWNING_ITEMS = [Pickups.CROSS, Pickups.AXE, Pickups.WINCH_LEVER, Pickups.CREST_HALF_A, Pickups.CREST_HALF_B,
|
|
Pickups.ROSE_BROOCH]
|
|
|
|
# Pickups that, in this randomizer, are actually using the appearance of a different pickup. Unique pickups that have
|
|
# an identical appearance to another pickup (like some keys) are being used to have more possible appearances for
|
|
# pickups to assume.
|
|
OTHER_APPEARANCE_PICKUPS = {item_names.permaup: Pickups.CLOCKTOWER_KEY_A,
|
|
item_names.quest_key_clock_a: Pickups.GARDEN_KEY,
|
|
item_names.quest_key_clock_b: Pickups.COPPER_KEY}
|
|
|
|
def get_item_names_to_ids() -> dict[str, int]:
|
|
return {item: data.item_id for item, data in ALL_CVLOD_ITEMS.items()}
|
|
|
|
|
|
def get_item_pool(world: "CVLoDWorld") -> list[CVLoDItem]:
|
|
"""Builds the player's entire Item pool based on a number of factors, including what stages are in, what Locations
|
|
are created, and chosen Options."""
|
|
|
|
active_locations = world.multiworld.get_unfilled_locations(world.player)
|
|
|
|
tier_1_filler = []
|
|
tier_2_filler = []
|
|
non_filler = []
|
|
|
|
def replace_filler(replacement_items: [CVLoDItem]) -> None:
|
|
"""Replaces filler Items in the already-created Item pool with specified, different Items. Tier 1 filler will
|
|
be replaced first, and then tier 2 when the less valuable tier 1 has run out. If there's no filler left, an
|
|
exception will be raised."""
|
|
nonlocal non_filler, tier_1_filler, tier_2_filler
|
|
|
|
for _ in range(len(replacement_items)):
|
|
# If the tier 1 filler list has stuff in it, remove a random Item from it.
|
|
if tier_1_filler:
|
|
del tier_1_filler[world.random.randrange(0, len(tier_1_filler))]
|
|
# If the tier 2 filler list has stuff in it, remove a random Item from it instead.
|
|
elif tier_2_filler:
|
|
del tier_2_filler[world.random.randrange(0, len(tier_2_filler))]
|
|
# Otherwise, if both lists were empty, raise an exception because something went wrong.
|
|
# We should NOT be hitting this to begin with.
|
|
else:
|
|
raise Exception(f"Ran out of replaceable filler for {world.player_name}. "
|
|
f"Something wasn't handled right...")
|
|
|
|
# Add the replacement Item to the non-Filler list.
|
|
non_filler += replacement_items
|
|
|
|
|
|
total_items = 0
|
|
extras_count = 0
|
|
|
|
# Get from each Location its vanilla Item and add it to the item lists.
|
|
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_name = CVLOD_LOCATIONS_INFO[loc.name].normal_item
|
|
|
|
# If the Item is a Winch Lever, and the Castle Wall State is Reinhardt/Carrie's, add a PowerUp instead because
|
|
# the Winch Lever is useless.
|
|
if item_name == item_names.quest_winch and \
|
|
world.options.castle_wall_state == CastleWallState.option_reinhardt_carrie:
|
|
item_name = item_names.powerup
|
|
|
|
# If the Item is Oldrey's Diary, and the Villa State is Reinhardt/Carrie's, add a Purifying instead because
|
|
# Oldrey's Diary is useless.
|
|
if item_name == item_names.quest_diary and world.options.villa_state == VillaState.option_reinhardt_carrie:
|
|
item_name = item_names.use_purifying
|
|
# Similar for the Rose Brooch but adding a Red Jewel (L) instead.
|
|
if item_name == item_names.quest_brooch and world.options.villa_state == VillaState.option_reinhardt_carrie:
|
|
item_name = item_names.jewel_rl
|
|
|
|
# If the Item we're adding is a PowerUp and Permanent PowerUps are on, add a random extra filler instead.
|
|
# The PermaUps will be added after the initial item pool is created.
|
|
if item_name == item_names.powerup and world.options.permanent_powerups:
|
|
item_name = world.get_filler_item_name()
|
|
|
|
# Create the Item object.
|
|
item_to_add = world.create_item(item_name)
|
|
|
|
# If the Item's classification is Filler, add it to one of the filler lists.
|
|
if item_to_add.classification == ItemClassification.filler:
|
|
# If the Item is a possible extra filler Item, consider it tier 1 filler. When we start modifying the pool,
|
|
# these will be the first replaced in it.
|
|
if item_to_add.name in POSSIBLE_EXTRA_FILLER:
|
|
tier_1_filler.append(item_to_add)
|
|
# Otherwise, consider it tier 2 filler. These filler items are more valuable than mere moneybags and jewels
|
|
# and as such won't be replaced until there's no more tier 1 filler.
|
|
else:
|
|
tier_2_filler.append(item_to_add)
|
|
# Otherwise, if the Item is not filler, add it to the non-filler list.
|
|
else:
|
|
non_filler.append(item_to_add)
|
|
|
|
# Add the extra key item copies if Spare Keys is on. Do it now before any Specials or anything else get added, as
|
|
# we check the classification of each individual Item in the non-filler list.
|
|
if world.options.spare_keys:
|
|
extra_copies = []
|
|
for item in non_filler:
|
|
# If the Item has the Progression classification bit set, consider it eligible for duping.
|
|
if item.classification & ItemClassification.progression:
|
|
# If the Spare Keys option is set to Chance, then there will be a 50% chance wherein we don't actually
|
|
# create it after all.
|
|
if world.options.spare_keys == SpareKeys.option_chance and not world.random.randint(0, 1):
|
|
continue
|
|
extra_copies.append(world.create_item(item.name))
|
|
replace_filler(extra_copies)
|
|
|
|
# Add the two PermaUps now if Permanent Powerups is on.
|
|
if world.options.permanent_powerups:
|
|
replace_filler([world.create_item(item_names.permaup), world.create_item(item_names.permaup)])
|
|
|
|
# Check if the total filler is less than the number of Specials we are adding. If it is, then we will need to adjust
|
|
# the Special totals. The PANIC adjuster, if you will!
|
|
total_specials = world.options.total_special1s.value + world.options.total_special2s.value
|
|
total_filler = len(tier_1_filler) + len(tier_2_filler)
|
|
if total_specials > total_filler:
|
|
# Figure out the new number of S1s and S2s by taking the total filler count and getting the percentages of it
|
|
# that the S1s and S2s consist of in the total Special count. When downsizing this way, the ratio between the
|
|
# two should remain the same.
|
|
new_s1s = int((world.options.total_special1s.value / total_specials * 100) * total_filler // 100)
|
|
new_s2s = int((world.options.total_special2s.value / total_specials * 100) * total_filler // 100)
|
|
|
|
# Create the initial part of the warning message.
|
|
special_count_warning = (f"[{world.player_name}] Not enough Locations to accommodate the chosen Total "
|
|
f"Special1s and/or Special2s. The following Special counts were adjusted:\n"
|
|
f"Total Special1s: {world.options.total_special1s} -> {new_s1s}\n"
|
|
f"Total Special2s: {world.options.total_special2s} -> {new_s2s}")
|
|
|
|
# Adjust the Special count option values proper.
|
|
world.options.total_special1s.value = new_s1s
|
|
world.options.total_special2s.value = new_s2s
|
|
|
|
# Adjust the world's required Special2s to be the specified percentage of the new number.
|
|
world.required_s2s = int(world.options.percent_special2s_required.value / 100 *
|
|
world.options.total_special2s.value)
|
|
|
|
# If this caused there to be not enough Special1s to unlock every warp, adjust Special1s Per Warp down as well.
|
|
if world.options.special1s_per_warp.value * (len(world.active_warp_list) - 1) > world.options.total_special1s:
|
|
new_s1s_per_warp = world.options.total_special1s // (len(world.active_warp_list) - 1)
|
|
special_count_warning += (f"\nConsequently, Special1s Per Warp also had to be lowered from "
|
|
f"{world.options.special1s_per_warp.value} to {new_s1s_per_warp}.")
|
|
world.options.special1s_per_warp.value = new_s1s_per_warp
|
|
|
|
# Throw the final warning message.
|
|
logging.warning(special_count_warning)
|
|
|
|
# Add the Special1s.
|
|
all_special1s = []
|
|
for _ in range(world.options.total_special1s.value):
|
|
# If Special1s Per Warp is 3 or lower, then the exact necessary amount of S1s needed to unlock the full menu
|
|
# will be marked regular Progression instead of Progression Deprioritized Skip Balancing.
|
|
if world.options.special1s_per_warp.value <= 3 and \
|
|
len(all_special1s) <= world.options.special1s_per_warp.value * (len(world.active_warp_list) - 1):
|
|
all_special1s.append(world.create_item(item_names.special1, ItemClassification.progression))
|
|
else:
|
|
all_special1s.append(world.create_item(item_names.special1))
|
|
replace_filler(all_special1s)
|
|
|
|
# Add the total Special2s if Dracula's Condition is Special2s (should be 0 if not).
|
|
if world.options.draculas_condition == DraculasCondition.option_specials:
|
|
# If there are 5 or fewer S2s present, then they will not be deprioritized. Otherwise, they will be.
|
|
if world.options.total_special2s <= 5:
|
|
replace_filler([world.create_item(item_names.special2, ItemClassification.progression_skip_balancing)
|
|
for _ in range(world.options.total_special2s.value)])
|
|
else:
|
|
replace_filler([world.create_item(item_names.special2) for _ in range(world.options.total_special2s.value)])
|
|
|
|
# TODO: Actually implement traps.
|
|
# Determine the Ice Trap count by taking a certain % of the total filler remaining at this point.
|
|
#item_counts[ItemClassification.trap][item_names.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[ItemClassification.trap][item_names.ice_trap]):
|
|
# # Subtract the remaining filler after determining the ice trap count.
|
|
# item_to_subtract = world.random.choice(list(item_counts[ItemClassification.filler].keys()))
|
|
# item_counts[ItemClassification.filler][item_to_subtract] -= 1
|
|
# if item_counts[ItemClassification.filler][item_to_subtract] == 0:
|
|
# del (item_counts[ItemClassification.filler][item_to_subtract])
|
|
|
|
# Return the final complete lists of created Item objects.
|
|
return tier_1_filler + tier_2_filler + non_filler
|