762 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			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
 |