Archipelago/worlds/cvcotm/aesthetics.py

762 lines
36 KiB
Python

from BaseClasses import ItemClassification, Location
from .options import ItemDropRandomization, Countdown, RequiredSkirmishes, IronMaidenBehavior
from .locations import cvcotm_location_info
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
from .data import iname
from typing import TYPE_CHECKING, Dict, List, Iterable, Tuple, NamedTuple, Optional, TypedDict
if TYPE_CHECKING:
from . import CVCotMWorld
class StatInfo(TypedDict):
# Amount this stat increases per Max Up the player starts with.
amount_per: int
# The most amount of this stat the player is allowed to start with. Problems arise if the stat exceeds 9999, so we
# must ensure it can't if the player raises any class to level 99 as well as collects 255 of that max up. The game
# caps hearts at 999 automatically, so it doesn't matter so much for that one.
max_allowed: int
# The key variable in extra_stats that the stat max up affects.
variable: str
extra_starting_stat_info: Dict[str, StatInfo] = {
iname.hp_max: {"amount_per": 10,
"max_allowed": 5289,
"variable": "extra health"},
iname.mp_max: {"amount_per": 10,
"max_allowed": 3129,
"variable": "extra magic"},
iname.heart_max: {"amount_per": 6,
"max_allowed": 999,
"variable": "extra hearts"},
}
other_player_subtype_bytes = {
0xE4: 0x03,
0xE6: 0x14,
0xE8: 0x0A
}
class OtherGameAppearancesInfo(TypedDict):
# What type of item to place for the other player.
type: int
# What item to display it as for the other player.
appearance: int
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
# NOTE: Symphony of the Night is currently an unsupported world not in main.
"Symphony of the Night": {"Life Vessel": {"type": 0xE4,
"appearance": 0x01},
"Heart Vessel": {"type": 0xE4,
"appearance": 0x00}},
"Timespinner": {"Max HP": {"type": 0xE4,
"appearance": 0x01},
"Max Aura": {"type": 0xE4,
"appearance": 0x02},
"Max Sand": {"type": 0xE8,
"appearance": 0x0F}}
}
# 0 = Holy water 22
# 1 = Axe 24
# 2 = Knife 32
# 3 = Cross 6
# 4 = Stopwatch 12
# 5 = Small heart
# 6 = Big heart
rom_sub_weapon_offsets = {
0xD034E: b"\x01",
0xD0462: b"\x02",
0xD064E: b"\x00",
0xD06F6: b"\x02",
0xD0882: b"\x00",
0xD0912: b"\x02",
0xD0C2A: b"\x02",
0xD0C96: b"\x01",
0xD0D92: b"\x02",
0xD0DCE: b"\x01",
0xD1332: b"\x00",
0xD13AA: b"\x01",
0xD1722: b"\x02",
0xD17A6: b"\x01",
0xD1926: b"\x01",
0xD19AA: b"\x02",
0xD1A9A: b"\x02",
0xD1AA6: b"\x00",
0xD1EBA: b"\x00",
0xD1ED2: b"\x01",
0xD2262: b"\x02",
0xD23B2: b"\x03",
0xD256E: b"\x02",
0xD2742: b"\x02",
0xD2832: b"\x04",
0xD2862: b"\x01",
0xD2A2A: b"\x01",
0xD2DBA: b"\x04",
0xD2DC6: b"\x00",
0xD2E02: b"\x02",
0xD2EFE: b"\x04",
0xD2F0A: b"\x02",
0xD302A: b"\x00",
0xD3042: b"\x01",
0xD304E: b"\x04",
0xD3066: b"\x02",
0xD322E: b"\x04",
0xD334E: b"\x04",
0xD3516: b"\x03",
0xD35CA: b"\x02",
0xD371A: b"\x01",
0xD38EE: b"\x00",
0xD3BE2: b"\x02",
0xD3D1A: b"\x01",
0xD3D56: b"\x02",
0xD3ECA: b"\x00",
0xD3EE2: b"\x02",
0xD4056: b"\x01",
0xD40E6: b"\x04",
0xD413A: b"\x04",
0xD4326: b"\x00",
0xD460E: b"\x00",
0xD48D2: b"\x00",
0xD49E6: b"\x01",
0xD4ABE: b"\x02",
0xD4B8A: b"\x01",
0xD4D0A: b"\x04",
0xD4EAE: b"\x02",
0xD4F0E: b"\x00",
0xD4F92: b"\x02",
0xD4FB6: b"\x01",
0xD503A: b"\x03",
0xD5646: b"\x01",
0xD5682: b"\x02",
0xD57C6: b"\x02",
0xD57D2: b"\x02",
0xD58F2: b"\x00",
0xD5922: b"\x01",
0xD5B9E: b"\x02",
0xD5E26: b"\x01",
0xD5E56: b"\x02",
0xD5E7A: b"\x02",
0xD5F5E: b"\x00",
0xD69EA: b"\x02",
0xD69F6: b"\x01",
0xD6A02: b"\x00",
0xD6A0E: b"\x04",
0xD6A1A: b"\x03",
0xD6BE2: b"\x00",
0xD6CBA: b"\x01",
0xD6CDE: b"\x02",
0xD6EEE: b"\x00",
0xD6F1E: b"\x02",
0xD6F42: b"\x01",
0xD6FC6: b"\x04",
0xD706E: b"\x00",
0xD716A: b"\x02",
0xD72AE: b"\x01",
0xD75BA: b"\x03",
0xD76AA: b"\x04",
0xD76B6: b"\x00",
0xD76C2: b"\x01",
0xD76CE: b"\x02",
0xD76DA: b"\x03",
0xD7D46: b"\x00",
0xD7D52: b"\x00",
}
LOW_ITEMS = [
41, # Potion
42, # Meat
48, # Mind Restore
51, # Heart
46, # Antidote
47, # Cure Curse
17, # Cotton Clothes
18, # Prison Garb
12, # Cotton Robe
1, # Leather Armor
2, # Bronze Armor
3, # Gold Armor
39, # Toy Ring
40, # Bear Ring
34, # Wristband
36, # Arm Guard
37, # Magic Gauntlet
38, # Miracle Armband
35, # Gauntlet
]
MID_ITEMS = [
43, # Spiced Meat
49, # Mind High
52, # Heart High
19, # Stylish Suit
20, # Night Suit
13, # Silk Robe
14, # Rainbow Robe
4, # Chainmail
5, # Steel Armor
6, # Platinum Armor
24, # Star Bracelet
29, # Cursed Ring
25, # Strength Ring
26, # Hard Ring
27, # Intelligence Ring
28, # Luck Ring
23, # Double Grips
]
HIGH_ITEMS = [
44, # Potion High
45, # Potion Ex
50, # Mind Ex
53, # Heart Ex
54, # Heart Mega
21, # Ninja Garb
22, # Soldier Fatigues
15, # Magic Robe
16, # Sage Robe
7, # Diamond Armor
8, # Mirror Armor
9, # Needle Armor
10, # Dark Armor
30, # Strength Armband
31, # Defense Armband
32, # Sage Armband
33, # Gambler Armband
]
COMMON_ITEMS = LOW_ITEMS + MID_ITEMS
RARE_ITEMS = LOW_ITEMS + MID_ITEMS + HIGH_ITEMS
class CVCotMEnemyData(NamedTuple):
name: str
hp: int
attack: int
defense: int
exp: int
type: Optional[str] = None
cvcotm_enemy_info: List[CVCotMEnemyData] = [
# Name HP ATK DEF EXP
CVCotMEnemyData("Medusa Head", 6, 120, 60, 2),
CVCotMEnemyData("Zombie", 48, 70, 20, 2),
CVCotMEnemyData("Ghoul", 100, 190, 79, 3),
CVCotMEnemyData("Wight", 110, 235, 87, 4),
CVCotMEnemyData("Clinking Man", 80, 135, 25, 21),
CVCotMEnemyData("Zombie Thief", 120, 185, 30, 58),
CVCotMEnemyData("Skeleton", 25, 65, 45, 4),
CVCotMEnemyData("Skeleton Bomber", 20, 50, 40, 4),
CVCotMEnemyData("Electric Skeleton", 42, 80, 50, 30),
CVCotMEnemyData("Skeleton Spear", 30, 65, 46, 6),
CVCotMEnemyData("Skeleton Boomerang", 60, 170, 90, 112),
CVCotMEnemyData("Skeleton Soldier", 35, 90, 60, 16),
CVCotMEnemyData("Skeleton Knight", 50, 140, 80, 39),
CVCotMEnemyData("Bone Tower", 84, 201, 280, 160),
CVCotMEnemyData("Fleaman", 60, 142, 45, 29),
CVCotMEnemyData("Poltergeist", 105, 360, 380, 510),
CVCotMEnemyData("Bat", 5, 50, 15, 4),
CVCotMEnemyData("Spirit", 9, 55, 17, 1),
CVCotMEnemyData("Ectoplasm", 12, 165, 51, 2),
CVCotMEnemyData("Specter", 15, 295, 95, 3),
CVCotMEnemyData("Axe Armor", 55, 120, 130, 31),
CVCotMEnemyData("Flame Armor", 160, 320, 300, 280),
CVCotMEnemyData("Flame Demon", 300, 315, 270, 600),
CVCotMEnemyData("Ice Armor", 240, 470, 520, 1500),
CVCotMEnemyData("Thunder Armor", 204, 340, 320, 800),
CVCotMEnemyData("Wind Armor", 320, 500, 460, 1800),
CVCotMEnemyData("Earth Armor", 130, 230, 280, 240),
CVCotMEnemyData("Poison Armor", 260, 382, 310, 822),
CVCotMEnemyData("Forest Armor", 370, 390, 390, 1280),
CVCotMEnemyData("Stone Armor", 90, 220, 320, 222),
CVCotMEnemyData("Ice Demon", 350, 492, 510, 4200),
CVCotMEnemyData("Holy Armor", 350, 420, 450, 1700),
CVCotMEnemyData("Thunder Demon", 180, 270, 230, 450),
CVCotMEnemyData("Dark Armor", 400, 680, 560, 3300),
CVCotMEnemyData("Wind Demon", 400, 540, 490, 3600),
CVCotMEnemyData("Bloody Sword", 30, 220, 500, 200),
CVCotMEnemyData("Golem", 650, 520, 700, 1400),
CVCotMEnemyData("Earth Demon", 150, 90, 85, 25),
CVCotMEnemyData("Were-wolf", 160, 265, 110, 140),
CVCotMEnemyData("Man Eater", 400, 330, 233, 700),
CVCotMEnemyData("Devil Tower", 10, 140, 200, 17),
CVCotMEnemyData("Skeleton Athlete", 100, 100, 50, 25),
CVCotMEnemyData("Harpy", 120, 275, 200, 271),
CVCotMEnemyData("Siren", 160, 443, 300, 880),
CVCotMEnemyData("Imp", 90, 220, 99, 103),
CVCotMEnemyData("Mudman", 25, 79, 30, 2),
CVCotMEnemyData("Gargoyle", 60, 160, 66, 3),
CVCotMEnemyData("Slime", 40, 102, 18, 11),
CVCotMEnemyData("Frozen Shade", 112, 490, 560, 1212),
CVCotMEnemyData("Heat Shade", 80, 240, 200, 136),
CVCotMEnemyData("Poison Worm", 120, 30, 20, 12),
CVCotMEnemyData("Myconid", 50, 250, 114, 25),
CVCotMEnemyData("Will O'Wisp", 11, 110, 16, 9),
CVCotMEnemyData("Spearfish", 40, 360, 450, 280),
CVCotMEnemyData("Merman", 60, 303, 301, 10),
CVCotMEnemyData("Minotaur", 410, 520, 640, 2000),
CVCotMEnemyData("Were-horse", 400, 540, 360, 1970),
CVCotMEnemyData("Marionette", 80, 160, 150, 127),
CVCotMEnemyData("Gremlin", 30, 80, 33, 2),
CVCotMEnemyData("Hopper", 40, 87, 35, 8),
CVCotMEnemyData("Evil Pillar", 20, 460, 800, 480),
CVCotMEnemyData("Were-panther", 200, 300, 130, 270),
CVCotMEnemyData("Were-jaguar", 270, 416, 170, 760),
CVCotMEnemyData("Bone Head", 24, 60, 80, 7),
CVCotMEnemyData("Fox Archer", 75, 130, 59, 53),
CVCotMEnemyData("Fox Hunter", 100, 290, 140, 272),
CVCotMEnemyData("Were-bear", 265, 250, 140, 227),
CVCotMEnemyData("Grizzly", 600, 380, 200, 960),
CVCotMEnemyData("Cerberus", 600, 150, 100, 500, "boss"),
CVCotMEnemyData("Beast Demon", 150, 330, 250, 260),
CVCotMEnemyData("Arch Demon", 320, 505, 400, 1000),
CVCotMEnemyData("Demon Lord", 460, 660, 500, 1950),
CVCotMEnemyData("Gorgon", 230, 215, 165, 219),
CVCotMEnemyData("Catoblepas", 550, 500, 430, 1800),
CVCotMEnemyData("Succubus", 150, 400, 350, 710),
CVCotMEnemyData("Fallen Angel", 370, 770, 770, 6000),
CVCotMEnemyData("Necromancer", 500, 200, 250, 2500, "boss"),
CVCotMEnemyData("Hyena", 93, 140, 70, 105),
CVCotMEnemyData("Fishhead", 80, 320, 504, 486),
CVCotMEnemyData("Dryad", 120, 300, 360, 300),
CVCotMEnemyData("Mimic Candle", 990, 600, 600, 6600, "candle"),
CVCotMEnemyData("Brain Float", 20, 50, 25, 10),
CVCotMEnemyData("Evil Hand", 52, 150, 120, 63),
CVCotMEnemyData("Abiondarg", 88, 388, 188, 388),
CVCotMEnemyData("Iron Golem", 640, 290, 450, 8000, "boss"),
CVCotMEnemyData("Devil", 1080, 800, 900, 10000),
CVCotMEnemyData("Witch", 144, 330, 290, 600),
CVCotMEnemyData("Mummy", 100, 100, 35, 3),
CVCotMEnemyData("Hipogriff", 300, 500, 210, 740),
CVCotMEnemyData("Adramelech", 1800, 380, 360, 16000, "boss"),
CVCotMEnemyData("Arachne", 330, 420, 288, 1300),
CVCotMEnemyData("Death Mantis", 200, 318, 240, 400),
CVCotMEnemyData("Alraune", 774, 490, 303, 2500),
CVCotMEnemyData("King Moth", 140, 290, 160, 150),
CVCotMEnemyData("Killer Bee", 8, 308, 108, 88),
CVCotMEnemyData("Dragon Zombie", 1400, 390, 440, 15000, "boss"),
CVCotMEnemyData("Lizardman", 100, 345, 400, 800),
CVCotMEnemyData("Franken", 1200, 700, 350, 2100),
CVCotMEnemyData("Legion", 420, 610, 375, 1590),
CVCotMEnemyData("Dullahan", 240, 550, 440, 2200),
CVCotMEnemyData("Death", 880, 600, 800, 60000, "boss"),
CVCotMEnemyData("Camilla", 1500, 650, 700, 80000, "boss"),
CVCotMEnemyData("Hugh", 1400, 570, 750, 120000, "boss"),
CVCotMEnemyData("Dracula", 1100, 805, 850, 150000, "boss"),
CVCotMEnemyData("Dracula", 3000, 1000, 1000, 0, "final boss"),
CVCotMEnemyData("Skeleton Medalist", 250, 100, 100, 1500),
CVCotMEnemyData("Were-jaguar", 320, 518, 260, 1200, "battle arena"),
CVCotMEnemyData("Were-wolf", 340, 525, 180, 1100, "battle arena"),
CVCotMEnemyData("Catoblepas", 560, 510, 435, 2000, "battle arena"),
CVCotMEnemyData("Hipogriff", 500, 620, 280, 1900, "battle arena"),
CVCotMEnemyData("Wind Demon", 490, 600, 540, 4000, "battle arena"),
CVCotMEnemyData("Witch", 210, 480, 340, 1000, "battle arena"),
CVCotMEnemyData("Stone Armor", 260, 585, 750, 3000, "battle arena"),
CVCotMEnemyData("Devil Tower", 50, 560, 700, 600, "battle arena"),
CVCotMEnemyData("Skeleton", 150, 400, 200, 500, "battle arena"),
CVCotMEnemyData("Skeleton Bomber", 150, 400, 200, 550, "battle arena"),
CVCotMEnemyData("Electric Skeleton", 150, 400, 200, 700, "battle arena"),
CVCotMEnemyData("Skeleton Spear", 150, 400, 200, 580, "battle arena"),
CVCotMEnemyData("Flame Demon", 680, 650, 600, 4500, "battle arena"),
CVCotMEnemyData("Bone Tower", 120, 500, 650, 800, "battle arena"),
CVCotMEnemyData("Fox Hunter", 160, 510, 220, 600, "battle arena"),
CVCotMEnemyData("Poison Armor", 380, 680, 634, 3600, "battle arena"),
CVCotMEnemyData("Bloody Sword", 55, 600, 1200, 2000, "battle arena"),
CVCotMEnemyData("Abiondarg", 188, 588, 288, 588, "battle arena"),
CVCotMEnemyData("Legion", 540, 760, 480, 2900, "battle arena"),
CVCotMEnemyData("Marionette", 200, 420, 400, 1200, "battle arena"),
CVCotMEnemyData("Minotaur", 580, 700, 715, 4100, "battle arena"),
CVCotMEnemyData("Arachne", 430, 590, 348, 2400, "battle arena"),
CVCotMEnemyData("Succubus", 300, 670, 630, 3100, "battle arena"),
CVCotMEnemyData("Demon Lord", 590, 800, 656, 4200, "battle arena"),
CVCotMEnemyData("Alraune", 1003, 640, 450, 5000, "battle arena"),
CVCotMEnemyData("Hyena", 210, 408, 170, 1000, "battle arena"),
CVCotMEnemyData("Devil Armor", 500, 804, 714, 6600),
CVCotMEnemyData("Evil Pillar", 55, 655, 900, 1500, "battle arena"),
CVCotMEnemyData("White Armor", 640, 770, 807, 7000),
CVCotMEnemyData("Devil", 1530, 980, 1060, 30000, "battle arena"),
CVCotMEnemyData("Scary Candle", 150, 300, 300, 900, "candle"),
CVCotMEnemyData("Trick Candle", 200, 400, 400, 1400, "candle"),
CVCotMEnemyData("Nightmare", 250, 550, 550, 2000),
CVCotMEnemyData("Lilim", 400, 800, 800, 8000),
CVCotMEnemyData("Lilith", 660, 960, 960, 20000),
]
# NOTE: Coffin is omitted from the end of this, as its presence doesn't
# actually impact the randomizer (all stats and drops inherited from Mummy).
BOSS_IDS = [enemy_id for enemy_id in range(len(cvcotm_enemy_info)) if cvcotm_enemy_info[enemy_id].type == "boss"]
ENEMY_TABLE_START = 0xCB2C4
NUMBER_ITEMS = 55
COUNTDOWN_TABLE_ADDR = 0x673400
ITEM_ID_SHINNING_ARMOR = 11
def shuffle_sub_weapons(world: "CVCotMWorld") -> Dict[int, bytes]:
"""Shuffles the sub-weapons amongst themselves."""
sub_bytes = list(rom_sub_weapon_offsets.values())
world.random.shuffle(sub_bytes)
return dict(zip(rom_sub_weapon_offsets, sub_bytes))
def get_countdown_flags(world: "CVCotMWorld", active_locations: Iterable[Location]) -> Dict[int, bytes]:
"""Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
count towards a number.
Which number to increase is determined by the Location's "countdown" attr in its CVCotMLocationData."""
next_pos = COUNTDOWN_TABLE_ADDR + 0x40
countdown_flags: List[List[int]] = [[] for _ in range(16)]
countdown_dict = {}
ptr_offset = COUNTDOWN_TABLE_ADDR
# Loop over every Location.
for loc in active_locations:
# If the Location's Item is not Progression/Useful-classified with the "Majors" Countdown being used, or if the
# Location is the Iron Maiden switch with the vanilla Iron Maiden behavior, skip adding its flag to the arrays.
if (not loc.item.classification & MAJORS_CLASSIFICATIONS and world.options.countdown ==
Countdown.option_majors):
continue
countdown_index = cvcotm_location_info[loc.name].countdown
# Take the Location's address if the above condition is satisfied, and get the flag value out of it.
countdown_flags[countdown_index] += [loc.address & 0xFF, 0]
# Write the Countdown flag arrays and array pointers correctly. Each flag list should end with a 0xFFFF to indicate
# the end of an area's list.
for area_flags in countdown_flags:
countdown_dict[ptr_offset] = int.to_bytes(next_pos | 0x08000000, 4, "little")
countdown_dict[next_pos] = bytes(area_flags + [0xFF, 0xFF])
ptr_offset += 4
next_pos += len(area_flags) + 2
return countdown_dict
def get_location_data(world: "CVCotMWorld", active_locations: Iterable[Location]) -> Dict[int, bytes]:
"""Gets ALL the Item data to go into the ROM. Items consist of four bytes; the first two represent the object ID
for the "category" of item that it belongs to, the third is the sub-value for which item within that "category" it
is, and the fourth controls the appearance it takes."""
location_bytes = {}
for loc in active_locations:
# Figure out the item ID bytes to put in each Location's offset here.
# If it's a CotM Item, always write the Item's primary type byte.
if loc.item.game == "Castlevania - Circle of the Moon":
type_byte = cvcotm_item_info[loc.item.name].code >> 8
# If the Item is for this player, set the subtype to actually be that Item.
# Otherwise, set a dummy subtype value that is different for every item type.
if loc.item.player == world.player:
subtype_byte = cvcotm_item_info[loc.item.name].code & 0xFF
else:
subtype_byte = other_player_subtype_bytes[type_byte]
# If it's a DSS Card, set the appearance based on whether it's progression or not; freeze combo cards should
# all appear blue in color while the others are standard purple/yellow. Otherwise, set the appearance the
# same way as the subtype for local items regardless of whether it's actually local or not.
if type_byte == 0xE6:
if loc.item.advancement:
appearance_byte = 1
else:
appearance_byte = 0
else:
appearance_byte = cvcotm_item_info[loc.item.name].code & 0xFF
# If it's not a CotM Item at all, always set the primary type to that of a Magic Item and the subtype to that of
# a dummy item. The AP Items are all under Magic Items.
else:
type_byte = 0xE8
subtype_byte = 0x0A
# Decide which AP Item to use to represent the other game item.
if loc.item.classification & ItemClassification.progression and \
loc.item.classification & ItemClassification.useful:
appearance_byte = 0x0E # Progression + Useful
elif loc.item.classification & ItemClassification.progression:
appearance_byte = 0x0C # Progression
elif loc.item.classification & ItemClassification.useful:
appearance_byte = 0x0B # Useful
elif loc.item.classification & ItemClassification.trap:
appearance_byte = 0x0D # Trap
else:
appearance_byte = 0x0A # Filler
# Check if the Item's game is in the other game item appearances' dict, and if so, if the Item is under that
# game's name. If it is, change the appearance accordingly.
# Right now, only SotN and Timespinner stat ups are supported.
other_game_name = world.multiworld.worlds[loc.item.player].game
if other_game_name in other_game_item_appearances:
if loc.item.name in other_game_item_appearances[other_game_name]:
type_byte = other_game_item_appearances[other_game_name][loc.item.name]["type"]
subtype_byte = other_player_subtype_bytes[type_byte]
appearance_byte = other_game_item_appearances[other_game_name][loc.item.name]["appearance"]
# Create the correct bytes object for the Item on that Location.
location_bytes[cvcotm_location_info[loc.name].offset] = bytes([type_byte, 1, subtype_byte, appearance_byte])
return location_bytes
def populate_enemy_drops(world: "CVCotMWorld") -> Dict[int, bytes]:
"""Randomizes the enemy-dropped items throughout the game within each other. There are three tiers of item drops:
Low, Mid, and High. Each enemy has two item slots that can both drop its own item; a Common slot and a Rare one.
On Normal item randomization, easy enemies (below 61 HP) will only have Low-tier drops in both of their stats,
bosses and candle enemies will be guaranteed to have High drops in one or both of their slots respectively (bosses
are made to only drop one slot 100% of the time), and everything else can have a Low or Mid-tier item in its Common
drop slot and a Low, Mid, OR High-tier item in its Rare drop slot.
If Item Drop Randomization is set to Tiered, the HP threshold for enemies being considered "easily" will raise to
below 144, enemies in the 144-369 HP range (inclusive) will have a Low-tier item in its Common slot and a Mid-tier
item in its rare slot, and enemies with more than 369 HP will have a Mid-tier in its Common slot and a High-tier in
its Rare slot. Candles and bosses still have Rares in all their slots, but now the guaranteed drops that land on
bosses will be exclusive to them; no other enemy in the game will have their item.
This and select_drop are the most directly adapted code from upstream CotMR in this package by far. Credit where
it's due to Spooky for writing the original, and Malaert64 for further refinements and updating what used to be
Random Item Hardmode to instead be Tiered Item Mode. The original C code this was adapted from can be found here:
https://github.com/calm-palm/cotm-randomizer/blob/master/Program/randomizer.c#L1028"""
placed_low_items = [0] * len(LOW_ITEMS)
placed_mid_items = [0] * len(MID_ITEMS)
placed_high_items = [0] * len(HIGH_ITEMS)
placed_common_items = [0] * len(COMMON_ITEMS)
placed_rare_items = [0] * len(RARE_ITEMS)
regular_drops = [0] * len(cvcotm_enemy_info)
regular_drop_chances = [0] * len(cvcotm_enemy_info)
rare_drops = [0] * len(cvcotm_enemy_info)
rare_drop_chances = [0] * len(cvcotm_enemy_info)
# Set boss items first to prevent boss drop duplicates.
# If Tiered mode is enabled, make these items exclusive to these enemies by adding an arbitrary integer larger
# than could be reached normally (e.g.the total number of enemies) and use the placed high items array instead of
# the placed rare items one.
if world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
for boss_id in BOSS_IDS:
regular_drops[boss_id] = select_drop(world, HIGH_ITEMS, placed_high_items, True)
else:
for boss_id in BOSS_IDS:
regular_drops[boss_id] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
# Setting drop logic for all enemies.
for i in range(len(cvcotm_enemy_info)):
# Give Dracula II Shining Armor occasionally as a joke.
if cvcotm_enemy_info[i].type == "final boss":
regular_drops[i] = rare_drops[i] = ITEM_ID_SHINNING_ARMOR
regular_drop_chances[i] = rare_drop_chances[i] = 5000
# Set bosses' secondary item to none since we already set the primary item earlier.
elif cvcotm_enemy_info[i].type == "boss":
# Set rare drop to none.
rare_drops[i] = 0
# Max out rare boss drops (normally, drops are capped to 50% and 25% for common and rare respectively, but
# Fuse's patch AllowAlwaysDrop.ips allows setting the regular item drop chance to 10000 to force a drop
# always)
regular_drop_chances[i] = 10000
rare_drop_chances[i] = 0
# Candle enemies use a similar placement logic to the bosses, except items that land on them are NOT exclusive
# to them on Tiered mode.
elif cvcotm_enemy_info[i].type == "candle":
if world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
regular_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
else:
regular_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
# Set base drop chances at 20-30% for common and 15-20% for rare.
regular_drop_chances[i] = 2000 + world.random.randint(0, 1000)
rare_drop_chances[i] = 1500 + world.random.randint(0, 500)
# On All Bosses and Battle Arena Required, the Shinning Armor at the end of Battle Arena is removed.
# We compensate for this by giving the Battle Arena Devil a 100% chance to drop Shinning Armor.
elif cvcotm_enemy_info[i].name == "Devil" and cvcotm_enemy_info[i].type == "battle arena" and \
world.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena:
regular_drops[i] = ITEM_ID_SHINNING_ARMOR
rare_drops[i] = 0
regular_drop_chances[i] = 10000
rare_drop_chances[i] = 0
# Low-tier items drop from enemies that are trivial to farm (60 HP or less)
# on Normal drop logic, or enemies under 144 HP on Tiered logic.
elif (world.options.item_drop_randomization == ItemDropRandomization.option_normal and
cvcotm_enemy_info[i].hp <= 60) or \
(world.options.item_drop_randomization == ItemDropRandomization.option_tiered and
cvcotm_enemy_info[i].hp <= 143):
# Low-tier enemy drops.
regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
rare_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
# Set base drop chances at 6-10% for common and 3-6% for rare.
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
# Rest of Tiered logic, by Malaert64.
elif world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
# If under 370 HP, mid-tier enemy.
if cvcotm_enemy_info[i].hp <= 369:
regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
rare_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items)
# Otherwise, enemy HP is 370+, thus high-tier enemy.
else:
regular_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items)
rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
# Set base drop chances at 6-10% for common and 3-6% for rare.
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
# Regular enemies outside Tiered logic.
else:
# Select a random regular and rare drop for every enemy from their respective lists.
regular_drops[i] = select_drop(world, COMMON_ITEMS, placed_common_items)
rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items)
# Set base drop chances at 6-10% for common and 3-6% for rare.
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
# Return the randomized drop data as bytes with their respective offsets.
enemy_address = ENEMY_TABLE_START
drop_data = {}
for i, enemy_info in enumerate(cvcotm_enemy_info):
drop_data[enemy_address] = bytes([regular_drops[i], 0, regular_drop_chances[i] & 0xFF,
regular_drop_chances[i] >> 8, rare_drops[i], 0, rare_drop_chances[i] & 0xFF,
rare_drop_chances[i] >> 8])
enemy_address += 20
return drop_data
def select_drop(world: "CVCotMWorld", drop_list: List[int], drops_placed: List[int], exclusive_drop: bool = False,
start_index: int = 0) -> int:
"""Chooses a drop from a given list of drops based on another given list of how many drops from that list were
selected before. In order to ensure an even number of drops are distributed, drops that were selected the least are
the ones that will be picked from.
Calling this with exclusive_drop param being True will force the number of the chosen item really high to ensure it
will never be picked again."""
# Take the list of placed item drops beginning from the starting index.
drops_from_start_index = drops_placed[start_index:]
# Determine the lowest drop counts and the indices with that drop count.
lowest_number = min(drops_from_start_index)
indices_with_lowest_number = [index for index, placed in enumerate(drops_from_start_index) if
placed == lowest_number]
random_index = world.random.choice(indices_with_lowest_number)
random_index += start_index # Add start_index back on
# Increment the number of this item placed, unless it should be exclusive to the boss / candle, in which case
# set it to an arbitrarily large number to make it exclusive.
if exclusive_drop:
drops_placed[random_index] += 999
else:
drops_placed[random_index] += 1
# Return the in-game item ID of the chosen item.
return drop_list[random_index]
def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bool]:
"""Calculate and return the starting inventory arrays. Different items go into different arrays, so they all have
to be handled accordingly."""
start_inventory_data = {}
magic_items_array = [0 for _ in range(8)]
cards_array = [0 for _ in range(20)]
extra_stats = {"extra health": 0,
"extra magic": 0,
"extra hearts": 0}
start_with_detonator = False
# If the Iron Maiden Behavior option is set to Start Broken, consider ourselves starting with the Maiden Detonator.
if world.options.iron_maiden_behavior == IronMaidenBehavior.option_start_broken:
start_with_detonator = True
# Always start with the Dash Boots.
magic_items_array[0] = 1
for item in world.multiworld.precollected_items[world.player]:
array_offset = item.code & 0xFF
# If it's a Maiden Detonator we're starting with, set the boolean for it to True.
if item.name == iname.ironmaidens:
start_with_detonator = True
# If it's a Max Up we're starting with, check if increasing the extra amount of that stat will put us over the
# max amount of the stat allowed. If it will, set the current extra amount to the max. Otherwise, increase it by
# the amount that it should.
elif "Max Up" in item.name:
info = extra_starting_stat_info[item.name]
if extra_stats[info["variable"]] + info["amount_per"] > info["max_allowed"]:
extra_stats[info["variable"]] = info["max_allowed"]
else:
extra_stats[info["variable"]] += info["amount_per"]
# If it's a DSS card we're starting with, set that card's value in the cards array.
elif "Card" in item.name:
cards_array[array_offset] = 1
# If it's none of the above, it has to be a regular Magic Item.
# Increase that Magic Item's value in the Magic Items array if it's not greater than 240. Last Keys are the only
# Magic Item wherein having more than one is relevant.
else:
# Decrease the Magic Item array offset by 1 if it's higher than the unused Map's item value.
if array_offset > 5:
array_offset -= 1
if magic_items_array[array_offset] < 240:
magic_items_array[array_offset] += 1
# Add the start inventory arrays to the offset data in bytes form.
start_inventory_data[0x680080] = bytes(magic_items_array)
start_inventory_data[0x6800A0] = bytes(cards_array)
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
# possible Max Ups.
# Vampire Killer
start_inventory_data[0xE08C6] = int.to_bytes(100 + extra_stats["extra health"], 2, "little")
start_inventory_data[0xE08CE] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little")
start_inventory_data[0xE08D4] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
# Magician
start_inventory_data[0xE090E] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
start_inventory_data[0xE0916] = int.to_bytes(400 + extra_stats["extra magic"], 2, "little")
start_inventory_data[0xE091C] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
# Fighter
start_inventory_data[0xE0932] = int.to_bytes(200 + extra_stats["extra health"], 2, "little")
start_inventory_data[0xE093A] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little")
start_inventory_data[0xE0940] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
# Shooter
start_inventory_data[0xE0832] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
start_inventory_data[0xE08F2] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little")
start_inventory_data[0xE08F8] = int.to_bytes(250 + extra_stats["extra hearts"], 2, "little")
# Thief
start_inventory_data[0xE0956] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
start_inventory_data[0xE095E] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little")
start_inventory_data[0xE0964] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
return start_inventory_data, start_with_detonator