diff --git a/worlds/mmx/Aesthetics.py b/worlds/mmx/Aesthetics.py new file mode 100644 index 000000000000..9be8c02191c5 --- /dev/null +++ b/worlds/mmx/Aesthetics.py @@ -0,0 +1,136 @@ +import struct + +from typing import Dict, List + +player_palettes = { + "blue": [ + "$35D0","$7BDE","$239D","$091E","$7B6F","$7A8A","$5963","$7E00", + "$7900","$40C4","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "gold_armor": [ + "$35D0","$7BDE","$239D","$091E","$7BFF","$1F9F","$09DF","$73DF", + "$0ADE","$055D","$42DF","$2597","$1110","$4FBF","$121D","$0C63", + ], + "acid_burst": [ + "$35D0","$7BDE","$239D","$091E","$1F1E","$11F8","$0132","$43F7", + "$1BAC","$1689","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "parasitic_bomb": [ + "$35D0","$7BDE","$239D","$091E","$6776","$424E","$2547","$03FF", + "$02D7","$018D","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "triad_thunder": [ + "$35D0","$7BDE","$239D","$091E","$4F9A","$274F","$222C","$7E14", + "$5D0E","$3C8C","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "spinning_blade": [ + "$35D0","$7BDE","$239D","$091E","$6318","$4210","$2108","$195F", + "$0098","$0010","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "ray_splasher": [ + "$35D0","$7BDE","$239D","$091E","$03FF","$02D7","$018D","$1EDE", + "$11B8","$00F2","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "gravity_well": [ + "$35D0","$7BDE","$239D","$091E","$531C","$3214","$110C","$7DFE", + "$6518","$5093","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "frost_shield": [ + "$35D0","$7BDE","$239D","$091E","$7E3A","$7174","$58EF","$7BAF", + "$6B29","$5264","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "tornado_fang": [ + "$35D0","$7BDE","$239D","$091E","$1EDE","$0976","$0050","$73D0", + "$5B08","$3A00","$42DF","$2597","$1110","$739C","$4E73","$0C63", + ], + "crystal_hunter": [ + "$35D0","$7BDE","$239D","$091E","$7F3E","$7256","$5DB1","$7B6F", + "$7A8A","$59A5","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "bubble_splash": [ + "$35D0","$7BDE","$239D","$091E","$773F","$5639","$3D16","$131F", + "$0657","$01B1","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "silk_shot": [ + "$35D0","$7BDE","$239D","$091E","$5FF4","$3F2C","$1E65","$1E1F", + "$011F","$00F6","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "spin_wheel": [ + "$35D0","$7BDE","$239D","$091E","$7F7F","$7E59","$75B5","$2B00", + "$2240","$1DC4","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "sonic_slicer": [ + "$35D0","$7BDE","$239D","$091E","$031F","$021F","$017B","$75AB", + "$590A","$28E8","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "strike_chain": [ + "$35D0","$7BDE","$239D","$091E","$779C","$62D6","$4E10","$7A7A", + "$75D2","$5CEF","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "magnet_mine": [ + "$35D0","$7BDE","$239D","$091E","$2FFF","$0F18","$0A52","$4E31", + "$398C","$2908","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "speed_burner": [ + "$35D0","$7BDE","$239D","$091E","$7F98","$7ACF","$6645","$10DF", + "$0CB7","$0452","$42DF","$2597","$1110","$7BDE","$4E73","$0C63", + ], + "homing_torpedo": [ + "$35D0","$7BDE","$22FF","$0059","$5B7F","$46DB","$3657","$4250", + "$35ED","$2168","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "chameleon_sting": [ + "$35D0","$7BDE","$22FF","$0059","$73D8","$574C","$3EE4","$2AC3", + "$2A44","$1541","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "rolling_shield": [ + "$35D0","$7BDE","$22FF","$0059","$6BB9","$5714","$46B0","$4E1E", + "$311C","$209A","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "fire_wave": [ + "$35D0","$7BDE","$22FF","$0059","$3F9F","$229F","$0D9E","$055E", + "$0CFA","$0074","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "storm_tornado": [ + "$35D0","$7BDE","$22FF","$0059","$731F","$625C","$5A1A","$4973", + "$4953","$3CCE","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "electric_spark": [ + "$35D0","$7BDE","$22FF","$0059","$6F7B","$5AD6","$5294","$06DF", + "$05F8","$014F","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "boomerang_cutter": [ + "$35D0","$7BDE","$22FF","$0059","$6BB9","$5714","$46B0","$49CE", + "$418C","$2908","$42DF","$2597","$1110","$7BDE","$4E73","$14A5", + ], + "shotgun_ice": [ + "$35D0","$7BDE","$239D","$091E","$2B9F","$1ABD","$11B5","$7F87", + "$766A","$79C7","$42DF","$2597","$1110","$7BDE","$4210","$0C63", + ], +} + + +def get_palette_bytes(palette: Dict[str, List]) -> bytearray: + output_data = bytearray() + for hexcol in palette: + if hexcol.startswith("$"): + hexcol = hexcol.replace("$", "") + colint = int(hexcol, 16) + output_data.extend(bytearray(struct.pack("H", colint))) + else: + if hexcol.startswith("#"): + hexcol = hexcol.replace("#", "") + colint = int(hexcol, 16) + col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) + col = tuple(x for x in col) + byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) + output_data.extend(bytearray(byte_data)) + return output_data + + +def rgb888_to_bgr555(red, green, blue) -> bytes: + red = red >> 3 + green = green >> 3 + blue = blue >> 3 + outcol = (blue << 10) + (green << 5) + red + return struct.pack("H", outcol) diff --git a/worlds/mmx/Client.py b/worlds/mmx/Client.py new file mode 100644 index 000000000000..cc2008d9f20e --- /dev/null +++ b/worlds/mmx/Client.py @@ -0,0 +1,1124 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient + +logger = logging.getLogger("Client") +snes_logger = logging.getLogger("SNES") + +# FXPAK Pro protocol memory mapping used by SNI +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +MMX_RAM = WRAM_START + 0x1EE00 +MMX_UPGRADE_DATA = WRAM_START + 0x01F10 +MMX_SETTINGS = ROM_START + 0x167C20 + +MMX_GAME_STATE = WRAM_START + 0x000D1 +MMX_MENU_STATE = WRAM_START + 0x000D2 +MMX_GAMEPLAY_STATE = WRAM_START + 0x000D3 +MMX_LEVEL_INDEX = WRAM_START + 0x01F7A + +MMX_WEAPON_ARRAY = WRAM_START + 0x01F88 +MMX_SUB_TANK_ARRAY = WRAM_START + 0x01F83 +MMX_UPGRADES = WRAM_START + 0x01F99 +MMX_HEART_TANKS = WRAM_START + 0x01F9C +MMX_HADOUKEN = WRAM_START + 0x01F7E +MMX_LIFE_COUNT = WRAM_START + 0x01F80 +MMX_MAX_HP = WRAM_START + 0x01F9A +MMX_CURRENT_HP = WRAM_START + 0x00BCF +MMX_UNLOCKED_CHARGED_SHOT = MMX_RAM + 0x0016 +MMX_UNLOCKED_AIR_DASH = MMX_RAM + 0x0022 +MMX_FORTRESS_PROGRESS = WRAM_START + 0x01F7B + +MMX_SFX_FLAG = MMX_RAM + 0x0003 +MMX_SFX_NUMBER = MMX_RAM + 0x0004 + +MMX_SIGMA_ACCESS = MMX_RAM + 0x0002 +MMX_COLLECTED_HEART_TANKS = MMX_RAM + 0x0005 +MMX_COLLECTED_UPGRADES = MMX_RAM + 0x0006 +MMX_COLLECTED_HADOUKEN = MMX_RAM + 0x0007 +MMX_DEFEATED_BOSSES = MMX_RAM + 0x0080 +MMX_COMPLETED_LEVELS = MMX_RAM + 0x0060 +MMX_COLLECTED_PICKUPS = MMX_RAM + 0x00C0 +MMX_UNLOCKED_LEVELS = MMX_RAM + 0x0040 + +MMX_RECV_INDEX = MMX_RAM + 0x0000 +MMX_ENERGY_LINK_PACKET = MMX_RAM + 0x0009 +MMX_VALIDATION_CHECK = MMX_RAM + 0x0013 + +MMX_RECEIVING_ITEM = MMX_RAM + 0x0015 +MMX_ENABLE_HEART_TANK = MMX_RAM + 0x000B +MMX_ENABLE_HP_REFILL = MMX_RAM + 0x000F +MMX_HP_REFILL_AMOUNT = MMX_RAM + 0x0010 +MMX_ENABLE_GIVE_1UP = MMX_RAM + 0x0012 +MMX_ENABLE_WEAPON_REFILL = MMX_RAM + 0x001A +MMX_WEAPON_REFILL_AMOUNT = MMX_RAM + 0x001B + +MMX_SCREEN_BRIGHTNESS = WRAM_START + 0x000B3 +MMX_PAUSE_STATE = WRAM_START + 0x01F24 +MMX_CAN_MOVE = WRAM_START + 0x01F13 + +MMX_PICKUPSANITY_ACTIVE = MMX_SETTINGS + 0x07 +MMX_ENERGY_LINK_ENABLED = MMX_SETTINGS + 0x08 +MMX_DEATH_LINK_ACTIVE = MMX_SETTINGS + 0x09 +MMX_JAMMED_BUSTER_ACTIVE = MMX_SETTINGS + 0x0A +MMX_ABILITIES_FLAGS = MMX_SETTINGS + 0x11 + +MMX_ENERGY_LINK_COUNT = MMX_RAM + 0x00100 +MMX_GLOBAL_TIMER = MMX_RAM + 0x00106 +MMX_GLOBAL_DEATHS = MMX_RAM + 0x0010A +MMX_GLOBAL_DMG_DEALT = MMX_RAM + 0x0010C +MMX_GLOBAL_DMG_TAKEN = MMX_RAM + 0x0010E +MMX_CHECKPOINTS_REACHED = MMX_RAM + 0x00120 +MMX_REFILL_REQUEST = MMX_RAM + 0x00110 +MMX_REFILL_TARGET = MMX_RAM + 0x00111 +MMX_ARSENAL_SYNC = MMX_RAM + 0x00112 + +EXCHANGE_RATE = 500000000 + +STARTING_ID = 0xBE0800 + +MMX_ROMHASH_START = 0x7FC0 +ROMHASH_SIZE = 0x15 + +PICKUP_ITEMS = ["1up", "hp refill", "weapon refill"] + +class MMXSNIClient(SNIClient): + game = "Mega Man X" + patch_suffix = ".apmmx" + + def __init__(self): + super().__init__() + self.game_state = False + self.last_death_link = 0 + self.energy_link_enabled = False + self.heal_request_command = None + self.weapon_refill_request_command = None + self.using_newer_client = False + self.energy_link_details = False + self.trade_request = None + self.data_storage_enabled = False + self.save_arsenal = False + self.resync_request = False + self.current_level_value = 42 + self.item_queue = [] + + + async def deathlink_kill_player(self, ctx): + from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read + + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + if game_data is None or game_state_data is None or game_progress_data is None: + return + + validation = int.from_bytes(game_data[0x13:0x15], "little") + if validation != 0xDEAD: + return + + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + can_move = game_progress_data[3:10] + pause_state = game_progress_data[14] + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + pause_state != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item != 0x00: + return + + snes_buffered_write(ctx, MMX_CURRENT_HP, bytes([0x80])) + snes_buffered_write(ctx, WRAM_START + 0x00BAA, bytes([0x0C])) + snes_buffered_write(ctx, WRAM_START + 0x00C12, bytes([0x0C])) + snes_buffered_write(ctx, WRAM_START + 0x00BAB, bytes([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x00BD7, bytes([0x08])) + + await snes_flush_writes(ctx) + + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + + + async def validate_rom(self, ctx): + from SNIClient import snes_read + + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + rom_name = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) + + if rom_name is None or game_settings is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:4] != b"MMX1": + if "resync" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("resync") + if "trade" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("trade") + 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") + return False + + ctx.game = self.game + ctx.items_handling = 0b111 + ctx.receive_option = 0 + ctx.send_option = 0 + ctx.allow_collect = True + + energy_link = game_settings[0x08] + if energy_link: + 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 "resync" not in ctx.command_processor.commands: + ctx.command_processor.commands["resync"] = cmd_resync + if "trade" not in ctx.command_processor.commands: + ctx.command_processor.commands["trade"] = cmd_trade + + death_link = game_settings[0x09] + if death_link: + await ctx.update_death_link(bool(death_link & 0b1)) + + ctx.rom = rom_name + + return True + + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + + # Discard uninitialized ROMs + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: + self.game_state = False + self.energy_link_enabled = False + self.current_level_value = 42 + self.item_queue = [] + return + + validation = int.from_bytes(game_data[0x13:0x15], "little") + if validation != 0xDEAD: + snes_logger.info(f'ROM not properly validated.') + self.game_state = False + return + + game_state = game_state_data[0] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + + if game_state == 0: + self.game_state = False + self.item_queue = [] + self.current_level_value = 42 + ctx.locations_checked = set() + + # Resync data if solicited + if self.resync_request: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_arsenal_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dict()}], + }]) + self.resync_request = False + logger.info(f"Successfully cleared save data!") + return + + if self.resync_request: + self.resync_request = False + logger.info(f"Invalid environment for a resync. Please try again during the Title Menu screen.") + + self.game_state = True + if "DeathLink" in ctx.tags and menu_state == 0x04 and ctx.last_death_link + 1 < time.time(): + currently_dead = gameplay_state == 0x06 + await ctx.handle_deathlink_state(currently_dead) + + current_hp = await snes_read(ctx, MMX_CURRENT_HP, 0x1) + screen_brightness = await snes_read(ctx, MMX_SCREEN_BRIGHTNESS, 0x1) + + if current_hp is not None and screen_brightness is not None: + game_ram = [ + game_data, + game_state_data, + game_progress_data, + game_settings, + current_hp, + ] + + keys = { + f"mmx_arsenal_{ctx.team}_{ctx.slot}", + f"mmx_checkpoints_{ctx.team}_{ctx.slot}", + f"mmx_global_timer_{ctx.team}_{ctx.slot}", + f"mmx_deaths_{ctx.team}_{ctx.slot}", + f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", + f"mmx_damage_taken_{ctx.team}_{ctx.slot}", + } + + if game_state != 0x00 and self.data_storage_enabled is True and \ + all(key in ctx.stored_data.keys() for key in keys): + await self.handle_data_storage(ctx, game_ram) + + # Handle DataStorage + if not self.using_newer_client: + if ctx.server and ctx.server.socket.open and not self.data_storage_enabled and ctx.team is not None: + self.data_storage_enabled = True + ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") + + if screen_brightness[0] == 0x0F: + self.handle_item_queue(ctx, game_ram) + + if self.trade_request is not None: + self.handle_hp_trade(ctx, game_ram) + + # This is going to be rewritten whenever SNIClient supports on_package + energy_link = game_settings[0x08] + if self.using_newer_client: + if energy_link != 0: + await self.handle_energy_link(ctx, game_ram) + else: + if energy_link != 0: + if self.energy_link_enabled and f'EnergyLink{ctx.team}' in ctx.stored_data: + await self.handle_energy_link(ctx, game_ram) + + if ctx.server and ctx.server.socket.open and not self.energy_link_enabled and ctx.team is not None: + self.energy_link_enabled = True + ctx.set_notify(f"EnergyLink{ctx.team}") + logger.info(f"Initialized EnergyLink{ctx.team}, use /help to get information about the EnergyLink commands.") + + await snes_flush_writes(ctx) + + from .Rom import weapon_rom_data, upgrades_rom_data, boss_access_rom_data, refill_rom_data + from .Levels import location_id_to_level_id + from worlds import AutoWorldRegister + + defeated_bosses = list(game_data[0x80:0xA0]) + cleared_levels = list(game_data[0x60:0x80]) + collected_heart_tanks = game_data[0x05] + collected_upgrades = game_data[0x06] + collected_hadouken = game_data[0x07] + collected_pickups = list(game_data[0xC0:0xE0]) + pickupsanity_enabled = game_settings[0x07] + completed_intro_level = game_progress_data[0x8B] + new_checks = [] + for loc_name, data in location_id_to_level_id.items(): + loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] + if loc_id not in ctx.locations_checked: + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + if defeated_bosses[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id == 0x001: + # Maverick Medal + if cleared_levels[data_bit] != 0: + new_checks.append(loc_id) + elif internal_id == 0x002: + # Heart Tank + masked_data = collected_heart_tanks & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x003: + # Mega Man upgrades + masked_data = collected_upgrades & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x004: + # Sub Tank + masked_data = collected_upgrades & data_bit + if masked_data != 0: + new_checks.append(loc_id) + elif internal_id == 0x005: + # Hadouken + if collected_hadouken != 0x00: + new_checks.append(loc_id) + elif internal_id == 0x007: + # Intro + if game_state_data == b'\x02\x00\x01' and completed_intro_level == 0x04: + new_checks.append(loc_id) + elif internal_id == 0x020: + # Pickups + if not pickupsanity_enabled or pickupsanity_enabled == 0: + continue + if collected_pickups[data_bit] != 0: + new_checks.append(loc_id) + + # Verify if game still active + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: + snes_logger.info(f'Exit Game.') + return + + rom = await snes_read(ctx, MMX_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + snes_logger.info(f'Exit ROM.') + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + + # Send Current Room for Tracker + current_level = game_progress_data[0x6A] + + if game_state_data[0] == 0x00 or game_state_data[0:2] == b'\x02\x04': + current_level = -1 + + if self.current_level_value != (current_level + 1): + self.current_level_value = current_level + 1 + + # Send level id data to tracker + await ctx.send_msgs( + [ + { + "cmd": "Set", + "key": f"mmx1_level_id_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [ + { + "operation": "replace", + "value": self.current_level_value, + } + ], + } + ] + ) + + recv_index = int.from_bytes(game_data[0:2], "little") + sync_arsenal = int.from_bytes(game_data[0x112:0x114], "little") + + if recv_index < len(ctx.items_received) and sync_arsenal != 0x1337: + item = ctx.items_received[recv_index] + recv_index += 1 + sending_game = ctx.slot_info[item.player].game + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, MMX_RECV_INDEX, bytes([recv_index])) + await snes_flush_writes(ctx) + + if item.item in weapon_rom_data: + self.add_item_to_queue("weapon", item.item) + + elif item.item == STARTING_ID + 0x0013: + self.add_item_to_queue("heart tank", item.item) + + elif item.item == STARTING_ID + 0x0014: + self.add_item_to_queue("sub tank", item.item) + + elif item.item in upgrades_rom_data: + self.add_item_to_queue("upgrade", item.item) + + elif item.item in boss_access_rom_data: + if item.item == STARTING_ID + 0x000A: + snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytearray([0x00])) + boss_access = bytearray(game_data[0x40:0x60]) + level = boss_access_rom_data[item.item] + boss_access[level[0]] = 0x01 + snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, boss_access) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2D])) + self.save_arsenal = True + + elif item.item in refill_rom_data: + self.add_item_to_queue(refill_rom_data[item.item][0], item.item, refill_rom_data[item.item][1]) + self.save_arsenal = True + + elif item.item == STARTING_ID: + # Handle goal + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + self.save_arsenal = True + return + + # Handle collected locations + game_data = await snes_read(ctx, MMX_RAM, 0x0140) + game_state_data = await snes_read(ctx, MMX_GAME_STATE, 0x3) + game_progress_data = await snes_read(ctx, MMX_UPGRADE_DATA, 0xF0) + game_settings = await snes_read(ctx, MMX_SETTINGS, 0x20) + + if game_data is None or game_state_data is None or game_progress_data is None or game_settings is None: + ctx.locations_checked = set() + return + + if game_state_data[0] != 0x02: + ctx.locations_checked = set() + return + + new_boss_clears = False + new_cleared_level = False + new_heart_tank = False + new_upgrade = False + new_pickup = False + new_hadouken = False + defeated_bosses = list(game_data[0x80:0xA0]) + cleared_levels = list(game_data[0x60:0x80]) + collected_pickups = list(game_data[0xC0:0xE0]) + collected_heart_tanks = game_data[0x05] + collected_upgrades = game_data[0x06] + collected_hadouken = game_data[0x07] + i = 0 + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked: + ctx.locations_checked.add(loc_id) + loc_name = ctx.location_names.lookup_in_game(loc_id) + + if loc_name not in location_id_to_level_id: + continue + + logging.info(f"Recovered checks ({i:03}): {loc_name}") + i += 1 + + data = location_id_to_level_id[loc_name] + internal_id = data[1] + data_bit = data[2] + + if internal_id == 0x000: + # Boss clear + defeated_bosses[data_bit] = 1 + new_boss_clears = True + elif internal_id == 0x001: + # Maverick Medal + cleared_levels[data_bit] = 0xFF + new_cleared_level = True + elif internal_id == 0x002: + # Heart Tank + collected_heart_tanks |= data_bit + new_heart_tank = True + elif internal_id == 0x003: + # Mega Man upgrades + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x004: + # Sub Tank + collected_upgrades |= data_bit + new_upgrade = True + elif internal_id == 0x005: + # Hadouken + collected_hadouken = 0xFF + new_hadouken = True + elif internal_id == 0x20: + # Pickups + collected_pickups[data_bit] = 0x01 + new_pickup = True + + if new_cleared_level: + snes_buffered_write(ctx, MMX_COMPLETED_LEVELS, bytes(cleared_levels)) + if new_boss_clears: + snes_buffered_write(ctx, MMX_DEFEATED_BOSSES, bytes(defeated_bosses)) + if new_pickup: + snes_buffered_write(ctx, MMX_COLLECTED_PICKUPS, bytes(collected_pickups)) + if new_hadouken: + snes_buffered_write(ctx, MMX_COLLECTED_HADOUKEN, bytearray([collected_hadouken])) + if new_upgrade: + snes_buffered_write(ctx, MMX_COLLECTED_UPGRADES, bytearray([collected_upgrades])) + if new_heart_tank: + snes_buffered_write(ctx, MMX_COLLECTED_HEART_TANKS, bytearray([collected_heart_tanks])) + await snes_flush_writes(ctx) + + def on_package(self, ctx, cmd: str, args: dict): + super().on_package(ctx, cmd, args) + + if cmd == "Connected": + ctx.set_notify(f"mmx_global_timer_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_deaths_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_taken_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_damage_dealt_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_checkpoints_{ctx.team}_{ctx.slot}") + ctx.set_notify(f"mmx_arsenal_{ctx.team}_{ctx.slot}") + self.data_storage_enabled = True + slot_data = args.get("slot_data", None) + self.using_newer_client = True + if slot_data["energy_link"]: + ctx.set_notify(f"EnergyLink{ctx.team}") + if ctx.ui: + ctx.ui.enable_energy_link() + ctx.ui.energy_link_label.text = "Energy: Standby" + logger.info(f"Initialized EnergyLink{ctx.team}") + + elif cmd == "SetReply" and args["key"].startswith("EnergyLink"): + if ctx.ui: + pool = (args["value"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + + elif cmd == "Retrieved": + if f"EnergyLink{ctx.team}" in args["keys"] and args["keys"][f"EnergyLink{ctx.team}"] and ctx.ui: + pool = (args["keys"][f"EnergyLink{ctx.team}"] or 0) / EXCHANGE_RATE + ctx.ui.energy_link_label.text = f"Energy: {pool:.2f}" + + + def handle_hp_trade(self, ctx, game_ram): + from SNIClient import snes_buffered_write + + game_data = game_ram[0] + game_state_data = game_ram[1] + current_hp = game_ram[4] + + # Can only process trades during the pause state + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + if menu_state != 0x04 or gameplay_state != 0x04 or receiving_item != 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 + 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, MMX_CURRENT_HP, bytearray([new_hp])) + 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, game_ram): + from SNIClient import snes_buffered_write + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] + + # Deposit heals into the pool regardless of energy_link setting + energy_packet_raw = int.from_bytes(game_data[0x09:0x0B], "little") + 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) / EXCHANGE_RATE) + (energy_packet_raw / 16) + snes_buffered_write(ctx, MMX_ENERGY_LINK_PACKET, bytearray([0x00, 0x00])) + + # Expose EnergyLink to the ROM + pause_state = game_progress_data[0x14] + if pause_state != 0x00: + pool = ctx.stored_data[f'EnergyLink{ctx.team}'] or 0 + total_energy = int(pool / EXCHANGE_RATE) + if total_energy < 9999: + snes_buffered_write(ctx, MMX_ENERGY_LINK_COUNT, bytearray([total_energy & 0xFF, (total_energy >> 8) & 0xFF])) + else: + snes_buffered_write(ctx, MMX_ENERGY_LINK_COUNT, bytearray([0x0F, 0x27])) + + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + can_move = game_progress_data[3:10] + heart_tank = game_data[0x0B] + hp_refill = game_data[0x0F] + weapon_refill = game_data[0x1A] + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item != 0x00 or \ + heart_tank != 0x00 or \ + hp_refill != 0x00 or \ + weapon_refill != 0x00: + 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 + if not skip_hp or not skip_weapon: + # Handle in-game requests + request = game_data[0x110] + target = game_data[0x111] + if request != 0: + if target == 0: + if self.heal_request_command is None: + self.heal_request_command = request + else: + if self.weapon_refill_request_command is None: + self.weapon_refill_request_command = request + snes_buffered_write(ctx, MMX_REFILL_REQUEST, bytearray([0x00])) + + if not skip_hp: + # 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 + 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, heal_needed) + pool = (pool / EXCHANGE_RATE) - heal_needed + logger.info(f"Healed by {heal_needed}. Energy 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, heal_needed) + 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): + if not hasattr(self, "item_queue"): + self.item_queue = [] + self.item_queue.append([item_type, item_id, item_additional]) + + + def handle_item_queue(self, ctx, game_ram): + from SNIClient import snes_buffered_write + from .Rom import weapon_rom_data, upgrades_rom_data + + if not hasattr(self, "item_queue") or len(self.item_queue) == 0: + return + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] + game_settings = game_ram[3] + current_hp = game_ram[4] + + # Do not give items if you can't move, are in pause state, not in the correct mode or not in gameplay state + receiving_item = game_data[0x15] + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + progress = game_progress_data[0x6B] + can_move = game_progress_data[3:10] + hp_refill = game_data[0x0F] + weapon_refill = game_data[0x1A] + + if menu_state != 0x04 or \ + gameplay_state != 0x04 or \ + progress >= 0x04 or \ + hp_refill != 0x00 or \ + weapon_refill != 0x00 or \ + can_move != b'\x00\x00\x00\x00\x00\x00\x00' or \ + receiving_item != 0x00: + return + + + next_item = self.item_queue[0] + item_id = next_item[1] + + if next_item[0] in PICKUP_ITEMS: + backup_item = self.item_queue.pop(0) + + if "hp refill" in next_item[0]: + max_hp = game_progress_data[0x8A] + + if current_hp[0] < max_hp: + snes_buffered_write(ctx, MMX_ENABLE_HP_REFILL, bytearray([0x02])) + snes_buffered_write(ctx, MMX_HP_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX_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, MMX_ENABLE_WEAPON_REFILL, bytearray([0x02])) + snes_buffered_write(ctx, MMX_WEAPON_REFILL_AMOUNT, bytearray([next_item[2]])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + + elif next_item[0] == "1up": + life_count = game_progress_data[0x70] + if life_count < 99: + snes_buffered_write(ctx, MMX_ENABLE_GIVE_1UP, bytearray([0x01])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + self.save_arsenal = True + else: + self.item_queue.append(backup_item) + + pause_state = game_progress_data[0x14] + if pause_state != 0x00: + if len(self.item_queue) != 0: + backup_item = self.item_queue.pop(0) + self.item_queue.append(backup_item) + return + + if next_item[0] == "weapon": + weapon = weapon_rom_data[item_id] + snes_buffered_write(ctx, WRAM_START + weapon[0], bytearray([weapon[1]])) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x0D])) + self.item_queue.pop(0) + self.save_arsenal = True + + elif next_item[0] == "heart tank": + heart_tanks = game_progress_data[0x8C] + heart_tank_count = heart_tanks.bit_count() + if heart_tank_count < 8: + heart_tanks |= 1 << heart_tank_count + snes_buffered_write(ctx, MMX_HEART_TANKS, bytearray([heart_tanks])) + snes_buffered_write(ctx, MMX_ENABLE_HEART_TANK, bytearray([0x02])) + snes_buffered_write(ctx, MMX_RECEIVING_ITEM, bytearray([0x01])) + self.item_queue.pop(0) + self.save_arsenal = True + + elif next_item[0] == "sub tank": + sub_tanks = list(game_progress_data[0x73:0x77]) + upgrades = game_progress_data[0x89] + sub_tank_count = (upgrades & 0xF0).bit_count() + if sub_tank_count < 4: + upgrades |= 0x10 << sub_tank_count + sub_tanks[sub_tank_count] = 0x8E + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + snes_buffered_write(ctx, MMX_SUB_TANK_ARRAY, bytearray(sub_tanks)) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x17])) + self.item_queue.pop(0) + self.save_arsenal = True + + elif next_item[0] == "upgrade": + upgrades = game_progress_data[0x89] + + upgrade = upgrades_rom_data[item_id] + bit = 1 << upgrade[0] + check = upgrades & bit + + if bit == 0x08: + air_dash_check = game_settings[0x11] & 0x02 + if air_dash_check != 0: + # check now becomes the air dash flag + check = game_data[0x22] + + if check == 0: + # Armor + original_value = upgrades + upgrades |= bit + if bit == 0x01: + snes_buffered_write(ctx, WRAM_START + 0x0BBE, bytearray([0x18])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + snes_buffered_write(ctx, WRAM_START + 0x1EE19, bytearray([0x80])) + elif bit == 0x02: + jam_check = game_settings[0x0A] + charge_shot_unlocked = game_data[0x16] + if jam_check == 1 and charge_shot_unlocked == 0: + snes_buffered_write(ctx, MMX_UNLOCKED_CHARGED_SHOT, bytearray([0x01])) + else: + snes_buffered_write(ctx, WRAM_START + 0x0C38, bytearray([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x0C42, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C43, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C39, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C48, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + elif bit == 0x04: + snes_buffered_write(ctx, WRAM_START + 0x0C58, bytearray([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x0C62, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C63, bytearray([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x0C59, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C68, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + elif bit == 0x08: + if air_dash_check != 0 and original_value & bit == 0x08: + snes_buffered_write(ctx, MMX_UNLOCKED_AIR_DASH, bytearray([0x01])) + else: + snes_buffered_write(ctx, WRAM_START + 0x0C78, bytearray([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x0C82, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C83, bytearray([0x02])) + snes_buffered_write(ctx, WRAM_START + 0x0C79, bytearray([0x00])) + snes_buffered_write(ctx, WRAM_START + 0x0C88, bytearray([0x5D])) + snes_buffered_write(ctx, MMX_UPGRADES, bytearray([upgrades])) + snes_buffered_write(ctx, MMX_SFX_FLAG, bytearray([0x01])) + snes_buffered_write(ctx, MMX_SFX_NUMBER, bytearray([0x2B])) + self.item_queue.pop(0) + self.save_arsenal = True + + + async def handle_data_storage(self, ctx, game_ram): + from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + + game_data = game_ram[0] + game_state_data = game_ram[1] + game_progress_data = game_ram[2] + + # Only do arsenal after the map's initial load or the intro stage is selected + menu_state = game_state_data[1] + gameplay_state = game_state_data[2] + map_state = await snes_read(ctx, WRAM_START + 0x1E49, 0x1) + map_state = 0 if map_state is None else map_state[0] + sync_arsenal = int.from_bytes(game_data[0x112:0x114], "little") + if (menu_state == 0x00 and map_state == 0x04) or (menu_state == 0x04 and gameplay_state == 0x04): + # Load Arsenal + if sync_arsenal == 0x1337: + arsenal = ctx.stored_data[f"mmx_arsenal_{ctx.team}_{ctx.slot}"] or dict() + if arsenal: + # Data in arsenal + snes_buffered_write(ctx, MMX_RECV_INDEX, bytes(arsenal["recv_index"].to_bytes(2, 'little'))) + snes_buffered_write(ctx, MMX_LIFE_COUNT, bytes(arsenal["life_count"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UPGRADES, bytes(arsenal["upgrades"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_MAX_HP, bytes(arsenal["max_hp"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_HEART_TANKS, bytes(arsenal["heart_tanks"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_SUB_TANK_ARRAY, bytearray(arsenal["sub_tanks"])) + snes_buffered_write(ctx, MMX_UNLOCKED_CHARGED_SHOT, bytes(arsenal["unlocked_buster"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UNLOCKED_AIR_DASH, bytes(arsenal["unlocked_air_dash"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_WEAPON_ARRAY, bytearray(arsenal["weapons"])) + snes_buffered_write(ctx, MMX_HADOUKEN, bytes(arsenal["hadouken"].to_bytes(1, 'little'))) + snes_buffered_write(ctx, MMX_UNLOCKED_LEVELS, bytearray(arsenal["levels"])) + snes_buffered_write(ctx, MMX_SIGMA_ACCESS, bytes(arsenal["sigma_access"].to_bytes(1, 'little'))) + + snes_buffered_write(ctx, MMX_ARSENAL_SYNC, bytearray([0x00,0x00])) + await snes_flush_writes(ctx) + + # Save Arsenal + if self.save_arsenal and sync_arsenal != 0x1337: + arsenal = dict() + arsenal["recv_index"] = int.from_bytes(game_data[0x00:0x02], "little") + arsenal["life_count"] = game_progress_data[0x70] + arsenal["upgrades"] = game_progress_data[0x89] + arsenal["max_hp"] = game_progress_data[0x8A] + arsenal["heart_tanks"] = game_progress_data[0x8C] + arsenal["sub_tanks"] = list(game_progress_data[0x73:0x77]) + arsenal["unlocked_buster"] = game_data[0x16] + arsenal["unlocked_air_dash"] = game_data[0x22] + arsenal["weapons"] = list(game_progress_data[0x78:0x88]) + arsenal["hadouken"] = game_progress_data[0x6E] + arsenal["levels"] = list(game_data[0x40:0x60]) + arsenal["sigma_access"] = game_data[0x02] + + # Attempt to not lose any previously saved data in case of RAM corruption + saved_arsenal = ctx.stored_data[f"mmx_arsenal_{ctx.team}_{ctx.slot}"] or dict() + if saved_arsenal: + if saved_arsenal["recv_index"] > arsenal["recv_index"]: + arsenal["recv_index"] = saved_arsenal["recv_index"] + if saved_arsenal["life_count"] > arsenal["life_count"]: + arsenal["life_count"] = saved_arsenal["life_count"] + if saved_arsenal["max_hp"] > arsenal["max_hp"]: + arsenal["max_hp"] = saved_arsenal["max_hp"] + for i in range(0x10): + arsenal["weapons"][i] |= saved_arsenal["weapons"][i] & 0x40 + for level in range(0x20): + arsenal["levels"][level] |= saved_arsenal["levels"][level] + arsenal["sigma_access"] = min(saved_arsenal["sigma_access"], arsenal["sigma_access"]) + + arsenal["upgrades"] |= saved_arsenal["upgrades"] + arsenal["unlocked_buster"] |= saved_arsenal["unlocked_buster"] + arsenal["unlocked_air_dash"] |= saved_arsenal["unlocked_air_dash"] + arsenal["hadouken"] |= saved_arsenal["hadouken"] & 0xE0 + arsenal["heart_tanks"] |= saved_arsenal["heart_tanks"] + for i in range(0x4): + arsenal["sub_tanks"][i] |= saved_arsenal["sub_tanks"][i] & 0x80 + + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_arsenal_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": arsenal}], + }]) + self.save_arsenal = False + + # Checkpoints reached + checkpoints = list(game_data[0x120:0x130]) + data_storage_checkpoints = ctx.stored_data[f"mmx_checkpoints_{ctx.team}_{ctx.slot}"] or [0 for _ in range(0xF)] + computed_checkpoints = list() + for i in range(0xF): + if checkpoints[i] >= data_storage_checkpoints[i]: + computed_checkpoints.append(checkpoints[i]) + else: + computed_checkpoints.append(data_storage_checkpoints[i]) + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_checkpoints_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": computed_checkpoints}], + }]) + snes_buffered_write(ctx, MMX_CHECKPOINTS_REACHED, bytes(computed_checkpoints)) + + # Global timer + timer = int.from_bytes(game_data[0x106:0x10A], "little") + data_storage_timer = ctx.stored_data[f"mmx_global_timer_{ctx.team}_{ctx.slot}"] or 0 + if timer >= data_storage_timer: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_global_timer_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": timer}, + {"operation": "min", "value": 0x03E73B3B}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_TIMER, data_storage_timer.to_bytes(4, "little")) + + # Death count + deaths = int.from_bytes(game_data[0x10A:0x10C], "little") + data_storage_deaths = ctx.stored_data[f"mmx_deaths_{ctx.team}_{ctx.slot}"] or 0 + if deaths >= data_storage_deaths: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_deaths_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": deaths}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DEATHS, data_storage_deaths.to_bytes(2, "little")) + + # Damage dealt + dmg_dealt = int.from_bytes(game_data[0x10C:0x10E], "little") + data_storage_dmg_dealt = ctx.stored_data[f"mmx_damage_dealt_{ctx.team}_{ctx.slot}"] or 0 + if dmg_dealt >= data_storage_dmg_dealt: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_damage_dealt_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_dealt}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DMG_DEALT, data_storage_dmg_dealt.to_bytes(2, "little")) + + # Damage taken + dmg_taken = int.from_bytes(game_data[0x10E:0x110], "little") + data_storage_dmg_taken = ctx.stored_data[f"mmx_damage_taken_{ctx.team}_{ctx.slot}"] or 0 + if dmg_taken >= data_storage_dmg_taken: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"mmx_damage_taken_{ctx.team}_{ctx.slot}", "operations": + [{"operation": "replace", "value": dmg_taken}, + {"operation": "min", "value": 9999}], + }]) + else: + snes_buffered_write(ctx, MMX_GLOBAL_DMG_TAKEN, data_storage_dmg_taken.to_bytes(2, "little")) + + +def cmd_heal(self, amount: str = ""): + """ + Request healing from EnergyLink. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + 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.heal_request_command is not None: + logger.info(f"You already placed a healing request.") + return + if amount: + try: + amount = int(amount) + except: + logger.info(f"You need to specify how much HP you will recover.") + return + 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 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 X": + logger.warning("This command can only be used while playing Mega Man X") + 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_trade(self, amount: str = ""): + """ + Trades HP to Weapon Energy. 1:1 ratio. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + 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.") + + +def cmd_resync(self): + """ + Resets the save data to force Archipelago to send over every item again. Locations reached aren't affected. + """ + if self.ctx.game != "Mega Man X": + logger.warning("This command can only be used while playing Mega Man X") + if (not self.ctx.server) or self.ctx.server.socket.closed or self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in the title screen.") + else: + if self.ctx.client_handler.resync_request: + logger.info(f"You already placed a resync request.") + return + else: + self.ctx.client_handler.resync_request = True + logger.info(f"Placing a resync request...") diff --git a/worlds/mmx/Items.py b/worlds/mmx/Items.py new file mode 100644 index 000000000000..1680664fbcc9 --- /dev/null +++ b/worlds/mmx/Items.py @@ -0,0 +1,114 @@ +from BaseClasses import Item +from .Names import ItemName + +from typing import NamedTuple, Optional, Dict + +class ItemData(NamedTuple): + code: Optional[int] + progression: bool + trap: bool = False + quantity: int = 1 + +STARTING_ID = 0xBE0800 + +class MMXItem(Item): + game = "Mega Man X" + +# Item tables +event_table = { + ItemName.victory: ItemData(STARTING_ID + 0x0000, True), + ItemName.maverick_medal: ItemData(STARTING_ID + 0x0001, True), +} + +access_codes_table = { + ItemName.stage_armored_armadillo: ItemData(STARTING_ID + 0x0002, True), + ItemName.stage_boomer_kuwanger: ItemData(STARTING_ID + 0x0003, True), + ItemName.stage_chill_penguin: ItemData(STARTING_ID + 0x0004, True), + ItemName.stage_flame_mammoth: ItemData(STARTING_ID + 0x0005, True), + ItemName.stage_launch_octopus: ItemData(STARTING_ID + 0x0006, True), + ItemName.stage_spark_mandrill: ItemData(STARTING_ID + 0x0007, True), + ItemName.stage_sting_chameleon: ItemData(STARTING_ID + 0x0008, True), + ItemName.stage_storm_eagle: ItemData(STARTING_ID + 0x0009, True), + ItemName.stage_sigma_fortress: ItemData(STARTING_ID + 0x000A, True), +} + +weapons = { + ItemName.shotgun_ice: ItemData(STARTING_ID + 0x000B, True), + ItemName.electric_spark: ItemData(STARTING_ID + 0x000C, True), + ItemName.rolling_shield: ItemData(STARTING_ID + 0x000D, True), + ItemName.homing_torpedo: ItemData(STARTING_ID + 0x000E, True), + ItemName.boomerang_cutter: ItemData(STARTING_ID + 0x000F, True), + ItemName.chameleon_sting: ItemData(STARTING_ID + 0x0010, True), + ItemName.storm_tornado: ItemData(STARTING_ID + 0x0011, True), + ItemName.fire_wave: ItemData(STARTING_ID + 0x0012, True), +} + +special_weapons = { + ItemName.hadouken: ItemData(STARTING_ID + 0x001A, True) +} + +tanks_table = { + ItemName.heart_tank: ItemData(STARTING_ID + 0x0013, True), + ItemName.sub_tank: ItemData(STARTING_ID + 0x0014, True), +} + +upgrade_table = { + ItemName.helmet: ItemData(STARTING_ID + 0x001C, True), + ItemName.body: ItemData(STARTING_ID + 0x001D, True), + ItemName.arms: ItemData(STARTING_ID + 0x001E, True), + ItemName.legs: ItemData(STARTING_ID + 0x001F, True), +} + +junk_table = { + ItemName.small_hp: ItemData(STARTING_ID + 0x0030, False), + ItemName.large_hp: ItemData(STARTING_ID + 0x0031, False), + ItemName.life: ItemData(STARTING_ID + 0x0034, False), +} + +junk_weapon_table = { + ItemName.small_weapon: ItemData(STARTING_ID + 0x0032, False), + ItemName.large_weapon: ItemData(STARTING_ID + 0x0033, False), +} + +item_groups = { + "Weapons": { + ItemName.shotgun_ice, + ItemName.electric_spark, + ItemName.rolling_shield, + ItemName.homing_torpedo, + ItemName.boomerang_cutter, + ItemName.chameleon_sting, + ItemName.storm_tornado, + ItemName.fire_wave, + }, + "Armor Upgrades": { + ItemName.helmet, + ItemName.body, + ItemName.arms, + ItemName.legs, + }, + "Access Codes": { + ItemName.stage_armored_armadillo, + ItemName.stage_boomer_kuwanger, + ItemName.stage_chill_penguin, + ItemName.stage_flame_mammoth, + ItemName.stage_launch_octopus, + ItemName.stage_spark_mandrill, + ItemName.stage_sting_chameleon, + ItemName.stage_storm_eagle, + ItemName.stage_sigma_fortress, + } +} + +item_table = { + **event_table, + **access_codes_table, + **weapons, + **upgrade_table, + **tanks_table, + **junk_table, + **junk_weapon_table, + **special_weapons, +} + +lookup_id_to_name: Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} \ No newline at end of file diff --git a/worlds/mmx/Levels.py b/worlds/mmx/Levels.py new file mode 100644 index 000000000000..63c7e5e10b1c --- /dev/null +++ b/worlds/mmx/Levels.py @@ -0,0 +1,101 @@ +from .Names import LocationName + +location_id_to_level_id = { + LocationName.intro_completed: [0x00, 0x007, 0x00], + LocationName.intro_mini_boss_1: [0x00, 0x000, 0x1C], + LocationName.intro_mini_boss_2: [0x00, 0x000, 0x1D], + LocationName.intro_hp_1: [0x00, 0x020, 0x00], + LocationName.intro_hp_2: [0x00, 0x020, 0x01], + + LocationName.armored_armadillo_boss: [0x03, 0x000, 0x00], + LocationName.armored_armadillo_clear: [0x03, 0x001, 0x04], + LocationName.armored_armadillo_sub_tank: [0x03, 0x004, 0x20], + LocationName.armored_armadillo_heart_tank: [0x03, 0x002, 0x02], + LocationName.armored_armadillo_hadouken: [0x03, 0x005, 0x00], + LocationName.armored_armadillo_mini_boss_1: [0x03, 0x000, 0x15], + LocationName.armored_armadillo_mini_boss_2: [0x03, 0x000, 0x16], + LocationName.armored_armadillo_hp_1: [0x00, 0x020, 0x05], + LocationName.armored_armadillo_hp_2: [0x00, 0x020, 0x06], + LocationName.armored_armadillo_hp_3: [0x00, 0x020, 0x07], + + LocationName.chill_penguin_boss: [0x08, 0x000, 0x01], + LocationName.chill_penguin_clear: [0x08, 0x001, 0x0E], + LocationName.chill_penguin_legs: [0x08, 0x003, 0x08], + LocationName.chill_penguin_heart_tank: [0x08, 0x002, 0x01], + LocationName.chill_penguin_hp_1: [0x08, 0x020, 0x12], + + LocationName.spark_mandrill_boss: [0x06, 0x000, 0x02], + LocationName.spark_mandrill_clear: [0x06, 0x001, 0x0A], + LocationName.spark_mandrill_sub_tank: [0x06, 0x004, 0x40], + LocationName.spark_mandrill_heart_tank: [0x06, 0x002, 0x40], + LocationName.spark_mandrill_mini_boss: [0x06, 0x000, 0x17], + + LocationName.launch_octopus_boss: [0x01, 0x000, 0x03], + LocationName.launch_octopus_clear: [0x01, 0x001, 0x00], + LocationName.launch_octopus_heart_tank: [0x01, 0x002, 0x80], + LocationName.launch_octopus_mini_boss_1: [0x01, 0x000, 0x18], + LocationName.launch_octopus_mini_boss_2: [0x01, 0x000, 0x19], + LocationName.launch_octopus_mini_boss_3: [0x01, 0x000, 0x1A], + LocationName.launch_octopus_mini_boss_4: [0x01, 0x000, 0x1B], + LocationName.launch_octopus_hp_1: [0x01, 0x020, 0x02], + + LocationName.boomer_kuwanger_boss: [0x07, 0x000, 0x04], + LocationName.boomer_kuwanger_clear: [0x07, 0x001, 0x0C], + LocationName.boomer_kuwanger_heart_tank: [0x07, 0x002, 0x20], + + LocationName.sting_chameleon_boss: [0x02, 0x000, 0x05], + LocationName.sting_chameleon_clear: [0x02, 0x001, 0x02], + LocationName.sting_chameleon_body: [0x02, 0x003, 0x04], + LocationName.sting_chameleon_heart_tank: [0x02, 0x002, 0x08], + LocationName.sting_chameleon_1up: [0x02, 0x020, 0x03], + LocationName.sting_chameleon_hp_1: [0x02, 0x020, 0x04], + + LocationName.storm_eagle_boss: [0x05, 0x000, 0x06], + LocationName.storm_eagle_clear: [0x05, 0x001, 0x08], + LocationName.storm_eagle_sub_tank: [0x05, 0x004, 0x10], + LocationName.storm_eagle_heart_tank: [0x05, 0x002, 0x04], + LocationName.storm_eagle_helmet: [0x05, 0x003, 0x01], + LocationName.storm_eagle_1up_3: [0x05, 0x020, 0x1B], + LocationName.storm_eagle_1up_1: [0x05, 0x020, 0x0E], + LocationName.storm_eagle_1up_2: [0x05, 0x020, 0x0F], + LocationName.storm_eagle_hp_1: [0x05, 0x020, 0x0B], + LocationName.storm_eagle_hp_2: [0x05, 0x020, 0x0C], + LocationName.storm_eagle_hp_3: [0x05, 0x020, 0x0D], + #LocationName.storm_eagle_hp_4: [0x05, 0x020, 0x10], + #LocationName.storm_eagle_energy_1: [0x05, 0x020, 0x11], + + LocationName.flame_mammoth_boss: [0x04, 0x000, 0x07], + LocationName.flame_mammoth_clear: [0x04, 0x001, 0x06], + LocationName.flame_mammoth_sub_tank: [0x04, 0x004, 0x80], + LocationName.flame_mammoth_heart_tank: [0x04, 0x002, 0x10], + LocationName.flame_mammoth_arms: [0x04, 0x003, 0x02], + LocationName.flame_mammoth_hp_1: [0x04, 0x020, 0x08], + LocationName.flame_mammoth_hp_2: [0x04, 0x020, 0x09], + LocationName.flame_mammoth_1up: [0x04, 0x020, 0x0A], + + LocationName.sigma_fortress_1_bospider: [0x09, 0x000, 0x08], + LocationName.sigma_fortress_1_vile: [0x09, 0x000, 0x09], + LocationName.sigma_fortress_1_boomer_kuwanger: [0x09, 0x000, 0x0A], + + LocationName.sigma_fortress_2_chill_penguin: [0x0A, 0x000, 0x0B], + LocationName.sigma_fortress_2_storm_eagle: [0x0A, 0x000, 0x0C], + LocationName.sigma_fortress_2_rangda_bangda: [0x0A, 0x000, 0x0D], + + LocationName.sigma_fortress_3_armored_armadillo:[0x0B, 0x000, 0x0E], + LocationName.sigma_fortress_3_sting_chameleon: [0x0B, 0x000, 0x0F], + LocationName.sigma_fortress_3_spark_mandrill: [0x0B, 0x000, 0x10], + LocationName.sigma_fortress_3_launch_octopus: [0x0B, 0x000, 0x11], + LocationName.sigma_fortress_3_flame_mammoth: [0x0B, 0x000, 0x12], + LocationName.sigma_fortress_3_d_rex: [0x0B, 0x000, 0x1E], + LocationName.sigma_fortress_3_hp_1: [0x0B, 0x020, 0x13], + LocationName.sigma_fortress_3_hp_2: [0x0B, 0x020, 0x15], + LocationName.sigma_fortress_3_hp_3: [0x0B, 0x020, 0x16], + LocationName.sigma_fortress_3_hp_4: [0x0B, 0x020, 0x18], + LocationName.sigma_fortress_3_energy_1: [0x0B, 0x020, 0x14], + LocationName.sigma_fortress_3_energy_2: [0x0B, 0x020, 0x17], + LocationName.sigma_fortress_3_energy_3: [0x0B, 0x020, 0x19], + LocationName.sigma_fortress_3_1up: [0x0B, 0x020, 0x1A], + + LocationName.sigma_fortress_4_velguarder: [0x0C, 0x000, 0x13], + LocationName.sigma_fortress_4_sigma: [0x0C, 0x000, 0x1F], +} diff --git a/worlds/mmx/Locations.py b/worlds/mmx/Locations.py new file mode 100644 index 000000000000..99218645f435 --- /dev/null +++ b/worlds/mmx/Locations.py @@ -0,0 +1,198 @@ +from BaseClasses import Location +from .Names import LocationName + +from typing import TYPE_CHECKING, Dict + +if TYPE_CHECKING: + from . import MMXWorld + +class MMXLocation(Location): + game = "Mega Man X" + + def __init__(self, player: int, name: str = '', address: int = None, parent=None): + super().__init__(player, name, address, parent) + +STARTING_ID = 0xBE0800 + +stage_location_table = { + LocationName.armored_armadillo_boss: STARTING_ID + 0x0000, + LocationName.chill_penguin_boss: STARTING_ID + 0x0001, + LocationName.spark_mandrill_boss: STARTING_ID + 0x0002, + LocationName.launch_octopus_boss: STARTING_ID + 0x0003, + LocationName.boomer_kuwanger_boss: STARTING_ID + 0x0004, + LocationName.sting_chameleon_boss: STARTING_ID + 0x0005, + LocationName.storm_eagle_boss: STARTING_ID + 0x0006, + LocationName.flame_mammoth_boss: STARTING_ID + 0x0007, + LocationName.sigma_fortress_1_bospider: STARTING_ID + 0x0008, + LocationName.sigma_fortress_1_vile: STARTING_ID + 0x0009, + LocationName.sigma_fortress_1_boomer_kuwanger: STARTING_ID + 0x000A, + LocationName.sigma_fortress_2_chill_penguin: STARTING_ID + 0x000B, + LocationName.sigma_fortress_2_storm_eagle: STARTING_ID + 0x000C, + LocationName.sigma_fortress_2_rangda_bangda: STARTING_ID + 0x000D, + LocationName.sigma_fortress_3_armored_armadillo: STARTING_ID + 0x000E, + LocationName.sigma_fortress_3_sting_chameleon: STARTING_ID + 0x000F, + LocationName.sigma_fortress_3_spark_mandrill: STARTING_ID + 0x0010, + LocationName.sigma_fortress_3_launch_octopus: STARTING_ID + 0x0011, + LocationName.sigma_fortress_3_flame_mammoth: STARTING_ID + 0x0012, + LocationName.sigma_fortress_3_d_rex: STARTING_ID + 0x0013, + LocationName.sigma_fortress_4_velguarder: STARTING_ID + 0x0014, + LocationName.sigma_fortress_4_sigma: STARTING_ID + 0x0015, + LocationName.intro_completed: STARTING_ID + 0x0016, + LocationName.intro_mini_boss_1: STARTING_ID + 0x001E, + LocationName.intro_mini_boss_2: STARTING_ID + 0x001F, + LocationName.launch_octopus_mini_boss_1: STARTING_ID + 0x0017, + LocationName.launch_octopus_mini_boss_2: STARTING_ID + 0x0018, + LocationName.launch_octopus_mini_boss_3: STARTING_ID + 0x0019, + LocationName.launch_octopus_mini_boss_4: STARTING_ID + 0x001A, + LocationName.spark_mandrill_mini_boss: STARTING_ID + 0x001B, + LocationName.armored_armadillo_mini_boss_1: STARTING_ID + 0x001C, + LocationName.armored_armadillo_mini_boss_2: STARTING_ID + 0x001D, +} + +tank_pickups = { + LocationName.armored_armadillo_heart_tank: STARTING_ID + 0x0030, + LocationName.armored_armadillo_sub_tank: STARTING_ID + 0x0031, + LocationName.chill_penguin_heart_tank: STARTING_ID + 0x0032, + LocationName.spark_mandrill_sub_tank: STARTING_ID + 0x0033, + LocationName.spark_mandrill_heart_tank: STARTING_ID + 0x0034, + LocationName.launch_octopus_heart_tank: STARTING_ID + 0x0035, + LocationName.boomer_kuwanger_heart_tank: STARTING_ID + 0x0036, + LocationName.sting_chameleon_heart_tank: STARTING_ID + 0x0037, + LocationName.storm_eagle_heart_tank: STARTING_ID + 0x0038, + LocationName.storm_eagle_sub_tank: STARTING_ID + 0x0039, + LocationName.flame_mammoth_heart_tank: STARTING_ID + 0x003A, + LocationName.flame_mammoth_sub_tank: STARTING_ID + 0x003B, +} + +upgrade_pickups = { + LocationName.armored_armadillo_hadouken: STARTING_ID + 0x0040, + LocationName.chill_penguin_legs: STARTING_ID + 0x0041, + LocationName.sting_chameleon_body: STARTING_ID + 0x0042, + LocationName.storm_eagle_helmet: STARTING_ID + 0x0043, + LocationName.flame_mammoth_arms: STARTING_ID + 0x0044, +} + +pickup_sanity = { + LocationName.intro_hp_1: STARTING_ID + 0x0050, + LocationName.intro_hp_2: STARTING_ID + 0x0051, + LocationName.armored_armadillo_hp_1: STARTING_ID + 0x0052, + LocationName.armored_armadillo_hp_2: STARTING_ID + 0x0053, + LocationName.armored_armadillo_hp_3: STARTING_ID + 0x0054, + LocationName.launch_octopus_hp_1: STARTING_ID + 0x0055, + LocationName.sting_chameleon_1up: STARTING_ID + 0x0056, + LocationName.sting_chameleon_hp_1: STARTING_ID + 0x0057, + LocationName.storm_eagle_1up_1: STARTING_ID + 0x0058, + LocationName.storm_eagle_1up_2: STARTING_ID + 0x0059, + LocationName.storm_eagle_1up_3: STARTING_ID + 0x006B, + LocationName.storm_eagle_hp_1: STARTING_ID + 0x005A, + LocationName.storm_eagle_hp_2: STARTING_ID + 0x005B, + LocationName.storm_eagle_hp_3: STARTING_ID + 0x005C, + #LocationName.storm_eagle_hp_4: STARTING_ID + 0x005D, + #LocationName.storm_eagle_energy_1: STARTING_ID + 0x005E, + LocationName.flame_mammoth_hp_1: STARTING_ID + 0x005F, + LocationName.flame_mammoth_hp_2: STARTING_ID + 0x0060, + LocationName.flame_mammoth_1up: STARTING_ID + 0x0061, + LocationName.sigma_fortress_3_hp_1: STARTING_ID + 0x0062, + LocationName.sigma_fortress_3_hp_2: STARTING_ID + 0x0063, + LocationName.sigma_fortress_3_energy_1: STARTING_ID + 0x0064, + LocationName.sigma_fortress_3_hp_3: STARTING_ID + 0x0065, + LocationName.sigma_fortress_3_energy_2: STARTING_ID + 0x0066, + LocationName.sigma_fortress_3_hp_4: STARTING_ID + 0x0067, + LocationName.sigma_fortress_3_energy_3: STARTING_ID + 0x0068, + LocationName.sigma_fortress_3_1up: STARTING_ID + 0x0069, + LocationName.chill_penguin_hp_1: STARTING_ID + 0x006A, +} + +stage_clears = { + LocationName.armored_armadillo_clear: STARTING_ID + 0x0070, + LocationName.chill_penguin_clear: STARTING_ID + 0x0071, + LocationName.spark_mandrill_clear: STARTING_ID + 0x0072, + LocationName.launch_octopus_clear: STARTING_ID + 0x0073, + LocationName.boomer_kuwanger_clear: STARTING_ID + 0x0074, + LocationName.sting_chameleon_clear: STARTING_ID + 0x0075, + LocationName.storm_eagle_clear: STARTING_ID + 0x0076, + LocationName.flame_mammoth_clear: STARTING_ID + 0x0077 +} + +all_locations = { + **stage_clears, + **stage_location_table, + **tank_pickups, + **upgrade_pickups, + **pickup_sanity +} + +location_table = {} + +location_groups = { + "Bosses": { + LocationName.armored_armadillo_boss, + LocationName.chill_penguin_boss, + LocationName.spark_mandrill_boss, + LocationName.launch_octopus_boss, + LocationName.boomer_kuwanger_boss, + LocationName.sting_chameleon_boss, + LocationName.storm_eagle_boss, + LocationName.flame_mammoth_boss, + LocationName.sigma_fortress_1_vile, + LocationName.sigma_fortress_1_bospider, + LocationName.sigma_fortress_2_rangda_bangda, + LocationName.sigma_fortress_3_d_rex, + LocationName.sigma_fortress_4_velguarder, + LocationName.sigma_fortress_4_sigma, + LocationName.spark_mandrill_mini_boss, + }, + "Heart Tanks": {location for location in all_locations.keys() if "- Heart Tank" in location}, + "Sub Tanks": {location for location in all_locations.keys() if "- Sub Tank" in location}, + "Upgrade Capsules": {location for location in all_locations.keys() if "Capsule" in location}, + "Intro Stage": {location for location in all_locations.keys() if "Intro Stage - " in location}, + "Armored Armadillo Stage": {location for location in all_locations.keys() if "Armored Armadillo - " in location}, + "Chill Penguin Stage": {location for location in all_locations.keys() if "Chill Penguin - " in location}, + "Spark Mandrill Stage": {location for location in all_locations.keys() if "Spark Mandrill - " in location}, + "Launch Octopus Stage": {location for location in all_locations.keys() if "Launch Octopus - " in location}, + "Boomer Kuwanger Stage": {location for location in all_locations.keys() if "Boomer Kuwanger - " in location}, + "Sting Chameleon Stage": {location for location in all_locations.keys() if "Sting Chameleon - " in location}, + "Storm Eagle Stage": {location for location in all_locations.keys() if "Storm Eagle - " in location}, + "Flame Mammoth Stage": {location for location in all_locations.keys() if "Flame Mammoth - " in location}, + "Sigma's Fortress Stage 1": { + LocationName.sigma_fortress_1_boomer_kuwanger, + LocationName.sigma_fortress_1_bospider, + LocationName.sigma_fortress_1_vile, + }, + "Sigma's Fortress Stage 2": { + LocationName.sigma_fortress_2_chill_penguin, + LocationName.sigma_fortress_2_rangda_bangda, + LocationName.sigma_fortress_2_storm_eagle, + }, + "Sigma's Fortress Stage 3": { + LocationName.sigma_fortress_3_1up, + LocationName.sigma_fortress_3_armored_armadillo, + LocationName.sigma_fortress_3_sting_chameleon, + LocationName.sigma_fortress_3_launch_octopus, + LocationName.sigma_fortress_3_flame_mammoth, + LocationName.sigma_fortress_3_spark_mandrill, + LocationName.sigma_fortress_3_d_rex, + LocationName.sigma_fortress_3_energy_1, + LocationName.sigma_fortress_3_energy_2, + LocationName.sigma_fortress_3_energy_3, + LocationName.sigma_fortress_3_hp_1, + LocationName.sigma_fortress_3_hp_2, + LocationName.sigma_fortress_3_hp_3, + LocationName.sigma_fortress_3_hp_4, + }, +} + +def setup_locations(world: "MMXWorld") -> Dict[int, str]: + location_table = { + **stage_clears, + **stage_location_table, + **tank_pickups, + **upgrade_pickups, + } + + if world.options.pickupsanity.value: + location_table.update({**pickup_sanity}) + + return location_table + +lookup_id_to_name: Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/mmx/Names/EventName.py b/worlds/mmx/Names/EventName.py new file mode 100644 index 000000000000..27738a960882 --- /dev/null +++ b/worlds/mmx/Names/EventName.py @@ -0,0 +1,12 @@ +armored_armadillo_clear = "Event: Armored Armadillo - Clear" +chill_penguin_clear = "Event: Chill Penguin - Clear" +flame_mammoth_clear = "Event: Flame Mammoth - Clear" +storm_eagle_clear = "Event: Storm Eagle - Clear" +sting_chameleon_clear = "Event: Sting Chameleon - Clear" +spark_mandrill_clear = "Event: Spark Mandrill - Clear" +boomer_kuwanger_clear = "Event: Boomer Kuwanger - Clear" +launch_octopus_clear = "Event: Launch Octopus - Clear" + +sigma_fortress_1_clear = "Event: Sigma Fortress 1 - Clear" +sigma_fortress_2_clear = "Event: Sigma Fortress 2 - Clear" +sigma_fortress_3_clear = "Event: Sigma Fortress 3 - Clear" \ No newline at end of file diff --git a/worlds/mmx/Names/ItemName.py b/worlds/mmx/Names/ItemName.py new file mode 100644 index 000000000000..956f5dd99fa0 --- /dev/null +++ b/worlds/mmx/Names/ItemName.py @@ -0,0 +1,42 @@ +# Stages +stage_armored_armadillo = "Armored Armadillo Access Codes" +stage_boomer_kuwanger = "Boomer Kuwanger Access Codes" +stage_chill_penguin = "Chill Penguin Access Codes" +stage_flame_mammoth = "Flame Mammoth Access Codes" +stage_launch_octopus = "Launch Octopus Access Codes" +stage_spark_mandrill = "Spark Mandrill Access Codes" +stage_sting_chameleon = "Sting Chameleon Access Codes" +stage_storm_eagle = "Storm Eagle Access Codes" +stage_sigma_fortress = "Sigma's Fortress Access Codes" + +# Armor +helmet = "Helmet Upgrade" +body = "Body Upgrade" +arms = "Arms Upgrade" +legs = "Legs Upgrade" + +# Weapons +shotgun_ice = "Shotgun Ice" +electric_spark = "Electric Spark" +rolling_shield = "Rolling Shield" +homing_torpedo = "Homing Torpedo" +boomerang_cutter = "Boomerang Cutter" +chameleon_sting = "Chameleon Sting" +storm_tornado = "Storm Tornado" +fire_wave = "Fire Wave" +hadouken = "Hadouken" + +# Tanks +heart_tank = "Heart Tank" +sub_tank = "Sub Tank" + +# Medals +maverick_medal = "Maverick Medal" +victory = "Sigma Destroyed" + +# Junk +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" diff --git a/worlds/mmx/Names/LocationName.py b/worlds/mmx/Names/LocationName.py new file mode 100644 index 000000000000..56e80f2e51cf --- /dev/null +++ b/worlds/mmx/Names/LocationName.py @@ -0,0 +1,97 @@ +intro_hp_1 = "Intro Stage - HP Pickup (after Bee Blader)" +intro_hp_2 = "Intro Stage - Small HP Pickup (after Bee Blader)" +intro_completed = "Intro Stage - Get Defeated By Vile" +intro_mini_boss_1 = "Intro Stage - Defeated Bee Blader #1" +intro_mini_boss_2 = "Intro Stage - Defeated Bee Blader #2" + +armored_armadillo_boss = "Defeated Armored Armadillo" +armored_armadillo_clear = "Armored Armadillo Clear" +armored_armadillo_sub_tank = "Armored Armadillo - Sub Tank" +armored_armadillo_hp_1 = "Armored Armadillo - HP Pickup 1 (In blocked ceiling)" +armored_armadillo_hp_2 = "Armored Armadillo - HP Pickup 2 (In blocked ceiling)" +armored_armadillo_heart_tank = "Armored Armadillo - Heart Tank" +armored_armadillo_hp_3 = "Armored Armadillo - HP Pickup 3 (Next to Hadouken capsule)" +armored_armadillo_hadouken = "Armored Armadillo - Hadouken Capsule" +armored_armadillo_mini_boss_1 = "Defeated Mole Borer #1" +armored_armadillo_mini_boss_2 = "Defeated Mole Borer #2" + +chill_penguin_boss = "Defeated Chill Penguin" +chill_penguin_clear = "Chill Penguin Clear" +chill_penguin_legs = "Chill Penguin - Legs Capsule" +chill_penguin_heart_tank = "Chill Penguin - Heart Tank" +chill_penguin_hp_1 = "Chill Penguin - Weapon Energy Pickup (Inside third dome)" + +spark_mandrill_boss = "Defeated Spark Mandrill" +spark_mandrill_clear = "Spark Mandrill Clear" +spark_mandrill_mini_boss = "Defeated Thunder Slimer" +spark_mandrill_sub_tank = "Spark Mandrill - Sub Tank" +spark_mandrill_heart_tank = "Spark Mandrill - Heart Tank" + +launch_octopus_boss = "Defeated Launch Octopus" +launch_octopus_clear = "Launch Octopus Clear" +launch_octopus_mini_boss_1 = "Defeated Anglerge #1" +launch_octopus_mini_boss_2 = "Defeated Anglerge #2" +launch_octopus_mini_boss_3 = "Defeated Utuboros #1" +launch_octopus_mini_boss_4 = "Defeated Utuboros #2" +launch_octopus_hp_1 = "Launch Octopus - HP Pickup (Crane platform above water)" +launch_octopus_heart_tank = "Launch Octopus - Heart Tank" + +boomer_kuwanger_boss = "Defeated Boomer Kuwanger" +boomer_kuwanger_clear = "Boomer Kuwanger Clear" +boomer_kuwanger_heart_tank = "Boomer Kuwanger - Heart Tank" + +sting_chameleon_boss = "Defeated Sting Chameleon" +sting_chameleon_clear = "Sting Chameleon Clear" +sting_chameleon_heart_tank = "Sting Chameleon - Heart Tank" +sting_chameleon_body = "Sting Chameleon - Body Capsule" +sting_chameleon_1up = "Sting Chameleon - 1up Pickup (Inside second mini-cave in mountain)" +sting_chameleon_hp_1 = "Sting Chameleon - HP Pickup (On top of platform in the swamp)" + +storm_eagle_boss = "Defeated Storm Eagle" +storm_eagle_clear = "Storm Eagle Clear" +storm_eagle_heart_tank = "Storm Eagle - Heart Tank" +storm_eagle_sub_tank = "Storm Eagle - Sub Tank" +storm_eagle_hp_1 = "Storm Eagle - HP Pickup 1 (Behind first set of gas tanks)" +storm_eagle_hp_2 = "Storm Eagle - HP Pickup 2 (Behind second set of gas tanks)" +storm_eagle_hp_3 = "Storm Eagle - HP Pickup 3 (Behind third set of gas tanks)" +storm_eagle_1up_3 = "Storm Eagle - 1up Pickup 1 (Behind wall in third set of gas tanks)" +storm_eagle_1up_1 = "Storm Eagle - 1up Pickup 2 (Behind fourth set of gas tanks)" +storm_eagle_helmet = "Storm Eagle - Helmet Capsule" +storm_eagle_1up_2 = "Storm Eagle - 1up Pickup 3 (Above helmet capsule)" +storm_eagle_hp_4 = "Storm Eagle - HP Pickup 4 (Right of plane)" +storm_eagle_energy_1 = "Storm Eagle - Weapon Energy Pickup (Right of plane)" + +flame_mammoth_boss = "Defeated Flame Mammoth" +flame_mammoth_clear = "Flame Mammoth Clear" +flame_mammoth_hp_1 = "Flame Mammoth - HP Pickup 1 (After first conveyor belts section)" +flame_mammoth_arms = "Flame Mammoth - Arms Capsule" +flame_mammoth_heart_tank = "Flame Mammoth - Heart Tank" +flame_mammoth_hp_2 = "Flame Mammoth - HP Pickup 2 (Top platform in Dig Labour section)" +flame_mammoth_1up = "Flame Mammoth - 1up Pickup (Top platform in Dig Labour section)" +flame_mammoth_sub_tank = "Flame Mammoth - Sub Tank" + +sigma_fortress_1_bospider = "Defeated Bospider" +sigma_fortress_1_vile = "Defeated Vile" +sigma_fortress_1_boomer_kuwanger = "Defeated Boomer Kuwanger (Rematch)" + +sigma_fortress_2_chill_penguin = "Defeated Chill Penguin (Rematch)" +sigma_fortress_2_storm_eagle = "Defeated Storm Eagle (Rematch)" +sigma_fortress_2_rangda_bangda = "Defeated Rangda Bangda" + +sigma_fortress_3_armored_armadillo = "Defeated Armored Armadillo (Rematch)" +sigma_fortress_3_hp_1 = "Sigma's Fortress 3 - HP Pickup 1 (Before Sting Chameleon rematch)" +sigma_fortress_3_sting_chameleon = "Defeated Sting Chameleon (Rematch)" +sigma_fortress_3_hp_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 1 (Before Spark Mandrill rematch)" +sigma_fortress_3_energy_1 = "Sigma's Fortress 3 - HP Pickup 2 (Before Spark Mandrill rematch)" +sigma_fortress_3_spark_mandrill = "Defeated Spark Mandrill (Rematch)" +sigma_fortress_3_hp_3 = "Sigma's Fortress 3 - HP Pickup 3 (Before Launch Octopus rematch)" +sigma_fortress_3_energy_2 = "Sigma's Fortress 3 - Weapon Energy Pickup 2 (Before Launch Octopus rematch)" +sigma_fortress_3_launch_octopus = "Defeated Launch Octopus (Rematch)" +sigma_fortress_3_hp_4 = "Sigma's Fortress 3 - HP Pickup 4 (On spikes)" +sigma_fortress_3_energy_3 = "Sigma's Fortress 3 - Weapon Energy Pickup 3 (On Spikes)" +sigma_fortress_3_1up = "Sigma's Fortress 3 - 1up Pickup (On spikes)" +sigma_fortress_3_flame_mammoth = "Defeated Flame Mammoth (Rematch)" +sigma_fortress_3_d_rex = "Defeated D-Rex" + +sigma_fortress_4_velguarder = "Defeated Velguarder" +sigma_fortress_4_sigma = "Defeated Sigma" diff --git a/worlds/mmx/Names/RegionName.py b/worlds/mmx/Names/RegionName.py new file mode 100644 index 000000000000..065a6f6ad276 --- /dev/null +++ b/worlds/mmx/Names/RegionName.py @@ -0,0 +1,91 @@ +intro = "Intro" + +armored_armadillo = "Armored Armadillo" +armored_armadillo_ride_1 = "Armored Armadillo - Ride 1" +armored_armadillo_excavator_1 = "Armored Armadillo - Excavator 1" +armored_armadillo_ride_2 = "Armored Armadillo - Ride 2" +armored_armadillo_excavator_2 = "Armored Armadillo - Excavator 2" +armored_armadillo_ride_3 = "Armored Armadillo - Ride 3" +armored_armadillo_boss = "Armored Armadillo - Boss" + +chill_penguin = "Chill Penguin" +chill_penguin_entrance = "Chill Penguin - Entrance" +chill_penguin_icicles = "Chill Penguin - Icicles" +chill_penguin_ride = "Chill Penguin - Ride Armor" +chill_penguin_boss = "Chill Penguin - Boss" + +spark_mandrill = "Spark Mandrill" +spark_mandrill_entrance = "Spark Mandrill - Entrance" +spark_mandrill_mid_boss = "Spark Mandrill - Mid Boss" +spark_mandrill_deep = "Spark Mandrill - Deep Inside" +spark_mandrill_boss = "Spark Mandrill - Boss" + +launch_octopus = "Launch Octopus" +launch_octopus_sea = "Launch Octopus - Sea Floor" +launch_octopus_base = "Launch Octopus - Underwater Base" +launch_octopus_boss = "Launch Octopus - Boss" + +boomer_kuwanger = "Boomer Kuwanger" +boomer_kuwanger_basement = "Boomer Kuwanger - Basement" +boomer_kuwanger_elevator = "Boomer Kuwanger - Elevator" +boomer_kuwanger_outside = "Boomer Kuwanger - Outside" +boomer_kuwanger_top = "Boomer Kuwanger - Top" +boomer_kuwanger_boss = "Boomer Kuwanger - Boss" + +sting_chameleon = "Sting Chameleon" +sting_chameleon_forest = "Sting Chameleon - Forest" +sting_chameleon_cave_top = "Sting Chameleon - Top of cave" +sting_chameleon_cave = "Sting Chameleon - Cave" +sting_chameleon_cave_bottom = "Sting Chameleon - Bottom of cave" +sting_chameleon_hill = "Sting Chameleon - Hill" +sting_chameleon_swamp = "Sting Chameleon - Swamp" +sting_chameleon_boss = "Sting Chameleon - Boss" + +storm_eagle = "Storm Eagle" +storm_eagle_airport = "Storm Eagle - Airport" +storm_eagle_glass = "Storm Eagle - Glass Tower" +storm_eagle_metal = "Storm Eagle - Metal Tower" +storm_eagle_aircraft = "Storm Eagle - Aircraft" +storm_eagle_boss = "Storm Eagle - Boss" + +flame_mammoth = "Flame Mammoth" +flame_mammoth_conveyors_1 = "Flame Mammoth - Conveyors 1" +flame_mammoth_lava_river_1 = "Flame Mammoth - Lava River 1" +flame_mammoth_conveyors_2 = "Flame Mammoth - Conveyors 2" +flame_mammoth_lava_river_2 = "Flame Mammoth - Lava River 2" +flame_mammoth_boss = "Flame Mammoth - Boss" + +sigma_fortress = "Sigma Fortress" + +sigma_fortress_1 = "Sigma Fortress 1" +sigma_fortress_1_outside = "Sigma Fortress 1 - Outside" +sigma_fortress_1_vile = "Sigma Fortress 1 - Vile" +sigma_fortress_1_vertical = "Sigma Fortress 1 - Vertical Room" +sigma_fortress_1_rematch_1 = "Sigma Fortress 1 - Rematch 1" +sigma_fortress_1_before_boss = "Sigma Fortress 1 - Before Boss" +sigma_fortress_1_boss = "Sigma Fortress 1 - Boss" + +sigma_fortress_2 = "Sigma Fortress 2" +sigma_fortress_2_start = "Sigma Fortress 2 - Start" +sigma_fortress_2_rematch_1 = "Sigma Fortress 2 - Rematch 1" +sigma_fortress_2_ride = "Sigma Fortress 2 - Ride Armor" +sigma_fortress_2_rematch_2 = "Sigma Fortress 2 - Rematch 2" +sigma_fortress_2_before_boss = "Sigma Fortress 2 - Before Boss" +sigma_fortress_2_boss = "Sigma Fortress 2 - Boss" + +sigma_fortress_3 = "Sigma Fortress 3" +sigma_fortress_3_rematch_1 = "Sigma Fortress 3 - Rematch 1" +sigma_fortress_3_rematch_2 = "Sigma Fortress 3 - Rematch 2" +sigma_fortress_3_rematch_3 = "Sigma Fortress 3 - Rematch 3" +sigma_fortress_3_rematch_4 = "Sigma Fortress 3 - Rematch 4" +sigma_fortress_3_rematch_5 = "Sigma Fortress 3 - Rematch 5" +sigma_fortress_3_after_rematch_1 = "Sigma Fortress 3 - After Rematch 1" +sigma_fortress_3_after_rematch_2 = "Sigma Fortress 3 - After Rematch 2" +sigma_fortress_3_after_rematch_3 = "Sigma Fortress 3 - After Rematch 3" +sigma_fortress_3_after_rematch_4 = "Sigma Fortress 3 - After Rematch 4" +sigma_fortress_3_after_rematch_5 = "Sigma Fortress 3 - After Rematch 5" +sigma_fortress_3_boss = "Sigma Fortress 3 - Boss" + +sigma_fortress_4 = "Sigma Fortress 4" +sigma_fortress_4_dog = "Sigma Fortress 4 - Dog" +sigma_fortress_4_sigma = "Sigma Fortress 4 - Sigma" diff --git a/worlds/mmx/Options.py b/worlds/mmx/Options.py new file mode 100644 index 000000000000..72232622c066 --- /dev/null +++ b/worlds/mmx/Options.py @@ -0,0 +1,535 @@ +from dataclasses import dataclass +import typing + +from Options import OptionGroup, Choice, Range, Toggle, DefaultOnToggle, OptionSet, OptionDict, DeathLink, PerGameCommonOptions, StartInventoryPool +from schema import Schema, And, Use, Optional + +from .Rom import action_buttons, action_names, x_palette_set_offsets +from .Weaknesses import boss_weaknesses, weapons_chaotic + +class EnergyLink(DefaultOnToggle): + """ + Enable EnergyLink support. + + EnergyLink in MMX2 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 /heal or /refill in the client. + """ + display_name = "Energy Link" + +class StartingLifeCount(Range): + """ + How many lives to start the game with. + Note: This number becomes the new default life count, meaning that it will persist after a game over. + """ + display_name = "Starting Life Count" + range_start = 0 + range_end = 99 + 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 Rolling Shield but not its + uncharged version. + """ + display_name = "Boss Weakness Randomization" + option_vanilla = 0 + option_shuffled = 1 + option_chaotic_double = 3 + option_chaotic_single = 2 + 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 level 3 to deal damage to the bosses + only_weakness: Only the weakness will deal damage to the bosses + """ + 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. + Note: This adds another Arms Upgrade into the item pool. + """ + display_name = "Jammed Buster" + +class HadoukenInPool(DefaultOnToggle): + """ + Adds Hadouken to the item pool. + Hadouken will deal the current HP as damage and half the current HP on strict weakness settings. + """ + display_name = "Hadouken In Pool" + +class EarlyLegs(Toggle): + """ + Places the Legs Upgrade item in sphere 1. + """ + display_name = "Early Legs" + +class PickupSanity(Toggle): + """ + Whether collecting freestanding 1ups, HP and Weapon Energy capsules will grant a check. + """ + display_name = "Pickupsanity" + +class LogicBossWeakness(DefaultOnToggle): + """ + Every main boss will logically expect you to have its weakness. + """ + display_name = "Boss Weakness Logic" + +class LogicLegSigma(DefaultOnToggle): + """ + Sigma's Fortress will logically expect you to have the legs upgrade. + """ + display_name = "Sigma's Fortress Legs Upgrade Logic" + +class LogicChargedShotgunIce(Toggle): + """ + Adds Charged Shotgun Ice as logic to some locations. Some of those may be hard to execute. + """ + display_name = "Charged Shotgun Ice Logic" + +class FortressBundleUnlock(Toggle): + """ + Whether to unlock Sigma's Fortress 1-3 levels as a group or not. + Unlocking level 4 requires getting all Fortress levels cleared. + """ + display_name = "Fortress Levels Bundle Unlock" + +class SigmaOpen(OptionSet): + """ + Under which conditions will Sigma's Fortress open. + If no options are selected a multiworld item granting access to the stage will be created. + + Medals: Consider Maverick medals to get access to the fortress. + Weapons: Consider weapons to get access to the fortress. + Armor Upgrades: Consider upgrades to get access to the fortress. + Heart Tanks: Consider heart tanks to get access to the fortress. + Sub Tanks: Consider sub tanks to get access to the fortress. + """ + display_name = "Sigma Fortress Rules" + valid_keys = { + "Medals", + "Weapons", + "Armor Upgrades", + "Heart Tanks", + "Sub Tanks", + } + default = { + "Medals", + } + +class SigmaMedalCount(Range): + """ + How many Maverick Medals are required to access Sigma's Fortress. + """ + display_name = "Sigma Medal Count" + range_start = 0 + range_end = 8 + default = 8 + +class SigmaWeaponCount(Range): + """ + How many weapons are required to access Sigma's Fortress. + """ + display_name = "Sigma Weapon Count" + range_start = 0 + range_end = 6 + default = 6 + +class SigmaArmorUpgradeCount(Range): + """ + How many armor upgrades are required to access Sigma's Fortress. + """ + display_name = "Sigma Armor Upgrade Count" + range_start = 0 + range_end = 4 + default = 3 + +class SigmaHeartTankCount(Range): + """ + How many Heart Tanks are required to access Sigma's Fortress. + """ + display_name = "Sigma Heart Tank Count" + range_start = 0 + range_end = 6 + default = 6 + +class SigmaSubTankCount(Range): + """ + How many Sub Tanks are required to access Sigma's Fortress. + """ + display_name = "Sigma Sub Tank Count" + range_start = 0 + range_end = 2 + default = 2 + +class ButtonConfiguration(OptionDict): + """ + Default buttons for every action. + """ + display_name = "Button Configuration" + schema = Schema({action_name: And(str, Use(str.upper), lambda s: s in action_buttons) for action_name in action_names}) + default = { + "SHOT": "Y", + "JUMP": "B", + "DASH": "A", + "SELECT_L": "L", + "SELECT_R": "R", + "MENU": "START" + } + +class PlandoWeaknesses(OptionDict): + """ + Forces bosses to have a specific weakness. Uses the names that appear on the chaotic weakness set. + + Format: + Boss Name: Weakness Name + """ + display_name = "Button Configuration" + schema = Schema({ + Optional(boss_name): + And(str, lambda weapon: weapon in weapons_chaotic.keys()) for boss_name in boss_weaknesses.keys() + }) + default = {} + +class BetterWallJump(Toggle): + """ + Enables performing a dash wall jump by holding down the button instead of pressing it every time. + """ + display_name = "Better Wall Jump" + +class AirDash(Toggle): + """ + Adds another Legs Upgrade that allows X to perform an Air Dash. + """ + display_name = "Air Dash" + +class LongJumps(Toggle): + """ + Allows X to perform longer jumps when holding down the Dash button. Only works after getting a Legs Upgrade. + """ + display_name = "Long Jumps" + +class LogicHelmetCheckpoints(Toggle): + """ + Makes the "Use Any Checkpoint" feature from the Helmet Upgrade be in logic + """ + display_name = "Helmet Checkpoints In Logic" + +class ChillPenguinTweaks(OptionSet): + """ + Behavior options for Chill Penguin. Everything can be stacked. + """ + display_name = "Chill Penguin Tweaks" + valid_keys = { + "Random horizontal slide speed", + "Jumps when starting slide", + "Random ice block horizontal speed", + "Random ice block vertical speed", + "Shoot random amount of ice blocks", + "Ice block shooting rate enhancer #1", + "Ice block shooting rate enhancer #2", + "Ice block shooting rate enhancer #3", + "Random blizzard strength", + "Fast falls after jumping", + "Random mist range", + "Can't be stunned/set on fire with incoming damage", + "Can't be set on fire with weakness", + } + default = {} + + +class ArmoredArmadilloTweaks(OptionSet): + """ + Behavior options for Armored Armadillo. Everything can be stacked. + """ + display_name = "Armored Armadillo Tweaks" + valid_keys = { + "Random bouncing speed", + "Random bouncing angle", + "Random energy horizontal speed", + "Random energy vertical speed", + "Energy shooting rate enhancer #1", + "Energy shooting rate enhancer #2", + "Don't absorb any projectile", + "Absorbs any projectile except weakness", + "Don't flinch from incoming damage without armor", + "Can't block incoming projectiles", + } + default = {} + + +class SparkMandrillTweaks(OptionSet): + """ + Behavior options for Spark Mandrill. Everything can be stacked. + """ + display_name = "Spark Mandrill Tweaks" + valid_keys = { + "Random Electric Spark speed", + "Additional Electric Spark #1", + "Additional Electric Spark #2", + "Landing creates Electric Spark", + "Hitting a wall creates Electric Spark", + "Can't be stunned during Dash Punch with weakness", + "Can't be frozen with weakness", + } + default = {} + +class BasePalette(Choice): + """ + Base class for palettes + """ + option_blue = 0 + option_gold_armor = 1 + option_acid_burst = 6 + option_parasitic_bomb = 7 + option_triad_thunder = 8 + option_spinning_blade = 9 + option_ray_splasher = 10 + option_gravity_well = 11 + option_frost_shield = 12 + option_tornado_fang = 13 + option_crystal_hunter = 14 + option_bubble_splash = 15 + option_silk_shot = 16 + option_spin_wheel = 17 + option_sonic_slicer = 18 + option_strike_chain = 19 + option_magnet_mine = 20 + option_speed_burner = 21 + option_homing_torpedo = 22 + option_chameleon_sting = 23 + option_rolling_shield = 24 + option_fire_wave = 25 + option_storm_tornado = 26 + option_electric_spark = 27 + option_boomerang_cutter = 28 + option_shotgun_ice = 29 + +class PaletteDefault(BasePalette): + """ + Which color to use for X's default color + """ + display_name = "X Palette" + default = 0 + +class PaletteHomingTorpedo(BasePalette): + """ + Which color to use for X's Homing Torpedo + """ + display_name = "Homing Torpedo Palette" + default = 22 + +class PaletteChameleonSting(BasePalette): + """ + Which color to use for X's Chameleon Sting + """ + display_name = "Chameleon Sting Palette" + default = 23 + +class PaletteRollingShield(BasePalette): + """ + Which color to use for X's Rolling Shield + """ + display_name = "Rolling Shield Palette" + default = 24 + +class PaletteFireWave(BasePalette): + """ + Which color to use for X's Fire Wave + """ + display_name = "Fire Wave Palette" + default = 25 + +class PaletteStormTornado(BasePalette): + """ + Which color to use for X's Storm Tornado + """ + display_name = "Storm Tornado Palette" + default = 26 + +class PaletteElectricSpark(BasePalette): + """ + Which color to use for X's Electric Spark + """ + display_name = "Electric Spark Palette" + default = 27 + +class PaletteBoomerangCutter(BasePalette): + """ + Which color to use for X's Boomerang Cutter + """ + display_name = "Boomerang Cutter Palette" + default = 28 + +class PaletteShotgunIce(BasePalette): + """ + Which color to use for X's Shotgun Ice + """ + display_name = "Shotgun Ice Palette" + default = 29 + +class SetPalettes(OptionDict): + """ + Allows you to create colors for each weapon X has. Includes charge levels and Gold Armor customization. + This will override the option preset + + Each one expects 16 values which are mapped to X's colors. + The values can be in SNES RGB (bgr555) with the $ prefix or PC RGB (rgb888) with the # prefix. + """ + display_name = "Set Custom Palettes" + schema = Schema({ + Optional(color_set): list for color_set in x_palette_set_offsets.keys() + }) + default = {} + +mmx_option_groups = [ + OptionGroup("Gameplay Options", [ + StartingLifeCount, + StartingHP, + HeartTankEffectiveness, + JammedBuster, + BetterWallJump, + LongJumps, + AirDash, + HadoukenInPool, + LogicChargedShotgunIce, + LogicHelmetCheckpoints, + ]), + OptionGroup("Sigma Fortress Options", [ + SigmaOpen, + SigmaMedalCount, + SigmaWeaponCount, + SigmaArmorUpgradeCount, + SigmaHeartTankCount, + SigmaSubTankCount, + FortressBundleUnlock, + LogicLegSigma, + ]), + OptionGroup("Boss Weaknesses", [ + BossWeaknessRando, + PlandoWeaknesses, + BossWeaknessStrictness, + BossRandomizedHP, + LogicBossWeakness, + ]), + OptionGroup("Enemy Tweaks", [ + ChillPenguinTweaks, + ArmoredArmadilloTweaks, + SparkMandrillTweaks, + ]), + OptionGroup("Aesthetic", [ + SetPalettes, + PaletteDefault, + PaletteHomingTorpedo, + PaletteChameleonSting, + PaletteRollingShield, + PaletteFireWave, + PaletteStormTornado, + PaletteElectricSpark, + PaletteBoomerangCutter, + PaletteShotgunIce, + ]), +] + +@dataclass +class MMXOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + death_link: DeathLink + energy_link: EnergyLink + button_configuration: ButtonConfiguration + starting_life_count: StartingLifeCount + starting_hp: StartingHP + heart_tank_effectiveness: HeartTankEffectiveness + boss_weakness_rando: BossWeaknessRando + boss_weakness_strictness: BossWeaknessStrictness + boss_weakness_plando: PlandoWeaknesses + boss_randomize_hp: BossRandomizedHP + jammed_buster: JammedBuster + better_walljump: BetterWallJump + air_dash: AirDash + long_jumps: LongJumps + hadouken_in_pool: HadoukenInPool + pickupsanity: PickupSanity + early_legs: EarlyLegs + logic_boss_weakness: LogicBossWeakness + logic_leg_sigma: LogicLegSigma + logic_charged_shotgun_ice: LogicChargedShotgunIce + logic_helmet_checkpoints: LogicHelmetCheckpoints + sigma_all_levels: FortressBundleUnlock + sigma_open: SigmaOpen + sigma_medal_count: SigmaMedalCount + sigma_weapon_count: SigmaWeaponCount + sigma_upgrade_count: SigmaArmorUpgradeCount + sigma_heart_tank_count: SigmaHeartTankCount + sigma_sub_tank_count: SigmaSubTankCount + chill_penguin_tweaks: ChillPenguinTweaks + armored_armadillo_tweaks: ArmoredArmadilloTweaks + spark_mandrill_tweaks: SparkMandrillTweaks + player_palettes: SetPalettes + palette_default: PaletteDefault + palette_homing_torpedo: PaletteHomingTorpedo + palette_chameleon_sting: PaletteChameleonSting + palette_rolling_shield: PaletteRollingShield + palette_fire_wave: PaletteFireWave + palette_storm_tornado: PaletteStormTornado + palette_electric_spark: PaletteElectricSpark + palette_boomerang_cutter: PaletteBoomerangCutter + palette_shotgun_ice: PaletteShotgunIce diff --git a/worlds/mmx/Regions.py b/worlds/mmx/Regions.py new file mode 100644 index 000000000000..51299c1fa28e --- /dev/null +++ b/worlds/mmx/Regions.py @@ -0,0 +1,482 @@ +from BaseClasses import MultiWorld, Region, ItemClassification +from .Locations import MMXLocation +from .Items import MMXItem +from .Names import LocationName, RegionName, EventName +from worlds.AutoWorld import World + +from typing import TYPE_CHECKING, Dict + +if TYPE_CHECKING: + from . import MMXWorld + + +def create_regions(world: "MMXWorld", active_locations: Dict[int, str]) -> None: + multiworld = world.multiworld + player = world.player + + menu = create_region(multiworld, player, active_locations, 'Menu') + + intro = create_region(multiworld, player, active_locations, RegionName.intro) + + armored_armadillo = create_region(multiworld, player, active_locations, RegionName.armored_armadillo) + armored_armadillo_ride_1 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_1) + armored_armadillo_excavator_1 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1) + armored_armadillo_ride_2 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_2) + armored_armadillo_excavator_2 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2) + armored_armadillo_ride_3 = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3) + armored_armadillo_boss = create_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss) + + chill_penguin = create_region(multiworld, player, active_locations, RegionName.chill_penguin) + chill_penguin_entrance = create_region(multiworld, player, active_locations, RegionName.chill_penguin_entrance) + chill_penguin_icicles = create_region(multiworld, player, active_locations, RegionName.chill_penguin_icicles) + chill_penguin_ride = create_region(multiworld, player, active_locations, RegionName.chill_penguin_ride) + chill_penguin_boss = create_region(multiworld, player, active_locations, RegionName.chill_penguin_boss) + + spark_mandrill = create_region(multiworld, player, active_locations, RegionName.spark_mandrill) + spark_mandrill_entrance = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_entrance) + spark_mandrill_mid_boss = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_mid_boss) + spark_mandrill_deep = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_deep) + spark_mandrill_boss = create_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss) + + launch_octopus = create_region(multiworld, player, active_locations, RegionName.launch_octopus) + launch_octopus_sea = create_region(multiworld, player, active_locations, RegionName.launch_octopus_sea) + launch_octopus_base = create_region(multiworld, player, active_locations, RegionName.launch_octopus_base) + launch_octopus_boss = create_region(multiworld, player, active_locations, RegionName.launch_octopus_boss) + + boomer_kuwanger = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger) + boomer_kuwanger_basement = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_basement) + boomer_kuwanger_elevator = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_elevator) + boomer_kuwanger_outside = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_outside) + boomer_kuwanger_top = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_top) + boomer_kuwanger_boss = create_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss) + + sting_chameleon = create_region(multiworld, player, active_locations, RegionName.sting_chameleon) + sting_chameleon_forest = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_forest) + sting_chameleon_cave_top = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_top) + sting_chameleon_cave = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave) + sting_chameleon_cave_bottom = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_bottom) + sting_chameleon_hill = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_hill) + sting_chameleon_swamp = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_swamp) + sting_chameleon_boss = create_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss) + + storm_eagle = create_region(multiworld, player, active_locations, RegionName.storm_eagle) + storm_eagle_airport = create_region(multiworld, player, active_locations, RegionName.storm_eagle_airport) + storm_eagle_glass = create_region(multiworld, player, active_locations, RegionName.storm_eagle_glass) + storm_eagle_metal = create_region(multiworld, player, active_locations, RegionName.storm_eagle_metal) + storm_eagle_aircraft = create_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft) + storm_eagle_boss = create_region(multiworld, player, active_locations, RegionName.storm_eagle_boss) + + flame_mammoth = create_region(multiworld, player, active_locations, RegionName.flame_mammoth) + flame_mammoth_conveyors_1 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_1) + flame_mammoth_lava_river_1 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1) + flame_mammoth_conveyors_2 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_2) + flame_mammoth_lava_river_2 = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_2) + flame_mammoth_boss = create_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss) + + sigma_fortress = create_region(multiworld, player, active_locations, RegionName.sigma_fortress) + + sigma_fortress_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1) + sigma_fortress_1_outside = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_outside) + sigma_fortress_1_vile = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vile) + sigma_fortress_1_vertical = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vertical) + sigma_fortress_1_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_rematch_1) + sigma_fortress_1_before_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_before_boss) + sigma_fortress_1_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_boss) + + sigma_fortress_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2) + sigma_fortress_2_start = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_start) + sigma_fortress_2_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_1) + sigma_fortress_2_ride = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_ride) + sigma_fortress_2_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_2) + sigma_fortress_2_before_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_before_boss) + sigma_fortress_2_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_boss) + + sigma_fortress_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3) + sigma_fortress_3_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_1) + sigma_fortress_3_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2) + sigma_fortress_3_rematch_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3) + sigma_fortress_3_rematch_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4) + sigma_fortress_3_rematch_5 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5) + sigma_fortress_3_after_rematch_1 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_1) + sigma_fortress_3_after_rematch_2 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2) + sigma_fortress_3_after_rematch_3 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3) + sigma_fortress_3_after_rematch_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4) + sigma_fortress_3_after_rematch_5 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_5) + sigma_fortress_3_boss = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_boss) + + sigma_fortress_4 = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4) + sigma_fortress_4_dog = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_dog) + sigma_fortress_4_sigma = create_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_sigma) + + multiworld.regions += [ + menu, + intro, + armored_armadillo, + armored_armadillo_ride_1, + armored_armadillo_excavator_1, + armored_armadillo_ride_2, + armored_armadillo_excavator_2, + armored_armadillo_ride_3, + armored_armadillo_boss, + chill_penguin, + chill_penguin_entrance, + chill_penguin_icicles, + chill_penguin_ride, + chill_penguin_boss, + spark_mandrill, + spark_mandrill_entrance, + spark_mandrill_mid_boss, + spark_mandrill_deep, + spark_mandrill_boss, + launch_octopus, + launch_octopus_sea, + launch_octopus_base, + launch_octopus_boss, + boomer_kuwanger, + boomer_kuwanger_basement, + boomer_kuwanger_elevator, + boomer_kuwanger_outside, + boomer_kuwanger_top, + boomer_kuwanger_boss, + sting_chameleon, + sting_chameleon_forest, + sting_chameleon_cave_top, + sting_chameleon_cave, + sting_chameleon_cave_bottom, + sting_chameleon_hill, + sting_chameleon_swamp, + sting_chameleon_boss, + storm_eagle, + storm_eagle_airport, + storm_eagle_glass, + storm_eagle_metal, + storm_eagle_aircraft, + storm_eagle_boss, + flame_mammoth, + flame_mammoth_conveyors_1, + flame_mammoth_lava_river_1, + flame_mammoth_conveyors_2, + flame_mammoth_lava_river_2, + flame_mammoth_boss, + sigma_fortress, + sigma_fortress_1, + sigma_fortress_1_outside, + sigma_fortress_1_vile, + sigma_fortress_1_vertical, + sigma_fortress_1_rematch_1, + sigma_fortress_1_before_boss, + sigma_fortress_1_boss, + sigma_fortress_2, + sigma_fortress_2_start, + sigma_fortress_2_rematch_1, + sigma_fortress_2_ride, + sigma_fortress_2_rematch_2, + sigma_fortress_2_before_boss, + sigma_fortress_2_boss, + sigma_fortress_3, + sigma_fortress_3_rematch_1, + sigma_fortress_3_rematch_2, + sigma_fortress_3_rematch_3, + sigma_fortress_3_rematch_4, + sigma_fortress_3_rematch_5, + sigma_fortress_3_after_rematch_1, + sigma_fortress_3_after_rematch_2, + sigma_fortress_3_after_rematch_3, + sigma_fortress_3_after_rematch_4, + sigma_fortress_3_after_rematch_5, + sigma_fortress_3_boss, + sigma_fortress_4, + sigma_fortress_4_dog, + sigma_fortress_4_sigma, + ] + + # Intro + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_completed) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_mini_boss_2) + + # Armored Armadillo + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2, LocationName.armored_armadillo_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_2, LocationName.armored_armadillo_mini_boss_2) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3, LocationName.armored_armadillo_hadouken) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss, LocationName.armored_armadillo_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_boss, LocationName.armored_armadillo_clear) + add_event_to_region(multiworld, player, RegionName.armored_armadillo_boss, EventName.armored_armadillo_clear) + + # Chill Penguin + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_ride, LocationName.chill_penguin_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_icicles, LocationName.chill_penguin_legs) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_boss, LocationName.chill_penguin_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_boss, LocationName.chill_penguin_clear) + add_event_to_region(multiworld, player, RegionName.chill_penguin_boss, EventName.chill_penguin_clear) + + # Spark Mandrill + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_entrance, LocationName.spark_mandrill_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_mid_boss, LocationName.spark_mandrill_mini_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_deep, LocationName.spark_mandrill_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss, LocationName.spark_mandrill_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.spark_mandrill_boss, LocationName.spark_mandrill_clear) + add_event_to_region(multiworld, player, RegionName.spark_mandrill_boss, EventName.spark_mandrill_clear) + + # Launch Octopus + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_1) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_2) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_3) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_mini_boss_4) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_base, LocationName.launch_octopus_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_boss, LocationName.launch_octopus_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_boss, LocationName.launch_octopus_clear) + add_event_to_region(multiworld, player, RegionName.launch_octopus_boss, EventName.launch_octopus_clear) + + # Boomer Kuwanger + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_top, LocationName.boomer_kuwanger_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss, LocationName.boomer_kuwanger_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.boomer_kuwanger_boss, LocationName.boomer_kuwanger_clear) + add_event_to_region(multiworld, player, RegionName.boomer_kuwanger_boss, EventName.boomer_kuwanger_clear) + + # Sting Chameleon + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_bottom, LocationName.sting_chameleon_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_cave_top, LocationName.sting_chameleon_body) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss, LocationName.sting_chameleon_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_boss, LocationName.sting_chameleon_clear) + add_event_to_region(multiworld, player, RegionName.sting_chameleon_boss, EventName.sting_chameleon_clear) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_glass, LocationName.storm_eagle_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_helmet) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_boss, LocationName.storm_eagle_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_boss, LocationName.storm_eagle_clear) + add_event_to_region(multiworld, player, RegionName.storm_eagle_boss, EventName.storm_eagle_clear) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_heart_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_sub_tank) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_arms) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss, LocationName.flame_mammoth_boss) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_boss, LocationName.flame_mammoth_clear) + add_event_to_region(multiworld, player, RegionName.flame_mammoth_boss, EventName.flame_mammoth_clear) + + # Sigma's Fortress 1 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_vile, LocationName.sigma_fortress_1_vile) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_rematch_1, LocationName.sigma_fortress_1_boomer_kuwanger) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_1_boss, LocationName.sigma_fortress_1_bospider) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_1_boss, EventName.sigma_fortress_1_clear) + + # Sigma's Fortress 2 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_1, LocationName.sigma_fortress_2_chill_penguin) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_rematch_2, LocationName.sigma_fortress_2_storm_eagle) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_2_boss, LocationName.sigma_fortress_2_rangda_bangda) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_2_boss, EventName.sigma_fortress_2_clear) + + # Sigma's Fortress 3 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_1, LocationName.sigma_fortress_3_armored_armadillo) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_2, LocationName.sigma_fortress_3_sting_chameleon) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_3, LocationName.sigma_fortress_3_spark_mandrill) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_4, LocationName.sigma_fortress_3_launch_octopus) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_rematch_5, LocationName.sigma_fortress_3_flame_mammoth) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_boss, LocationName.sigma_fortress_3_d_rex) + add_event_to_region(multiworld, player, RegionName.sigma_fortress_3_boss, EventName.sigma_fortress_3_clear) + + # Sigma's Fortress 4 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_dog, LocationName.sigma_fortress_4_velguarder) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_4_sigma, LocationName.sigma_fortress_4_sigma) + + if world.options.pickupsanity: + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.intro, LocationName.intro_hp_2) + + # Armored Armadillo + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_excavator_1, LocationName.armored_armadillo_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.armored_armadillo_ride_3, LocationName.armored_armadillo_hp_3) + + # Chill Penguin + add_location_to_region(multiworld, player, active_locations, RegionName.chill_penguin_ride, LocationName.chill_penguin_hp_1) + + # Launch Octopus + add_location_to_region(multiworld, player, active_locations, RegionName.launch_octopus_sea, LocationName.launch_octopus_hp_1) + + # Sting Chameleon + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_hill, LocationName.sting_chameleon_1up) + add_location_to_region(multiworld, player, active_locations, RegionName.sting_chameleon_swamp, LocationName.sting_chameleon_hp_1) + + # Storm Eagle + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_hp_3) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_airport, LocationName.storm_eagle_1up_3) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_1) + add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_metal, LocationName.storm_eagle_1up_2) + #add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft, LocationName.storm_eagle_hp_4) + #add_location_to_region(multiworld, player, active_locations, RegionName.storm_eagle_aircraft, LocationName.storm_eagle_energy_1) + + # Flame Mammoth + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_conveyors_1, LocationName.flame_mammoth_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.flame_mammoth_lava_river_1, LocationName.flame_mammoth_1up) + + # Sigma's Fortress 3 + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_1, LocationName.sigma_fortress_3_hp_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2, LocationName.sigma_fortress_3_hp_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_2, LocationName.sigma_fortress_3_energy_1) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3, LocationName.sigma_fortress_3_hp_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_3, LocationName.sigma_fortress_3_energy_2) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_hp_4) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_energy_3) + add_location_to_region(multiworld, player, active_locations, RegionName.sigma_fortress_3_after_rematch_4, LocationName.sigma_fortress_3_1up) + + +def connect_regions(world: World) -> None: + connect(world, "Menu", RegionName.intro) + + connect(world, RegionName.intro, RegionName.armored_armadillo) + connect(world, RegionName.intro, RegionName.chill_penguin) + connect(world, RegionName.intro, RegionName.spark_mandrill) + connect(world, RegionName.intro, RegionName.launch_octopus) + connect(world, RegionName.intro, RegionName.boomer_kuwanger) + connect(world, RegionName.intro, RegionName.sting_chameleon) + connect(world, RegionName.intro, RegionName.storm_eagle) + connect(world, RegionName.intro, RegionName.flame_mammoth) + + connect(world, RegionName.armored_armadillo, RegionName.armored_armadillo_ride_1) + connect(world, RegionName.armored_armadillo_ride_1, RegionName.armored_armadillo_excavator_1) + connect(world, RegionName.armored_armadillo_excavator_1, RegionName.armored_armadillo_ride_2) + connect(world, RegionName.armored_armadillo_ride_2, RegionName.armored_armadillo_excavator_2) + connect(world, RegionName.armored_armadillo_excavator_2, RegionName.armored_armadillo_ride_3) + connect(world, RegionName.armored_armadillo_ride_3, RegionName.armored_armadillo_boss) + + connect(world, RegionName.chill_penguin, RegionName.chill_penguin_entrance) + connect(world, RegionName.chill_penguin_entrance, RegionName.chill_penguin_icicles) + connect(world, RegionName.chill_penguin_icicles, RegionName.chill_penguin_ride) + connect(world, RegionName.chill_penguin_ride, RegionName.chill_penguin_boss) + + connect(world, RegionName.spark_mandrill, RegionName.spark_mandrill_entrance) + connect(world, RegionName.spark_mandrill_entrance, RegionName.spark_mandrill_mid_boss) + connect(world, RegionName.spark_mandrill_mid_boss, RegionName.spark_mandrill_deep) + connect(world, RegionName.spark_mandrill_deep, RegionName.spark_mandrill_boss) + + connect(world, RegionName.launch_octopus, RegionName.launch_octopus_sea) + connect(world, RegionName.launch_octopus_sea, RegionName.launch_octopus_base) + connect(world, RegionName.launch_octopus_sea, RegionName.launch_octopus_boss) + + connect(world, RegionName.boomer_kuwanger, RegionName.boomer_kuwanger_basement) + connect(world, RegionName.boomer_kuwanger_basement, RegionName.boomer_kuwanger_elevator) + connect(world, RegionName.boomer_kuwanger_elevator, RegionName.boomer_kuwanger_outside) + connect(world, RegionName.boomer_kuwanger_outside, RegionName.boomer_kuwanger_top) + connect(world, RegionName.boomer_kuwanger_top, RegionName.boomer_kuwanger_boss) + + connect(world, RegionName.sting_chameleon, RegionName.sting_chameleon_forest) + connect(world, RegionName.sting_chameleon_forest, RegionName.sting_chameleon_cave) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_cave_top) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_cave_bottom) + connect(world, RegionName.sting_chameleon_cave, RegionName.sting_chameleon_hill) + connect(world, RegionName.sting_chameleon_hill, RegionName.sting_chameleon_swamp) + connect(world, RegionName.sting_chameleon_swamp, RegionName.sting_chameleon_boss) + + connect(world, RegionName.storm_eagle, RegionName.storm_eagle_airport) + connect(world, RegionName.storm_eagle_airport, RegionName.storm_eagle_glass) + connect(world, RegionName.storm_eagle_glass, RegionName.storm_eagle_metal) + connect(world, RegionName.storm_eagle_metal, RegionName.storm_eagle_aircraft) + connect(world, RegionName.storm_eagle_aircraft, RegionName.storm_eagle_boss) + + connect(world, RegionName.flame_mammoth, RegionName.flame_mammoth_conveyors_1) + connect(world, RegionName.flame_mammoth_conveyors_1, RegionName.flame_mammoth_lava_river_1) + connect(world, RegionName.flame_mammoth_lava_river_1, RegionName.flame_mammoth_conveyors_2) + connect(world, RegionName.flame_mammoth_conveyors_2, RegionName.flame_mammoth_lava_river_2) + connect(world, RegionName.flame_mammoth_lava_river_2, RegionName.flame_mammoth_boss) + + connect(world, RegionName.intro, RegionName.sigma_fortress) + + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_1) + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_outside) + connect(world, RegionName.sigma_fortress_1_outside, RegionName.sigma_fortress_1_vile) + connect(world, RegionName.sigma_fortress_1_vile, RegionName.sigma_fortress_1_vertical) + connect(world, RegionName.sigma_fortress_1_vertical, RegionName.sigma_fortress_1_rematch_1) + connect(world, RegionName.sigma_fortress_1_rematch_1, RegionName.sigma_fortress_1_before_boss) + connect(world, RegionName.sigma_fortress_1_before_boss, RegionName.sigma_fortress_1_boss) + + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_start) + connect(world, RegionName.sigma_fortress_2_start, RegionName.sigma_fortress_2_rematch_1) + connect(world, RegionName.sigma_fortress_2_rematch_1, RegionName.sigma_fortress_2_ride) + connect(world, RegionName.sigma_fortress_2_ride, RegionName.sigma_fortress_2_rematch_2) + connect(world, RegionName.sigma_fortress_2_rematch_2, RegionName.sigma_fortress_2_before_boss) + connect(world, RegionName.sigma_fortress_2_before_boss, RegionName.sigma_fortress_2_boss) + + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_rematch_1) + connect(world, RegionName.sigma_fortress_3_rematch_1, RegionName.sigma_fortress_3_after_rematch_1) + connect(world, RegionName.sigma_fortress_3_after_rematch_1, RegionName.sigma_fortress_3_rematch_2) + connect(world, RegionName.sigma_fortress_3_rematch_2, RegionName.sigma_fortress_3_after_rematch_2) + connect(world, RegionName.sigma_fortress_3_after_rematch_2, RegionName.sigma_fortress_3_rematch_3) + connect(world, RegionName.sigma_fortress_3_rematch_3, RegionName.sigma_fortress_3_after_rematch_3) + connect(world, RegionName.sigma_fortress_3_after_rematch_3, RegionName.sigma_fortress_3_rematch_4) + connect(world, RegionName.sigma_fortress_3_rematch_4, RegionName.sigma_fortress_3_after_rematch_4) + connect(world, RegionName.sigma_fortress_3_after_rematch_4, RegionName.sigma_fortress_3_rematch_5) + connect(world, RegionName.sigma_fortress_3_rematch_5, RegionName.sigma_fortress_3_after_rematch_5) + connect(world, RegionName.sigma_fortress_3_after_rematch_5, RegionName.sigma_fortress_3_boss) + + connect(world, RegionName.sigma_fortress_4, RegionName.sigma_fortress_4_dog) + connect(world, RegionName.sigma_fortress_4_dog, RegionName.sigma_fortress_4_sigma) + + if world.options.sigma_all_levels: + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_2) + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_3) + connect(world, RegionName.sigma_fortress, RegionName.sigma_fortress_4) + else: + connect(world, RegionName.sigma_fortress_1_boss, RegionName.sigma_fortress_2) + connect(world, RegionName.sigma_fortress_2_boss, RegionName.sigma_fortress_3) + connect(world, RegionName.sigma_fortress_3_boss, RegionName.sigma_fortress_4) + + # Connect checkpoints + if world.options.logic_helmet_checkpoints.value: + connect(world, RegionName.spark_mandrill, RegionName.spark_mandrill_deep) + + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_vertical) + connect(world, RegionName.sigma_fortress_1, RegionName.sigma_fortress_1_before_boss) + + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_ride) + connect(world, RegionName.sigma_fortress_2, RegionName.sigma_fortress_2_before_boss) + + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_1) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_2) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_3) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_4) + connect(world, RegionName.sigma_fortress_3, RegionName.sigma_fortress_3_after_rematch_5) + + +def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None) -> Region: + ret = Region(name, player, multiworld) + if locations: + for locationName in locations: + loc_id = active_locations.get(locationName, 0) + if loc_id: + location = MMXLocation(player, locationName, loc_id, ret) + ret.locations.append(location) + + return ret + + +def add_event_to_region(multiworld: MultiWorld, player: int, region_name: str, event_name: str, event_item=None) -> None: + region = multiworld.get_region(region_name, player) + event = MMXLocation(player, event_name, None, region) + if event_item: + event.place_locked_item(MMXItem(event_item, ItemClassification.progression, None, player)) + else: + event.place_locked_item(MMXItem(event_name, ItemClassification.progression, None, player)) + region.locations.append(event) + + +def add_location_to_region(multiworld: MultiWorld, player: int, active_locations, region_name: str, location_name: str) -> None: + region = multiworld.get_region(region_name, player) + loc_id = active_locations.get(location_name, 0) + if loc_id: + location = MMXLocation(player, location_name, loc_id, region) + region.locations.append(location) + + +def connect(world: World, source: str, target: str) -> None: + source_region: Region = world.multiworld.get_region(source, world.player) + target_region: Region = world.multiworld.get_region(target, world.player) + source_region.connect(target_region) diff --git a/worlds/mmx/Rom.py b/worlds/mmx/Rom.py new file mode 100644 index 000000000000..3dc5befe0ec0 --- /dev/null +++ b/worlds/mmx/Rom.py @@ -0,0 +1,397 @@ +import Utils +import hashlib +import os +import settings + +from worlds.AutoWorld import World +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes + +from .Aesthetics import get_palette_bytes, player_palettes + +from typing import TYPE_CHECKING, Iterable + +if TYPE_CHECKING: + from . import MMXWorld + +HASH_US = 'a10071fa78554b57538d0b459e00d224' +HASH_US_REV_1 = 'df1cc0c8c8c4b61e3b834cc03366611c' +HASH_LEGACY = 'f1dfbbcdc3d8cdeafa4b4b9aa51a56d6' + +STARTING_ID = 0xBE0800 + +action_names = ("SHOT", "JUMP", "DASH", "SELECT_L", "SELECT_R", "MENU") +action_buttons = ("Y", "B", "A", "L", "R", "X", "START", "SELECT") + +weapon_rom_data = { + STARTING_ID + 0x000E: [0x1F88, 0xFF], + STARTING_ID + 0x0010: [0x1F8A, 0xFF], + STARTING_ID + 0x000D: [0x1F8C, 0xFF], + STARTING_ID + 0x0012: [0x1F8E, 0xFF], + STARTING_ID + 0x0011: [0x1F90, 0xFF], + STARTING_ID + 0x000C: [0x1F92, 0xFF], + STARTING_ID + 0x000F: [0x1F94, 0xFF], + STARTING_ID + 0x000B: [0x1F96, 0xFF], + STARTING_ID + 0x001A: [0x1F7E, 0x80], +} + +upgrades_rom_data = { + STARTING_ID + 0x001C: [0x00], + STARTING_ID + 0x001D: [0x02], + STARTING_ID + 0x001E: [0x01], + STARTING_ID + 0x001F: [0x03], +} + +boss_access_rom_data = { + STARTING_ID + 0x0006: [0x01], + STARTING_ID + 0x0008: [0x02], + STARTING_ID + 0x0002: [0x03], + STARTING_ID + 0x0005: [0x04], + STARTING_ID + 0x0009: [0x05], + STARTING_ID + 0x0007: [0x06], + STARTING_ID + 0x0003: [0x07], + STARTING_ID + 0x0004: [0x08], + STARTING_ID + 0x000A: [0x09], +} + +refill_rom_data = { + STARTING_ID + 0x0030: ["hp refill", 2], + STARTING_ID + 0x0031: ["hp refill", 8], + STARTING_ID + 0x0034: ["1up", 0], + STARTING_ID + 0x0032: ["weapon refill", 2], + STARTING_ID + 0x0033: ["weapon refill", 8], +} + +x_palette_set_offsets = { + "Default": 0x02B700, + "Homing Torpedo": 0x02CC40, + "Chameleon Sting": 0x02CC60, + "Rolling Shield": 0x02CD20, + "Fire Wave": 0x02CD00, + "Storm Tornado": 0x02CCC0, + "Electric Spark": 0x02CCE0, + "Boomerang Cutter": 0x02CCA0, + "Shotgun Ice": 0x02CC80, +} + +boss_weakness_offsets = { + "Sting Chameleon": 0x37E20, + "Storm Eagle": 0x37E60, + "Flame Mammoth": 0x37E80, + "Chill Penguin": 0x37EA0, + "Spark Mandrill": 0x3708B, + "Armored Armadillo": 0x370A9, + "Launch Octopus": 0x370C7, + "Boomer Kuwanger": 0x370E5, + "Thunder Slimer": 0x37F00, + "Vile": 0x37E00, + "Bospider": 0x37EC0, + "Rangda Bangda": 0x37031, + "D-Rex": 0x37E40, + "Velguarder": 0x37EE0, + "Sigma": 0x3717B, + "Wolf Sigma": 0x37199, +} + +boss_hp_caps_offsets = { + "Sting Chameleon": 0x406C9, + "Storm Eagle": 0x3D95F, + "Flame Mammoth": 0x392BD, + "Chill Penguin": 0x0B5FB, + "Spark Mandrill": 0x41D29, + "Armored Armadillo": 0x1B2B5, + "Launch Octopus": 0x0C504, + "Boomer Kuwanger": 0x38BE8, + "Vile": 0x45C34, + "Bospider": 0x15C0E, + #"Rangda Bangda": 0x42A38, + #"D-Rex": 0x440FD, + "Velguarder": 0x148DF, + "Sigma": 0x4467B, + "Wolf Sigma": 0x44B78, +} + +enemy_tweaks_offsets = { + "Chill Penguin": 0x158000, + "Armored Armadillo": 0x158002, + "Spark Mandrill": 0x158004, +} + +enemy_tweaks_indexes = { + "Chill Penguin": { + "Random horizontal slide speed": 0x0001, + "Jumps when starting slide": 0x0002, + "Random ice block horizontal speed": 0x0004, + "Random ice block vertical speed": 0x0008, + "Shoot random amount of ice blocks": 0x0010, + "Ice block shooting rate enhancer #1": 0x0020, + "Ice block shooting rate enhancer #2": 0x0040, + "Ice block shooting rate enhancer #3": 0x0080, + "Random blizzard strength": 0x0100, + "Fast falls after jumping": 0x0200, + "Random mist range": 0x0400, + "Can't be stunned/set on fire with incoming damage": 0x4000, + "Can't be set on fire with weakness": 0x8000, + }, + "Armored Armadillo": { + "Random bouncing speed": 0x0001, + "Random bouncing angle": 0x0002, + "Random energy horizontal speed": 0x0004, + "Random energy vertical speed": 0x0008, + "Energy shooting rate enhancer #1": 0x0010, + "Energy shooting rate enhancer #2": 0x0020, + "Don't absorb any projectile": 0x1000, + "Absorbs any projectile except weakness": 0x2000, + "Don't flinch from incoming damage without armor": 0x4000, + "Can't block incoming projectiles": 0x8000, + }, + "Spark Mandrill": { + "Random Electric Spark speed": 0x0001, + "Additional Electric Spark #1": 0x0002, + "Additional Electric Spark #2": 0x0004, + "Landing creates Electric Spark": 0x0008, + "Hitting a wall creates Electric Spark": 0x0010, + "Can't be stunned during Dash Punch with weakness": 0x4000, + "Can't be frozen with weakness": 0x8000, + } +} + +class MMXProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [HASH_US, HASH_LEGACY] + game = "Mega Man X" + patch_file_ending = ".apmmx" + result_file_ending = ".sfc" + name: bytearray + procedure = [ + ("apply_tokens", ["token_patch.bin"]), + ("apply_bsdiff4", ["mmx_basepatch.bsdiff4"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + def write_byte(self, offset, value) -> None: + self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) + + def write_bytes(self, offset, value: Iterable[int]) -> None: + self.write_token(APTokenTypes.WRITE, offset, bytes(value)) + + +def adjust_palettes(world: "MMXWorld", patch: MMXProcedurePatch) -> None: + player_palette_options = { + "Default": world.options.palette_default.current_key, + "Homing Torpedo": world.options.palette_homing_torpedo.current_key, + "Chameleon Sting": world.options.palette_chameleon_sting.current_key, + "Rolling Shield": world.options.palette_rolling_shield.current_key, + "Fire Wave": world.options.palette_fire_wave.current_key, + "Storm Tornado": world.options.palette_storm_tornado.current_key, + "Electric Spark": world.options.palette_electric_spark.current_key, + "Boomerang Cutter": world.options.palette_boomerang_cutter.current_key, + "Shotgun Ice": world.options.palette_shotgun_ice.current_key, + } + player_custom_palettes = world.options.player_palettes + for palette_set, offset in x_palette_set_offsets.items(): + palette_option = player_palette_options[palette_set] + palette = player_palettes[palette_option] + + if palette_set in player_custom_palettes.keys(): + if len(player_custom_palettes[palette_set]) == 0x10: + palette = player_custom_palettes[palette_set] + else: + print (f"[{world.multiworld.player_name[world.player]}] Custom palette set for {palette_set} doesn't have exactly 16 colors. Falling back to the selected preset ({palette_option})") + data = get_palette_bytes(palette) + patch.write_bytes(offset, data) + + +def adjust_boss_damage_table(world: "MMXWorld", patch: MMXProcedurePatch) -> None: + for boss, data in world.boss_weakness_data.items(): + offset = boss_weakness_offsets[boss] + patch.write_bytes(offset, bytearray(data)) + + # Fix second anglerge having different weakness + patch.write_byte(0x12E62, 0x01) + + # Write weaknesses to a table + offset = 0x17E9A2 + for _, entries in world.boss_weaknesses.items(): + data = [0xFF for _ in range(16)] + i = 0 + for entry in entries: + data[i] = entry[1] + i += 1 + patch.write_bytes(offset, bytearray(data)) + offset += 16 + + +def adjust_boss_hp(world: "MMXWorld", patch: MMXProcedurePatch) -> None: + 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 _, offset in boss_hp_caps_offsets.items(): + patch.write_byte(offset, world.random.randint(ranges[0], ranges[1])) + + +def patch_rom(world: "MMXWorld", patch: MMXProcedurePatch) -> None: + # Prepare some ROM locations to receive the basepatch output + patch.write_bytes(0x00098C, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x0009AE, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x001261, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x001271, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF])) + patch.write_bytes(0x00131F, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x00132E, bytearray([0xFF,0xFF])) + patch.write_bytes(0x001352, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0025CA, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0046F3, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x006A61, bytearray([0xFF,0xFF])) + patch.write_bytes(0x006D67, bytearray([0xFF,0xFF,0xFF])) + patch.write_bytes(0x006F97, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x00700F, bytearray([0xFF,0xFF])) + patch.write_bytes(0x007BF0, bytearray([0xFF,0xFF])) + patch.write_bytes(0x00EB4A, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x011646, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01B392, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01C67E, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x01D84F, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021D51, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021E94, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x021F5A, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x02268C, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x03D0B2, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x03D0D9, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x044BEC, bytearray([0xFF,0xFF,0xFF,0xFF])) + patch.write_bytes(0x0457CC, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF])) + + patch.write_bytes(0x0312B0, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF])) + + adjust_boss_damage_table(world, patch) + adjust_palettes(world, patch) + + if world.options.boss_randomize_hp != "off": + adjust_boss_hp(world, patch) + + patch.write_byte(0x014FF, world.options.starting_hp.value) + patch.write_byte(0x01DDC, 0x7F) + + # Remap buttons + button_values = { + "A": 0x20, + "B": 0x80, + "X": 0x10, + "Y": 0x40, + "L": 0x08, + "R": 0x04, + "START": 0x01, + "SELECT": 0x02, + } + action_offsets = { + "SHOT": 0x36E20, + "JUMP": 0x36E21, + "DASH": 0x36E22, + "SELECT_L": 0x36E23, + "SELECT_R": 0x36E24, + "MENU": 0x36E25, + } + button_config = world.options.button_configuration.value + for action, button in button_config.items(): + patch.write_byte(action_offsets[action], button_values[button]) + + # Write tweaks + enemy_tweaks_available = { + "Chill Penguin": world.options.chill_penguin_tweaks.value, + "Armored Armadillo": world.options.armored_armadillo_tweaks.value, + "Spark Mandrill": world.options.spark_mandrill_tweaks.value, + } + for boss, offset in enemy_tweaks_offsets.items(): + selected_tweaks = enemy_tweaks_available[boss] + final_value = 0 + for tweak in selected_tweaks: + final_value |= enemy_tweaks_indexes[boss][tweak] + patch.write_bytes(offset, bytearray([final_value & 0xFF, (final_value >> 8) & 0xFF])) + + # Edit the ROM header + from Utils import __version__ + patch.name = bytearray(f'MMX1{__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) + + # Write options to the ROM + value = 0 + sigma_open = world.options.sigma_open.value + if "Medals" in sigma_open: + value |= 0x01 + if "Weapons" in sigma_open: + value |= 0x02 + if "Armor Upgrades" in sigma_open: + value |= 0x04 + if "Heart Tanks" in sigma_open: + value |= 0x08 + if "Sub Tanks" in sigma_open: + value |= 0x10 + patch.write_byte(0x167C20, value) + patch.write_byte(0x167C21, world.options.sigma_medal_count.value) + patch.write_byte(0x167C22, world.options.sigma_weapon_count.value) + patch.write_byte(0x167C23, world.options.sigma_upgrade_count.value) + patch.write_byte(0x167C24, world.options.sigma_heart_tank_count.value) + patch.write_byte(0x167C25, world.options.sigma_sub_tank_count.value) + patch.write_byte(0x167C26, world.options.starting_life_count.value) + patch.write_byte(0x167C27, world.options.pickupsanity.value) + patch.write_byte(0x167C28, world.options.energy_link.value) + patch.write_byte(0x167C29, world.options.death_link.value) + patch.write_byte(0x167C2A, world.options.jammed_buster.value) + patch.write_byte(0x167C2B, world.options.logic_boss_weakness.value) + patch.write_byte(0x167C2C, world.options.boss_weakness_rando.value) + patch.write_byte(0x167C2D, world.options.starting_hp.value) + patch.write_byte(0x167C2E, world.options.heart_tank_effectiveness.value) + patch.write_byte(0x167C2F, world.options.sigma_all_levels.value) + patch.write_byte(0x167C30, world.options.boss_weakness_strictness.value) + + value = 0 + if world.options.better_walljump.value: + value |= 0x01 + if world.options.air_dash.value: + value |= 0x02 + if world.options.long_jumps.value: + value |= 0x04 + patch.write_byte(0x167C31, value) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {HASH_US, HASH_LEGACY}: + raise Exception('Supplied Base Rom does not match known MD5 for US or LC release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: settings.Settings = settings.get_settings() + if not file_name: + file_name = options["mmx_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/mmx/Rules.py b/worlds/mmx/Rules.py new file mode 100644 index 000000000000..be13670b1e5b --- /dev/null +++ b/worlds/mmx/Rules.py @@ -0,0 +1,331 @@ +from worlds.generic.Rules import add_rule, set_rule +from BaseClasses import CollectionState + +from .Names import LocationName, ItemName, RegionName, EventName + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MMXWorld + +bosses = { + "Sting Chameleon": [ + f"{RegionName.sting_chameleon_swamp} -> {RegionName.sting_chameleon_boss}", + f"{RegionName.sigma_fortress_3_after_rematch_1} -> {RegionName.sigma_fortress_3_rematch_2}" + ], + "Storm Eagle": [ + f"{RegionName.storm_eagle_aircraft} -> {RegionName.storm_eagle_boss}", + f"{RegionName.sigma_fortress_2_ride} -> {RegionName.sigma_fortress_2_rematch_2}" + ], + "Flame Mammoth": [ + f"{RegionName.flame_mammoth_lava_river_2} -> {RegionName.flame_mammoth_boss}", + f"{RegionName.sigma_fortress_3_after_rematch_4} -> {RegionName.sigma_fortress_3_rematch_5}" + ], + "Chill Penguin": [ + f"{RegionName.chill_penguin_ride} -> {RegionName.chill_penguin_boss}", + f"{RegionName.sigma_fortress_2_start} -> {RegionName.sigma_fortress_2_rematch_1}" + ], + "Spark Mandrill": [ + f"{RegionName.spark_mandrill_deep} -> {RegionName.spark_mandrill_boss}", + f"{RegionName.sigma_fortress_3_after_rematch_2} -> {RegionName.sigma_fortress_3_rematch_3}" + ], + "Armored Armadillo": [ + f"{RegionName.armored_armadillo_ride_3} -> {RegionName.armored_armadillo_boss}", + f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_rematch_1}" + ], + "Launch Octopus": [ + f"{RegionName.launch_octopus_sea} -> {RegionName.launch_octopus_boss}", + f"{RegionName.sigma_fortress_3_after_rematch_3} -> {RegionName.sigma_fortress_3_rematch_4}" + ], + "Boomer Kuwanger": [ + f"{RegionName.boomer_kuwanger_top} -> {RegionName.boomer_kuwanger_boss}", + f"{RegionName.sigma_fortress_1_vertical} -> {RegionName.sigma_fortress_1_rematch_1}" + ], + "Thunder Slimer": [ + f"{RegionName.spark_mandrill_entrance} -> {RegionName.spark_mandrill_mid_boss}" + ], + "Vile": [ + f"{RegionName.sigma_fortress_1_outside} -> {RegionName.sigma_fortress_1_vile}" + ], + "Bospider": [ + f"{RegionName.sigma_fortress_1_before_boss} -> {RegionName.sigma_fortress_1_boss}" + ], + "Rangda Bangda": [ + f"{RegionName.sigma_fortress_2_before_boss} -> {RegionName.sigma_fortress_2_boss}" + ], + "D-Rex": [ + f"{RegionName.sigma_fortress_3_after_rematch_5} -> {RegionName.sigma_fortress_3_boss}" + ], + "Velguarder": [ + f"{RegionName.sigma_fortress_4} -> {RegionName.sigma_fortress_4_dog}" + ], + "Sigma": [ + f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}" + ], + "Wolf Sigma": [ + f"{RegionName.sigma_fortress_4_dog} -> {RegionName.sigma_fortress_4_sigma}" + ], +} + + +def build_rules(world: "MMXWorld") -> None: + player = world.player + multiworld = world.multiworld + jammed_buster = world.options.jammed_buster.value + + multiworld.completion_condition[player] = lambda state: state.has(ItemName.victory, player) + + # Intro entrance rules + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.armored_armadillo}"), + lambda state: state.has(ItemName.stage_armored_armadillo, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.boomer_kuwanger}"), + lambda state: state.has(ItemName.stage_boomer_kuwanger, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.chill_penguin}"), + lambda state: state.has(ItemName.stage_chill_penguin, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.flame_mammoth}"), + lambda state: state.has(ItemName.stage_flame_mammoth, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.launch_octopus}"), + lambda state: state.has(ItemName.stage_launch_octopus, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.spark_mandrill}"), + lambda state: state.has(ItemName.stage_spark_mandrill, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.sting_chameleon}"), + lambda state: state.has(ItemName.stage_sting_chameleon, player)) + set_rule(world.get_entrance(f"{RegionName.intro} -> {RegionName.storm_eagle}"), + lambda state: state.has(ItemName.stage_storm_eagle, player)) + + # Fortress entrance rules + fortress_open = world.options.sigma_open.value + entrance = world.get_entrance(f"{RegionName.intro} -> {RegionName.sigma_fortress}") + + if len(fortress_open) == 0: + add_rule(entrance, lambda state: state.has(ItemName.stage_sigma_fortress, player)) + else: + if "Medals" in fortress_open and world.options.sigma_medal_count > 0: + add_rule(entrance, lambda state: state.has(ItemName.maverick_medal, player, world.options.sigma_medal_count.value)) + if "Weapons" in fortress_open and world.options.sigma_weapon_count > 0: + add_rule(entrance, lambda state: state.has_group_unique("Weapons", player, world.options.sigma_weapon_count.value)) + if "Armor Upgrades" in fortress_open and world.options.sigma_upgrade_count > 0: + add_rule(entrance, lambda state: state.has_group_unique("Armor Upgrades", player, world.options.sigma_upgrade_count.value)) + if "Heart Tanks" in fortress_open and world.options.sigma_heart_tank_count > 0: + add_rule(entrance, lambda state: state.has(ItemName.heart_tank, player, world.options.sigma_heart_tank_count.value)) + if "Sub Tanks" in fortress_open and world.options.sigma_sub_tank_count > 0: + add_rule(entrance, lambda state: state.has(ItemName.sub_tank, player, world.options.sigma_sub_tank_count.value)) + + if world.options.logic_leg_sigma: + add_rule(entrance, lambda state: state.has(ItemName.legs, player)) + + # Sigma Fortress level rules + if world.options.sigma_all_levels: + set_rule(world.get_entrance(f"{RegionName.sigma_fortress} -> {RegionName.sigma_fortress_4}"), + lambda state: ( + state.has(EventName.sigma_fortress_1_clear, player) and + state.has(EventName.sigma_fortress_2_clear, player) and + state.has(EventName.sigma_fortress_3_clear, player) + )) + else: + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1_boss} -> {RegionName.sigma_fortress_2}"), + lambda state: state.has(EventName.sigma_fortress_1_clear, player)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2_boss} -> {RegionName.sigma_fortress_3}"), + lambda state: state.has(EventName.sigma_fortress_2_clear, player)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3_boss} -> {RegionName.sigma_fortress_4}"), + lambda state: state.has(EventName.sigma_fortress_3_clear, player)) + + # Sigma rules + add_rule(world.get_location(LocationName.sigma_fortress_4_sigma), + lambda state: state.has(ItemName.arms, player, jammed_buster + 1)) + + # Chill Penguin collectibles + set_rule(world.get_location(LocationName.chill_penguin_heart_tank), + lambda state: state.has(ItemName.fire_wave, player)) + + # Flame Mammoth collectibles + set_rule(world.get_location(LocationName.flame_mammoth_arms), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(ItemName.helmet, player) + )) + set_rule(world.get_location(LocationName.flame_mammoth_heart_tank), + lambda state: ( + state.has(EventName.chill_penguin_clear, player) or + ( + state.has(ItemName.chameleon_sting, player) and + state.has(ItemName.arms, player, jammed_buster + 1) + ) + )) + set_rule(world.get_location(LocationName.flame_mammoth_sub_tank), + lambda state: state.has(ItemName.legs, player)) + + # Boomer Kuwanger collectibles + set_rule(world.get_location(LocationName.boomer_kuwanger_heart_tank), + lambda state: state.has(ItemName.boomerang_cutter, player)) + + # Sting Chameleon collectibles + set_rule(world.get_location(LocationName.sting_chameleon_body), + lambda state: state.has(ItemName.legs, player)) + set_rule(world.get_location(LocationName.sting_chameleon_heart_tank), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(EventName.launch_octopus_clear, player) + )) + + # Spark Mandrill collectibles + set_rule(world.get_location(LocationName.spark_mandrill_sub_tank), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(world.get_location(LocationName.spark_mandrill_heart_tank), + lambda state: ( + state.has(ItemName.boomerang_cutter, player) or + state.has(ItemName.legs, player) + )) + + # Storm Eagle collectibles + set_rule(world.get_location(LocationName.storm_eagle_heart_tank), + lambda state: state.has(ItemName.legs, player)) + set_rule(world.get_location(LocationName.storm_eagle_helmet), + lambda state: state.has(ItemName.legs, player)) + + # Handle pickupsanity + if world.options.pickupsanity: + add_pickupsanity_logic(world) + + # Handle bosses weakness + if world.options.logic_boss_weakness or world.options.boss_weakness_strictness >= 2: + add_boss_weakness_logic(world) + + # Handle charged shotgun ice logic + if world.options.logic_charged_shotgun_ice: + add_charged_shotgun_ice_logic(world) + + # Handle helmet logic + if world.options.logic_helmet_checkpoints: + add_helmet_logic(world) + + +def add_pickupsanity_logic(world: "MMXWorld") -> None: + player = world.player + jammed_buster = world.options.jammed_buster.value + + set_rule(world.get_location(LocationName.chill_penguin_hp_1), + lambda state: state.has(ItemName.fire_wave, player)) + + set_rule(world.get_location(LocationName.armored_armadillo_hp_1), + lambda state: state.has(ItemName.helmet, player)) + set_rule(world.get_location(LocationName.armored_armadillo_hp_2), + lambda state: state.has(ItemName.helmet, player)) + + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_1), + lambda state: ( + state.has(ItemName.legs, player) and + state.has(ItemName.boomerang_cutter, player) + )) + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_2), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(world.get_location(LocationName.sigma_fortress_3_energy_1), + lambda state: state.has(ItemName.boomerang_cutter, player)) + set_rule(world.get_location(LocationName.sigma_fortress_3_hp_4), + lambda state: ( + state.has(ItemName.arms, player, jammed_buster + 1) and + state.has(ItemName.chameleon_sting, player) + )) + set_rule(world.get_location(LocationName.sigma_fortress_3_energy_3), + lambda state: ( + state.has(ItemName.arms, player, jammed_buster + 1) and + state.has(ItemName.chameleon_sting, player) + )) + set_rule(world.get_location(LocationName.sigma_fortress_3_1up), + lambda state: ( + state.has(ItemName.arms, player, jammed_buster + 1) and + ( + state.has(ItemName.chameleon_sting, player) or + state.has(ItemName.shotgun_ice, player) + ) + )) + + +def check_weaknesses(state: CollectionState, player: int, rulesets: list) -> bool: + states = list() + for i in range(len(rulesets)): + valid = state.has_all_counts(rulesets[i], player) + states.append(valid) + return any(states) + + +def add_boss_weakness_logic(world: "MMXWorld") -> None: + player = world.player + jammed_buster = world.options.jammed_buster.value + + for boss, regions in bosses.items(): + weaknesses = world.boss_weaknesses[boss] + rulesets = list() + for weakness in weaknesses: + if weakness[0] is None: + rulesets = None + break + weakness = weakness[0] + ruleset = dict() + if "Check Charge" in weakness[0]: + ruleset[ItemName.arms] = jammed_buster + int(weakness[0][-1:]) - 1 + elif "Check Dash" in weakness[0]: + ruleset[ItemName.legs] = 1 + else: + ruleset[weakness[0]] = 1 + if len(weakness) != 1: + ruleset[weakness[1]] = 1 + rulesets.append(ruleset) + + if rulesets is not None: + for region in regions: + add_rule(world.get_entrance(region), + lambda state, rulesets=rulesets: check_weaknesses(state, player, rulesets)) + + +def add_charged_shotgun_ice_logic(world: "MMXWorld") -> None: + player = world.player + jammed_buster = world.options.jammed_buster.value + + # Flame Mammoth collectibles + add_rule(world.get_location(LocationName.flame_mammoth_sub_tank), + lambda state: ( + state.has(ItemName.arms, player, jammed_buster + 1) and + state.has(ItemName.boomerang_cutter, player) and + state.has(ItemName.shotgun_ice, player) + )) + # Boomer Kuwanger collectibles + add_rule(world.get_location(LocationName.boomer_kuwanger_heart_tank), + lambda state: ( + state.has(ItemName.shotgun_ice, player) and + state.has(ItemName.arms, player, jammed_buster + 1) + )) + # Sting Chameleon collectibles + add_rule(world.get_location(LocationName.sting_chameleon_body), + lambda state: ( + state.has(ItemName.shotgun_ice, player) and + state.has(ItemName.arms, player, jammed_buster + 1) + )) + + +def add_helmet_logic(world: "MMXWorld") -> None: + player = world.player + + set_rule(world.get_entrance(f"{RegionName.spark_mandrill} -> {RegionName.spark_mandrill_deep}"), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_vertical}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_1} -> {RegionName.sigma_fortress_1_before_boss}"), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_ride}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_2} -> {RegionName.sigma_fortress_2_before_boss}"), + lambda state: state.has(ItemName.helmet, player, 1)) + + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_1}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_2}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_3}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_4}"), + lambda state: state.has(ItemName.helmet, player, 1)) + set_rule(world.get_entrance(f"{RegionName.sigma_fortress_3} -> {RegionName.sigma_fortress_3_after_rematch_5}"), + lambda state: state.has(ItemName.helmet, player, 1)) diff --git a/worlds/mmx/Weaknesses.py b/worlds/mmx/Weaknesses.py new file mode 100644 index 000000000000..77acca52831b --- /dev/null +++ b/worlds/mmx/Weaknesses.py @@ -0,0 +1,439 @@ +from .Names import ItemName + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MMXWorld + +WEAKNESS_UNCHARGED_DMG = 0x03 +WEAKNESS_CHARGED_DMG = 0x05 + +boss_weaknesses = { + "Sting Chameleon": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Storm Eagle": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Flame Mammoth": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + [[ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Chill Penguin": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], + ], + "Spark Mandrill": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Armored Armadillo": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Launch Octopus": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + ], + "Boomer Kuwanger": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Thunder Slimer": [ + [None, 0x00, 0x02], + [None, 0x06, 0x04], + [None, 0x01, 0x03], + [None, 0x03, 0x04], + [None, 0x02, 0x05], + [None, 0x1D, 0x02], + ], + "Vile": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Bospider": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Rangda Bangda": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "D-Rex": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Velguarder": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], + "Sigma": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Wolf Sigma": [ + [["Check Charge 2"], 0x02, 0x05], + [["Check Charge 2"], 0x1D, 0x02], + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + ], +} + +weapon_id = { + 0x00: "Lemon", + 0x01: "Charged Shot (Level 1)", + 0x02: "Charged Shot (Level 3, Bullet Stream)", + 0x03: "Charged Shot (Level 2)", + 0x04: "Hadouken", + 0x06: "Lemon (Dash)", + 0x07: "Uncharged Homing Torpedo", + 0x08: "Uncharged Chameleon Sting", + 0x09: "Uncharged Rolling Shield", + 0x0A: "Uncharged Fire Wave", + 0x0B: "Uncharged Storm Tornado", + 0x0C: "Uncharged Electric Spark", + 0x0D: "Uncharged Boomerang Cutter", + 0x0E: "Uncharged Shotgun Ice", + 0x10: "Charged Homing Torpedo", + 0x12: "Charged Rolling Shield", + 0x13: "Charged Fire Wave", + 0x14: "Charged Storm Tornado", + 0x15: "Charged Electric Spark", + 0x16: "Charged Boomerang Cutter", + 0x17: "Charged Shotgun Ice", + 0x1D: "Charged Shot (Level 3, Shockwave)", +} + +damage_templates = { + "Allow Buster": [ + 0x01,0x02,0x03,0x03,0x20,0x00,0x02,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x01 + ], + "Allow Upgraded Buster": [ + 0x80,0x80,0x03,0x80,0x20,0x00,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x01 + ], + "Only Weakness": [ + 0x80,0x80,0x80,0x80,0x20,0x00,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x7F,0x80,0x80 + ], +} + +boss_weakness_data = { + "Sting Chameleon": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Storm Eagle": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Flame Mammoth": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Chill Penguin": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Spark Mandrill": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Armored Armadillo": [ + 0x01,0x01,0x01,0x01,0x20,0x02,0x02,0x01, + 0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Launch Octopus": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Boomer Kuwanger": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Thunder Slimer": [ + 0x01,0x02,0x04,0x04,0x20,0x04,0x02,0x02, + 0x02,0x02,0x01,0x01,0x02,0x02,0x02,0x02, + 0x03,0x00,0x04,0x01,0x04,0x06,0x06,0x04, + 0x04,0x05,0x0A,0x7F,0x10,0x01 + ], + "Vile": [ + 0x01,0x02,0x04,0x04,0x20,0x04,0x02,0x02, + 0x02,0x02,0x01,0x01,0x02,0x02,0x02,0x02, + 0x03,0x00,0x04,0x01,0x04,0x06,0x06,0x06, + 0x04,0x05,0x0A,0x7F,0x10,0x01 + ], + "Bospider": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Rangda Bangda": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "D-Rex": [ + 0x01,0x01,0x02,0x02,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Velguarder": [ + 0x01,0x02,0x03,0x03,0x20,0x02,0x02,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x02,0x00,0x02,0x02,0x02,0x02,0x02,0x02, + 0x01,0x01,0x01,0x7F,0x01,0x01 + ], + "Sigma": [ + 0x01,0x01,0x01,0x01,0x20,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01 + ], + "Wolf Sigma": [ + 0x80,0x80,0x01,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x01 + ], +} + +boss_excluded_weapons = { + "Sting Chameleon": [ + "Charged Shotgun Ice", + ], + "Storm Eagle": [ + ], + "Flame Mammoth": [ + ], + "Chill Penguin": [ + ], + "Spark Mandrill": [ + ], + "Armored Armadillo": [ + ], + "Launch Octopus": [ + "Fire Wave", + ], + "Boomer Kuwanger": [ + ], + "Thunder Slimer": [ + ], + "Vile": [ + ], + "Bospider": [ + ], + "Rangda Bangda": [ + "Charged Shotgun Ice", + ], + "D-Rex": [ + ], + "Velguarder": [ + ], + "Sigma": [ + "Charged Rolling Shield", + ], + "Wolf Sigma": [ + ], +} + +weapons = { + "Buster": [ + [None, 0x00, 0x02], + [None, 0x06, 0x04], + [None, 0x01, 0x03], + [None, 0x03, 0x04], + [None, 0x02, 0x05], + [None, 0x1D, 0x02], + ], + "Homing Torpedo": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + [[ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Chameleon Sting": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Rolling Shield": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG], + [[ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+1], + ], + "Fire Wave": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], + ], + "Storm Tornado": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + [[ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Electric Spark": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + [[ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Boomerang Cutter": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_CHARGED_DMG], + [[ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Shotgun Ice": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_CHARGED_DMG], + [[ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], +} + +weapons_chaotic = { + "Lemon": [ + [None, 0x00, 0x02], + ], + "Lemon (Dash)": [ + [["Check Dash"], 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"], 0x02, 0x05], + [["Check Charge 2"], 0x1D, 0x02], + ], + "Homing Torpedo": [ + [[ItemName.homing_torpedo], 0x07, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Homing Torpedo": [ + [["Check Charge 2", ItemName.homing_torpedo], 0x10, WEAKNESS_CHARGED_DMG], + ], + "Chameleon Sting": [ + [[ItemName.chameleon_sting], 0x08, WEAKNESS_UNCHARGED_DMG], + ], + "Rolling Shield": [ + [[ItemName.rolling_shield], 0x09, WEAKNESS_UNCHARGED_DMG+1], + ], + "Charged Rolling Shield": [ + [["Check Charge 2", ItemName.rolling_shield], 0x12, WEAKNESS_CHARGED_DMG+2], + ], + "Fire Wave": [ + [[ItemName.fire_wave], 0x0A, WEAKNESS_UNCHARGED_DMG], + [[ItemName.fire_wave], 0x13, WEAKNESS_CHARGED_DMG+3], + ], + "Storm Tornado": [ + [[ItemName.storm_tornado], 0x0B, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Storm Tornado": [ + [["Check Charge 2", ItemName.storm_tornado], 0x14, WEAKNESS_CHARGED_DMG], + ], + "Electric Spark": [ + [[ItemName.electric_spark], 0x0C, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Electric Spark": [ + [["Check Charge 2", ItemName.electric_spark], 0x15, WEAKNESS_CHARGED_DMG], + ], + "Boomerang Cutter": [ + [[ItemName.boomerang_cutter], 0x0D, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Boomerang Cutter": [ + [["Check Charge 2", ItemName.boomerang_cutter], 0x16, WEAKNESS_CHARGED_DMG], + ], + "Shotgun Ice": [ + [[ItemName.shotgun_ice], 0x0E, WEAKNESS_UNCHARGED_DMG], + ], + "Charged Shotgun Ice": [ + [["Check Charge 2", ItemName.shotgun_ice], 0x17, WEAKNESS_CHARGED_DMG+3], + ], +} + +def handle_weaknesses(world: "MMXWorld") -> None: + shuffle_type = world.options.boss_weakness_rando.value + strictness_type = world.options.boss_weakness_strictness.value + boss_weakness_plando = world.options.boss_weakness_plando.value + + if shuffle_type != "vanilla": + 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(): + 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() + world.boss_weaknesses[boss].append(weapons_chaotic["Charged Shot (Level 3)"][0]) + world.boss_weaknesses[boss].append(weapons_chaotic["Charged Shot (Level 3)"][1]) + else: + damage_table = damage_templates["Only Weakness"].copy() + + if boss in boss_weakness_plando.keys(): + if shuffle_type != "vanilla": + chosen_weapon = boss_weakness_plando[boss] + if chosen_weapon not in boss_excluded_weapons[boss]: + 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() + continue + + print (f"[{world.multiworld.player_name[world.player]}] Weakness plando failed for {boss}, contains an excluded weapon. Choosing an alternate weapon...") + + if shuffle_type != "vanilla": + 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(shuffle_type - 1): + 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() + + else: + for entry in boss_weaknesses[boss]: + world.boss_weaknesses[boss].append(entry) + damage = entry[2] + damage_table[entry[1]] = damage diff --git a/worlds/mmx/__init__.py b/worlds/mmx/__init__.py new file mode 100644 index 000000000000..7eb23b27371c --- /dev/null +++ b/worlds/mmx/__init__.py @@ -0,0 +1,402 @@ +import os +import settings +import threading +import pkgutil + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from .Items import MMXItem, item_table, junk_table, item_groups +from .Locations import setup_locations, all_locations, location_groups +from .Regions import create_regions, connect_regions +from .Names import ItemName, LocationName +from .Options import MMXOptions, mmx_option_groups +from .Client import MMXSNIClient +from .Rules import build_rules +from .Levels import location_id_to_level_id +from .Weaknesses import handle_weaknesses, weapon_id +from .Rom import patch_rom, MMXProcedurePatch, HASH_US, HASH_LEGACY + +from typing import Dict, List, Any, ClassVar, TextIO + +class MMXSettings(settings.Group): + class RomFile(settings.SNESRomPath): + """File name of the Mega Man X US ROM""" + description = "Mega Man X (USA) ROM File" + copy_to = "Mega Man X (USA).sfc" + md5s = [HASH_US, HASH_LEGACY] + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class MMXWeb(WebWorld): + theme = "ice" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to playing Mega Man X with Archipelago", + "English", + "setup_en.md", + "setup/en", + ["lx5"] + ) + + setup_es = Tutorial( + "Guía de configuración de Multiworld", + "Guía para jugar Mega Man X en Archipelago", + "Spanish", + "setup_es.md", + "setup/es", + ["lx5"] + ) + + tutorials = [setup_en, setup_es] + + option_groups = mmx_option_groups + + +class MMXWorld(World): + """ + Mega Man X for the SNES is a classic action-platformer game released in 1993. It's a spin-off of + the original Mega Man series and introduces players to a new protagonist, X, a Maverick Hunter in a + futuristic world. The game features upgraded gameplay mechanics, such as the ability to dash and + climb walls, which were new to the series at the time. + """ + game = "Mega Man X" + web = MMXWeb() + + settings: ClassVar[MMXSettings] + + options_dataclass = MMXOptions + options: MMXOptions + + required_client_version = (0, 5, 0) + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + item_name_groups = item_groups + location_name_groups = location_groups + hint_blacklist = { + LocationName.armored_armadillo_clear, + LocationName.chill_penguin_clear, + LocationName.boomer_kuwanger_clear, + LocationName.sting_chameleon_clear, + LocationName.storm_eagle_clear, + LocationName.flame_mammoth_clear, + LocationName.spark_mandrill_clear, + LocationName.launch_octopus_clear, + LocationName.intro_completed, + } + + def __init__(self, multiworld: MultiWorld, player: int) -> None: + self.rom_name_available_event = threading.Event() + super().__init__(multiworld, player) + + def create_regions(self) -> None: + location_table = setup_locations(self) + create_regions(self, location_table) + + itempool: List[MMXItem] = [] + + connect_regions(self) + + total_required_locations = 47 + if self.options.pickupsanity: + total_required_locations += 26 + + # Add levels into the pool + start_inventory = self.options.start_inventory.value.copy() + stage_list = [ + ItemName.stage_armored_armadillo, + ItemName.stage_boomer_kuwanger, + ItemName.stage_chill_penguin, + ItemName.stage_flame_mammoth, + ItemName.stage_launch_octopus, + ItemName.stage_spark_mandrill, + ItemName.stage_sting_chameleon, + ItemName.stage_storm_eagle, + ] + stage_selected = self.random.randint(0, 7) + if any(stage in self.options.start_inventory_from_pool for stage in stage_list) or \ + any(stage in start_inventory for stage in stage_list): + total_required_locations += 1 + for i in range(len(stage_list)): + if stage_list[i] not in start_inventory: + itempool += [self.create_item(stage_list[i])] + else: + for i in range(len(stage_list)): + if i == stage_selected: + self.multiworld.get_location(LocationName.intro_completed, self.player).place_locked_item(self.create_item(stage_list[i])) + else: + itempool += [self.create_item(stage_list[i])] + + if len(self.options.sigma_open.value) == 0: + itempool += [self.create_item(ItemName.stage_sigma_fortress)] + + # Add weapons into the pool + itempool += [self.create_item(ItemName.electric_spark)] + itempool += [self.create_item(ItemName.homing_torpedo)] + itempool += [self.create_item(ItemName.storm_tornado)] + itempool += [self.create_item(ItemName.shotgun_ice)] + itempool += [self.create_item(ItemName.rolling_shield)] + itempool += [self.create_item(ItemName.chameleon_sting)] + itempool += [self.create_item(ItemName.fire_wave)] + itempool += [self.create_item(ItemName.boomerang_cutter)] + + if self.options.hadouken_in_pool: + itempool += [self.create_item(ItemName.hadouken, ItemClassification.useful)] + + # Add upgrades into the pool + sigma_open = self.options.sigma_open.value + if "Armor Upgrades" in sigma_open and self.options.sigma_upgrade_count.value > 0: + itempool += [self.create_item(ItemName.body)] + else: + itempool += [self.create_item(ItemName.body, ItemClassification.useful)] + + itempool += [self.create_item(ItemName.arms)] + if self.options.jammed_buster.value: + itempool += [self.create_item(ItemName.arms)] + + itempool += [self.create_item(ItemName.helmet)] + + itempool += [self.create_item(ItemName.legs)] + if self.options.air_dash.value: + if "Armor Upgrades" in sigma_open and self.options.sigma_upgrade_count.value > 0: + itempool += [self.create_item(ItemName.legs)] + else: + itempool += [self.create_item(ItemName.legs, ItemClassification.useful)] + + # Add heart tanks into the pool + if "Heart Tanks" in sigma_open and self.options.sigma_heart_tank_count.value > 0: + i = self.options.sigma_heart_tank_count.value + itempool += [self.create_item(ItemName.heart_tank) for _ in range(i)] + if i != 8: + itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8 - i)] + else: + itempool += [self.create_item(ItemName.heart_tank, ItemClassification.useful) for _ in range(8)] + + # Add sub tanks into the pool + if "Sub Tanks" in sigma_open and self.options.sigma_sub_tank_count.value > 0: + i = self.options.sigma_sub_tank_count.value + itempool += [self.create_item(ItemName.sub_tank) for _ in range(i)] + if i != 4: + itempool += [self.create_item(ItemName.sub_tank, ItemClassification.useful) for _ in range(4 - i)] + else: + itempool += [self.create_item(ItemName.sub_tank, ItemClassification.useful) for _ in range(4)] + + # Add junk items into the pool + 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_pool = [] + for i in range(junk_count): + junk_item = self.random.choice(junk_weights) + junk_pool.append(self.create_item(junk_item)) + + itempool += junk_pool + + # Set Maverick Medals + maverick_location_names = [ + LocationName.armored_armadillo_clear, + LocationName.boomer_kuwanger_clear, + LocationName.chill_penguin_clear, + LocationName.flame_mammoth_clear, + LocationName.launch_octopus_clear, + LocationName.spark_mandrill_clear, + LocationName.sting_chameleon_clear, + LocationName.storm_eagle_clear + ] + for location_name in maverick_location_names: + self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item(ItemName.maverick_medal)) + + # Set victory item + self.multiworld.get_location(LocationName.sigma_fortress_4_sigma, self.player).place_locked_item(self.create_item(ItemName.victory)) + + # Finish + self.multiworld.itempool += itempool + + + def create_item(self, name: str, force_classification: ItemClassification = False) -> Item: + data = item_table[name] + + if force_classification: + classification = force_classification + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap + else: + classification = ItemClassification.filler + + created_item = MMXItem(name, classification, data.code, self.player) + + return created_item + + + def set_rules(self) -> None: + if hasattr(self.multiworld, "generation_is_fake"): + if hasattr(self.multiworld, "re_gen_passthrough"): + if "Mega Man X" in self.multiworld.re_gen_passthrough: + self.boss_weaknesses = self.multiworld.re_gen_passthrough["Mega Man X"]["weakness_rules"] + build_rules(self) + + + def fill_slot_data(self) -> Dict[int, Any]: + slot_data = {} + # Write options to slot_data + slot_data["energy_link"] = self.options.energy_link.value + slot_data["boss_weakness_rando"] = self.options.boss_weakness_rando.value + slot_data["boss_weakness_strictness"] = self.options.boss_weakness_strictness.value + slot_data["pickupsanity"] = self.options.pickupsanity.value + slot_data["jammed_buster"] = self.options.jammed_buster.value + slot_data["hadouken_in_pool"] = self.options.hadouken_in_pool.value + slot_data["pickupsanity"] = self.options.pickupsanity.value + slot_data["logic_boss_weakness"] = self.options.logic_boss_weakness.value + slot_data["logic_leg_sigma"] = self.options.logic_leg_sigma.value + slot_data["logic_charged_shotgun_ice"] = self.options.logic_charged_shotgun_ice.value + slot_data["sigma_all_levels"] = self.options.sigma_all_levels.value + value = 0 + sigma_open = self.options.sigma_open.value + if "Medals" in sigma_open: + value |= 0x01 + if "Weapons" in sigma_open: + value |= 0x02 + if "Armor Upgrades" in sigma_open: + value |= 0x04 + if "Heart Tanks" in sigma_open: + value |= 0x08 + if "Sub Tanks" in sigma_open: + value |= 0x10 + slot_data["sigma_open"] = value + slot_data["sigma_medal_count"] = self.options.sigma_medal_count.value + slot_data["sigma_weapon_count"] = self.options.sigma_weapon_count.value + slot_data["sigma_upgrade_count"] = self.options.sigma_upgrade_count.value + slot_data["sigma_heart_tank_count"] = self.options.sigma_heart_tank_count.value + slot_data["sigma_sub_tank_count"] = self.options.sigma_sub_tank_count.value + + # Write boss weaknesses to slot_data (and for UT) + slot_data["boss_weaknesses"] = {} + slot_data["weakness_rules"] = {} + for boss, entries in self.boss_weaknesses.items(): + slot_data["weakness_rules"][boss] = entries.copy() + slot_data["boss_weaknesses"][boss] = [] + for entry in entries: + slot_data["boss_weaknesses"][boss].append(entry[1]) + + return slot_data + + + def generate_early(self) -> None: + if ItemName.legs not in self.options.start_inventory_from_pool and self.options.early_legs: + self.multiworld.early_items[self.player][ItemName.legs] = 1 + + # Generate weaknesses + self.boss_weakness_data = {} + self.boss_weaknesses = {} + handle_weaknesses(self) + + + def interpret_slot_data(self, slot_data: dict) -> Dict[str, Any]: + local_weaknesses = dict() + for boss, entries in slot_data["weakness_rules"].items(): + local_weaknesses[boss] = entries.copy() + return {"weakness_rules": local_weaknesses} + + + def write_spoiler(self, spoiler_handle: TextIO) -> None: + if self.options.boss_weakness_rando != "vanilla": + spoiler_handle.write(f"\nMega Man X 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 extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: + if not self.options.boss_weakness_rando: + return + + boss_to_id = { + 0x00: "Armored Armadillo", + 0x01: "Chill Penguin", + 0x02: "Spark Mandrill", + 0x03: "Launch Octopus", + 0x04: "Boomer Kuwanger", + 0x05: "Sting Chameleon", + 0x06: "Storm Eagle", + 0x07: "Flame Mammoth", + 0x08: "Bospider", + 0x09: "Vile", + 0x0A: "Boomer Kuwanger", + 0x0B: "Chill Penguin", + 0x0C: "Storm Eagle", + 0x0D: "Rangda Bangda", + 0x0E: "Armored Armadillo", + 0x0F: "Sting Chameleon", + 0x10: "Spark Mandrill", + 0x11: "Launch Octopus", + 0x12: "Flame Mammoth", + 0x17: "Thunder Slimer", + 0x1E: "D-Rex", + 0x13: "Velguarder", + 0x1F: "Sigma", + } + boss_weakness_hint_data = {} + for loc_name, level_data in location_id_to_level_id.items(): + if level_data[1] == 0x000: + boss_id = level_data[2] + if boss_id not in boss_to_id.keys(): + continue + boss = boss_to_id[boss_id] + data = self.boss_weaknesses[boss] + weaknesses = "" + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + if boss == "Sigma": + data = self.boss_weaknesses["Wolf Sigma"] + weaknesses += ". Wolf Sigma: " + for i in range(len(data)): + weaknesses += f"{weapon_id[data[i][1]]}, " + weaknesses = weaknesses[:-2] + location = self.multiworld.get_location(loc_name, self.player) + boss_weakness_hint_data[location.address] = weaknesses + + hint_data[self.player] = boss_weakness_hint_data + + + def get_filler_item_name(self) -> str: + return self.random.choice(list(junk_table.keys())) + + + def generate_output(self, output_directory: str) -> None: + try: + patch = MMXProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) + patch.write_file("mmx_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx_basepatch.bsdiff4")) + patch_rom(self, patch) + + self.rom_name = patch.name + + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) + except Exception: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + + def modify_multidata(self, multidata: dict) -> None: + import base64 + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] diff --git a/worlds/mmx/data/mmx_basepatch.bsdiff4 b/worlds/mmx/data/mmx_basepatch.bsdiff4 new file mode 100644 index 000000000000..f3e9dc0045db Binary files /dev/null and b/worlds/mmx/data/mmx_basepatch.bsdiff4 differ diff --git a/worlds/mmx/docs/en_Mega Man X.md b/worlds/mmx/docs/en_Mega Man X.md new file mode 100644 index 000000000000..f39d36844751 --- /dev/null +++ b/worlds/mmx/docs/en_Mega Man X.md @@ -0,0 +1,75 @@ +# Mega Man X + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Access to each Maverick Stage, weapons obtained from Mavericks and upgrades obtained from Dr. Light's capsules +are randomized in the multiworld. The requirements for entering Sigma's Fortress can be randomized to require different +amount of items (Medals from Mavericks, Weapon count, Upgrade count, Heart Tank and Sub Tank count). + +The game will be marked as completed when Wolf Sigma is defeated. + +## What Mega Man X items can appear in other players' worlds? +- Maverick Access Codes +- Maverick Weapons +- Armor Upgrades (Helmet/Arms/Body/Legs) +- Heart Tanks +- Sub Tanks +- 1-Ups +- HP Refill + +## What is considered a location check in Mega Man X? +- Defeating a Boss Enemy +- Using a Dr. Light Capsule +- Collecting a Heart Tank or a Sub Tank item +- Optionally, collecting a Pickup Item (1-Up/HP/Weapon) present within stages + +## When the player receives an item, what happens? +A sound effect will play based on the type of item received, and the effects of the item will be immediately applied, +such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving +a HP refill while at full health), the remaining are withheld until they can be applied. + +## Quality of Life +The implementation features several enhancements to the original game's systems which attempt to make Mega Man X a +much smoother experience. +- **Checkpoint Selector:** Allows you to travel to any previously visited checkpoint in the game by selecting a +checkpoint at the stage select screen. Switch between different checkpoints with `L` or `R`. +- **Enhanced Helmet:** By getting the Helmet Upgrade item, the Checkpoint Selector will allow you to travel to any +checkpoint regardless if you have visited them or not. +- **Sigma's Fortress Selector:** You can switch which Fortress level you will travel to by pressing `SELECT` at +the stage select screen. + +## What is EnergyLink? +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man + X, when enabled, deposits a certain amount of Energy to the EnergyLink pool. Only a quarter of the collected Energy is +successfully sent to the EnergyLink pool. + +Energy from the EnergyLink pool can be transmuted into HP and Weapon Energy with the same conversion rate. +The transmutation can happen within the game itself and the client. In the client, you use `/heal ` to request +a heal by `` or use a `/refill ` to request a weapon refill. In the game, you press `SELECT` on the item +you want to request a refill of during the pause menu screen. + +Weapon refills will be applied to either the current weapon, the current selected weapon on the pause menu or will be +filled from top to bottom according to the pause menu's order if none of them are selected or being used. + +## Boss weakness plando +You can enforce a singular weakness into a boss with this option, ignoring weaknesses generated by the world in case +weaknesses are shuffled. The format is the following: +```yaml +boss_weakness_plando: + Spark Mandrill: Lemon (Dash) + Armored Armadillo: Electric Spark +``` +This will force `Spark Mandrill` to receive increased damage from the basic shot performed when dashing and will force +`Armored Armadillo` to receive increased damage from Electric Spark. + +## Unique Local Commands +- `/resync` Deletes the current saved data in the server which will force every item to be given again. Only has +effect during the title screen. +- `/heal ` Only present with EnergyLink. Request a HP refill using EnergyLink's pool. +- `/refill ` Only present with EnergyLink. Request a Weapon Energy refill using EnergyLink's pool. +- `/trade ` Exchanges HP for Weapon Energy. The conversion rate is 1:1. diff --git a/worlds/mmx/docs/setup_en.md b/worlds/mmx/docs/setup_en.md new file mode 100644 index 000000000000..e5b82c2f1397 --- /dev/null +++ b/worlds/mmx/docs/setup_en.md @@ -0,0 +1,100 @@ +# Mega Man X setup guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above. +- Software capable of loading and playing SNES ROM files: + - [snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases) + - [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) + - [BSNES-plus](https://github.com/black-sliver/bsnes-plus). **Note:** Do not reset within the emulator. It will cause + RAM corruption. +- Your Mega Man X US ROM file from the original cartridge or extracted from the Legacy Collection. Archipelago can't +provide these. + - SNES US MD5: `cfe8c11f0dce19e4fa5f3fd75775e47c` + - Legacy Collection US MD5: `ff683b75e75e9b59f0c713c7512a016b` + +## Optional Software +- [Map & Level tracker for Mega Man X Archipelago](https://github.com/BrianCumminger/megamanx-ap-poptracker/releases), +to be used with [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Emulator Lua Scripts](https://github.com/Coltaho/emulator_lua_scripts), +for [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) and [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) + +### Alternative ways of playing +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) has reports of working fine but it isn't an officially endorsed way to play the game by the developer. Proceed at your own risk. +- RetroArch doesn't have any report of working fine. Proceed at your own risk. +- sd2snes/FX Pak don't work with this game due to limitations on the cartridge's internal hardware. + +## Installation process + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information.** +2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Setup your YAML + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can generate a yaml or download a template by visiting the [Mega Man X Player Options Page](/games/Mega%20Man%20X%20/player-options) + +## Joining a MultiWorld Game + +### Get your Mega Man X patch + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apmmx` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the multiworld + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +To connect the client with the server, write `
:` in the text box located at the top and hit Enter (if the +server has a password, then write `/connect
: [password]` in the bottom text box) + +Each emulator requires following a specific procedure to be able to play. Follow whichever fits your preferences. + +#### snes9x-nwa + +1. Click on the Network Menu and check **Enable Emu Network Control** +2. Load your ROM file if it hasn't already been loaded. +3. The emulator should automatically connect while SNI is running. + +#### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/`. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +#### BSNES-Plus + +1. Load your ROM file if it hasn't already been loaded. +2. The emulator should automatically connect while SNI is running. + +## Final notes + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! You can execute various commands in your client. For more information regarding +these commands you can use `/help` for local client commands and `!help` for server commands. diff --git a/worlds/mmx/docs/setup_es.md b/worlds/mmx/docs/setup_es.md new file mode 100644 index 000000000000..34895d023038 --- /dev/null +++ b/worlds/mmx/docs/setup_es.md @@ -0,0 +1,103 @@ +# Mega Man X guía de instalación + +## Software requerido + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). Este viene proporcionado junto a la instalación de Archipelago. +- Software capaz de cargar y permitir jugar archivos ROM de SNES: + - [snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases) + - [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) + - [BSNES-plus](https://github.com/black-sliver/bsnes-plus). **Nota:** No usen el `Reset` del emulador, causa + corrupción de RAM y puede mandar Checks de manera aleatoria. +- Una copia de tu Mega Man X US proveniente del cartucho original o de la Legacy Collection. La comunidad de +Archipelago no puede proveer ni uno de estos. + - SNES US MD5: `cfe8c11f0dce19e4fa5f3fd75775e47c` + - Legacy Collection US MD5: `ff683b75e75e9b59f0c713c7512a016b` + +## Software opcional +- [Tracker de mapa y niveles para Mega Man X Archipelago](https://github.com/BrianCumminger/megamanx-ap-poptracker/releases), +para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Scripts Lua que muestran información variada en el mismo emulador](https://github.com/Coltaho/emulator_lua_scripts), +para [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) y [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) + +### Métodos de jugar no soportados oficialmente +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) tiene reportes de funcionar adecuadamente, pero no es un +método de jugar que el desarollador utiliza. Procede bajo tu propio riesgo. +- RetroArch no tiene reportes de funcionar. Procede bajo tu propio riesgo. +- sd2snes/FX Pak no funcionan en esta implementación debido a limitantes de los componentes internos de dichos cartuchos. + +## Procedimiento de instalación + +1. Descarga e instala [Archipelago](). **El instalador se +encuentra en la sección de `Assets` después de la información de la versión.** +2. Asocia los archivos `.sfc` con el emulador deseado: + 1. Extrae el emulador y sus archivos en algún lugar de tu computadora que puedas recordar. + 2. Da clic derecho en un ROM y selecciona **Abrir con...** + 3. Activa la casilla enseguida de **Siempre usar esta aplicación para abrir archivos .sfc** + 4. Mueve el menú hasta encontrar al final de la lista la opción llamada **Buscar otra aplicación en el equipo** + 5. Busca el archivo ejecutable del emulador (`.exe`) y da click en **Abrir**. El archivo puede encontrarse en donde + extrajíste el emulador en el primer paso. + +## Configura tu archivo YAML + +### ¿Qué es un archivo YAML y por qué necesito uno? + +Tu archivo YAML contiene un número de opciones que proveen al generador con información sobre como debe generar tu +juego. Cada jugador de un multiworld entregará su propio archivo YAML. Esto permite que cada jugador disfrute de una +experiencia personalizada a su manera, y que diferentes jugadores dentro del mismo multiworld pueden tener diferentes +opciones. + +### ¿Dónde puedo obtener un archivo YAML? + +Puedes generar un archivo YAML or descargar su plantilla en la [página de configuración de jugador de Mega Man X](/games/Mega%20Man%20X%20/player-options) + +## Unirse a un juego MultiWorld + +### Obtener tu parche de Mega Man X + +Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo esté organizando. +Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de +todos. Tu archivo tiene una extensión `.apmmx`. + +Haz doble clic en tu archivo `.apmmx` para que se ejecute el cliente y realice el parcheado del ROM. +Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automáticamente (si es que se +ha asociado la extensión al emulador tal como fue recomendado) + +### Conectarse al multiserver + +Cuando el cliente se ejecuta automaticamente, SNI también se debe ejecutar automaticamente en segundo plano. Si es la +primera vez que ejecutas el cliente, es posible que se te pida que permitas a la aplicación a través del Firewall de +Windows. + +Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona +enter (si el servidor tiene contraseña, en la caja de texto inferior escribe `/connect : [contraseña]`) + +Cada emulador tiene un procedimiento distinto para poder jugar, sigue el que se acomode a tus preferencias. + +#### snes9x-nwa + +1. Da click en el menú de Network y activa **Enable Emu Network Control** +2. Carga tu ROM parcheado si aún no ha sido cargado +3. El emulador debe de conectarse automáticamente mientras SNI está ejecutandose en segundo plano + +#### snes9x-rr + +1. Carga tu ROM parcheado si aún no ha sido cargado +2. Da click en el menú de File y coloca el cursor sobre **Lua Scripting** +3. Da click en **New Lua Script Window...** +4. En la ventana que aparece da click en **Browse..** +5. Selecciona el archivo conector incluido con el cliente + - Busca en la carpeta de Archipelago el directorio de `/SNI/lua/` +6. Si llega a aparecer un error al cargar el script que diga que no cuentas con `socket.dll` o algo similar, ve a la +carpeta del lua que estás utilizando y copia el archivo `socket.dll` a la carpeta raíz de tu snes9x + +#### BSNES-Plus + +1. Carga tu ROM parcheado si aún no ha sido cargado +2. El emulador debe de conectarse automáticamente mientras SNI está ejecutándose en segundo plano + +## Notas finales para jugar + +Cuando el cliente muestra que el dispositivo de SNES y el Server están conectados, estas listo para comenzar a jugar. +Dentro del mismo cliente puedes encontrar diferentes comandos que puedes ejecutar. Para más información acerca de los +comandos disponibles puedes escribir `/help` para comandos del cliente y `!help` para comandos del server.