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 2af623a3f4ce..fd789fb67f48 100644 Binary files a/worlds/mmx3/data/mmx3_basepatch.bsdiff4 and b/worlds/mmx3/data/mmx3_basepatch.bsdiff4 differ