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
|