From 8f4c78609db40e0b1429089259da782678c4f04a Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Mon, 13 May 2024 17:28:14 -0700 Subject: [PATCH] v1.0.0 files --- worlds/mmx3/Client.py | 276 +++++++++---- worlds/mmx3/Items.py | 2 + worlds/mmx3/Names/ItemName.py | 10 +- worlds/mmx3/Options.py | 98 ++++- worlds/mmx3/Rom.py | 124 +++++- worlds/mmx3/Rules.py | 350 +++++++++-------- worlds/mmx3/Weaknesses.py | 497 ++++++++++++++++++++++++ worlds/mmx3/__init__.py | 27 +- worlds/mmx3/data/mmx3_basepatch.bsdiff4 | Bin 4225 -> 5263 bytes 9 files changed, 1119 insertions(+), 265 deletions(-) create mode 100644 worlds/mmx3/Weaknesses.py diff --git a/worlds/mmx3/Client.py b/worlds/mmx3/Client.py index 070a124a8388..d16281957f88 100644 --- a/worlds/mmx3/Client.py +++ b/worlds/mmx3/Client.py @@ -25,6 +25,7 @@ MMX3_RIDE_CHIPS = WRAM_START + 0x01FD7 MMX3_UPGRADES = WRAM_START + 0x01FD1 MMX3_CURRENT_HP = WRAM_START + 0x009FF +MMX3_CURRENT_WEAPON = WRAM_START + 0x00A0B MMX3_MAX_HP = WRAM_START + 0x01FD2 MMX3_LIFE_COUNT = WRAM_START + 0x01FB4 MMX3_ACTIVE_CHARACTER = WRAM_START + 0x00A8E @@ -38,6 +39,8 @@ MMX3_ENABLE_HP_REFILL = WRAM_START + 0x0F4E4 MMX3_HP_REFILL_AMOUNT = WRAM_START + 0x0F4E5 MMX3_ENABLE_GIVE_1UP = WRAM_START + 0x0F4E7 +MMX3_ENABLE_WEAPON_REFILL = WRAM_START + 0x0F4E8 +MMX3_WEAPON_REFILL_AMOUNT = WRAM_START + 0x0F4E9 MMX3_RECEIVING_ITEM = WRAM_START + 0x0F4FF MMX3_UNLOCKED_CHARGED_SHOT = WRAM_START + 0x0F46C @@ -66,15 +69,14 @@ MMX3_DEATH_LINK_ACTIVE = ROM_START + 0x17FFF8 MMX3_JAMMED_BUSTER_ACTIVE = ROM_START + 0x17FFF9 -HP_EXCHANGE_RATE = 500000000 +EXCHANGE_RATE = 500000000 MMX3_RECV_INDEX = WRAM_START + 0x0F460 MMX3_ROMHASH_START = 0x7FC0 ROMHASH_SIZE = 0x15 -X_Z_ITEMS = ["small hp refill", "large hp refill", "1up", "hp refill"] -HP_REFILLS = ["small hp refill", "large hp refill", "hp refill"] +X_Z_ITEMS = ["1up", "hp refill", "weapon refill"] BOSS_MEDAL = [0xFF, 0xFF, 0x02, 0xFF, 0x0C, 0x0A, 0x00, 0xFF, 0x04, 0x06, 0x0E, 0xFF, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, @@ -92,6 +94,8 @@ def __init__(self): self.auto_heal = False self.energy_link_enabled = False self.heal_request_command = None + self.weapon_refill_request_command = None + self.trade_request = None self.item_queue = [] @@ -114,12 +118,7 @@ async def deathlink_kill_player(self, ctx): can_move[0] != 0x00 or \ pause_state[0] != 0x00 or \ receiving_item[0] != 0x00 or \ - ( - going_through_gate[0] != 0x00 and \ - going_through_gate[1] != 0x00 and \ - going_through_gate[2] != 0x00 and \ - going_through_gate[3] != 0x00 \ - ): + going_through_gate != b'\x00\x00\x00\x00': return snes_buffered_write(ctx, MMX3_CURRENT_HP, bytes([0x80])) @@ -142,10 +141,14 @@ async def validate_rom(self, ctx): if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX3": if "pool" in ctx.command_processor.commands: ctx.command_processor.commands.pop("pool") - if "heal" in ctx.command_processor.commands: - ctx.command_processor.commands.pop("heal") if "autoheal" in ctx.command_processor.commands: ctx.command_processor.commands.pop("autoheal") + if "heal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("heal") + if "refill" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("refill") + if "trade" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("trade") return False ctx.game = self.game @@ -153,13 +156,17 @@ async def validate_rom(self, ctx): ctx.receive_option = 0 ctx.send_option = 0 ctx.allow_collect = True - if energy_link: + if energy_link[0]: if "pool" not in ctx.command_processor.commands: ctx.command_processor.commands["pool"] = cmd_pool - if "heal" not in ctx.command_processor.commands: - ctx.command_processor.commands["heal"] = cmd_heal if "autoheal" not in ctx.command_processor.commands: ctx.command_processor.commands["autoheal"] = cmd_autoheal + if "refill" not in ctx.command_processor.commands: + ctx.command_processor.commands["heal"] = cmd_heal + if "refill" not in ctx.command_processor.commands: + ctx.command_processor.commands["refill"] = cmd_refill + if "trade" not in ctx.command_processor.commands: + ctx.command_processor.commands["trade"] = cmd_trade death_link = await snes_read(ctx, MMX3_DEATH_LINK_ACTIVE, 1) if death_link[0]: @@ -170,6 +177,50 @@ async def validate_rom(self, ctx): return True + async def handle_hp_trade(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + validation = await snes_read(ctx, MMX3_VALIDATION_CHECK, 0x2) + if validation is None: + return + validation = validation[0] | (validation[1] << 8) + if validation != 0xDEAD: + return + + # Can only process trades during the pause state + menu_state = await snes_read(ctx, MMX3_MENU_STATE, 0x1) + gameplay_state = await snes_read(ctx, MMX3_GAMEPLAY_STATE, 0x1) + can_move = await snes_read(ctx, MMX3_CAN_MOVE, 0x1) + pause_state = await snes_read(ctx, MMX3_PAUSE_STATE, 0x1) + if menu_state[0] != 0x04 or \ + gameplay_state[0] != 0x04 or \ + can_move[0] != 0x00: + return + + if pause_state[0] == 0x00: + return + + for item in self.item_queue: + if item[0] == "weapon refill": + self.trade_request = None + logger.info(f"You already have a Weapon Energy request pending to be received.") + return + + # Can trade HP -> WPN if HP is above 1 + current_hp = await snes_read(ctx, MMX3_CURRENT_HP, 0x1) + if current_hp[0] > 0x01: + max_trade = current_hp[0] - 1 + set_trade = self.trade_request if self.trade_request <= max_trade else max_trade + self.add_item_to_queue("weapon refill", None, set_trade) + new_hp = current_hp[0] - set_trade + snes_buffered_write(ctx, MMX3_CURRENT_HP, bytearray([new_hp])) + await snes_flush_writes(ctx) + self.trade_request = None + logger.info(f"Traded {set_trade} HP for {set_trade} Weapon Energy.") + else: + logger.info("Couldn't process trade. HP is too low.") + + async def handle_energy_link(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read @@ -178,15 +229,15 @@ async def handle_energy_link(self, ctx): if energy_packet is None: return energy_packet_raw = energy_packet[0] | (energy_packet[1] << 8) - energy_packet = (energy_packet_raw * HP_EXCHANGE_RATE) >> 4 + energy_packet = (energy_packet_raw * EXCHANGE_RATE) >> 4 if energy_packet != 0: await ctx.send_msgs([{ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": [{"operation": "add", "value": energy_packet}, {"operation": "max", "value": 0}], }]) - pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / HP_EXCHANGE_RATE) + (energy_packet_raw / 16) - logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the healing pool. Healing available: {pool:.2f}") + pool = ((ctx.stored_data[f'EnergyLink{ctx.team}'] or 0) / EXCHANGE_RATE) + (energy_packet_raw / 16) + logger.info(f"Deposited {energy_packet_raw / 16:.2f} into the energy pool. Energy available: {pool:.2f}") snes_buffered_write(ctx, MMX3_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) await snes_flush_writes(ctx) @@ -213,50 +264,73 @@ async def handle_energy_link(self, ctx): can_move[0] != 0x00 or \ pause_state[0] != 0x00 or \ receiving_item[0] != 0x00 or \ - ( - going_through_gate[0] != 0x00 and \ - going_through_gate[1] != 0x00 and \ - going_through_gate[2] != 0x00 and \ - going_through_gate[3] != 0x00 \ - ): + going_through_gate != b'\x00\x00\x00\x00': return - if any(item in self.item_queue for item in HP_REFILLS): - logger.info(f"Can't provide a heal. You already have a heal in queue.") - self.heal_request_command = None - return + skip_hp = False + skip_weapon = False + for item in self.item_queue: + if item[0] == "hp refill": + skip_hp = True + self.heal_request_command = None + elif item[0] == "weapon refill": + skip_weapon = True + self.weapon_refill_request_command = None pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 - # Perform auto heals - if self.auto_heal: - if self.heal_request_command is None: - if pool < HP_EXCHANGE_RATE: + if not skip_hp: + # Perform auto heals + if self.auto_heal: + if self.heal_request_command is None: + if pool < EXCHANGE_RATE: + return + current_hp = await snes_read(ctx, MMX3_CURRENT_HP, 0x1) + max_hp = await snes_read(ctx, MMX3_MAX_HP, 0x1) + if max_hp[0] > current_hp[0]: + self.heal_request_command = max_hp[0] - current_hp[0] + + # Handle heal requests + if self.heal_request_command: + heal_needed = self.heal_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.heal_request_command = None return - current_hp = await snes_read(ctx, MMX3_CURRENT_HP, 0x1) - max_hp = await snes_read(ctx, MMX3_MAX_HP, 0x1) - if max_hp[0] > current_hp[0]: - self.heal_request_command = max_hp[0] - current_hp[0] - - # Handle manual heal requests - if self.heal_request_command: - heal_needed = self.heal_request_command - heal_needed_rate = heal_needed * HP_EXCHANGE_RATE - if pool < HP_EXCHANGE_RATE: - logger.info(f"There's not enough healing for your request ({heal_needed}). Healing available: {pool / HP_EXCHANGE_RATE:.2f}") + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("hp refill", None, self.heal_request_command) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Healed by {heal_needed}. Energy available: {pool:.2f}") self.heal_request_command = None - return - elif pool < heal_needed_rate: - heal_needed = int(pool / HP_EXCHANGE_RATE) - heal_needed_rate = heal_needed * HP_EXCHANGE_RATE - await ctx.send_msgs([{ - "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": - [{"operation": "add", "value": -heal_needed_rate}, - {"operation": "max", "value": 0}], - }]) - self.add_item_to_queue("hp refill", None, self.heal_request_command) - pool = (pool / HP_EXCHANGE_RATE) - heal_needed - logger.info(f"Healed by {heal_needed}. Healing available: {pool:.2f}") - self.heal_request_command = None + + if not skip_weapon: + # Handle weapon refill requests + if self.weapon_refill_request_command: + heal_needed = self.weapon_refill_request_command + heal_needed_rate = heal_needed * EXCHANGE_RATE + if pool < EXCHANGE_RATE: + logger.info(f"There's not enough Energy for your request ({heal_needed}). Energy available: {pool / EXCHANGE_RATE:.2f}") + self.weapon_refill_request_command = None + return + elif pool < heal_needed_rate: + heal_needed = int(pool / EXCHANGE_RATE) + heal_needed_rate = heal_needed * EXCHANGE_RATE + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": -heal_needed_rate}, + {"operation": "max", "value": 0}], + }]) + self.add_item_to_queue("weapon refill", None, self.weapon_refill_request_command) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Refilled current weapon by {heal_needed}. Energy available: {pool:.2f}") + self.weapon_refill_request_command = None def add_item_to_queue(self, item_type, item_id, item_additional = None): @@ -301,12 +375,7 @@ async def handle_item_queue(self, ctx): can_move[0] != 0x00 or \ pause_state[0] != 0x00 or \ receiving_item[0] != 0x00 or \ - ( - going_through_gate[0] != 0x00 and \ - going_through_gate[1] != 0x00 and \ - going_through_gate[2] != 0x00 and \ - going_through_gate[3] != 0x00 \ - ): + going_through_gate != b'\x00\x00\x00\x00': backup_item = self.item_queue.pop(0) self.item_queue.append(backup_item) return @@ -314,23 +383,23 @@ async def handle_item_queue(self, ctx): # Handle items that Zero can also get if next_item[0] in X_Z_ITEMS: backup_item = self.item_queue.pop(0) - - if "hp refill" in next_item[0]: + + if next_item[0] == "hp refill": current_hp = await snes_read(ctx, MMX3_CURRENT_HP, 0x1) max_hp = await snes_read(ctx, MMX3_MAX_HP, 0x1) if current_hp[0] < max_hp[0]: snes_buffered_write(ctx, MMX3_ENABLE_HP_REFILL, bytearray([0x02])) - if next_item[0] == "small hp refill": - snes_buffered_write(ctx, MMX3_HP_REFILL_AMOUNT, bytearray([0x02])) - elif next_item[0] == "large hp refill": - snes_buffered_write(ctx, MMX3_HP_REFILL_AMOUNT, bytearray([0x08])) - else: - snes_buffered_write(ctx, MMX3_HP_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX3_HP_REFILL_AMOUNT, bytearray([next_item[2]])) snes_buffered_write(ctx, MMX3_RECEIVING_ITEM, bytearray([0x01])) else: # TODO: Sub Tank logic self.item_queue.append(backup_item) + + elif next_item[0] == "weapon refill": + snes_buffered_write(ctx, MMX3_ENABLE_WEAPON_REFILL, bytearray([0x02])) + snes_buffered_write(ctx, MMX3_WEAPON_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX3_RECEIVING_ITEM, bytearray([0x01])) elif next_item[0] == "1up": life_count = await snes_read(ctx, MMX3_LIFE_COUNT, 0x1) @@ -488,6 +557,9 @@ async def game_watcher(self, ctx): currently_dead = gameplay_state[0] == 0x06 await ctx.handle_deathlink_state(currently_dead) + if self.trade_request is not None: + await self.handle_hp_trade(ctx) + await self.handle_item_queue(ctx) # This is going to be rewritten whenever SNIClient supports on_package @@ -670,7 +742,7 @@ async def game_watcher(self, ctx): self.add_item_to_queue("boss access", item.item) elif item.item in refill_rom_data: - self.add_item_to_queue(refill_rom_data[item.item][0], item.item) + self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) elif item.item == 0xBD0000: # Handle goal @@ -806,7 +878,7 @@ def cmd_pool(self): if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: logger.info(f"Must be connected to server and in game.") else: - pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / HP_EXCHANGE_RATE + pool = (self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / EXCHANGE_RATE logger.info(f"Healing available: {pool:.2f}") @@ -828,14 +900,42 @@ def cmd_heal(self, amount: str = ""): except: logger.info(f"You need to specify how much HP you will recover.") return - if amount > 16: - self.ctx.client_handler.heal_request_command = 16 + if amount <= 0: + logger.info(f"You need to specify how much HP you will recover.") + return self.ctx.client_handler.heal_request_command = amount - logger.info(f"Requested {amount} HP from the healing pool.") + logger.info(f"Requested {amount} HP from the energy pool.") else: logger.info(f"You need to specify how much HP you will request.") +def cmd_refill(self, amount: str = ""): + """ + Request weapon energy from EnergyLink. + """ + if self.ctx.game != "Mega Man X3": + logger.warning("This command can only be used while playing Mega Man X3") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.weapon_refill_request_command is not None: + logger.info(f"You already placed a weapon refill request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + self.ctx.client_handler.weapon_refill_request_command = amount + logger.info(f"Requested {amount} Weapon Energy from the energy pool.") + else: + logger.info(f"You need to specify how much Weapon Energy you will request.") + + def cmd_autoheal(self): """ Enable auto heal from EnergyLink. @@ -851,3 +951,31 @@ def cmd_autoheal(self): else: self.ctx.client_handler.auto_heal = True logger.info(f"Auto healing enabled.") + + +def cmd_trade(self, amount: str = ""): + """ + Trades HP to Weapon Energy. 1:1 ratio. + """ + if self.ctx.game != "Mega Man X3": + logger.warning("This command can only be used while playing Mega Man X3") + if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + else: + if self.ctx.client_handler.trade_request is not None: + logger.info(f"You already placed a weapon refill request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + if amount <= 0: + logger.info(f"You need to specify how much Weapon Energy you will recover.") + return + self.ctx.client_handler.trade_request = amount + logger.info(f"Set up trade for {amount} Weapon Energy. Pause the game to process the trade.") + else: + logger.info(f"You need to specify how much Weapon Energy you will request.") + \ No newline at end of file diff --git a/worlds/mmx3/Items.py b/worlds/mmx3/Items.py index b71bc41f0182..0bd6800f42ec 100644 --- a/worlds/mmx3/Items.py +++ b/worlds/mmx3/Items.py @@ -67,6 +67,8 @@ class MMX3Item(Item): junk_table = { ItemName.small_hp: ItemData(0xBD0030, False), ItemName.large_hp: ItemData(0xBD0031, False), + ItemName.small_weapon: ItemData(0xBD0032, False), + ItemName.large_weapon: ItemData(0xBD0033, False), ItemName.life: ItemData(0xBD0034, False), } diff --git a/worlds/mmx3/Names/ItemName.py b/worlds/mmx3/Names/ItemName.py index cae658c21ae7..7381e4431287 100644 --- a/worlds/mmx3/Names/ItemName.py +++ b/worlds/mmx3/Names/ItemName.py @@ -11,10 +11,10 @@ stage_doppler_lab = "Dr. Doppler's Lab Access Codes" # Third Armor -third_armor_helmet = "Progressive Third Armor Helmet" -third_armor_body = "Progressive Third Armor Body" -third_armor_arms = "Progressive Third Armor Arms" -third_armor_legs = "Progressive Third Armor Legs" +third_armor_helmet = "Helmet Upgrade" +third_armor_body = "Body Upgrade" +third_armor_arms = "Arms Upgrade" +third_armor_legs = "Legs Upgrade" # Weapons parasitic_bomb = "Parasitic Bomb" @@ -45,3 +45,5 @@ small_hp = "Small HP Refill" large_hp = "Large HP Refill" life = "1-Up" +small_weapon = "Small Weapon Energy Refill" +large_weapon = "Large Weapon Energy Refill" \ No newline at end of file diff --git a/worlds/mmx3/Options.py b/worlds/mmx3/Options.py index cbde4a9507ac..8594a7cb63ee 100644 --- a/worlds/mmx3/Options.py +++ b/worlds/mmx3/Options.py @@ -19,8 +19,9 @@ class LogicZSaber(Choice): class EnergyLink(DefaultOnToggle): """ Enable EnergyLink support. - EnergyLink works as a big Sub Tank/HP pool where players can request HP manually or automatically when - they lose HP. You make use of this feature by typing /pool, /heal or /autoheal in the client. + EnergyLink in MMX3 works as a big HP and Weapon Energy pool that the players can use to request HP + or Weapon Energy whenever they need to. + You make use of this feature by typing /pool, /heal , /refill or /autoheal in the client. """ display_name = "Energy Link" @@ -34,6 +35,81 @@ class StartingLifeCount(Range): range_end = 9 default = 2 +class StartingHP(Range): + """ + How much HP X will have at the start of the game. + Note: Going over 32 HP may cause visual bugs in either gameplay or the pause menu. + The max HP is capped at 56. + """ + display_name = "Starting HP" + range_start = 1 + range_end = 32 + default = 16 + +class HeartTankEffectiveness(Range): + """ + How many units of HP each Heart tank will provide to the user. + Note: Going over 32 HP may cause visual bugs in either gameplay or the pause menu. + The max HP is capped at 56. + """ + display_name = "Heart Tank Effectiveness" + range_start = 1 + range_end = 8 + default = 2 + +class BossWeaknessRando(Choice): + """ + Every main boss will have its weakness randomized. + vanilla: Bosses retain their original weaknesses + shuffled: Bosses have their weaknesses shuffled + chaotic_double: Bosses will have two random weaknesses under the chaotic set + chaotic_single: Bosses will have one random weakness under the chaotic set + + The chaotic set makes every weapon charge level a separate weakness instead of keeping + them together, meaning that a boss can be weak to Charged Frost Shield but not its + uncharged version. + """ + display_name = "Boss Weakness Randomization" + option_vanilla = 0 + option_shuffled = 1 + option_chaotic_double = 2 + option_chaotic_single = 3 + default = 0 + +class BossWeaknessStrictness(Choice): + """ + How strict boss weaknesses will be. + not_strict: Allow every weapon to deal damage to the bosses + weakness_and_buster: Only allow the weakness and buster to deal damage to the bosses + weakness_and_upgraded_buster: Only allow the weakness and buster charge levels 3 & 4 to deal damage to the bosses + only_weakness: Only the weakness will deal damage to the bosses + + Z-Saber damage output will be cut to 50%/37.5%/25% of its original damage according to the strictness setting. + """ + display_name = "Boss Weakness Strictness" + option_not_strict = 0 + option_weakness_and_buster = 1 + option_weakness_and_upgraded_buster = 2 + option_only_weakness = 3 + default = 0 + +class BossRandomizedHP(Choice): + """ + Wheter to randomize the boss' hp or not. + off: Bosses' HP will not be randomized + weak: Bosses will have [1,32] HP + regular: Bosses will have [16,48] HP + strong: Bosses will have [32,64] HP + chaotic: Bosses will have [1,64] HP + """ + display_name = "Boss Randomize HP" + option_off = 0 + option_weak = 1 + option_regular = 2 + option_strong = 3 + option_chaotic = 4 + default = 0 + class JammedBuster(Toggle): """ Jams X's buster making it only able to shoot lemons. @@ -50,6 +126,8 @@ class DisableChargeFreeze(DefaultOnToggle): class LogicBossWeakness(DefaultOnToggle): """ Every main boss will logically expect you to have its weakness. + This option will be forced if the Boss Weakness Strictness setting is set to require only the weakness or + the upgraded buster option. """ display_name = "Boss Weakness Logic" @@ -98,6 +176,14 @@ class Lab3BossRematchCount(Range): range_end = 8 default = 8 +class LabsBundleUnlock(Toggle): + """ + Whether to unlock Dr. Doppler's Lab 1-3 levels as a group or not. + Unlocking level 4 requires getting all Lab levels cleared. + """ + display_name = "Doppler Lab Levels Bundle Unlock" + + class DopplerOpen(Choice): """ Under what conditions will Dr. Doppler's lab open. @@ -257,14 +343,20 @@ class MMX3Options(PerGameCommonOptions): death_link: DeathLink energy_link: EnergyLink starting_life_count: StartingLifeCount + starting_hp: StartingHP + heart_tank_effectiveness: HeartTankEffectiveness + boss_weakness_rando: BossWeaknessRando + boss_weakness_strictness: BossWeaknessStrictness + boss_randomize_hp: BossRandomizedHP + pickupsanity: PickupSanity jammed_buster: JammedBuster disable_charge_freeze: DisableChargeFreeze - pickupsanity: PickupSanity logic_boss_weakness: LogicBossWeakness logic_vile_required: LogicRequireVileDefeatForDoppler logic_z_saber: LogicZSaber doppler_lab_2_boss: Lab2Boss doppler_lab_3_boss_rematch_count: Lab3BossRematchCount + doppler_all_labs: LabsBundleUnlock doppler_open: DopplerOpen doppler_medal_count: DopplerMedalCount doppler_weapon_count: DopplerWeaponCount diff --git a/worlds/mmx3/Rom.py b/worlds/mmx3/Rom.py index e6aa149b5265..2afb4013f944 100644 --- a/worlds/mmx3/Rom.py +++ b/worlds/mmx3/Rom.py @@ -3,12 +3,14 @@ import Utils import hashlib import os -from typing import Optional, TYPE_CHECKING +from typing import Optional from pkgutil import get_data from worlds.AutoWorld import World from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes +from .Weaknesses import boss_weakness_data + HASH_US = 'cfe8c11f0dce19e4fa5f3fd75775e47c' HASH_LEGACY = 'ff683b75e75e9b59f0c713c7512a016b' @@ -51,13 +53,61 @@ } refill_rom_data = { - 0xBD0030: ["small hp refill"], - 0xBD0031: ["large hp refill"], - 0xBD0034: ["1up"], - #0xBD0032: ["small weapon refill"], - #0xBD0033: ["large weapon refill"] + 0xBD0030: ["hp refill", 2], + 0xBD0031: ["hp refill", 8], + 0xBD0034: ["1up", 0], + 0xBD0032: ["weapon refill", 2], + 0xBD0033: ["weapon refill", 8] +} + +boss_weakness_offsets = { + "Blast Hornet": 0x03674B, + "Blizzard Buffalo": 0x036771, + "Gravity Beetle": 0x036797, + "Toxic Seahorse": 0x0367BD, + "Volt Catfish": 0x0367E3, + "Crush Crawfish": 0x036809, + "Tunnel Rhino": 0x03682F, + "Neon Tiger": 0x036855, + "Bit": 0x03687B, + "Byte": 0x0368A1, + "Vile": 0x0368C7, + "Vile Goliath": 0x0368ED, + "Doppler": 0x036913, + "Sigma": 0x036939, + "Kaiser Sigma": 0x03695F, + "Godkarmachine": 0x037F00, + "Press Disposer": 0x037FC8, + "Worm Seeker-R": 0x03668D, + "Shurikein": 0x037F28, + "Hotareeca": 0x037F50, + "Volt Kurageil": 0x037F78, + "Hell Crusher": 0x037FA0, } +boss_hp_caps_offsets = { + "Maoh": 0x016985, + "Blast Hornet": 0x1C9DC2, + "Blizzard Buffalo": 0x01C9CB, + "Gravity Beetle": 0x09F3C3, + "Toxic Seahorse": 0x09E612, + "Volt Catfish": 0x09EBC0, + "Crush Crawfish": 0x01D1B2, + "Tunnel Rhino": 0x1FE765, + "Neon Tiger": 0x09DE11, + "Bit": 0x0390F2, + "Byte": 0x1E4614, + "Vile": 0x02AC3E, + "Vile Kangaroo": 0x03958F, + "Vile Goliath": 0x02A4EA, + "Doppler": 0x09D737, + "Sigma": 0x0294F2, + "Kaiser Sigma": 0x029B1F, + "Godkarmachine": 0x028F60, + "Press Disposer": 0x09C6B9, +} + + class MMX3ProcedurePatch(APProcedurePatch, APTokenMixin): hash = [HASH_US, HASH_LEGACY] game = "Mega Man X3" @@ -79,6 +129,42 @@ def write_byte(self, offset, value): def write_bytes(self, offset, value: typing.Iterable[int]): self.write_token(APTokenTypes.WRITE, offset, bytes(value)) +def adjust_boss_damage_table(world: World, patch: MMX3ProcedurePatch): + for boss, data in world.boss_weakness_data.items(): + try: + offset = boss_weakness_offsets[boss] + if boss == "Worm Seeker-R": + for x in range(len(data)): + if x == 0x02 or x == 0x04 or x == 0x05: + data[x] = 0x7F + else: + data[x] = data[x]*3 if data[x] < 0x80 else data[x] + except: + continue + patch.write_bytes(offset, bytearray(data)) + + # Adjust Charged Triad Thunder lag (lasts 90 less frames) + patch.write_byte(0x1FD2D1, 0x14) + +def adjust_boss_hp(world: World, patch: MMX3ProcedurePatch): + option = world.options.boss_randomize_hp + if option == "weak": + ranges = [1,32] + elif option == "regular": + ranges = [16,48] + elif option == "strong": + ranges = [32,64] + elif option == "chaotic": + ranges = [1,64] + + for boss, offset in boss_hp_caps_offsets.items(): + if boss == "Blast Hornet": + patch.write_byte(offset, world.random.randint(ranges[0], 32)) + else: + patch.write_byte(offset, world.random.randint(ranges[0], ranges[1])) + + + def patch_rom(world: World, patch: MMX3ProcedurePatch): from Utils import __version__ @@ -116,11 +202,27 @@ def patch_rom(world: World, patch: MMX3ProcedurePatch): patch.write_bytes(0x0FF84, bytearray([0xFF for _ in range(0x007C)])) patch.write_bytes(0x1FA80, bytearray([0xFF for _ in range(0x0580)])) + if world.options.boss_weakness_rando != "vanilla": + adjust_boss_damage_table(world, patch) + + if world.options.boss_randomize_hp != "off": + adjust_boss_hp(world, patch) + # Edit the ROM header patch.name = bytearray(f'MMX3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] patch.name.extend([0] * (21 - len(patch.name))) patch.write_bytes(0x7FC0, patch.name) + # Setup starting HP + patch.write_byte(0x007487, world.options.starting_hp.value) + patch.write_byte(0x0019B6, world.options.starting_hp.value) + patch.write_byte(0x0021CC, (world.options.starting_hp.value + (world.options.heart_tank_effectiveness.value * 8)) | 0x80) + + # Setup starting life count + patch.write_byte(0x0019B1, world.options.starting_life_count.value) + patch.write_byte(0x0072C3, world.options.starting_life_count.value) + patch.write_byte(0x0021BE, world.options.starting_life_count.value) + # Write options to the ROM patch.write_byte(0x17FFE0, world.options.doppler_open.value) patch.write_byte(0x17FFE1, world.options.doppler_medal_count.value) @@ -170,11 +272,11 @@ def patch_rom(world: World, patch: MMX3ProcedurePatch): patch.write_byte(0x17FFF8, world.options.death_link.value) patch.write_byte(0x17FFF9, world.options.jammed_buster.value) - - # Setup starting life count - patch.write_byte(0x0019B1, world.options.starting_life_count.value) - patch.write_byte(0x0072C3, world.options.starting_life_count.value) - patch.write_byte(0x0021BE, world.options.starting_life_count.value) + patch.write_byte(0x17FFFA, world.options.boss_weakness_rando.value) + patch.write_byte(0x17FFFB, world.options.boss_weakness_strictness.value) + patch.write_byte(0x17FFFC, world.options.starting_hp.value) + patch.write_byte(0x17FFFD, world.options.heart_tank_effectiveness.value) + patch.write_byte(0x17FFFE, world.options.doppler_all_labs.value) patch.write_file("token_patch.bin", patch.get_token_binary()) diff --git a/worlds/mmx3/Rules.py b/worlds/mmx3/Rules.py index e64f6e78e026..afdacd403106 100644 --- a/worlds/mmx3/Rules.py +++ b/worlds/mmx3/Rules.py @@ -1,7 +1,106 @@ from worlds.generic.Rules import add_rule, set_rule -from . import MMX3World +from . import MMX3World, item_groups from .Names import LocationName, ItemName, RegionName, EventName +from .Weaknesses import boss_weaknesses + +mavericks = [ + "Blizzard Buffalo", + "Toxic Seahorse", + "Tunnel Rhino", + "Volt Catfish", + "Crush Crawfish", + "Neon Tiger", + "Gravity Beetle", + "Blast Hornet", +] + +bosses = { + "Blizzard Buffalo": [ + f"{RegionName.blizzard_buffalo_after_bit_byte} -> {RegionName.blizzard_buffalo_boss}", + LocationName.doppler_lab_3_blizzard_buffalo, + EventName.blizzard_buffalo_rematch + ], + "Toxic Seahorse": [ + f"{RegionName.toxic_seahorse_dam} -> {RegionName.toxic_seahorse_boss}", + LocationName.doppler_lab_3_toxic_seahorse, + EventName.toxic_seahorse_rematch + ], + "Tunnel Rhino": [ + f"{RegionName.tunnel_rhino_climbing} -> {RegionName.tunnel_rhino_boss}", + LocationName.doppler_lab_3_tunnel_rhino, + EventName.tunnel_rhino_rematch + ], + "Volt Catfish": [ + f"{RegionName.volt_catfish_inside} -> {RegionName.volt_catfish_boss}", + LocationName.doppler_lab_3_volt_catfish, + EventName.volt_catfish_rematch + ], + "Crush Crawfish": [ + f"{RegionName.crush_crawfish_inside} -> {RegionName.crush_crawfish_boss}", + LocationName.doppler_lab_3_crush_crawfish, + EventName.crush_crawfish_rematch + ], + "Neon Tiger": [ + f"{RegionName.neon_tiger_hill} -> {RegionName.neon_tiger_boss}", + LocationName.doppler_lab_3_neon_tiger, + EventName.neon_tiger_rematch + ], + "Gravity Beetle": [ + f"{RegionName.gravity_beetle_inside} -> {RegionName.gravity_beetle_boss}", + LocationName.doppler_lab_3_gravity_beetle, + EventName.gravity_beetle_rematch, + ], + "Blast Hornet": [ + f"{RegionName.blast_hornet_bit_byte} -> {RegionName.blast_hornet_boss}", + LocationName.doppler_lab_3_blast_hornet, + EventName.blast_hornet_rematch + ], + "Bit": [ + LocationName.bit_defeat + ], + "Byte": [ + LocationName.byte_defeat + ], + "Hotareeca": [ + f"{RegionName.toxic_seahorse_underwater} -> {RegionName.toxic_seahorse_hootareca}" + ], + "Hell Crusher": [ + f"{RegionName.tunnel_rhino_wall_jump} -> {RegionName.tunnel_rhino_hell_crusher}", + ], + "Worm Seeker-R": [ + f"{RegionName.neon_tiger_start} -> {RegionName.neon_tiger_worm}", + ], + "Shurikein": [ + f"{RegionName.blast_hornet_conveyors} -> {RegionName.blast_hornet_shurikein}", + ], + "Vile": [ + f"{RegionName.vile_before} -> {RegionName.vile_boss}", + ], + "Press Disposer": [ + LocationName.doppler_lab_1_boss, + EventName.dr_doppler_lab_1_clear + ], + "Godkarmachine": [ + LocationName.doppler_lab_1_boss, + EventName.dr_doppler_lab_1_clear + ], + "Dr. Doppler's Lab 2 Boss": [ + LocationName.doppler_lab_2_boss, + EventName.dr_doppler_lab_2_clear + ], + "Doppler": [ + LocationName.doppler_lab_3_boss, + EventName.dr_doppler_lab_3_clear + ], + "Sigma": [ + f"{RegionName.dr_doppler_lab_3_boss} -> {RegionName.dr_doppler_lab_4}" + ], + "Kaiser Sigma": [ + f"{RegionName.dr_doppler_lab_3_boss} -> {RegionName.dr_doppler_lab_4}" + ] +} + def set_rules(world: MMX3World): player = world.player @@ -211,11 +310,14 @@ def set_rules(world: MMX3World): set_rule(multiworld.get_location(LocationName.blast_hornet_helmet, player), lambda state: ( state.has(ItemName.third_armor_legs, player, 2) or - state.has(ItemName.ride_hawk, player) + ( + state.has(ItemName.third_armor_legs, player, 1) and + state.has(ItemName.ride_hawk, player) + ) )) # Handle bosses weakness - if world.options.logic_boss_weakness.value: + if world.options.logic_boss_weakness.value or world.options.boss_weakness_strictness.value >= 2: add_boss_weakness_logic(world) # Z-Saber logic @@ -230,175 +332,85 @@ def set_rules(world: MMX3World): def add_boss_weakness_logic(world): player = world.player multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value - # Set Blizzard Buffalo rules - set_rule(multiworld.get_entrance(f"{RegionName.blizzard_buffalo_after_bit_byte} -> {RegionName.blizzard_buffalo_boss}", player), - lambda state: state.has(ItemName.parasitic_bomb, player)) - set_rule(multiworld.get_entrance(f"{RegionName.blizzard_buffalo_after_bit_byte} -> {RegionName.blizzard_buffalo_boss}", player), - lambda state: state.has(ItemName.parasitic_bomb, player)) - - # Set Toxic Seahorse rules - set_rule(multiworld.get_entrance(f"{RegionName.toxic_seahorse_dam} -> {RegionName.toxic_seahorse_boss}", player), - lambda state: state.has(ItemName.frost_shield, player)) - set_rule(multiworld.get_entrance(f"{RegionName.toxic_seahorse_dam} -> {RegionName.toxic_seahorse_boss}", player), - lambda state: state.has(ItemName.frost_shield, player)) - - # Set Tunnel Rhino rules - set_rule(multiworld.get_entrance(f"{RegionName.tunnel_rhino_climbing} -> {RegionName.tunnel_rhino_boss}", player), - lambda state: state.has(ItemName.acid_burst, player)) - set_rule(multiworld.get_entrance(f"{RegionName.tunnel_rhino_climbing} -> {RegionName.tunnel_rhino_boss}", player), - lambda state: state.has(ItemName.acid_burst, player)) - - # Set Volt Catfish rules - set_rule(multiworld.get_entrance(f"{RegionName.volt_catfish_inside} -> {RegionName.volt_catfish_boss}", player), - lambda state: state.has(ItemName.tornado_fang, player)) - set_rule(multiworld.get_entrance(f"{RegionName.volt_catfish_inside} -> {RegionName.volt_catfish_boss}", player), - lambda state: state.has(ItemName.tornado_fang, player)) - - # Set Crush Crawfish rules - set_rule(multiworld.get_entrance(f"{RegionName.crush_crawfish_inside} -> {RegionName.crush_crawfish_boss}", player), - lambda state: state.has(ItemName.triad_thunder, player)) - set_rule(multiworld.get_entrance(f"{RegionName.crush_crawfish_inside} -> {RegionName.crush_crawfish_boss}", player), - lambda state: state.has(ItemName.triad_thunder, player)) - - # Set Neon Tiger rules - set_rule(multiworld.get_entrance(f"{RegionName.neon_tiger_hill} -> {RegionName.neon_tiger_boss}", player), - lambda state: state.has(ItemName.spinning_blade, player)) - set_rule(multiworld.get_entrance(f"{RegionName.neon_tiger_hill} -> {RegionName.neon_tiger_boss}", player), - lambda state: state.has(ItemName.spinning_blade, player)) - - # Set Gravity Beetle rules - set_rule(multiworld.get_entrance(f"{RegionName.gravity_beetle_inside} -> {RegionName.gravity_beetle_boss}", player), - lambda state: state.has(ItemName.ray_splasher, player)) - set_rule(multiworld.get_entrance(f"{RegionName.gravity_beetle_inside} -> {RegionName.gravity_beetle_boss}", player), - lambda state: state.has(ItemName.ray_splasher, player)) - - # Set Blast Hornet rules - set_rule(multiworld.get_entrance(f"{RegionName.blast_hornet_bit_byte} -> {RegionName.blast_hornet_boss}", player), - lambda state: state.has(ItemName.gravity_well, player)) - set_rule(multiworld.get_entrance(f"{RegionName.blast_hornet_bit_byte} -> {RegionName.blast_hornet_boss}", player), - lambda state: state.has(ItemName.gravity_well, player)) - - # Set maverick rematch rules - if world.options.doppler_lab_3_boss_rematch_count.value != 0: - set_rule(multiworld.get_location(LocationName.doppler_lab_3_blizzard_buffalo, player), - lambda state: state.has(ItemName.parasitic_bomb, player)) - set_rule(multiworld.get_location(EventName.blizzard_buffalo_rematch, player), - lambda state: state.has(ItemName.parasitic_bomb, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_toxic_seahorse, player), - lambda state: state.has(ItemName.frost_shield, player)) - set_rule(multiworld.get_location(EventName.toxic_seahorse_rematch, player), - lambda state: state.has(ItemName.frost_shield, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_tunnel_rhino, player), - lambda state: state.has(ItemName.acid_burst, player)) - set_rule(multiworld.get_location(EventName.tunnel_rhino_rematch, player), - lambda state: state.has(ItemName.acid_burst, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_volt_catfish, player), - lambda state: state.has(ItemName.tornado_fang, player)) - set_rule(multiworld.get_location(EventName.volt_catfish_rematch, player), - lambda state: state.has(ItemName.tornado_fang, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_crush_crawfish, player), - lambda state: state.has(ItemName.triad_thunder, player)) - set_rule(multiworld.get_location(EventName.crush_crawfish_rematch, player), - lambda state: state.has(ItemName.triad_thunder, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_neon_tiger, player), - lambda state: state.has(ItemName.spinning_blade, player)) - set_rule(multiworld.get_location(EventName.neon_tiger_rematch, player), - lambda state: state.has(ItemName.spinning_blade, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_gravity_beetle, player), - lambda state: state.has(ItemName.ray_splasher, player)) - set_rule(multiworld.get_location(EventName.gravity_beetle_rematch, player), - lambda state: state.has(ItemName.ray_splasher, player)) - set_rule(multiworld.get_location(LocationName.doppler_lab_3_blast_hornet, player), - lambda state: state.has(ItemName.gravity_well, player)) - set_rule(multiworld.get_location(EventName.blast_hornet_rematch, player), - lambda state: state.has(ItemName.gravity_well, player)) - - # Set Bit rules - add_rule(multiworld.get_location(LocationName.bit_defeat, player), - lambda state: ( - state.has(ItemName.frost_shield, player) or - state.has(ItemName.triad_thunder, player) - )) - - # Set Byte rules - add_rule(multiworld.get_location(LocationName.byte_defeat, player), - lambda state: ( - state.has(ItemName.tornado_fang, player) or - state.has(ItemName.ray_splasher, player) - )) - - # Set Vile rules - add_rule(multiworld.get_entrance(f"{RegionName.vile_before} -> {RegionName.vile_boss}", player), - lambda state: ( - state.has(ItemName.spinning_blade, player) or - state.has(ItemName.ray_splasher, player) - )) - - # Set Press Disposer rules - add_rule(multiworld.get_location(EventName.dr_doppler_lab_1_clear, player), - lambda state: ( - state.has(ItemName.tornado_fang, player) or - state.has(ItemName.ray_splasher, player) - )) - add_rule(multiworld.get_location(LocationName.doppler_lab_1_boss, player), - lambda state: ( - state.has(ItemName.tornado_fang, player) or - state.has(ItemName.ray_splasher, player) - )) - # Set Godkarmachine O' Inary rules - add_rule(multiworld.get_location(EventName.dr_doppler_lab_1_clear, player), - lambda state: state.has(ItemName.ray_splasher, player)) - add_rule(multiworld.get_location(LocationName.doppler_lab_1_boss, player), - lambda state: state.has(ItemName.ray_splasher, player)) + if world.options.doppler_lab_3_boss_rematch_count.value == 0: + for boss in mavericks: + bosses[boss].pop() + bosses[boss].pop() - if world.options.doppler_lab_2_boss == "volt_kurageil": - # Set Volt Kurageil rules - add_rule(multiworld.get_location(EventName.dr_doppler_lab_2_clear, player), - lambda state: ( - state.has(ItemName.frost_shield, player) or - state.has(ItemName.triad_thunder, player) - )) - add_rule(multiworld.get_location(LocationName.doppler_lab_2_boss, player), - lambda state: ( - state.has(ItemName.frost_shield, player) or - state.has(ItemName.triad_thunder, player) - )) - elif world.options.doppler_lab_2_boss == "vile": - # Set Vile rematch rules - add_rule(multiworld.get_location(EventName.dr_doppler_lab_2_clear, player), - lambda state: ( - ( - state.has(ItemName.parasitic_bomb, player) or - state.has(ItemName.tornado_fang, player) - ) and ( - state.has(ItemName.spinning_blade, player) or - state.has(ItemName.ray_splasher, player) - ) - )) - add_rule(multiworld.get_location(LocationName.doppler_lab_2_boss, player), - lambda state: ( - ( - state.has(ItemName.parasitic_bomb, player) or - state.has(ItemName.tornado_fang, player) - ) and ( - state.has(ItemName.spinning_blade, player) or - state.has(ItemName.ray_splasher, player) - ) - )) - - # Set Dr. Doppler rules - add_rule(multiworld.get_location(LocationName.doppler_lab_3_boss, player), - lambda state: state.has(ItemName.acid_burst, player)) - add_rule(multiworld.get_location(EventName.dr_doppler_lab_3_clear, player), - lambda state: state.has(ItemName.acid_burst, player)) - # Set Sigma rules - set_rule(multiworld.get_location(LocationName.doppler_lab_3_boss, player), - lambda state: ( - state.has(ItemName.spinning_blade, player) or - state.has(ItemName.frost_shield, player) - )) + for boss, regions in bosses.items(): + weaknesses = boss_weaknesses[boss] + for weakness in weaknesses: + weakness = weakness[0] + if weakness is not None: + for region in regions: + is_entrance = "->" in region + if "Check Charge" in weakness[0]: + charge_level = int(weakness[0][-1:]) - 1 + if len(weakness) == 1: + if is_entrance: + add_rule(multiworld.get_entrance(region, player), + lambda state: state.has(ItemName.third_armor_arms, player, jammed_buster + charge_level)) + else: + add_rule(multiworld.get_location(region, player), + lambda state: state.has(ItemName.third_armor_arms, player, jammed_buster + charge_level)) + else: + if is_entrance: + add_rule(multiworld.get_entrance(region, player), + lambda state, weapons=tuple([weakness[1]]): ( + state.has(ItemName.third_armor_arms, player, jammed_buster + charge_level) and + state.has_all(weapons, player) + )) + else: + add_rule(multiworld.get_location(region, player), + lambda state, weapons=tuple([weakness[1]]): ( + state.has(ItemName.third_armor_arms, player, jammed_buster + charge_level) and + state.has_all(weapons, player) + )) + else: + if is_entrance: + add_rule(multiworld.get_entrance(region, player), + lambda state, weapons=tuple(weakness): state.has_all(weapons, player)) + else: + add_rule(multiworld.get_location(region, player), + lambda state, weapons=tuple(weakness): state.has_all(weapons, player)) + if world.options.boss_weakness_rando == "vanilla": + if world.options.doppler_lab_2_boss == "volt_kurageil": + # Set Volt Kurageil rules + set_rule(multiworld.get_location(EventName.dr_doppler_lab_2_clear, player), + lambda state: ( + state.has(ItemName.frost_shield, player) or + state.has(ItemName.triad_thunder, player) + )) + set_rule(multiworld.get_location(LocationName.doppler_lab_2_boss, player), + lambda state: ( + state.has(ItemName.frost_shield, player) or + state.has(ItemName.triad_thunder, player) + )) + elif world.options.doppler_lab_2_boss == "vile": + # Set Vile rematch rules + set_rule(multiworld.get_location(EventName.dr_doppler_lab_2_clear, player), + lambda state: ( + ( + state.has(ItemName.parasitic_bomb, player) or + state.has(ItemName.tornado_fang, player) + ) and ( + state.has(ItemName.spinning_blade, player) or + state.has(ItemName.ray_splasher, player) + ) + )) + set_rule(multiworld.get_location(LocationName.doppler_lab_2_boss, player), + lambda state: ( + ( + state.has(ItemName.parasitic_bomb, player) or + state.has(ItemName.tornado_fang, player) + ) and ( + state.has(ItemName.spinning_blade, player) or + state.has(ItemName.ray_splasher, player) + ) + )) def add_z_saber_logic(world): player = world.player diff --git a/worlds/mmx3/Weaknesses.py b/worlds/mmx3/Weaknesses.py new file mode 100644 index 000000000000..184d81418710 --- /dev/null +++ b/worlds/mmx3/Weaknesses.py @@ -0,0 +1,497 @@ +from .Names import LocationName, ItemName, RegionName, EventName + +boss_weaknesses = { + "Blizzard Buffalo": [[[ItemName.parasitic_bomb]]], + "Toxic Seahorse": [[[ItemName.frost_shield]]], + "Tunnel Rhino": [[[ItemName.acid_burst]]], + "Volt Catfish": [[[ItemName.tornado_fang]]], + "Crush Crawfish": [[[ItemName.triad_thunder]]], + "Neon Tiger": [[[ItemName.spinning_blade]]], + "Gravity Beetle": [[[ItemName.ray_splasher]]], + "Blast Hornet": [[[ItemName.gravity_well]]], + "Hotareeca": [[None]], + "Worm Seeker-R": [[None]], + "Hell Crusher": [[None]], + "Shurikein": [[None]], + #"Hotareeca": [[[ItemName.frost_shield, ItemName.triad_thunder]]], + #"Worm Seeker-R": [[[ItemName.triad_thunder, ItemName.acid_burst]]], + #"Hell Crusher": [[[ItemName.tornado_fang, ItemName.ray_splasher]]], + #"Shurikein": [[[ItemName.acid_burst]]], + "Bit": [[[ItemName.frost_shield, ItemName.triad_thunder]]], + "Byte": [[[ItemName.tornado_fang, ItemName.ray_splasher]]], + "Vile": [[[ItemName.spinning_blade, ItemName.ray_splasher]]], + "Press Disposer": [[[ItemName.tornado_fang, ItemName.ray_splasher]]], + "Godkarmachine": [[[ItemName.ray_splasher]]], + "Dr. Doppler's Lab 2 Boss": [[None]], + "Volt Kurageil": [[[ItemName.frost_shield, ItemName.triad_thunder]]], + "Vile Goliath": [[[ItemName.parasitic_bomb, ItemName.tornado_fang]]], + "Doppler": [[[ItemName.acid_burst]]], + "Sigma": [[[ItemName.spinning_blade, ItemName.frost_shield]]], + "Kaiser Sigma": [[None]], +} + +WEAKNESS_UNCHARGED_DMG = 0x03 +WEAKNESS_CHARGED_DMG = 0x05 + +weapon_id = { + 0x00: "Lemon", + 0x01: "Charged Shot (Level 1)", + 0x02: "Z-Saber (Slash)", + 0x03: "Charged Shot (Level 2)", + 0x04: "Z-Saber (Beam)", + 0x05: "Z-Saber (Beam slashes)", + 0x06: "Lemon (Dash)", + 0x07: "Uncharged Acid Burst", + 0x08: "Uncharged Parasitic Bomb", + 0x09: "Uncharged Triad Thunder (Contact)", + 0x0A: "Uncharged Spinning Blade", + 0x0C: "Gravity Well", + 0x0D: "Uncharged Frost Shield", + 0x0E: "Uncharged Tornado Fang", + 0x10: "Charged Acid Burst", + 0x11: "Charged Parasitic Bomb", + 0x12: "Charged Triad Thunder", + 0x13: "Charged Spinning Blade", + 0x15: "Gravity Well", + 0x16: "Charged Frost Shield (On hand)", + 0x17: "Charged Tornado Fang", + 0x18: "Acid Burst (Small uncharged bubbles)", + 0x1B: "Uncharged Triad Thunder (Thunder)", + 0x1C: "Ray Splasher", + 0x1D: "Charged Shot (Level 3)", + 0x1F: "Charged Shot (Level 4, Main projectile)", + 0x20: "Charged Shot (Level 4, Secondary projectile)", + 0x21: "Charged Frost Shield (Lotus)" +} + +damage_templates = { + "Allow Buster": [ + 0x01, 0x01, 0x04, 0x02, 0x04, 0x02, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x02, 0x80, 0x04, + 0x02, 0x80, 0x80, 0x80, 0x80, 0x80 + ], + "Allow Upgraded Buster": [ + 0x80, 0x01, 0x06, 0x80, 0x03, 0x02, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x01, 0x80, 0x02, + 0x01, 0x80, 0x80, 0x80, 0x80, 0x80 + ], + "Only Weakness": [ + 0x80, 0x80, 0x04, 0x80, 0x02, 0x01, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 + ], +} + +boss_weakness_data = { + "Blast Hornet": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x02,0x01,0x03,0x01,0x80,0x01, + 0x02,0x01,0x01,0x03,0x01,0x03,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Blizzard Buffalo": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x80,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x80,0x02,0x04,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Gravity Beetle": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x05,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Toxic Seahorse": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x02, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Volt Catfish": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x80,0x01,0x80,0x00,0x01,0x01,0x80, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x80,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Crush Crawfish": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x80,0x01,0x80,0x01,0x00,0x01,0x80,0x01, + 0x01,0x80,0x01,0x80,0x01,0x00,0x01,0x80, + 0x01,0x01,0x03,0x02,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Tunnel Rhino": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x80,0x80,0x01,0x01,0x00,0x01,0x80,0x80, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x80,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Neon Tiger": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x80,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x80,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x80,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Bit": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Byte": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Vile": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x80,0x01,0x01,0x00,0x01,0x80,0x80, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x80,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Vile Goliath": [ + 0x01,0x02,0x10,0x03,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Hell Crusher": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Shurikein": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x03, + 0x80,0x80,0x01,0x01,0x00,0x01,0x80,0x80, + 0x05,0x01,0x02,0x01,0x02,0x00,0x01,0x80, + 0x02,0x02,0x03,0x80,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Hotareeca": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Worm Seeker-R": [ + 0x03,0x05,0x1E,0x09,0x80,0x04,0x05,0x09, + 0x02,0x0F,0x09,0x05,0x00,0x0F,0x80,0x09, + 0x09,0x02,0x80,0x1E,0x09,0x00,0x0F,0x80, + 0x05,0x05,0x09,0x05,0x05,0x09,0x0F,0x12, + 0x09,0x09,0x1E,0x09,0x06,0x7F, + ], + "Volt Kurageil": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Godkarmachine": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x02,0x01,0x01,0x01,0x00,0x01,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Press Disposer": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x01,0x01,0x01,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Doppler": [ + 0x01,0x02,0x10,0x03,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Sigma": [ + 0x01,0x01,0x10,0x02,0x08,0x04,0x01,0x01, + 0x01,0x01,0x01,0x01,0x00,0x01,0x80,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x04,0x80, + 0x01,0x02,0x03,0x01,0x01,0x02,0x80,0x03, + 0x02,0x01,0x02,0x01,0x01,0x00, + ], + "Kaiser Sigma": [ + 0x80,0x01,0x10,0x02,0x08,0x04,0x80,0x80, + 0x80,0x80,0x80,0x80,0x00,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x00,0x80,0x80, + 0x80,0x02,0x03,0x80,0x80,0x02,0x80,0x03, + 0x02,0x80,0x02,0x01,0x01,0x00, + ] +} + +boss_excluded_weapons = { + "Blast Hornet": [ + ], + "Blizzard Buffalo": [ + "Charged Triad Thunder", + ], + "Gravity Beetle": [ + ], + "Toxic Seahorse": [ + ], + "Volt Catfish": [ + ], + "Crush Crawfish": [ + ], + "Tunnel Rhino": [ + ], + "Neon Tiger": [ + ], + "Bit": [ + ], + "Byte": [ + "Charged Triad Thunder", + ], + "Vile": [ + "Charged Triad Thunder", + ], + "Vile Goliath": [ + ], + "Hell Crusher": [ + ], + "Shurikein": [ + ], + "Hotareeca": [ + "Acid Burst", + "Charged Acid Burst", + "Charged Frost Shield", + "Charged Triad Thunder", + ], + "Worm Seeker-R": [ + "Charged Parasitic Bomb", + ], + "Volt Kurageil": [ + "Acid Burst", + "Charged Acid Burst", + "Charged Frost Shield", + "Charged Triad Thunder", + ], + "Godkarmachine": [ + "Charged Triad Thunder", + ], + "Press Disposer": [ + "Charged Tornado Fang", + ], + "Doppler": [ + ], + "Sigma": [ + "Charged Parasitic Bomb", + ], + "Kaiser Sigma": [ + "Charged Tornado Fang", + "Charged Parasitic Bomb", + ] +} + +blast_hornet_data = { + "Gravity Well": [ + [[ItemName.gravity_well], 0x0C, 0x03], + [[ItemName.gravity_well], 0x15, 0x03], + ] +} +weapons = { + "Buster": [ + [None, 0x00, 0x02], + [None, 0x01, 0x03], + [None, 0x03, 0x04], + [None, 0x06, 0x03], + [None, 0x1D, 0x04], + [None, 0x1F, 0x05], + [None, 0x20, 0x03], + ], + "Acid Burst": [ + [[ItemName.acid_burst], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.acid_burst], 0x10, WEAKNESS_CHARGED_DMG], + [[ItemName.acid_burst], 0x18, WEAKNESS_UNCHARGED_DMG-1], + ], + "Parasitic Bomb": [ + [[ItemName.parasitic_bomb], 0x08, WEAKNESS_UNCHARGED_DMG], + [[ItemName.parasitic_bomb], 0x11, WEAKNESS_CHARGED_DMG], + ], + "Triad Thunder": [ + [[ItemName.triad_thunder], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.triad_thunder], 0x12, WEAKNESS_CHARGED_DMG+1], + [[ItemName.triad_thunder], 0x1B, WEAKNESS_UNCHARGED_DMG], + ], + "Spinning Blade": [ + [[ItemName.spinning_blade], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.spinning_blade], 0x12, WEAKNESS_CHARGED_DMG], + ], + "Ray Splasher": [ + [[ItemName.ray_splasher], 0x1C, WEAKNESS_UNCHARGED_DMG], + ], + "Frost Shield": [ + [[ItemName.frost_shield], 0x0D, WEAKNESS_UNCHARGED_DMG], + [[ItemName.frost_shield], 0x16, WEAKNESS_CHARGED_DMG+2], + [[ItemName.frost_shield], 0x21, WEAKNESS_CHARGED_DMG], + ], + "Tornado Fang": [ + [[ItemName.tornado_fang], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.tornado_fang], 0x17, WEAKNESS_CHARGED_DMG+2], + ], +} + +weapons_chaotic = { + "Lemon": [ + [None, 0x00, 0x02], + ], + "Lemon (Dash)": [ + [None, 0x06, 0x03], + ], + "Charged Shot (Level 1)": [ + [["Check Charge 1"], 0x01, 0x03], + ], + "Charged Shot (Level 2)": [ + [["Check Charge 1"], 0x03, 0x04], + ], + "Charged Shot (Level 3)": [ + [["Check Charge 2"], 0x1D, 0x04], + [["Check Charge 2"], 0x20, 0x03], + ], + "Charged Shot (Level 4)": [ + [["Check Charge 2"], 0x1F, 0x05], + [["Check Charge 2"], 0x20, 0x03], + ], + "Acid Burst": [ + [[ItemName.acid_burst], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.acid_burst], 0x18, WEAKNESS_UNCHARGED_DMG-1], + ], + "Charged Acid Burst": [ + [["Check Charge 2", ItemName.acid_burst], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Parasitic Bomb": [ + [[ItemName.parasitic_bomb], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Parasitic Bomb": [ + [["Check Charge 2", ItemName.parasitic_bomb], 0x11, WEAKNESS_CHARGED_DMG], + ], + "Triad Thunder": [ + [[ItemName.triad_thunder], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.triad_thunder], 0x1B, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Triad Thunder": [ + [["Check Charge 2", ItemName.triad_thunder], 0x12, WEAKNESS_CHARGED_DMG], + ], + "Spinning Blade": [ + [[ItemName.spinning_blade], 0x0A, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Spinning Blade": [ + [["Check Charge 2", ItemName.spinning_blade], 0x12, WEAKNESS_CHARGED_DMG], + ], + "Ray Splasher": [ + [[ItemName.ray_splasher], 0x1C, WEAKNESS_UNCHARGED_DMG], + ], + "Frost Shield": [ + [[ItemName.frost_shield], 0x0D, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Frost Shield": [ + [["Check Charge 2", ItemName.frost_shield], 0x16, WEAKNESS_CHARGED_DMG], + [["Check Charge 2", ItemName.frost_shield], 0x21, WEAKNESS_CHARGED_DMG], + ], + "Tornado Fang": [ + [[ItemName.tornado_fang], 0x0E, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Tornado Fang": [ + [["Check Charge 2", ItemName.tornado_fang], 0x17, WEAKNESS_CHARGED_DMG], + ], +} + + +def randomize_weaknesses(world): + shuffle_type = world.options.boss_weakness_rando.value + strictness_type = world.options.boss_weakness_strictness.value + + weapon_list = weapons.keys() + if shuffle_type == 2 or shuffle_type == 3: + weapon_list = weapons_chaotic.keys() + weapon_list = list(weapon_list) + + for boss in boss_weaknesses.keys(): + if boss == "Dr. Doppler's Lab 2 Boss": + continue + world.boss_weaknesses[boss] = [] + + if strictness_type == 0: + damage_table = boss_weakness_data[boss].copy() + elif strictness_type == 1: + damage_table = damage_templates["Allow Buster"].copy() + elif strictness_type == 2: + damage_table = damage_templates["Allow Upgraded Buster"].copy() + else: + damage_table = damage_templates["Only Weakness"].copy() + + if boss == "Blast Hornet": + world.boss_weaknesses[boss].append(blast_hornet_data["Gravity Well"][0]) + damage_table[0x0C] = 0x03 + damage_table[0x15] = 0x03 + + copied_weapon_list = weapon_list.copy() + for weapon in boss_excluded_weapons[boss]: + if weapon in copied_weapon_list: + copied_weapon_list.remove(weapon) + + if shuffle_type == 1: + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons[chosen_weapon] + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + + + elif shuffle_type == 2: + for _ in range(2): + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons_chaotic[chosen_weapon].copy() + copied_weapon_list.remove(chosen_weapon) + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + + + elif shuffle_type == 3: + chosen_weapon = world.random.choice(copied_weapon_list) + data = weapons_chaotic[chosen_weapon].copy() + for entry in data: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage + world.boss_weakness_data[boss] = damage_table.copy() + + if world.options.doppler_lab_2_boss == "volt_kurageil": + world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"] = world.boss_weaknesses["Volt Kurageil"] + else: + world.boss_weaknesses["Dr. Doppler's Lab 2 Boss"] = [ + world.boss_weaknesses["Vile"][0], + world.boss_weaknesses["Vile Goliath"][0], + ] + diff --git a/worlds/mmx3/__init__.py b/worlds/mmx3/__init__.py index 8cc85a0f8ef8..5891b8673dd8 100644 --- a/worlds/mmx3/__init__.py +++ b/worlds/mmx3/__init__.py @@ -16,6 +16,7 @@ from .Names import ItemName, LocationName, EventName from .Options import MMX3Options from .Client import MMX3SNIClient +from .Weaknesses import randomize_weaknesses, boss_weaknesses, weapon_id from .Rom import patch_rom, MMX3ProcedurePatch, HASH_US, HASH_LEGACY class MMX3Settings(settings.Group): @@ -179,9 +180,11 @@ def create_regions(self) -> None: junk_count = total_required_locations - len(itempool) junk_weights = [] - junk_weights += ([ItemName.small_hp] * 30) - junk_weights += ([ItemName.large_hp] * 40) - junk_weights += ([ItemName.life] * 30) + junk_weights += ([ItemName.small_weapon] * 5) + junk_weights += ([ItemName.large_weapon] * 10) + junk_weights += ([ItemName.small_hp] * 25) + junk_weights += ([ItemName.large_hp] * 35) + junk_weights += ([ItemName.life] * 25) junk_pool = [] for i in range(junk_count): @@ -239,9 +242,25 @@ def fill_slot_data(self): return slot_data def generate_early(self): + if self.options.boss_weakness_rando != "vanilla": + self.boss_weaknesses = {} + self.boss_weakness_data = {} + randomize_weaknesses(self) early_stage = self.random.choice(list(item_groups["Access Codes"])) self.multiworld.local_early_items[self.player][early_stage] = 1 + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + if self.options.boss_weakness_rando != "vanilla": + spoiler_handle.write(f"\nMega Man X3 boss weaknesses for {self.multiworld.player_name[self.player]}:\n") + + for boss, data in self.boss_weaknesses.items(): + weaknesses = "" + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + spoiler_handle.writelines(f"{boss + ':':<30s}{weaknesses}\n") + + def get_filler_item_name(self) -> str: return self.random.choice(list(junk_table.keys())) @@ -272,4 +291,4 @@ def modify_multidata(self, multidata: dict): @classmethod def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations): - return \ No newline at end of file + return diff --git a/worlds/mmx3/data/mmx3_basepatch.bsdiff4 b/worlds/mmx3/data/mmx3_basepatch.bsdiff4 index 2af623a3f4ce1ce626409d2ab4d81e5be3ebc6fd..fd789fb67f4850beb23fe0af51f8bac1323d2c49 100644 GIT binary patch literal 5263 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 T+l3OWZ1(>BB|CP?L=^Zxhsi4M literal 4225 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 W-M3iGw}E3!9shn5VU`iQvHu5n{zCKs