Mega Man 2: Implement New Game (#3256)
* initial (broken) commit * small work on init * Update Items.py * beginning work, some rom patches * commit progress from bh branch * deathlink, fix soft-reset kill, e-tank loss * begin work on targeting new bhclient * write font * definitely didn't forget to add the other two hashes no * update to modern options, begin colors * fix 6th letter bug * palette shuffle + logic rewrite * fix a bunch of pointers * fix color changes, deathlink, and add wily 5 req * adjust weapon weakness generation * Update Rules.py * attempt wily 5 softlock fix * add explicit test for rbm weaknesses * fix difficulty and hard reset * fix connect deathlink and off by one item color * fix atomic fire again * de-jank deathlink * rewrite wily5 rule * fix rare solo-gen fill issue, hopefully * Update Client.py * fix wily 5 requirements * undo fill hook * fix picopico-kun rules * for real this time * update minimum damage requirement * begin move to procedure patch * finish move to APPP, allow rando boobeam, color updates * fix color bug, UT support? * what do you mean I forgot the procedure * fix UT? * plando weakness and fixes * sfx when item received, more time stopper edge cases * Update test_weakness.py * fix rules and color bug * fix color bug, support reduced flashing * major world overhaul * Update Locations.py * fix first found bugs * mypy cleanup * headerless roms * Update Rom.py * further cleanup * work on energylink * el fixes * update to energylink 2.0 packet * energylink balancing * potentially break other clients, more balancing * Update Items.py * remove startup change from basepatch we write that in patch, since we also need to clean the area before applying * el balancing and feedback * hopefully less test failures? * implement world version check * add weapon/health option * Update Rom.py * x/x2 * specials * Update Color.py * Update Options.py * finally apply location groups * bump minor version number instead * fix duplicate stage sends * validate wily 5, tests * see if renaming fixes * add shuffled weakness * remove passwords * refresh rbm select, fix wily 5 validation * forgot we can't check 0 * oops I broke the basepatch (remove failing test later) * fix solo gen fill error? * fix webhost patch recognition * fix imports, basepatch * move to flexibility metric for boss validation * special case boobeam trap * block strobe on stage select init * more energylink balancing * bump world version * wily HP inaccurate in validation * fix validation edge case * save last completed wily to data storage * mypy and pep8 cleanup * fix file browse validation * fix test failure, add enemy weakness * remove test seed * update enemy damage * inno setup * Update en_Mega Man 2.md * setup guide * Update en_Mega Man 2.md * finish plando weakness section * starting rbm edge case * remove * imports * properly wrap later weakness additions in regen playthrough * fix import * forgot readme * remove time stopper special casing since we moved to proper wily 5 validation, this special casing is no longer important * properly type added locations * Update CODEOWNERS * add animation reduction * deprioritize Time Stopper in rush checks * special case wily phase 1 * fix key error * forgot the test * music and general cleanup * the great rename * fix import * thanks pycharm * reorder palette shuffle * account for alien on shuffled weakness * apply suggestions * fix seedbleed * fix invalid buster passthrough * fix weakness landing beneath required amount * fix failsafe * finish music * fix Time Stopper on Flash/Alien * asar pls * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world helpers * init cleanup * apostrophes * clearer wording * mypy and cleanup * options doc cleanup * Update rom.py * rules cleanup * Update __init__.py * Update __init__.py * move to defaultdict * cleanup world helpers * Update __init__.py * remove unnecessary line from fill hook * forgot the other one * apply code review * remove collect * Update rules.py * forgot another --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
parent
c4e7b6ca82
commit
0e6e359747
|
@ -74,6 +74,7 @@ Currently, the following games are supported:
|
|||
* A Hat in Time
|
||||
* Old School Runescape
|
||||
* Kingdom Hearts 1
|
||||
* Mega Man 2
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
|
@ -106,6 +106,9 @@
|
|||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
|
|
|
@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
|
|||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
import hashlib
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Any, TYPE_CHECKING, Optional, Sequence, Tuple, ClassVar, List
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .names import (dr_wily, heat_man_stage, air_man_stage, wood_man_stage, bubble_man_stage, quick_man_stage,
|
||||
flash_man_stage, metal_man_stage, crash_man_stage)
|
||||
from .items import (item_table, item_names, MM2Item, filler_item_weights, robot_master_weapon_table,
|
||||
stage_access_table, item_item_table, lookup_item_to_id)
|
||||
from .locations import (MM2Location, mm2_regions, MM2Region, energy_pickups, etank_1ups, lookup_location_to_id,
|
||||
location_groups)
|
||||
from .rom import patch_rom, MM2ProcedurePatch, MM2LCHASH, PROTEUSHASH, MM2VCHASH, MM2NESHASH
|
||||
from .options import MM2Options, Consumables
|
||||
from .client import MegaMan2Client
|
||||
from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
|
||||
import os
|
||||
import threading
|
||||
import base64
|
||||
import settings
|
||||
logger = logging.getLogger("Mega Man 2")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
class MM2Settings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the MM2 EN rom"""
|
||||
description = "Mega Man 2 ROM File"
|
||||
copy_to: Optional[str] = "Mega Man 2 (USA).nes"
|
||||
md5s = [MM2NESHASH, MM2VCHASH, MM2LCHASH, PROTEUSHASH]
|
||||
|
||||
def browse(self: settings.T,
|
||||
filetypes: Optional[Sequence[Tuple[str, Sequence[str]]]] = None,
|
||||
**kwargs: Any) -> Optional[settings.T]:
|
||||
if not filetypes:
|
||||
file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
|
||||
return super().browse(file_types, **kwargs)
|
||||
else:
|
||||
return super().browse(filetypes, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Try to open and validate file against hashes"""
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
try:
|
||||
f.seek(0)
|
||||
if f.read(4) == b"NES\x1A":
|
||||
f.seek(16)
|
||||
else:
|
||||
f.seek(0)
|
||||
cls._validate_stream_hashes(f)
|
||||
base_rom_bytes = f.read()
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
# we need special behavior here
|
||||
cls.copy_to = None
|
||||
except ValueError:
|
||||
raise ValueError(f"File hash does not match for {path}")
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class MM2WebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
|
||||
Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Mega Man 2 randomizer connected to an Archipelago Multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Silvris"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class MM2World(World):
|
||||
"""
|
||||
In the year 200X, following his prior defeat by Mega Man, the evil Dr. Wily has returned to take over the world with
|
||||
his own group of Robot Masters. Mega Man once again sets out to defeat the eight Robot Masters and stop Dr. Wily.
|
||||
|
||||
"""
|
||||
|
||||
game = "Mega Man 2"
|
||||
settings: ClassVar[MM2Settings]
|
||||
options_dataclass = MM2Options
|
||||
options: MM2Options
|
||||
item_name_to_id = lookup_item_to_id
|
||||
location_name_to_id = lookup_location_to_id
|
||||
item_name_groups = item_names
|
||||
location_name_groups = location_groups
|
||||
web = MM2WebWorld()
|
||||
rom_name: bytearray
|
||||
world_version: Tuple[int, int, int] = (0, 3, 1)
|
||||
wily_5_weapons: Dict[int, List[int]]
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.rom_name = bytearray()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(world, player)
|
||||
self.weapon_damage = deepcopy(weapon_damage)
|
||||
self.wily_5_weapons = {}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu = MM2Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu)
|
||||
for region in mm2_regions:
|
||||
stage = MM2Region(region, self.player, self.multiworld)
|
||||
required_items = mm2_regions[region][0]
|
||||
locations = mm2_regions[region][1]
|
||||
prev_stage = mm2_regions[region][2]
|
||||
if prev_stage is None:
|
||||
menu.connect(stage, f"To {region}",
|
||||
lambda state, items=required_items: state.has_all(items, self.player))
|
||||
else:
|
||||
old_stage = self.get_region(prev_stage)
|
||||
old_stage.connect(stage, f"To {region}",
|
||||
lambda state, items=required_items: state.has_all(items, self.player))
|
||||
stage.add_locations(locations, MM2Location)
|
||||
for location in stage.get_locations():
|
||||
if location.address is None and location.name != dr_wily:
|
||||
location.place_locked_item(MM2Item(location.name, ItemClassification.progression,
|
||||
None, self.player))
|
||||
if region in etank_1ups and self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
stage.add_locations(etank_1ups[region], MM2Location)
|
||||
if region in energy_pickups and self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
stage.add_locations(energy_pickups[region], MM2Location)
|
||||
self.multiworld.regions.append(stage)
|
||||
|
||||
def create_item(self, name: str) -> MM2Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression:
|
||||
classification = ItemClassification.progression_skip_balancing \
|
||||
if item.skip_balancing else ItemClassification.progression
|
||||
if item.useful:
|
||||
classification |= ItemClassification.useful
|
||||
return MM2Item(name, classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()))[0]
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
# grab first robot master
|
||||
robot_master = self.item_id_to_name[0x880101 + self.options.starting_robot_master.value]
|
||||
self.multiworld.push_precollected(self.create_item(robot_master))
|
||||
itempool.extend([self.create_item(name) for name in stage_access_table.keys()
|
||||
if name != robot_master])
|
||||
itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
|
||||
itempool.extend([self.create_item(name) for name in item_item_table.keys()])
|
||||
total_checks = 24
|
||||
if self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
total_checks += 20
|
||||
if self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
total_checks += 27
|
||||
remaining = total_checks - len(itempool)
|
||||
itempool.extend([self.create_item(name)
|
||||
for name in self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()),
|
||||
k=remaining)])
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if (not self.options.yoku_jumps
|
||||
and self.options.starting_robot_master == "heat_man") or \
|
||||
(not self.options.enable_lasers
|
||||
and self.options.starting_robot_master == "quick_man"):
|
||||
robot_master_pool = [1, 2, 3, 5, 6, 7, ]
|
||||
if self.options.yoku_jumps:
|
||||
robot_master_pool.append(0)
|
||||
if self.options.enable_lasers:
|
||||
robot_master_pool.append(4)
|
||||
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
|
||||
logger.warning(
|
||||
f"Mega Man 2 ({self.player_name}): "
|
||||
f"Incompatible starting Robot Master, changing to "
|
||||
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
goal_location = self.get_location(dr_wily)
|
||||
goal_location.place_locked_item(MM2Item("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def fill_hook(self,
|
||||
progitempool: List["Item"],
|
||||
usefulitempool: List["Item"],
|
||||
filleritempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
# on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
|
||||
# since MM2 can have a 2 item sphere 1, and 3 items are required for Wily
|
||||
if self.multiworld.players > 1:
|
||||
return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
|
||||
rbm_to_item = {
|
||||
0: heat_man_stage,
|
||||
1: air_man_stage,
|
||||
2: wood_man_stage,
|
||||
3: bubble_man_stage,
|
||||
4: quick_man_stage,
|
||||
5: flash_man_stage,
|
||||
6: metal_man_stage,
|
||||
7: crash_man_stage
|
||||
}
|
||||
affected_rbm = [2, 3] # Wood and Bubble will always have this happen
|
||||
possible_rbm = [1, 5] # Air and Flash are always valid targets, due to Item 2/3 receive
|
||||
if self.options.consumables:
|
||||
possible_rbm.append(6) # Metal has 3 consumables
|
||||
possible_rbm.append(7) # Crash has 3 consumables
|
||||
if self.options.enable_lasers:
|
||||
possible_rbm.append(4) # Quick has a lot of consumables, but needs logical time stopper if not enabled
|
||||
else:
|
||||
affected_rbm.extend([6, 7]) # only two checks on non consumables
|
||||
if self.options.yoku_jumps:
|
||||
possible_rbm.append(0) # Heat has 3 locations always, but might need 2 items logically
|
||||
if self.options.starting_robot_master.value in affected_rbm:
|
||||
rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
|
||||
valid_second = [item for item in progitempool
|
||||
if item.name in rbm_names
|
||||
and item.player == self.player]
|
||||
placed_item = self.random.choice(valid_second)
|
||||
rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
|
||||
f" - Defeated")
|
||||
rbm_location = self.get_location(rbm_defeated)
|
||||
rbm_location.place_locked_item(placed_item)
|
||||
progitempool.remove(placed_item)
|
||||
fill_locations.remove(rbm_location)
|
||||
target_rbm = (placed_item.code & 0xF) - 1
|
||||
if self.options.strict_weakness or (self.options.random_weakness
|
||||
and not (self.weapon_damage[0][target_rbm] > 0)):
|
||||
# we need to find a weakness for this boss
|
||||
weaknesses = [weapon for weapon in range(1, 9)
|
||||
if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
|
||||
weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
|
||||
valid_weapons = [item for item in progitempool
|
||||
if item.name in weapons
|
||||
and item.player == self.player]
|
||||
placed_weapon = self.random.choice(valid_weapons)
|
||||
weapon_name = next(name for name, idx in lookup_location_to_id.items()
|
||||
if idx == 0x880101 + self.options.starting_robot_master.value)
|
||||
weapon_location = self.get_location(weapon_name)
|
||||
weapon_location.place_locked_item(placed_weapon)
|
||||
progitempool.remove(placed_weapon)
|
||||
fill_locations.remove(weapon_location)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
patch = MM2ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
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 fill_slot_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"death_link": self.options.death_link.value,
|
||||
"weapon_damage": self.weapon_damage,
|
||||
"wily_5_weapons": self.wily_5_weapons,
|
||||
}
|
||||
|
||||
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
|
||||
local_wily = {int(key): value for key, value in slot_data["wily_5_weapons"].items()}
|
||||
return {"weapon_damage": local_weapon, "wily_5_weapons": local_wily}
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
|
||||
# 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.player_name]
|
|
@ -0,0 +1,562 @@
|
|||
import logging
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING, Dict, Tuple, List, Optional, Any
|
||||
from NetUtils import ClientStatus, color, NetworkItem
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
|
||||
|
||||
nes_logger = logging.getLogger("NES")
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
MM2_ROBOT_MASTERS_UNLOCKED = 0x8A
|
||||
MM2_ROBOT_MASTERS_DEFEATED = 0x8B
|
||||
MM2_ITEMS_ACQUIRED = 0x8C
|
||||
MM2_LAST_WILY = 0x8D
|
||||
MM2_RECEIVED_ITEMS = 0x8E
|
||||
MM2_DEATHLINK = 0x8F
|
||||
MM2_ENERGYLINK = 0x90
|
||||
MM2_RBM_STROBE = 0x91
|
||||
MM2_WEAPONS_UNLOCKED = 0x9A
|
||||
MM2_ITEMS_UNLOCKED = 0x9B
|
||||
MM2_WEAPON_ENERGY = 0x9C
|
||||
MM2_E_TANKS = 0xA7
|
||||
MM2_LIVES = 0xA8
|
||||
MM2_DIFFICULTY = 0xCB
|
||||
MM2_HEALTH = 0x6C0
|
||||
MM2_COMPLETED_STAGES = 0x770
|
||||
MM2_CONSUMABLES = 0x780
|
||||
|
||||
MM2_SFX_QUEUE = 0x580
|
||||
MM2_SFX_STROBE = 0x66
|
||||
|
||||
MM2_CONSUMABLE_TABLE: Dict[int, Tuple[int, int]] = {
|
||||
# Item: (byte offset, bit mask)
|
||||
0x880201: (0, 8),
|
||||
0x880202: (16, 1),
|
||||
0x880203: (16, 2),
|
||||
0x880204: (16, 4),
|
||||
0x880205: (16, 8),
|
||||
0x880206: (16, 16),
|
||||
0x880207: (16, 32),
|
||||
0x880208: (16, 64),
|
||||
0x880209: (16, 128),
|
||||
0x88020A: (20, 1),
|
||||
0x88020B: (20, 4),
|
||||
0x88020C: (20, 64),
|
||||
0x88020D: (21, 1),
|
||||
0x88020E: (21, 2),
|
||||
0x88020F: (21, 4),
|
||||
0x880210: (24, 1),
|
||||
0x880211: (24, 2),
|
||||
0x880212: (24, 4),
|
||||
0x880213: (28, 1),
|
||||
0x880214: (28, 2),
|
||||
0x880215: (28, 4),
|
||||
0x880216: (33, 4),
|
||||
0x880217: (33, 8),
|
||||
0x880218: (37, 8),
|
||||
0x880219: (37, 16),
|
||||
0x88021A: (38, 1),
|
||||
0x88021B: (38, 2),
|
||||
0x880227: (38, 4),
|
||||
0x880228: (38, 32),
|
||||
0x880229: (38, 128),
|
||||
0x88022A: (39, 4),
|
||||
0x88022B: (39, 2),
|
||||
0x88022C: (39, 1),
|
||||
0x88022D: (38, 64),
|
||||
0x88022E: (38, 16),
|
||||
0x88022F: (38, 8),
|
||||
0x88021C: (39, 32),
|
||||
0x88021D: (39, 64),
|
||||
0x88021E: (39, 128),
|
||||
0x88021F: (41, 16),
|
||||
0x880220: (42, 2),
|
||||
0x880221: (42, 4),
|
||||
0x880222: (42, 8),
|
||||
0x880223: (46, 1),
|
||||
0x880224: (46, 2),
|
||||
0x880225: (46, 4),
|
||||
0x880226: (46, 8),
|
||||
}
|
||||
|
||||
|
||||
class MM2EnergyLinkType(IntEnum):
|
||||
Life = 0
|
||||
AtomicFire = 1
|
||||
AirShooter = 2
|
||||
LeafShield = 3
|
||||
BubbleLead = 4
|
||||
QuickBoomerang = 5
|
||||
TimeStopper = 6
|
||||
MetalBlade = 7
|
||||
CrashBomber = 8
|
||||
Item1 = 9
|
||||
Item2 = 10
|
||||
Item3 = 11
|
||||
OneUP = 12
|
||||
|
||||
|
||||
request_to_name: Dict[str, str] = {
|
||||
"HP": "health",
|
||||
"AF": "Atomic Fire energy",
|
||||
"AS": "Air Shooter energy",
|
||||
"LS": "Leaf Shield energy",
|
||||
"BL": "Bubble Lead energy",
|
||||
"QB": "Quick Boomerang energy",
|
||||
"TS": "Time Stopper energy",
|
||||
"MB": "Metal Blade energy",
|
||||
"CB": "Crash Bomber energy",
|
||||
"I1": "Item 1 energy",
|
||||
"I2": "Item 2 energy",
|
||||
"I3": "Item 3 energy",
|
||||
"1U": "lives"
|
||||
}
|
||||
|
||||
HP_EXCHANGE_RATE = 500000000
|
||||
WEAPON_EXCHANGE_RATE = 250000000
|
||||
ONEUP_EXCHANGE_RATE = 14000000000
|
||||
|
||||
|
||||
def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
|
||||
"""Check the current pool of EnergyLink, and requestable refills from it."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
|
||||
health_points = energylink // HP_EXCHANGE_RATE
|
||||
weapon_points = energylink // WEAPON_EXCHANGE_RATE
|
||||
lives = energylink // ONEUP_EXCHANGE_RATE
|
||||
logger.info(f"Healing available: {health_points}\n"
|
||||
f"Weapon refill available: {weapon_points}\n"
|
||||
f"Lives available: {lives}")
|
||||
|
||||
|
||||
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
"""Request a refill from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
valid_targets: Dict[str, MM2EnergyLinkType] = {
|
||||
"HP": MM2EnergyLinkType.Life,
|
||||
"AF": MM2EnergyLinkType.AtomicFire,
|
||||
"AS": MM2EnergyLinkType.AirShooter,
|
||||
"LS": MM2EnergyLinkType.LeafShield,
|
||||
"BL": MM2EnergyLinkType.BubbleLead,
|
||||
"QB": MM2EnergyLinkType.QuickBoomerang,
|
||||
"TS": MM2EnergyLinkType.TimeStopper,
|
||||
"MB": MM2EnergyLinkType.MetalBlade,
|
||||
"CB": MM2EnergyLinkType.CrashBomber,
|
||||
"I1": MM2EnergyLinkType.Item1,
|
||||
"I2": MM2EnergyLinkType.Item2,
|
||||
"I3": MM2EnergyLinkType.Item3,
|
||||
"1U": MM2EnergyLinkType.OneUP
|
||||
}
|
||||
if target.upper() not in valid_targets:
|
||||
logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
|
||||
return
|
||||
ctx = self.ctx
|
||||
assert isinstance(ctx, BizHawkClientContext)
|
||||
client = ctx.client_handler
|
||||
assert isinstance(client, MegaMan2Client)
|
||||
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
|
||||
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
|
||||
|
||||
|
||||
def cmd_autoheal(self) -> None:
|
||||
"""Enable auto heal from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
else:
|
||||
assert isinstance(self.ctx.client_handler, MegaMan2Client)
|
||||
if self.ctx.client_handler.auto_heal:
|
||||
self.ctx.client_handler.auto_heal = False
|
||||
logger.info(f"Auto healing disabled.")
|
||||
else:
|
||||
self.ctx.client_handler.auto_heal = True
|
||||
logger.info(f"Auto healing enabled.")
|
||||
|
||||
|
||||
def get_sfx_writes(sfx: int) -> Tuple[Tuple[int, bytes, str], ...]:
|
||||
return (MM2_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"), (MM2_SFX_STROBE, 0x01.to_bytes(1, "little"), "RAM")
|
||||
|
||||
|
||||
class MegaMan2Client(BizHawkClient):
|
||||
game = "Mega Man 2"
|
||||
system = "NES"
|
||||
patch_suffix = ".apmm2"
|
||||
item_queue: List[NetworkItem] = []
|
||||
pending_death_link: bool = False
|
||||
# default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
|
||||
sending_death_link: bool = True
|
||||
death_link: bool = False
|
||||
energy_link: bool = False
|
||||
rom: Optional[bytes] = None
|
||||
weapon_energy: int = 0
|
||||
health_energy: int = 0
|
||||
auto_heal: bool = False
|
||||
refill_queue: List[Tuple[MM2EnergyLinkType, int]] = []
|
||||
last_wily: Optional[int] = None # default to wily 1
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from worlds._bizhawk import RequestFailedError, read
|
||||
from . import MM2World
|
||||
|
||||
try:
|
||||
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"),
|
||||
(0x3FFC8, 3, "PRG ROM")]))
|
||||
if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version):
|
||||
if game_name[:3] == b"MM2":
|
||||
# I think this is an easier check than the other?
|
||||
older_version = "0.2.1" if version == b"\xFF\xFF\xFF" else f"{version[0]}.{version[1]}.{version[2]}"
|
||||
logger.warning(f"This Mega Man 2 patch was generated for an different version of the apworld. "
|
||||
f"Please use that version to connect instead.\n"
|
||||
f"Patch version: ({older_version})\n"
|
||||
f"Client version: ({'.'.join([str(i) for i in MM2World.world_version])})")
|
||||
if "pool" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("pool")
|
||||
if "request" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("request")
|
||||
if "autoheal" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("autoheal")
|
||||
return False
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except RequestFailedError:
|
||||
return False # Should verify on the next pass
|
||||
|
||||
ctx.game = self.game
|
||||
self.rom = game_name
|
||||
ctx.items_handling = 0b111
|
||||
ctx.want_slot_data = False
|
||||
deathlink = (await read(ctx.bizhawk_ctx, [(0x3FFC5, 1, "PRG ROM")]))[0][0]
|
||||
if deathlink & 0x01:
|
||||
self.death_link = True
|
||||
if deathlink & 0x02:
|
||||
self.energy_link = True
|
||||
|
||||
if self.energy_link:
|
||||
if "pool" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["pool"] = cmd_pool
|
||||
if "request" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["request"] = cmd_request
|
||||
if "autoheal" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["autoheal"] = cmd_autoheal
|
||||
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
if self.rom:
|
||||
ctx.auth = b64encode(self.rom).decode()
|
||||
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: Dict[str, Any]) -> None:
|
||||
if cmd == "Bounced":
|
||||
if "tags" in args:
|
||||
assert ctx.slot is not None
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
self.on_deathlink(ctx)
|
||||
elif cmd == "Retrieved":
|
||||
if f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.last_wily = args["keys"][f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]
|
||||
elif cmd == "Connected":
|
||||
if self.energy_link:
|
||||
ctx.set_notify(f"EnergyLink{ctx.team}")
|
||||
if ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
self.sending_death_link = True
|
||||
ctx.last_death_link = time.time()
|
||||
await ctx.send_death("Mega Man was defeated.")
|
||||
|
||||
def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
ctx.last_death_link = time.time()
|
||||
self.pending_death_link = True
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
from worlds._bizhawk import read, write
|
||||
|
||||
if ctx.server is None:
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
|
||||
# get our relevant bytes
|
||||
robot_masters_unlocked, robot_masters_defeated, items_acquired, \
|
||||
weapons_unlocked, items_unlocked, items_received, \
|
||||
completed_stages, consumable_checks, \
|
||||
e_tanks, lives, weapon_energy, health, difficulty, death_link_status, \
|
||||
energy_link_packet, last_wily = await read(ctx.bizhawk_ctx, [
|
||||
(MM2_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
|
||||
(MM2_ITEMS_ACQUIRED, 1, "RAM"),
|
||||
(MM2_WEAPONS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_ITEMS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_RECEIVED_ITEMS, 1, "RAM"),
|
||||
(MM2_COMPLETED_STAGES, 0xE, "RAM"),
|
||||
(MM2_CONSUMABLES, 52, "RAM"),
|
||||
(MM2_E_TANKS, 1, "RAM"),
|
||||
(MM2_LIVES, 1, "RAM"),
|
||||
(MM2_WEAPON_ENERGY, 11, "RAM"),
|
||||
(MM2_HEALTH, 1, "RAM"),
|
||||
(MM2_DIFFICULTY, 1, "RAM"),
|
||||
(MM2_DEATHLINK, 1, "RAM"),
|
||||
(MM2_ENERGYLINK, 1, "RAM"),
|
||||
(MM2_LAST_WILY, 1, "RAM"),
|
||||
])
|
||||
|
||||
if difficulty[0] not in (0, 1):
|
||||
return # Game is not initialized
|
||||
|
||||
if not ctx.finished_game and completed_stages[0xD] != 0:
|
||||
# this sets on credits fade, no real better way to do this
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
writes = []
|
||||
|
||||
# deathlink
|
||||
if self.death_link:
|
||||
await ctx.update_death_link(self.death_link)
|
||||
if self.pending_death_link:
|
||||
writes.append((MM2_DEATHLINK, bytes([0x01]), "RAM"))
|
||||
self.pending_death_link = False
|
||||
self.sending_death_link = True
|
||||
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
|
||||
if health[0] == 0x00 and not self.sending_death_link:
|
||||
await self.send_deathlink(ctx)
|
||||
elif health[0] != 0x00 and not death_link_status[0]:
|
||||
self.sending_death_link = False
|
||||
|
||||
if self.last_wily != last_wily[0]:
|
||||
if self.last_wily is None:
|
||||
# revalidate last wily from data storage
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 8}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
|
||||
elif last_wily[0] == 0:
|
||||
writes.append((MM2_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
|
||||
else:
|
||||
# correct our setting
|
||||
self.last_wily = last_wily[0]
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.last_wily}
|
||||
]}])
|
||||
|
||||
# handle receiving items
|
||||
recv_amount = items_received[0]
|
||||
if recv_amount < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_amount]
|
||||
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_amount, len(ctx.items_received)))
|
||||
|
||||
if item.item & 0x130 == 0:
|
||||
# Robot Master Weapon
|
||||
new_weapons = weapons_unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_WEAPONS_UNLOCKED, new_weapons.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x21))
|
||||
elif item.item & 0x30 == 0:
|
||||
# Robot Master Stage Access
|
||||
new_stages = robot_masters_unlocked[0] & ~(1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_ROBOT_MASTERS_UNLOCKED, new_stages.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x3a))
|
||||
writes.append((MM2_RBM_STROBE, b"\x01", "RAM"))
|
||||
elif item.item & 0x20 == 0:
|
||||
# Items
|
||||
new_items = items_unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_ITEMS_UNLOCKED, new_items.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x21))
|
||||
else:
|
||||
# append to the queue, so we handle it later
|
||||
self.item_queue.append(item)
|
||||
recv_amount += 1
|
||||
writes.append((MM2_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if energy_link_packet[0]:
|
||||
pickup = energy_link_packet[0]
|
||||
if pickup in (0x76, 0x77):
|
||||
# Health pickups
|
||||
if pickup == 0x77:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif pickup in (0x78, 0x79):
|
||||
# Weapon Energy
|
||||
if pickup == 0x79:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
elif pickup == 0x7B:
|
||||
# 1-Up
|
||||
value = 1
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
# if we managed to pickup something else, we should just fall through
|
||||
value = 0
|
||||
exchange_rate = 0
|
||||
contribution = (value * exchange_rate) >> 1
|
||||
if contribution:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": contribution},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
|
||||
writes.append((MM2_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if self.weapon_energy:
|
||||
# Weapon Energy
|
||||
# We parse the whole thing to spread it as thin as possible
|
||||
current_energy = self.weapon_energy
|
||||
weapon_energy = bytearray(weapon_energy)
|
||||
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
|
||||
if weapon < 0x1C:
|
||||
missing = 0x1C - weapon
|
||||
if missing > self.weapon_energy:
|
||||
missing = self.weapon_energy
|
||||
self.weapon_energy -= missing
|
||||
weapon_energy[i] = weapon + missing
|
||||
if not self.weapon_energy:
|
||||
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
break
|
||||
else:
|
||||
if current_energy != self.weapon_energy:
|
||||
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
|
||||
if self.health_energy or self.auto_heal:
|
||||
# Health Energy
|
||||
# We save this if the player has not taken any damage
|
||||
current_health = health[0]
|
||||
if 0 < current_health < 0x1C:
|
||||
health_diff = 0x1C - current_health
|
||||
if self.health_energy:
|
||||
if health_diff > self.health_energy:
|
||||
health_diff = self.health_energy
|
||||
self.health_energy -= health_diff
|
||||
else:
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
if health_diff * HP_EXCHANGE_RATE > pool:
|
||||
health_diff = int(pool // HP_EXCHANGE_RATE)
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
current_health += health_diff
|
||||
writes.append((MM2_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if self.refill_queue:
|
||||
refill_type, refill_amount = self.refill_queue.pop()
|
||||
if refill_type == MM2EnergyLinkType.Life:
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif refill_type == MM2EnergyLinkType.OneUP:
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
request = exchange_rate * refill_amount
|
||||
if request > pool:
|
||||
logger.warning(
|
||||
f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
|
||||
else:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -request},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
if refill_type == MM2EnergyLinkType.Life:
|
||||
refill_ptr = MM2_HEALTH
|
||||
elif refill_type == MM2EnergyLinkType.OneUP:
|
||||
refill_ptr = MM2_LIVES
|
||||
else:
|
||||
refill_ptr = MM2_WEAPON_ENERGY - 1 + refill_type
|
||||
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
|
||||
new_value = min(0x1C if refill_type != MM2EnergyLinkType.OneUP else 99, current_value + refill_amount)
|
||||
writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if len(self.item_queue):
|
||||
item = self.item_queue.pop(0)
|
||||
idx = item.item & 0xF
|
||||
if idx == 0:
|
||||
# 1-Up
|
||||
current_lives = lives[0]
|
||||
if current_lives > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_lives += 1
|
||||
writes.append((MM2_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x42))
|
||||
elif idx == 1:
|
||||
self.weapon_energy += 0xE
|
||||
writes.extend(get_sfx_writes(0x28))
|
||||
elif idx == 2:
|
||||
self.health_energy += 0xE
|
||||
writes.extend(get_sfx_writes(0x28))
|
||||
elif idx == 3:
|
||||
# E-Tank
|
||||
# visuals only allow 4, but we're gonna go up to 9 anyway? May change
|
||||
current_tanks = e_tanks[0]
|
||||
if current_tanks < 9:
|
||||
current_tanks += 1
|
||||
writes.append((MM2_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x42))
|
||||
else:
|
||||
self.item_queue.append(item)
|
||||
|
||||
await write(ctx.bizhawk_ctx, writes)
|
||||
|
||||
new_checks = []
|
||||
# check for locations
|
||||
for i in range(8):
|
||||
flag = 1 << i
|
||||
if robot_masters_defeated[0] & flag:
|
||||
wep_id = 0x880101 + i
|
||||
if wep_id not in ctx.checked_locations:
|
||||
new_checks.append(wep_id)
|
||||
|
||||
for i in range(3):
|
||||
flag = 1 << i
|
||||
if items_acquired[0] & flag:
|
||||
itm_id = 0x880111 + i
|
||||
if itm_id not in ctx.checked_locations:
|
||||
new_checks.append(itm_id)
|
||||
|
||||
for i in range(0xD):
|
||||
rbm_id = 0x880001 + i
|
||||
if completed_stages[i] != 0:
|
||||
if rbm_id not in ctx.checked_locations:
|
||||
new_checks.append(rbm_id)
|
||||
|
||||
for consumable in MM2_CONSUMABLE_TABLE:
|
||||
if consumable not in ctx.checked_locations:
|
||||
is_checked = consumable_checks[MM2_CONSUMABLE_TABLE[consumable][0]] \
|
||||
& MM2_CONSUMABLE_TABLE[consumable][1]
|
||||
if is_checked:
|
||||
new_checks.append(consumable)
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||
nes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
|
@ -0,0 +1,276 @@
|
|||
from typing import Dict, Tuple, List, TYPE_CHECKING, Union
|
||||
from . import names
|
||||
from zlib import crc32
|
||||
import struct
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
from .rom import MM2ProcedurePatch
|
||||
|
||||
HTML_TO_NES: Dict[str, int] = {
|
||||
"SNOW": 0x20,
|
||||
"LINEN": 0x36,
|
||||
"SEASHELL": 0x36,
|
||||
"AZURE": 0x3C,
|
||||
"LAVENDER": 0x33,
|
||||
"WHITE": 0x30,
|
||||
"BLACK": 0x0F,
|
||||
"GREY": 0x00,
|
||||
"GRAY": 0x00,
|
||||
"ROYALBLUE": 0x12,
|
||||
"BLUE": 0x11,
|
||||
"SKYBLUE": 0x21,
|
||||
"LIGHTBLUE": 0x31,
|
||||
"TURQUOISE": 0x2B,
|
||||
"CYAN": 0x2C,
|
||||
"AQUAMARINE": 0x3B,
|
||||
"DARKGREEN": 0x0A,
|
||||
"GREEN": 0x1A,
|
||||
"YELLOW": 0x28,
|
||||
"GOLD": 0x28,
|
||||
"WHEAT": 0x37,
|
||||
"TAN": 0x37,
|
||||
"CHOCOLATE": 0x07,
|
||||
"BROWN": 0x07,
|
||||
"SALMON": 0x26,
|
||||
"ORANGE": 0x27,
|
||||
"CORAL": 0x36,
|
||||
"TOMATO": 0x16,
|
||||
"RED": 0x16,
|
||||
"PINK": 0x25,
|
||||
"MAROON": 0x06,
|
||||
"MAGENTA": 0x24,
|
||||
"FUSCHIA": 0x24,
|
||||
"VIOLET": 0x24,
|
||||
"PLUM": 0x33,
|
||||
"PURPLE": 0x14,
|
||||
"THISTLE": 0x34,
|
||||
"DARKBLUE": 0x01,
|
||||
"SILVER": 0x10,
|
||||
"NAVY": 0x02,
|
||||
"TEAL": 0x1C,
|
||||
"OLIVE": 0x18,
|
||||
"LIME": 0x2A,
|
||||
"AQUA": 0x2C,
|
||||
# can add more as needed
|
||||
}
|
||||
|
||||
MM2_COLORS: Dict[str, Tuple[int, int]] = {
|
||||
names.atomic_fire: (0x28, 0x15),
|
||||
names.air_shooter: (0x20, 0x11),
|
||||
names.leaf_shield: (0x20, 0x19),
|
||||
names.bubble_lead: (0x20, 0x00),
|
||||
names.time_stopper: (0x34, 0x25),
|
||||
names.quick_boomerang: (0x34, 0x14),
|
||||
names.metal_blade: (0x37, 0x18),
|
||||
names.crash_bomber: (0x20, 0x26),
|
||||
names.item_1: (0x20, 0x16),
|
||||
names.item_2: (0x20, 0x16),
|
||||
names.item_3: (0x20, 0x16),
|
||||
names.heat_man_stage: (0x28, 0x15),
|
||||
names.air_man_stage: (0x28, 0x11),
|
||||
names.wood_man_stage: (0x36, 0x17),
|
||||
names.bubble_man_stage: (0x30, 0x19),
|
||||
names.quick_man_stage: (0x28, 0x15),
|
||||
names.flash_man_stage: (0x30, 0x12),
|
||||
names.metal_man_stage: (0x28, 0x15),
|
||||
names.crash_man_stage: (0x30, 0x16)
|
||||
}
|
||||
|
||||
MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = {
|
||||
**MM2_COLORS,
|
||||
# Street Fighter, technically
|
||||
"Hadouken": (0x3C, 0x11),
|
||||
"Shoryuken": (0x38, 0x16),
|
||||
# X Series
|
||||
"Z-Saber": (0x20, 0x16),
|
||||
# X1
|
||||
"Homing Torpedo": (0x3D, 0x37),
|
||||
"Chameleon Sting": (0x3B, 0x1A),
|
||||
"Rolling Shield": (0x3A, 0x25),
|
||||
"Fire Wave": (0x37, 0x26),
|
||||
"Storm Tornado": (0x34, 0x14),
|
||||
"Electric Spark": (0x3D, 0x28),
|
||||
"Boomerang Cutter": (0x3B, 0x2D),
|
||||
"Shotgun Ice": (0x28, 0x2C),
|
||||
# X2
|
||||
"Crystal Hunter": (0x33, 0x21),
|
||||
"Bubble Splash": (0x35, 0x28),
|
||||
"Spin Wheel": (0x34, 0x1B),
|
||||
"Silk Shot": (0x3B, 0x27),
|
||||
"Sonic Slicer": (0x27, 0x01),
|
||||
"Strike Chain": (0x30, 0x23),
|
||||
"Magnet Mine": (0x28, 0x2D),
|
||||
"Speed Burner": (0x31, 0x16),
|
||||
# X3
|
||||
"Acid Burst": (0x28, 0x2A),
|
||||
"Tornado Fang": (0x28, 0x2C),
|
||||
"Triad Thunder": (0x2B, 0x23),
|
||||
"Spinning Blade": (0x20, 0x16),
|
||||
"Ray Splasher": (0x28, 0x17),
|
||||
"Gravity Well": (0x38, 0x14),
|
||||
"Parasitic Bomb": (0x31, 0x28),
|
||||
"Frost Shield": (0x23, 0x2C),
|
||||
}
|
||||
|
||||
palette_pointers: Dict[str, List[int]] = {
|
||||
"Mega Buster": [0x3D314],
|
||||
"Atomic Fire": [0x3D318],
|
||||
"Air Shooter": [0x3D31C],
|
||||
"Leaf Shield": [0x3D320],
|
||||
"Bubble Lead": [0x3D324],
|
||||
"Quick Boomerang": [0x3D328],
|
||||
"Time Stopper": [0x3D32C],
|
||||
"Metal Blade": [0x3D330],
|
||||
"Crash Bomber": [0x3D334],
|
||||
"Item 1": [0x3D338],
|
||||
"Item 2": [0x3D33C],
|
||||
"Item 3": [0x3D340],
|
||||
"Heat Man": [0x34B6, 0x344F7],
|
||||
"Air Man": [0x74B6, 0x344FF],
|
||||
"Wood Man": [0xB4EC, 0x34507],
|
||||
"Bubble Man": [0xF4B6, 0x3450F],
|
||||
"Quick Man": [0x134C8, 0x34517],
|
||||
"Flash Man": [0x174B6, 0x3451F],
|
||||
"Metal Man": [0x1B4A4, 0x34527],
|
||||
"Crash Man": [0x1F4EC, 0x3452F],
|
||||
}
|
||||
|
||||
|
||||
def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None:
|
||||
"""
|
||||
Add a color combo for Mega Man 2 to recognize as the color to display for a given item.
|
||||
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
|
||||
"""
|
||||
MM2_KNOWN_COLORS[name] = validate_colors(*color)
|
||||
|
||||
|
||||
def extrapolate_color(color: int) -> Tuple[int, int]:
|
||||
if color > 0x1F:
|
||||
color_1 = color
|
||||
color_2 = color_1 - 0x10
|
||||
else:
|
||||
color_2 = color
|
||||
color_1 = color_2 + 0x10
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> Tuple[int, int]:
|
||||
# Black should be reserved for outlines, a gray should suffice
|
||||
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_1 = 0x10
|
||||
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_2 = 0x10
|
||||
|
||||
# one final check, make sure we don't have two matching
|
||||
if not allow_match and color_1 == color_2:
|
||||
color_1 = 0x30 # color 1 to white works with about any paired color
|
||||
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def get_colors_for_item(name: str) -> Tuple[int, int]:
|
||||
if name in MM2_KNOWN_COLORS:
|
||||
return MM2_KNOWN_COLORS[name]
|
||||
|
||||
check_colors = {color: color in name.upper().replace(" ", "") for color in HTML_TO_NES}
|
||||
colors = [color for color in check_colors if check_colors[color]]
|
||||
if colors:
|
||||
# we have at least one color pattern matched
|
||||
if len(colors) > 1:
|
||||
# we have at least 2
|
||||
color_1 = HTML_TO_NES[colors[0]]
|
||||
color_2 = HTML_TO_NES[colors[1]]
|
||||
else:
|
||||
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
|
||||
else:
|
||||
# generate hash
|
||||
crc_hash = crc32(name.encode("utf-8"))
|
||||
hash_color = struct.pack("I", crc_hash)
|
||||
color_1 = hash_color[0] % 0x3F
|
||||
color_2 = hash_color[1] % 0x3F
|
||||
|
||||
if color_1 < color_2:
|
||||
temp = color_1
|
||||
color_1 = color_2
|
||||
color_2 = temp
|
||||
|
||||
color_1, color_2 = validate_colors(color_1, color_2)
|
||||
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def parse_color(colors: List[str]) -> Tuple[int, int]:
|
||||
color_a = colors[0]
|
||||
if color_a.startswith("$"):
|
||||
color_1 = int(color_a[1:], 16)
|
||||
else:
|
||||
# assume it's in our list of colors
|
||||
color_1 = HTML_TO_NES[color_a.upper()]
|
||||
|
||||
if len(colors) == 1:
|
||||
color_1, color_2 = extrapolate_color(color_1)
|
||||
else:
|
||||
color_b = colors[1]
|
||||
if color_b.startswith("$"):
|
||||
color_2 = int(color_b[1:], 16)
|
||||
else:
|
||||
color_2 = HTML_TO_NES[color_b.upper()]
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def write_palette_shuffle(world: "MM2World", rom: "MM2ProcedurePatch") -> None:
|
||||
palette_shuffle: Union[int, str] = world.options.palette_shuffle.value
|
||||
palettes_to_write: Dict[str, Tuple[int, int]] = {}
|
||||
if isinstance(palette_shuffle, str):
|
||||
color_sets = palette_shuffle.split(";")
|
||||
if len(color_sets) == 1:
|
||||
palette_shuffle = world.options.palette_shuffle.option_none
|
||||
# singularity is more correct, but this is faster
|
||||
else:
|
||||
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
|
||||
for color_set in color_sets:
|
||||
if "-" in color_set:
|
||||
character, color = color_set.split("-")
|
||||
if character.title() not in palette_pointers:
|
||||
logging.warning(f"Player {world.multiworld.get_player_name(world.player)} "
|
||||
f"attempted to set color for unrecognized option {character}")
|
||||
colors = color.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
palettes_to_write[character.title()] = real_colors
|
||||
else:
|
||||
# If color is provided with no character, assume singularity
|
||||
colors = color_set.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
for character in palette_pointers:
|
||||
palettes_to_write[character] = real_colors
|
||||
# Now we handle the real values
|
||||
if palette_shuffle == 1:
|
||||
shuffled_colors = list(MM2_COLORS.values())
|
||||
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
|
||||
world.random.shuffle(shuffled_colors)
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = shuffled_colors.pop()
|
||||
elif palette_shuffle > 1:
|
||||
if palette_shuffle == 2:
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
palettes_to_write[character] = real_colors
|
||||
else:
|
||||
# singularity
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = real_colors
|
||||
|
||||
for character in palettes_to_write:
|
||||
for pointer in palette_pointers[character]:
|
||||
rom.write_bytes(pointer, bytes(palettes_to_write[character]))
|
||||
|
||||
if character == "Atomic Fire":
|
||||
# special case, we need to update Atomic Fire's flashing routine
|
||||
rom.write_byte(0x3DE4A, palettes_to_write[character][1])
|
||||
rom.write_byte(0x3DE4C, palettes_to_write[character][1])
|
Binary file not shown.
|
@ -0,0 +1,114 @@
|
|||
# Mega Man 2
|
||||
|
||||
## 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?
|
||||
|
||||
Weapons received from Robot Masters, access to each individual stage, and Items from Dr. Light are randomized
|
||||
into the multiworld. Access to the Wily Stages is locked behind receiving Item 1, 2, and 3. The game is completed when
|
||||
viewing the ending sequence after defeating the Alien.
|
||||
|
||||
## What Mega Man 2 items can appear in other players' worlds?
|
||||
- Robot Master weapons
|
||||
- Robot Master Access Codes (stage access)
|
||||
- Items 1/2/3
|
||||
- 1-Ups
|
||||
- E-Tanks
|
||||
- Health Energy (L)
|
||||
- Weapon Energy (L)
|
||||
|
||||
## What is considered a location check in Mega Man 2?
|
||||
- The defeat of a Robot Master or Wily Boss
|
||||
- Receiving a weapon or item from Dr. Light
|
||||
- Optionally, 1-Ups and E-Tanks present within stages
|
||||
- Optionally, Weapon and Health Energy pickups 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
|
||||
Health Energy while at full health), the leftover amount is withheld until it can be applied.
|
||||
|
||||
## What is EnergyLink?
|
||||
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
|
||||
2, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
|
||||
Half of the energy that would be gained is lost upon transfer to the EnergyLink.
|
||||
|
||||
Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
|
||||
You can find out how much of each type you can pull using the `/pool` command in the client. Additionally, you can have it
|
||||
automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
|
||||
Finally, you can use the `/request` command to request a certain type of energy from the storage.
|
||||
|
||||
## Plando Palettes
|
||||
The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
|
||||
so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
|
||||
the following:
|
||||
- Mega Buster
|
||||
- Atomic Fire
|
||||
- Air Shooter
|
||||
- Leaf Shield
|
||||
- Bubble Lead
|
||||
- Quick Boomerang
|
||||
- Time Stopper
|
||||
- Metal Blade
|
||||
- Crash Bomber
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Heat Man
|
||||
- Air Man
|
||||
- Wood Man
|
||||
- Bubble Man
|
||||
- Quick Man
|
||||
- Flash Man
|
||||
- Metal Man
|
||||
- Crash Man
|
||||
|
||||
Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
|
||||
found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/color.py#L11). Alternatively, colors can
|
||||
be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
|
||||
|
||||
You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
|
||||
given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
|
||||
all weapons/bosses that did not have a prior color specified.
|
||||
|
||||
The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
|
||||
plando placements.
|
||||
|
||||
## Plando Weaknesses
|
||||
Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
|
||||
weaknesses generated by strict/random weakness options. Formatting for this is as follows:
|
||||
```yaml
|
||||
plando_weakness:
|
||||
Air Man:
|
||||
Atomic Fire: 0
|
||||
Bubble Lead: 4
|
||||
```
|
||||
This would cause Air Man to take 4 damage from Bubble Lead, and 0 from Atomic Fire.
|
||||
|
||||
Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
|
||||
becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
|
||||
Robot Master.
|
||||
|
||||
|
||||
## Unique Local Commands
|
||||
- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
|
||||
- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
|
||||
restore Mega Man's health.
|
||||
- `/request <amount> <type>` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
|
||||
the EnergyLink. Types are as follows:
|
||||
- `HP` Health
|
||||
- `AF` Atomic Fire
|
||||
- `AS` Air Shooter
|
||||
- `LS` Leaf Shield
|
||||
- `BL` Bubble Lead
|
||||
- `QB` Quick Boomerang
|
||||
- `TS` Time Stopper
|
||||
- `MB` Metal Blade
|
||||
- `CB` Crash Bomber
|
||||
- `I1` Item 1
|
||||
- `I2` Item 2
|
||||
- `I3` Item 3
|
||||
- `1U` Lives
|
|
@ -0,0 +1,53 @@
|
|||
# Mega Man 2 Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- An English Mega Man 2 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
|
||||
|
||||
### Configuring Bizhawk
|
||||
|
||||
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||
|
||||
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||
tabbed out of EmuHawk.
|
||||
- Open a `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||
`Controllers…`, load any `.nes` ROM first.
|
||||
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||
clear it.
|
||||
|
||||
## Generating and Patching a Game
|
||||
|
||||
1. Create your options file (YAML). You can make one on the
|
||||
[Mega Man 2 options page](../../../games/Mega%20Man%202/player-options).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have the `.apmm2` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
4. Select "Open Patch" on the left side and select your patch file.
|
||||
5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy
|
||||
Collection, provide `Proteus.exe` in place of your rom.
|
||||
6. A patched `.nes` file will be created in the same place as the patch file.
|
||||
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||
BizHawk install.
|
||||
|
||||
## Connecting to a Server
|
||||
|
||||
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||
in case you have to close and reopen a window mid-game for some reason.
|
||||
|
||||
1. Mega Man 2 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
|
||||
you can re-open it from the launcher.
|
||||
2. Ensure EmuHawk is running the patched ROM.
|
||||
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
|
||||
connected and recognized Mega Man 2.
|
||||
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||
top text field of the client and click Connect.
|
||||
|
||||
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
|
||||
perfectly safe to make progress offline; everything will re-sync when you reconnect.
|
|
@ -0,0 +1,72 @@
|
|||
from BaseClasses import Item
|
||||
from typing import NamedTuple, Dict
|
||||
from . import names
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: int
|
||||
progression: bool
|
||||
useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
|
||||
skip_balancing: bool = False
|
||||
|
||||
|
||||
class MM2Item(Item):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
robot_master_weapon_table = {
|
||||
names.atomic_fire: ItemData(0x880001, True),
|
||||
names.air_shooter: ItemData(0x880002, True),
|
||||
names.leaf_shield: ItemData(0x880003, True),
|
||||
names.bubble_lead: ItemData(0x880004, True),
|
||||
names.quick_boomerang: ItemData(0x880005, True),
|
||||
names.time_stopper: ItemData(0x880006, True, True),
|
||||
names.metal_blade: ItemData(0x880007, True, True),
|
||||
names.crash_bomber: ItemData(0x880008, True),
|
||||
}
|
||||
|
||||
stage_access_table = {
|
||||
names.heat_man_stage: ItemData(0x880101, True),
|
||||
names.air_man_stage: ItemData(0x880102, True),
|
||||
names.wood_man_stage: ItemData(0x880103, True),
|
||||
names.bubble_man_stage: ItemData(0x880104, True),
|
||||
names.quick_man_stage: ItemData(0x880105, True),
|
||||
names.flash_man_stage: ItemData(0x880106, True),
|
||||
names.metal_man_stage: ItemData(0x880107, True),
|
||||
names.crash_man_stage: ItemData(0x880108, True),
|
||||
}
|
||||
|
||||
item_item_table = {
|
||||
names.item_1: ItemData(0x880011, True, True, True),
|
||||
names.item_2: ItemData(0x880012, True, True, True),
|
||||
names.item_3: ItemData(0x880013, True, True, True)
|
||||
}
|
||||
|
||||
filler_item_table = {
|
||||
names.one_up: ItemData(0x880020, False),
|
||||
names.weapon_energy: ItemData(0x880021, False),
|
||||
names.health_energy: ItemData(0x880022, False),
|
||||
names.e_tank: ItemData(0x880023, False, True),
|
||||
}
|
||||
|
||||
filler_item_weights = {
|
||||
names.one_up: 1,
|
||||
names.weapon_energy: 4,
|
||||
names.health_energy: 1,
|
||||
names.e_tank: 2,
|
||||
}
|
||||
|
||||
item_table = {
|
||||
**robot_master_weapon_table,
|
||||
**stage_access_table,
|
||||
**item_item_table,
|
||||
**filler_item_table,
|
||||
}
|
||||
|
||||
item_names = {
|
||||
"Weapons": {name for name in robot_master_weapon_table.keys()},
|
||||
"Stages": {name for name in stage_access_table.keys()},
|
||||
"Items": {name for name in item_item_table.keys()}
|
||||
}
|
||||
|
||||
lookup_item_to_id: Dict[str, int] = {item_name: data.code for item_name, data in item_table.items()}
|
|
@ -0,0 +1,239 @@
|
|||
from BaseClasses import Location, Region
|
||||
from typing import Dict, Tuple, Optional
|
||||
from . import names
|
||||
|
||||
|
||||
class MM2Location(Location):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
class MM2Region(Region):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
heat_man_locations: Dict[str, Optional[int]] = {
|
||||
names.heat_man: 0x880001,
|
||||
names.atomic_fire_get: 0x880101,
|
||||
names.item_1_get: 0x880111,
|
||||
}
|
||||
|
||||
air_man_locations: Dict[str, Optional[int]] = {
|
||||
names.air_man: 0x880002,
|
||||
names.air_shooter_get: 0x880102,
|
||||
names.item_2_get: 0x880112
|
||||
}
|
||||
|
||||
wood_man_locations: Dict[str, Optional[int]] = {
|
||||
names.wood_man: 0x880003,
|
||||
names.leaf_shield_get: 0x880103
|
||||
}
|
||||
|
||||
bubble_man_locations: Dict[str, Optional[int]] = {
|
||||
names.bubble_man: 0x880004,
|
||||
names.bubble_lead_get: 0x880104
|
||||
}
|
||||
|
||||
quick_man_locations: Dict[str, Optional[int]] = {
|
||||
names.quick_man: 0x880005,
|
||||
names.quick_boomerang_get: 0x880105,
|
||||
}
|
||||
|
||||
flash_man_locations: Dict[str, Optional[int]] = {
|
||||
names.flash_man: 0x880006,
|
||||
names.time_stopper_get: 0x880106,
|
||||
names.item_3_get: 0x880113,
|
||||
}
|
||||
|
||||
metal_man_locations: Dict[str, Optional[int]] = {
|
||||
names.metal_man: 0x880007,
|
||||
names.metal_blade_get: 0x880107
|
||||
}
|
||||
|
||||
crash_man_locations: Dict[str, Optional[int]] = {
|
||||
names.crash_man: 0x880008,
|
||||
names.crash_bomber_get: 0x880108
|
||||
}
|
||||
|
||||
wily_1_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_1: 0x880009,
|
||||
names.wily_stage_1: None
|
||||
}
|
||||
|
||||
wily_2_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_2: 0x88000A,
|
||||
names.wily_stage_2: None
|
||||
}
|
||||
|
||||
wily_3_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_3: 0x88000B,
|
||||
names.wily_stage_3: None
|
||||
}
|
||||
|
||||
wily_4_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_4: 0x88000C,
|
||||
names.wily_stage_4: None
|
||||
}
|
||||
|
||||
wily_5_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_5: 0x88000D,
|
||||
names.wily_stage_5: None
|
||||
}
|
||||
|
||||
wily_6_locations: Dict[str, Optional[int]] = {
|
||||
names.dr_wily: None
|
||||
}
|
||||
|
||||
etank_1ups: Dict[str, Dict[str, Optional[int]]] = {
|
||||
"Heat Man Stage": {
|
||||
names.heat_man_c1: 0x880201,
|
||||
},
|
||||
"Quick Man Stage": {
|
||||
names.quick_man_c1: 0x880202,
|
||||
names.quick_man_c2: 0x880203,
|
||||
names.quick_man_c3: 0x880204,
|
||||
names.quick_man_c7: 0x880208,
|
||||
},
|
||||
"Flash Man Stage": {
|
||||
names.flash_man_c2: 0x88020B,
|
||||
names.flash_man_c6: 0x88020F,
|
||||
},
|
||||
"Metal Man Stage": {
|
||||
names.metal_man_c1: 0x880210,
|
||||
names.metal_man_c2: 0x880211,
|
||||
names.metal_man_c3: 0x880212,
|
||||
},
|
||||
"Crash Man Stage": {
|
||||
names.crash_man_c2: 0x880214,
|
||||
names.crash_man_c3: 0x880215,
|
||||
},
|
||||
"Wily Stage 1": {
|
||||
names.wily_1_c1: 0x880216,
|
||||
},
|
||||
"Wily Stage 2": {
|
||||
names.wily_2_c3: 0x88021A,
|
||||
names.wily_2_c4: 0x88021B,
|
||||
names.wily_2_c5: 0x88021C,
|
||||
names.wily_2_c6: 0x88021D,
|
||||
},
|
||||
"Wily Stage 3": {
|
||||
names.wily_3_c2: 0x880220,
|
||||
},
|
||||
"Wily Stage 4": {
|
||||
names.wily_4_c3: 0x880225,
|
||||
names.wily_4_c4: 0x880226,
|
||||
}
|
||||
}
|
||||
|
||||
energy_pickups: Dict[str, Dict[str, Optional[int]]] = {
|
||||
"Quick Man Stage": {
|
||||
names.quick_man_c4: 0x880205,
|
||||
names.quick_man_c5: 0x880206,
|
||||
names.quick_man_c6: 0x880207,
|
||||
names.quick_man_c8: 0x880209,
|
||||
},
|
||||
"Flash Man Stage": {
|
||||
names.flash_man_c1: 0x88020A,
|
||||
names.flash_man_c3: 0x88020C,
|
||||
names.flash_man_c4: 0x88020D,
|
||||
names.flash_man_c5: 0x88020E,
|
||||
},
|
||||
"Crash Man Stage": {
|
||||
names.crash_man_c1: 0x880213,
|
||||
},
|
||||
"Wily Stage 1": {
|
||||
names.wily_1_c2: 0x880217,
|
||||
},
|
||||
"Wily Stage 2": {
|
||||
names.wily_2_c1: 0x880218,
|
||||
names.wily_2_c2: 0x880219,
|
||||
names.wily_2_c7: 0x88021E,
|
||||
names.wily_2_c8: 0x880227,
|
||||
names.wily_2_c9: 0x880228,
|
||||
names.wily_2_c10: 0x880229,
|
||||
names.wily_2_c11: 0x88022A,
|
||||
names.wily_2_c12: 0x88022B,
|
||||
names.wily_2_c13: 0x88022C,
|
||||
names.wily_2_c14: 0x88022D,
|
||||
names.wily_2_c15: 0x88022E,
|
||||
names.wily_2_c16: 0x88022F,
|
||||
},
|
||||
"Wily Stage 3": {
|
||||
names.wily_3_c1: 0x88021F,
|
||||
names.wily_3_c3: 0x880221,
|
||||
names.wily_3_c4: 0x880222,
|
||||
},
|
||||
"Wily Stage 4": {
|
||||
names.wily_4_c1: 0x880223,
|
||||
names.wily_4_c2: 0x880224,
|
||||
}
|
||||
}
|
||||
|
||||
mm2_regions: Dict[str, Tuple[Tuple[str, ...], Dict[str, Optional[int]], Optional[str]]] = {
|
||||
"Heat Man Stage": ((names.heat_man_stage,), heat_man_locations, None),
|
||||
"Air Man Stage": ((names.air_man_stage,), air_man_locations, None),
|
||||
"Wood Man Stage": ((names.wood_man_stage,), wood_man_locations, None),
|
||||
"Bubble Man Stage": ((names.bubble_man_stage,), bubble_man_locations, None),
|
||||
"Quick Man Stage": ((names.quick_man_stage,), quick_man_locations, None),
|
||||
"Flash Man Stage": ((names.flash_man_stage,), flash_man_locations, None),
|
||||
"Metal Man Stage": ((names.metal_man_stage,), metal_man_locations, None),
|
||||
"Crash Man Stage": ((names.crash_man_stage,), crash_man_locations, None),
|
||||
"Wily Stage 1": ((names.item_1, names.item_2, names.item_3), wily_1_locations, None),
|
||||
"Wily Stage 2": ((names.wily_stage_1,), wily_2_locations, "Wily Stage 1"),
|
||||
"Wily Stage 3": ((names.wily_stage_2,), wily_3_locations, "Wily Stage 2"),
|
||||
"Wily Stage 4": ((names.wily_stage_3,), wily_4_locations, "Wily Stage 3"),
|
||||
"Wily Stage 5": ((names.wily_stage_4,), wily_5_locations, "Wily Stage 4"),
|
||||
"Wily Stage 6": ((names.wily_stage_5,), wily_6_locations, "Wily Stage 5")
|
||||
}
|
||||
|
||||
location_table: Dict[str, Optional[int]] = {
|
||||
**heat_man_locations,
|
||||
**air_man_locations,
|
||||
**wood_man_locations,
|
||||
**bubble_man_locations,
|
||||
**quick_man_locations,
|
||||
**flash_man_locations,
|
||||
**metal_man_locations,
|
||||
**crash_man_locations,
|
||||
**wily_1_locations,
|
||||
**wily_2_locations,
|
||||
**wily_3_locations,
|
||||
**wily_4_locations,
|
||||
**wily_5_locations,
|
||||
}
|
||||
|
||||
for table in etank_1ups:
|
||||
location_table.update(etank_1ups[table])
|
||||
|
||||
for table in energy_pickups:
|
||||
location_table.update(energy_pickups[table])
|
||||
|
||||
location_groups = {
|
||||
"Get Equipped": {
|
||||
names.atomic_fire_get,
|
||||
names.air_shooter_get,
|
||||
names.leaf_shield_get,
|
||||
names.bubble_lead_get,
|
||||
names.quick_boomerang_get,
|
||||
names.time_stopper_get,
|
||||
names.metal_blade_get,
|
||||
names.crash_bomber_get,
|
||||
names.item_1_get,
|
||||
names.item_2_get,
|
||||
names.item_3_get
|
||||
},
|
||||
"Heat Man Stage": {*heat_man_locations.keys(), *etank_1ups["Heat Man Stage"].keys()},
|
||||
"Air Man Stage": {*air_man_locations.keys()},
|
||||
"Wood Man Stage": {*wood_man_locations.keys()},
|
||||
"Bubble Man Stage": {*bubble_man_locations.keys()},
|
||||
"Quick Man Stage": {*quick_man_locations.keys(), *etank_1ups["Quick Man Stage"].keys(),
|
||||
*energy_pickups["Quick Man Stage"].keys()},
|
||||
"Flash Man Stage": {*flash_man_locations.keys(), *etank_1ups["Flash Man Stage"].keys(),
|
||||
*energy_pickups["Flash Man Stage"].keys()},
|
||||
"Metal Man Stage": {*metal_man_locations.keys(), *etank_1ups["Metal Man Stage"].keys()},
|
||||
"Crash Man Stage": {*crash_man_locations.keys(), *etank_1ups["Crash Man Stage"].keys(),
|
||||
*energy_pickups["Crash Man Stage"].keys()},
|
||||
"Wily 2 Weapon Energy": {names.wily_2_c8, names.wily_2_c9, names.wily_2_c10, names.wily_2_c11, names.wily_2_c12,
|
||||
names.wily_2_c13, names.wily_2_c14, names.wily_2_c15, names.wily_2_c16}
|
||||
}
|
||||
|
||||
lookup_location_to_id: Dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}
|
|
@ -0,0 +1,114 @@
|
|||
# Robot Master Weapons
|
||||
crash_bomber = "Crash Bomber"
|
||||
metal_blade = "Metal Blade"
|
||||
quick_boomerang = "Quick Boomerang"
|
||||
bubble_lead = "Bubble Lead"
|
||||
atomic_fire = "Atomic Fire"
|
||||
leaf_shield = "Leaf Shield"
|
||||
time_stopper = "Time Stopper"
|
||||
air_shooter = "Air Shooter"
|
||||
|
||||
# Stage Entry
|
||||
crash_man_stage = "Crash Man Access Codes"
|
||||
metal_man_stage = "Metal Man Access Codes"
|
||||
quick_man_stage = "Quick Man Access Codes"
|
||||
bubble_man_stage = "Bubble Man Access Codes"
|
||||
heat_man_stage = "Heat Man Access Codes"
|
||||
wood_man_stage = "Wood Man Access Codes"
|
||||
flash_man_stage = "Flash Man Access Codes"
|
||||
air_man_stage = "Air Man Access Codes"
|
||||
|
||||
# The Items
|
||||
item_1 = "Item 1 - Propeller"
|
||||
item_2 = "Item 2 - Rocket"
|
||||
item_3 = "Item 3 - Bouncy"
|
||||
|
||||
# Misc. Items
|
||||
one_up = "1-Up"
|
||||
weapon_energy = "Weapon Energy (L)"
|
||||
health_energy = "Health Energy (L)"
|
||||
e_tank = "E-Tank"
|
||||
|
||||
# Locations
|
||||
crash_man = "Crash Man - Defeated"
|
||||
metal_man = "Metal Man - Defeated"
|
||||
quick_man = "Quick Man - Defeated"
|
||||
bubble_man = "Bubble Man - Defeated"
|
||||
heat_man = "Heat Man - Defeated"
|
||||
wood_man = "Wood Man - Defeated"
|
||||
flash_man = "Flash Man - Defeated"
|
||||
air_man = "Air Man - Defeated"
|
||||
crash_bomber_get = "Crash Bomber - Received"
|
||||
metal_blade_get = "Metal Blade - Received"
|
||||
quick_boomerang_get = "Quick Boomerang - Received"
|
||||
bubble_lead_get = "Bubble Lead - Received"
|
||||
atomic_fire_get = "Atomic Fire - Received"
|
||||
leaf_shield_get = "Leaf Shield - Received"
|
||||
time_stopper_get = "Time Stopper - Received"
|
||||
air_shooter_get = "Air Shooter - Received"
|
||||
item_1_get = "Item 1 - Received"
|
||||
item_2_get = "Item 2 - Received"
|
||||
item_3_get = "Item 3 - Received"
|
||||
wily_1 = "Mecha Dragon - Defeated"
|
||||
wily_2 = "Picopico-kun - Defeated"
|
||||
wily_3 = "Guts Tank - Defeated"
|
||||
wily_4 = "Boobeam Trap - Defeated"
|
||||
wily_5 = "Wily Machine 2 - Defeated"
|
||||
dr_wily = "Dr. Wily (Alien) - Defeated"
|
||||
|
||||
# Wily Stage Event Items
|
||||
wily_stage_1 = "Wily Stage 1 - Completed"
|
||||
wily_stage_2 = "Wily Stage 2 - Completed"
|
||||
wily_stage_3 = "Wily Stage 3 - Completed"
|
||||
wily_stage_4 = "Wily Stage 4 - Completed"
|
||||
wily_stage_5 = "Wily Stage 5 - Completed"
|
||||
|
||||
# Consumable Locations
|
||||
heat_man_c1 = "Heat Man Stage - 1-Up" # 3, requires Yoku jumps or Item 2
|
||||
flash_man_c1 = "Flash Man Stage - Health Energy 1" # 0
|
||||
flash_man_c2 = "Flash Man Stage - 1-Up" # 2, requires any Item
|
||||
flash_man_c3 = "Flash Man Stage - Health Energy 2" # 6, requires Crash Bomber
|
||||
flash_man_c4 = "Flash Man Stage - Weapon Energy 1" # 8, requires Crash Bomber
|
||||
flash_man_c5 = "Flash Man Stage - Health Energy 3" # 9
|
||||
flash_man_c6 = "Flash Man Stage - E-Tank" # 10
|
||||
quick_man_c1 = "Quick Man Stage - 1-Up 1" # 0, needs any Item
|
||||
quick_man_c2 = "Quick Man Stage - E-Tank" # 1, requires allow lasers or Time Stopper
|
||||
quick_man_c3 = "Quick Man Stage - 1-Up 2" # 2, requires allow lasers or Time Stopper
|
||||
quick_man_c4 = "Quick Man Stage - Weapon Energy 1" # 3, requires allow lasers or Time Stopper
|
||||
quick_man_c5 = "Quick Man Stage - Weapon Energy 2" # 4, requires allow lasers or Time Stopper
|
||||
quick_man_c6 = "Quick Man Stage - Health Energy" # 5, requires allow lasers or Time Stopper
|
||||
quick_man_c7 = "Quick Man Stage - 1-Up 3" # 6, requires allow lasers or Time Stopper
|
||||
quick_man_c8 = "Quick Man Stage - Weapon Energy 3" # 7, requires allow lasers or Time Stopper
|
||||
metal_man_c1 = "Metal Man Stage - E-Tank 1" # 0
|
||||
metal_man_c2 = "Metal Man Stage - 1-Up" # 1, needs Item 1/2
|
||||
metal_man_c3 = "Metal Man Stage - E-Tank 2" # 2, needs Item 1/2 (without putting dying in logic at least)
|
||||
crash_man_c1 = "Crash Man Stage - Health Energy" # 0
|
||||
crash_man_c2 = "Crash Man Stage - E-Tank" # 1
|
||||
crash_man_c3 = "Crash Man Stage - 1-Up" # 2, any Item
|
||||
wily_1_c1 = "Wily Stage 1 - 1-Up" # 10
|
||||
wily_1_c2 = "Wily Stage 1 - Weapon Energy 1" # 11
|
||||
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1" # 11
|
||||
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2" # 12
|
||||
wily_2_c3 = "Wily Stage 2 - E-Tank 1" # 16
|
||||
wily_2_c4 = "Wily Stage 2 - 1-Up 1" # 17
|
||||
# 18 - 27 are all small weapon energies, might force these local junk?
|
||||
wily_2_c8 = "Wily Stage 2 - Weapon Energy 3" # 18
|
||||
wily_2_c9 = "Wily Stage 2 - Weapon Energy 4" # 19
|
||||
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5" # 20
|
||||
wily_2_c11 = "Wily Stage 2 - Weapon Energy 6" # 21
|
||||
wily_2_c12 = "Wily Stage 2 - Weapon Energy 7" # 22
|
||||
wily_2_c13 = "Wily Stage 2 - Weapon Energy 8" # 23
|
||||
wily_2_c14 = "Wily Stage 2 - Weapon Energy 9" # 24
|
||||
wily_2_c15 = "Wily Stage 2 - Weapon Energy 10" # 25
|
||||
wily_2_c16 = "Wily Stage 2 - Weapon Energy 11" # 26
|
||||
wily_2_c5 = "Wily Stage 2 - 1-Up 2" # 29, requires Crash Bomber
|
||||
wily_2_c6 = "Wily Stage 2 - E-Tank 2" # 30, requires Crash Bomber
|
||||
wily_2_c7 = "Wily Stage 2 - Health Energy" # 31, item 2 (already required to reach wily 2)
|
||||
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # 12, requires Crash Bomber
|
||||
wily_3_c2 = "Wily Stage 3 - E-Tank" # 17, requires Crash Bomber
|
||||
wily_3_c3 = "Wily Stage 3 - Weapon Energy 2" # 18
|
||||
wily_3_c4 = "Wily Stage 3 - Weapon Energy 3" # 19
|
||||
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1" # 16
|
||||
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2" # 17
|
||||
wily_4_c3 = "Wily Stage 4 - 1-Up 1" # 18
|
||||
wily_4_c4 = "Wily Stage 4 - E-Tank 1" # 19
|
|
@ -0,0 +1,229 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Toggle, DeathLink, DefaultOnToggle, TextChoice, Range, OptionDict, PerGameCommonOptions
|
||||
from schema import Schema, And, Use, Optional
|
||||
|
||||
bosses = {
|
||||
"Heat Man": 0,
|
||||
"Air Man": 1,
|
||||
"Wood Man": 2,
|
||||
"Bubble Man": 3,
|
||||
"Quick Man": 4,
|
||||
"Flash Man": 5,
|
||||
"Metal Man": 6,
|
||||
"Crash Man": 7,
|
||||
"Mecha Dragon": 8,
|
||||
"Picopico-kun": 9,
|
||||
"Guts Tank": 10,
|
||||
"Boobeam Trap": 11,
|
||||
"Wily Machine 2": 12,
|
||||
"Alien": 13
|
||||
}
|
||||
|
||||
weapons_to_id = {
|
||||
"Mega Buster": 0,
|
||||
"Atomic Fire": 1,
|
||||
"Air Shooter": 2,
|
||||
"Leaf Shield": 3,
|
||||
"Bubble Lead": 4,
|
||||
"Quick Boomerang": 5,
|
||||
"Metal Blade": 7,
|
||||
"Crash Bomber": 6,
|
||||
"Time Stopper": 8,
|
||||
}
|
||||
|
||||
|
||||
class EnergyLink(Toggle):
|
||||
"""
|
||||
Enables EnergyLink support.
|
||||
When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
|
||||
be requested from the EnergyLink pool.
|
||||
Some of the energy sent to the pool will be lost on transfer.
|
||||
"""
|
||||
display_name = "EnergyLink"
|
||||
|
||||
|
||||
class StartingRobotMaster(Choice):
|
||||
"""
|
||||
The initial stage unlocked at the start.
|
||||
"""
|
||||
display_name = "Starting Robot Master"
|
||||
option_heat_man = 0
|
||||
option_air_man = 1
|
||||
option_wood_man = 2
|
||||
option_bubble_man = 3
|
||||
option_quick_man = 4
|
||||
option_flash_man = 5
|
||||
option_metal_man = 6
|
||||
option_crash_man = 7
|
||||
default = "random"
|
||||
|
||||
|
||||
class YokuJumps(Toggle):
|
||||
"""
|
||||
When enabled, the player is expected to be able to perform the yoku block sequence in Heat Man's
|
||||
stage without Item 2.
|
||||
"""
|
||||
display_name = "Yoku Block Jumps"
|
||||
|
||||
|
||||
class EnableLasers(Toggle):
|
||||
"""
|
||||
When enabled, the player is expected to complete (and acquire items within) the laser sections of Quick Man's
|
||||
stage without the Time Stopper.
|
||||
"""
|
||||
display_name = "Enable Lasers"
|
||||
|
||||
|
||||
class Consumables(Choice):
|
||||
"""
|
||||
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
|
||||
E-Tanks and 1-Ups add 20 checks to the pool.
|
||||
Weapon/Health Energy add 27 checks to the pool.
|
||||
"""
|
||||
display_name = "Consumables"
|
||||
option_none = 0
|
||||
option_1up_etank = 1
|
||||
option_weapon_health = 2
|
||||
option_all = 3
|
||||
default = 1
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: int) -> str:
|
||||
if value == 1:
|
||||
return "1-Ups/E-Tanks"
|
||||
if value == 2:
|
||||
return "Weapon/Health Energy"
|
||||
return super().get_option_name(value)
|
||||
|
||||
|
||||
class Quickswap(DefaultOnToggle):
|
||||
"""
|
||||
When enabled, the player can quickswap through all received weapons by pressing Select.
|
||||
"""
|
||||
display_name = "Quickswap"
|
||||
|
||||
|
||||
class PaletteShuffle(TextChoice):
|
||||
"""
|
||||
Change the color of Mega Man and the Robot Masters.
|
||||
None: The palettes are unchanged.
|
||||
Shuffled: Palette colors are shuffled amongst the robot masters.
|
||||
Randomized: Random (usually good) palettes are generated for each robot master.
|
||||
Singularity: one palette is generated and used for all robot masters.
|
||||
Supports custom palettes using HTML named colors in the
|
||||
following format: Mega Buster-Lavender|Violet;randomized
|
||||
The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
|
||||
that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
|
||||
a semicolon.
|
||||
"""
|
||||
display_name = "Palette Shuffle"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_singularity = 3
|
||||
|
||||
|
||||
class EnemyWeaknesses(Toggle):
|
||||
"""
|
||||
Randomizes the damage dealt to enemies by weapons. Friender will always take damage from the buster.
|
||||
"""
|
||||
display_name = "Random Enemy Weaknesses"
|
||||
|
||||
|
||||
class StrictWeaknesses(Toggle):
|
||||
"""
|
||||
Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
|
||||
Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Alien).
|
||||
"""
|
||||
display_name = "Strict Boss Weaknesses"
|
||||
|
||||
|
||||
class RandomWeaknesses(Choice):
|
||||
"""
|
||||
None: Bosses will have their regular weaknesses.
|
||||
Shuffled: Weapon damage will be shuffled amongst the weapons, so Metal Blade may do Bubble Lead damage.
|
||||
Time Stopper will deplete half of a random Robot Master's HP.
|
||||
Randomized: Weapon damage will be fully randomized.
|
||||
"""
|
||||
display_name = "Random Boss Weaknesses"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class Wily5Requirement(Range):
|
||||
"""Change the number of Robot Masters that are required to be defeated for
|
||||
the teleporter to the Wily Machine to appear."""
|
||||
display_name = "Wily 5 Requirement"
|
||||
default = 8
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
|
||||
|
||||
class WeaknessPlando(OptionDict):
|
||||
"""
|
||||
Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
|
||||
plando_weakness:
|
||||
Robot Master:
|
||||
Weapon: Damage
|
||||
"""
|
||||
display_name = "Plando Weaknesses"
|
||||
schema = Schema({
|
||||
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
|
||||
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14))
|
||||
}
|
||||
})
|
||||
default = {}
|
||||
|
||||
|
||||
class ReduceFlashing(Choice):
|
||||
"""
|
||||
Reduce flashing seen in gameplay, such as the stage select and when defeating a Wily boss.
|
||||
Virtual Console: increases length of most flashes, changes some flashes from white to a dark gray.
|
||||
Minor: VC changes + decreasing the speed of Bubble/Metal Man stage animations.
|
||||
Full: VC changes + further decreasing the brightness of most flashes and
|
||||
disables stage animations for Metal/Bubble Man stages.
|
||||
"""
|
||||
display_name = "Reduce Flashing"
|
||||
option_none = 0
|
||||
option_virtual_console = 1
|
||||
option_minor = 2
|
||||
option_full = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class RandomMusic(Choice):
|
||||
"""
|
||||
Vanilla: music is unchanged
|
||||
Shuffled: stage and certain menu music is shuffled.
|
||||
Randomized: stage and certain menu music is randomly selected
|
||||
None: no music will play
|
||||
"""
|
||||
display_name = "Random Music"
|
||||
option_vanilla = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_none = 3
|
||||
|
||||
@dataclass
|
||||
class MM2Options(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
starting_robot_master: StartingRobotMaster
|
||||
consumables: Consumables
|
||||
yoku_jumps: YokuJumps
|
||||
enable_lasers: EnableLasers
|
||||
enemy_weakness: EnemyWeaknesses
|
||||
strict_weakness: StrictWeaknesses
|
||||
random_weakness: RandomWeaknesses
|
||||
wily_5_requirement: Wily5Requirement
|
||||
plando_weakness: WeaknessPlando
|
||||
palette_shuffle: PaletteShuffle
|
||||
quickswap: Quickswap
|
||||
reduce_flashing: ReduceFlashing
|
||||
random_music: RandomMusic
|
|
@ -0,0 +1,415 @@
|
|||
import pkgutil
|
||||
from typing import Optional, TYPE_CHECKING, Iterable, Dict, Sequence
|
||||
import hashlib
|
||||
import Utils
|
||||
import os
|
||||
|
||||
import settings
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from . import names
|
||||
from .rules import minimum_weakness_requirement
|
||||
from .text import MM2TextEntry
|
||||
from .color import get_colors_for_item, write_palette_shuffle
|
||||
from .options import Consumables, ReduceFlashing, RandomMusic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
|
||||
MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497"
|
||||
PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4"
|
||||
MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632"
|
||||
MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3"
|
||||
|
||||
enemy_weakness_ptrs: Dict[int, int] = {
|
||||
0: 0x3E9A8,
|
||||
1: 0x3EA24,
|
||||
2: 0x3EA9C,
|
||||
3: 0x3EB14,
|
||||
4: 0x3EB8C,
|
||||
5: 0x3EC04,
|
||||
6: 0x3EC7C,
|
||||
7: 0x3ECF4,
|
||||
}
|
||||
|
||||
enemy_addresses: Dict[str, int] = {
|
||||
"Shrink": 0x00,
|
||||
"M-445": 0x04,
|
||||
"Claw": 0x08,
|
||||
"Tanishi": 0x0A,
|
||||
"Kerog": 0x0C,
|
||||
"Petit Kerog": 0x0D,
|
||||
"Anko": 0x0F,
|
||||
"Batton": 0x16,
|
||||
"Robitto": 0x17,
|
||||
"Friender": 0x1C,
|
||||
"Monking": 0x1D,
|
||||
"Kukku": 0x1F,
|
||||
"Telly": 0x22,
|
||||
"Changkey Maker": 0x23,
|
||||
"Changkey": 0x24,
|
||||
"Pierrobot": 0x29,
|
||||
"Fly Boy": 0x2C,
|
||||
# "Crash Wall": 0x2D
|
||||
# "Friender Wall": 0x2E
|
||||
"Blocky": 0x31,
|
||||
"Neo Metall": 0x34,
|
||||
"Matasaburo": 0x36,
|
||||
"Pipi": 0x38,
|
||||
"Pipi Egg": 0x3A,
|
||||
"Copipi": 0x3C,
|
||||
"Kaminari Goro": 0x3E,
|
||||
"Petit Goblin": 0x45,
|
||||
"Springer": 0x46,
|
||||
"Mole (Up)": 0x48,
|
||||
"Mole (Down)": 0x49,
|
||||
"Shotman (Left)": 0x4B,
|
||||
"Shotman (Right)": 0x4C,
|
||||
"Sniper Armor": 0x4E,
|
||||
"Sniper Joe": 0x4F,
|
||||
"Scworm": 0x50,
|
||||
"Scworm Worm": 0x51,
|
||||
"Picopico-kun": 0x6A,
|
||||
"Boobeam Trap": 0x6D,
|
||||
"Big Fish": 0x71
|
||||
}
|
||||
|
||||
# addresses printed when assembling basepatch
|
||||
consumables_ptr: int = 0x3F2FE
|
||||
quickswap_ptr: int = 0x3F363
|
||||
wily_5_ptr: int = 0x3F3A1
|
||||
energylink_ptr: int = 0x3F46B
|
||||
get_equipped_sound_ptr: int = 0x3F384
|
||||
|
||||
|
||||
class RomData:
|
||||
def __init__(self, file: bytes, name: str = "") -> None:
|
||||
self.file = bytearray(file)
|
||||
self.name = name
|
||||
|
||||
def read_byte(self, offset: int) -> int:
|
||||
return self.file[offset]
|
||||
|
||||
def read_bytes(self, offset: int, length: int) -> bytearray:
|
||||
return self.file[offset:offset + length]
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.file[offset] = value
|
||||
|
||||
def write_bytes(self, offset: int, values: Sequence[int]) -> None:
|
||||
self.file[offset:offset + len(values)] = values
|
||||
|
||||
def write_to_file(self, file: str) -> None:
|
||||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.file)
|
||||
|
||||
|
||||
class MM2ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [MM2LCHASH, MM2NESHASH, MM2VCHASH]
|
||||
game = "Mega Man 2"
|
||||
patch_file_ending = ".apmm2"
|
||||
result_file_ending = ".nes"
|
||||
name: bytearray
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["mm2_basepatch.bsdiff4"]),
|
||||
("apply_tokens", ["token_patch.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
|
||||
|
||||
def write_bytes(self, offset: int, value: Iterable[int]) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
|
||||
|
||||
|
||||
def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
|
||||
patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm2_basepatch.bsdiff4")))
|
||||
# text writing
|
||||
patch.write_bytes(0x37E2A, MM2TextEntry("FOR ", 0xCB).resolve())
|
||||
patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED ", 0x0B).resolve())
|
||||
patch.write_bytes(0x37EBA, MM2TextEntry("WITH ", 0x2B).resolve())
|
||||
|
||||
base_address = 0x3F650
|
||||
color_address = 0x37F6C
|
||||
for i, location in zip(range(11), [
|
||||
names.atomic_fire_get,
|
||||
names.air_shooter_get,
|
||||
names.leaf_shield_get,
|
||||
names.bubble_lead_get,
|
||||
names.quick_boomerang_get,
|
||||
names.time_stopper_get,
|
||||
names.metal_blade_get,
|
||||
names.crash_bomber_get,
|
||||
names.item_1_get,
|
||||
names.item_2_get,
|
||||
names.item_3_get
|
||||
]):
|
||||
item = world.multiworld.get_location(location, world.player).item
|
||||
if item:
|
||||
if len(item.name) <= 14:
|
||||
# we want to just place it in the center
|
||||
first_str = ""
|
||||
second_str = item.name
|
||||
third_str = ""
|
||||
elif len(item.name) <= 28:
|
||||
# spread across second and third
|
||||
first_str = ""
|
||||
second_str = item.name[:14]
|
||||
third_str = item.name[14:]
|
||||
else:
|
||||
# all three
|
||||
first_str = item.name[:14]
|
||||
second_str = item.name[14:28]
|
||||
third_str = item.name[28:]
|
||||
if len(third_str) > 16:
|
||||
third_str = third_str[:16]
|
||||
player_str = world.multiworld.get_player_name(item.player)
|
||||
if len(player_str) > 14:
|
||||
player_str = player_str[:14]
|
||||
patch.write_bytes(base_address + (64 * i), MM2TextEntry(first_str, 0x4B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 16, MM2TextEntry(second_str, 0x6B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 32, MM2TextEntry(third_str, 0x8B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 48, MM2TextEntry(player_str, 0xEB).resolve())
|
||||
|
||||
colors = get_colors_for_item(item.name)
|
||||
if i > 7:
|
||||
patch.write_bytes(color_address + 27 + ((i - 8) * 2), colors)
|
||||
else:
|
||||
patch.write_bytes(color_address + (i * 2), colors)
|
||||
|
||||
write_palette_shuffle(world, patch)
|
||||
|
||||
enemy_weaknesses: Dict[str, Dict[int, int]] = {}
|
||||
|
||||
if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
|
||||
# we need to write boss weaknesses
|
||||
output = bytearray()
|
||||
for weapon in world.weapon_damage:
|
||||
if weapon == 8:
|
||||
continue # Time Stopper is a special case
|
||||
weapon_damage = [world.weapon_damage[weapon][i]
|
||||
if world.weapon_damage[weapon][i] >= 0
|
||||
else 256 + world.weapon_damage[weapon][i]
|
||||
for i in range(14)]
|
||||
output.extend(weapon_damage)
|
||||
patch.write_bytes(0x2E952, bytes(output))
|
||||
time_stopper_damage = world.weapon_damage[8]
|
||||
time_offset = 0x2C03B
|
||||
damage_table = {
|
||||
4: 0xF,
|
||||
3: 0x17,
|
||||
2: 0x1E,
|
||||
1: 0x25
|
||||
}
|
||||
for boss, damage in enumerate(time_stopper_damage):
|
||||
if damage > 4:
|
||||
damage = 4 # 4 is a guaranteed kill, no need to exceed
|
||||
if damage <= 0:
|
||||
patch.write_byte(time_offset + 14 + boss, 0)
|
||||
else:
|
||||
patch.write_byte(time_offset + 14 + boss, 1)
|
||||
patch.write_byte(time_offset + boss, damage_table[damage])
|
||||
if world.options.random_weakness:
|
||||
wily_5_weaknesses = [i for i in range(8) if world.weapon_damage[i][12] > minimum_weakness_requirement[i]]
|
||||
world.random.shuffle(wily_5_weaknesses)
|
||||
if len(wily_5_weaknesses) >= 3:
|
||||
weak1 = wily_5_weaknesses.pop()
|
||||
weak2 = wily_5_weaknesses.pop()
|
||||
weak3 = wily_5_weaknesses.pop()
|
||||
elif len(wily_5_weaknesses) == 2:
|
||||
weak1 = weak2 = wily_5_weaknesses.pop()
|
||||
weak3 = wily_5_weaknesses.pop()
|
||||
else:
|
||||
weak1 = weak2 = weak3 = 0
|
||||
patch.write_byte(0x2DA2E, weak1)
|
||||
patch.write_byte(0x2DA32, weak2)
|
||||
patch.write_byte(0x2DA3A, weak3)
|
||||
enemy_weaknesses["Picopico-kun"] = {weapon: world.weapon_damage[weapon][9] for weapon in range(8)}
|
||||
enemy_weaknesses["Boobeam Trap"] = {weapon: world.weapon_damage[weapon][11] for weapon in range(8)}
|
||||
|
||||
if world.options.enemy_weakness:
|
||||
for enemy in enemy_addresses:
|
||||
if enemy in ("Picopico-kun", "Boobeam Trap"):
|
||||
continue
|
||||
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
|
||||
if enemy == "Friender":
|
||||
# Friender has to be killed, need buster damage to not break logic
|
||||
enemy_weaknesses[enemy][0] = max(enemy_weaknesses[enemy][0], 1)
|
||||
|
||||
for enemy, damage_table in enemy_weaknesses.items():
|
||||
for weapon in enemy_weakness_ptrs:
|
||||
if damage_table[weapon] < 0:
|
||||
damage_table[weapon] = 256 + damage_table[weapon]
|
||||
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage_table[weapon])
|
||||
|
||||
if world.options.quickswap:
|
||||
patch.write_byte(quickswap_ptr + 1, 0x01)
|
||||
|
||||
if world.options.consumables != Consumables.option_all:
|
||||
value_a = 0x7C
|
||||
value_b = 0x76
|
||||
if world.options.consumables == Consumables.option_1up_etank:
|
||||
value_b = 0x7A
|
||||
else:
|
||||
value_a = 0x7A
|
||||
patch.write_byte(consumables_ptr - 3, value_a)
|
||||
patch.write_byte(consumables_ptr + 1, value_b)
|
||||
|
||||
patch.write_byte(wily_5_ptr + 1, world.options.wily_5_requirement.value)
|
||||
|
||||
if world.options.energy_link:
|
||||
patch.write_byte(energylink_ptr + 1, 1)
|
||||
|
||||
if world.options.reduce_flashing:
|
||||
if world.options.reduce_flashing.value == ReduceFlashing.option_virtual_console:
|
||||
color = 0x2D # Dark Gray
|
||||
speed = -1
|
||||
elif world.options.reduce_flashing.value == ReduceFlashing.option_minor:
|
||||
color = 0x2D
|
||||
speed = 0x08
|
||||
else:
|
||||
color = 0x0F
|
||||
speed = 0x00
|
||||
patch.write_byte(0x2D1B0, color) # Change white to a dark gray, Mecha Dragon
|
||||
patch.write_byte(0x2D397, 0x0F) # Longer flash time, Mecha Dragon kill
|
||||
patch.write_byte(0x2D3A0, color) # Change white to a dark gray, Picopico-kun/Boobeam Trap
|
||||
patch.write_byte(0x2D65F, color) # Change white to a dark gray, Guts Tank
|
||||
patch.write_byte(0x2DA94, color) # Change white to a dark gray, Wily Machine
|
||||
patch.write_byte(0x2DC97, color) # Change white to a dark gray, Alien
|
||||
patch.write_byte(0x2DD68, 0x10) # Longer flash time, Alien kill
|
||||
patch.write_bytes(0x2DF14, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA]) # Reduce final Alien flash to 1 big flash
|
||||
patch.write_byte(0x34132, 0x08) # Longer flash time, Stage Select
|
||||
|
||||
if world.options.reduce_flashing.value == ReduceFlashing.option_full:
|
||||
# reduce color of stage flashing
|
||||
patch.write_bytes(0x344C9, [0x2D, 0x10, 0x00, 0x2D,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x2D, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00])
|
||||
# remove wily castle flash
|
||||
patch.write_byte(0x3596D, 0x0F)
|
||||
|
||||
if speed != -1:
|
||||
patch.write_byte(0xFE01, speed) # Bubble Man Stage
|
||||
patch.write_byte(0x1BE01, speed) # Metal Man Stage
|
||||
|
||||
if world.options.random_music:
|
||||
if world.options.random_music == RandomMusic.option_none:
|
||||
pool = [0xFF] * 20
|
||||
# A couple of additional mutes we want here
|
||||
patch.write_byte(0x37819, 0xFF) # Credits
|
||||
patch.write_byte(0x378A4, 0xFF) # Credits #2
|
||||
patch.write_byte(0x37149, 0xFF) # Game Over Jingle
|
||||
patch.write_byte(0x341BA, 0xFF) # Robot Master Jingle
|
||||
patch.write_byte(0x2E0B4, 0xFF) # Robot Master Defeated
|
||||
patch.write_byte(0x35B78, 0xFF) # Wily Castle
|
||||
patch.write_byte(0x2DFA5, 0xFF) # Wily Defeated
|
||||
|
||||
elif world.options.random_music == RandomMusic.option_shuffled:
|
||||
pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 9, 0x10, 0xC, 0xB, 0x17, 0x13, 0xE, 0xD]
|
||||
world.random.shuffle(pool)
|
||||
else:
|
||||
pool = world.random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xB, 0xC, 0xD, 0xE, 0x10, 0x13, 0x17], k=20)
|
||||
patch.write_bytes(0x381E0, pool[:13])
|
||||
patch.write_byte(0x36318, pool[13]) # Game Start
|
||||
patch.write_byte(0x37181, pool[13]) # Game Over
|
||||
patch.write_byte(0x340AE, pool[14]) # RBM Select
|
||||
patch.write_byte(0x39005, pool[15]) # Robot Master Battle
|
||||
patch.write_byte(get_equipped_sound_ptr + 1, pool[16]) # Get Equipped, we actually hook this already lmao
|
||||
patch.write_byte(0x3775A, pool[17]) # Epilogue
|
||||
patch.write_byte(0x36089, pool[18]) # Intro
|
||||
patch.write_byte(0x361F1, pool[19]) # Title
|
||||
|
||||
|
||||
|
||||
from Utils import __version__
|
||||
patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
patch.name.extend([0] * (21 - len(patch.name)))
|
||||
patch.write_bytes(0x3FFC0, patch.name)
|
||||
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
|
||||
patch.write_byte(0x3FFD5, deathlink_byte)
|
||||
|
||||
patch.write_bytes(0x3FFD8, world.world_version)
|
||||
|
||||
version_map = {
|
||||
"0": 0x90,
|
||||
"1": 0x91,
|
||||
"2": 0x92,
|
||||
"3": 0x93,
|
||||
"4": 0x94,
|
||||
"5": 0x95,
|
||||
"6": 0x96,
|
||||
"7": 0x97,
|
||||
"8": 0x98,
|
||||
"9": 0x99,
|
||||
".": 0xDC
|
||||
}
|
||||
patch.write_token(APTokenTypes.RLE, 0x36EE0, (11, 0))
|
||||
patch.write_token(APTokenTypes.RLE, 0x36EEE, (25, 0))
|
||||
|
||||
# BY SILVRIS
|
||||
patch.write_bytes(0x36EE0, [0xC2, 0xD9, 0xC0, 0xD3, 0xC9, 0xCC, 0xD6, 0xD2, 0xC9, 0xD3])
|
||||
# ARCHIPELAGO x.x.x
|
||||
patch.write_bytes(0x36EF2, [0xC1, 0xD2, 0xC3, 0xC8, 0xC9, 0xD0, 0xC5, 0xCC, 0xC1, 0xC7, 0xCF, 0xC0])
|
||||
patch.write_bytes(0x36EFE, list(map(lambda c: version_map[c], __version__)))
|
||||
|
||||
patch.write_file("token_patch.bin", patch.get_token_binary())
|
||||
|
||||
|
||||
header = b"\x4E\x45\x53\x1A\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
|
||||
def read_headerless_nes_rom(rom: bytes) -> bytes:
|
||||
if rom[:4] == b"NES\x1A":
|
||||
return rom[16:]
|
||||
else:
|
||||
return rom
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes: Optional[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 = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
base_rom_bytes = extract_mm2(base_rom_bytes)
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {MM2LCHASH, MM2NESHASH, MM2VCHASH}:
|
||||
print(basemd5.hexdigest())
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
|
||||
"Get the correct game and version, then dump it")
|
||||
headered_rom = bytearray(base_rom_bytes)
|
||||
headered_rom[0:0] = header
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
|
||||
return bytes(headered_rom)
|
||||
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["mm2_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
PRG_OFFSET = 0x8ED70
|
||||
PRG_SIZE = 0x40000
|
||||
|
||||
|
||||
def extract_mm2(proteus: bytes) -> bytes:
|
||||
mm2 = bytearray(proteus[PRG_OFFSET:PRG_OFFSET + PRG_SIZE])
|
||||
return bytes(mm2)
|
|
@ -0,0 +1,319 @@
|
|||
from math import ceil
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from . import names
|
||||
from .locations import heat_man_locations, air_man_locations, wood_man_locations, bubble_man_locations, \
|
||||
quick_man_locations, flash_man_locations, metal_man_locations, crash_man_locations, wily_1_locations, \
|
||||
wily_2_locations, wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations
|
||||
from .options import bosses, weapons_to_id, Consumables, RandomWeaknesses
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
weapon_damage: Dict[int, List[int]] = {
|
||||
0: [2, 2, 1, 1, 2, 2, 1, 1, 1, 7, 1, 0, 1, -1], # Mega Buster
|
||||
1: [-1, 6, 0xE, 0, 0xA, 6, 4, 6, 8, 13, 8, 0, 0xE, -1], # Atomic Fire
|
||||
2: [2, 0, 4, 0, 2, 0, 0, 0xA, 0, 0, 0, 0, 1, -1], # Air Shooter
|
||||
3: [0, 8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1], # Leaf Shield
|
||||
4: [6, 0, 0, -1, 0, 2, 0, 1, 0, 14, 1, 0, 0, 1], # Bubble Lead
|
||||
5: [2, 2, 0, 2, 0, 0, 4, 1, 1, 7, 2, 0, 1, -1], # Quick Boomerang
|
||||
6: [-1, 0, 2, 2, 4, 3, 0, 0, 1, 0, 1, 0x14, 1, -1], # Crash Bomber
|
||||
7: [1, 0, 2, 4, 0, 4, 0xE, 0, 0, 7, 0, 0, 1, -1], # Metal Blade
|
||||
8: [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], # Time Stopper
|
||||
}
|
||||
|
||||
weapons_to_name: Dict[int, str] = {
|
||||
1: names.atomic_fire,
|
||||
2: names.air_shooter,
|
||||
3: names.leaf_shield,
|
||||
4: names.bubble_lead,
|
||||
5: names.quick_boomerang,
|
||||
6: names.crash_bomber,
|
||||
7: names.metal_blade,
|
||||
8: names.time_stopper
|
||||
}
|
||||
|
||||
minimum_weakness_requirement: Dict[int, int] = {
|
||||
0: 1, # Mega Buster is free
|
||||
1: 14, # 2 shots of Atomic Fire
|
||||
2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot
|
||||
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
|
||||
4: 1, # 56 uses of Bubble Lead
|
||||
5: 1, # 224 uses of Quick Boomerang
|
||||
6: 4, # 7 uses of Crash Bomber
|
||||
7: 1, # 112 uses of Metal Blade
|
||||
8: 4, # 1 use of Time Stopper, but setting to 4 means we shave the entire HP bar
|
||||
}
|
||||
|
||||
robot_masters: Dict[int, str] = {
|
||||
0: "Heat Man Defeated",
|
||||
1: "Air Man Defeated",
|
||||
2: "Wood Man Defeated",
|
||||
3: "Bubble Man Defeated",
|
||||
4: "Quick Man Defeated",
|
||||
5: "Flash Man Defeated",
|
||||
6: "Metal Man Defeated",
|
||||
7: "Crash Man Defeated"
|
||||
}
|
||||
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 10,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 0.5,
|
||||
5: 0.125,
|
||||
6: 4,
|
||||
7: 0.25,
|
||||
8: 7,
|
||||
}
|
||||
|
||||
|
||||
def can_defeat_enough_rbms(state: "CollectionState", player: int,
|
||||
required: int, boss_requirements: Dict[int, List[int]]):
|
||||
can_defeat = 0
|
||||
for boss, reqs in boss_requirements.items():
|
||||
if boss in robot_masters:
|
||||
if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
|
||||
can_defeat += 1
|
||||
if can_defeat >= required:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def set_rules(world: "MM2World") -> None:
|
||||
# most rules are set on region, so we only worry about rules required within stage access
|
||||
# or rules variable on settings
|
||||
if (hasattr(world.multiworld, "re_gen_passthrough")
|
||||
and "Mega Man 2" in getattr(world.multiworld, "re_gen_passthrough")):
|
||||
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 2"]
|
||||
world.weapon_damage = slot_data["weapon_damage"]
|
||||
world.wily_5_weapons = slot_data["wily_5_weapons"]
|
||||
else:
|
||||
if world.options.random_weakness == RandomWeaknesses.option_shuffled:
|
||||
weapon_tables = [table for weapon, table in weapon_damage.items() if weapon not in (0, 8)]
|
||||
world.random.shuffle(weapon_tables)
|
||||
for i in range(1, 8):
|
||||
world.weapon_damage[i] = weapon_tables.pop()
|
||||
# alien must take minimum required damage from his weakness
|
||||
alien_weakness = next(weapon for weapon in range(8) if world.weapon_damage[weapon][13] != -1)
|
||||
world.weapon_damage[alien_weakness][13] = minimum_weakness_requirement[alien_weakness]
|
||||
world.weapon_damage[8] = [0 for _ in range(14)]
|
||||
world.weapon_damage[8][world.random.choice(range(8))] = 2
|
||||
elif world.options.random_weakness == RandomWeaknesses.option_randomized:
|
||||
world.weapon_damage = {i: [] for i in range(9)}
|
||||
for boss in range(13):
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(min(14, max(-1, int(world.random.normalvariate(3, 3)))))
|
||||
if not any([world.weapon_damage[weapon][boss] >= max(4, minimum_weakness_requirement[weapon])
|
||||
for weapon in range(1, 7)]):
|
||||
# failsafe, there should be at least one defined non-Buster weakness
|
||||
weapon = world.random.randint(1, 7)
|
||||
world.weapon_damage[weapon][boss] = world.random.randint(
|
||||
max(4, minimum_weakness_requirement[weapon]), 14) # Force weakness
|
||||
# special case, if boobeam trap has a weakness to Crash, it needs to be max damage
|
||||
if world.weapon_damage[6][11] > 4:
|
||||
world.weapon_damage[6][11] = 14
|
||||
# handle the alien
|
||||
boss = 13
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(-1)
|
||||
weapon = world.random.choice(list(world.weapon_damage.keys()))
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.options.strict_weakness:
|
||||
for weapon in weapon_damage:
|
||||
for i in range(13):
|
||||
if weapon == 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif i in (8, 12) and not world.options.random_weakness:
|
||||
continue
|
||||
# Mecha Dragon only has damage range of 0-1, so allow the 1
|
||||
# Wily Machine needs all three weaknesses present, so allow
|
||||
elif 4 > world.weapon_damage[weapon][i] > 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
# handle special cases
|
||||
for boss in range(14):
|
||||
for weapon in (1, 3, 6, 8):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
|
||||
# Weapon does not have enough possible ammo to kill the boss, raise the damage
|
||||
if boss == 9:
|
||||
if weapon != 3:
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
elif boss == 11:
|
||||
if weapon == 1:
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
else:
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
starting = world.options.starting_robot_master.value
|
||||
world.weapon_damage[0][starting] = 1
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \
|
||||
and not any(w != p_weapon
|
||||
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]
|
||||
for w in world.weapon_damage):
|
||||
# we need to replace this weakness
|
||||
weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon])
|
||||
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
|
||||
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
|
||||
= world.options.plando_weakness[p_boss][p_weapon]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
|
||||
|
||||
# final special case
|
||||
# There's a vanilla crash if Time Stopper kills Wily phase 1
|
||||
# There's multiple fixes, but ensuring Wily cannot take Time Stopper damage is best
|
||||
if world.weapon_damage[8][12] > 0:
|
||||
world.weapon_damage[8][12] = 0
|
||||
|
||||
# weakness validation, it is better to confirm a completable seed than respect plando
|
||||
boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]}
|
||||
|
||||
weapon_energy = {key: float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
|
||||
for boss in [*range(8), 12]}
|
||||
flexibility = {
|
||||
boss: (
|
||||
sum(damage_value > 0 for damage_value in
|
||||
weapon_damages.values()) # Amount of weapons that hit this boss
|
||||
* sum(weapon_damages.values()) # Overall damage that those weapons do
|
||||
)
|
||||
for boss, weapon_damages in weapon_boss.items() if boss != 12
|
||||
}
|
||||
flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons = {i: set() for i in [*range(8), 12]}
|
||||
for boss in [*flexibility, 12]:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon] > 0}
|
||||
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
|
||||
# We get exactly one use of Time Stopper during the rush
|
||||
# So we want to make sure that use is absolutely needed
|
||||
weapon_weight[8] = min(weapon_weight[8], 0.001)
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0] > 0:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
used_weapons[boss].add(wp)
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
# so find the weapon that has the most uses, and apply that as an additional weakness
|
||||
# it should be impossible to be out of energy, simply because even if every boss took 1 from
|
||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
|
||||
# be able to cover
|
||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
|
||||
if weapon != 0)
|
||||
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
|
||||
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
|
||||
ceil(boss_health[boss] // minimum_weakness_requirement[wp]))
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
|
||||
weapon_weight.pop(wp)
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
|
||||
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}
|
||||
|
||||
for i, boss_locations in enumerate([
|
||||
heat_man_locations,
|
||||
air_man_locations,
|
||||
wood_man_locations,
|
||||
bubble_man_locations,
|
||||
quick_man_locations,
|
||||
flash_man_locations,
|
||||
metal_man_locations,
|
||||
crash_man_locations,
|
||||
wily_1_locations,
|
||||
wily_2_locations,
|
||||
wily_3_locations,
|
||||
wily_4_locations,
|
||||
wily_5_locations,
|
||||
wily_6_locations
|
||||
]):
|
||||
if world.weapon_damage[0][i] > 0:
|
||||
continue # this can always be in logic
|
||||
weapons = []
|
||||
for weapon in range(1, 9):
|
||||
if world.weapon_damage[weapon][i] > 0:
|
||||
if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
|
||||
continue # Atomic Fire can only be considered logical for bosses it can kill in 2 hits
|
||||
weapons.append(weapons_to_name[weapon])
|
||||
if not weapons:
|
||||
raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
|
||||
for location in boss_locations:
|
||||
if i == 12:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
|
||||
# TODO: when has_list gets added, check for a subset of possible weaknesses
|
||||
else:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
|
||||
|
||||
# Always require Crash Bomber for Boobeam Trap
|
||||
add_rule(world.get_location(names.wily_4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_stage_4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
|
||||
# Need to defeat x amount of robot masters for Wily 5
|
||||
add_rule(world.get_location(names.wily_5),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
|
||||
world.wily_5_weapons))
|
||||
add_rule(world.get_location(names.wily_stage_5),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
|
||||
world.wily_5_weapons))
|
||||
|
||||
if not world.options.yoku_jumps:
|
||||
add_rule(world.get_entrance("To Heat Man Stage"),
|
||||
lambda state: state.has(names.item_2, world.player))
|
||||
|
||||
if not world.options.enable_lasers:
|
||||
add_rule(world.get_entrance("To Quick Man Stage"),
|
||||
lambda state: state.has(names.time_stopper, world.player))
|
||||
|
||||
if world.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
add_rule(world.get_location(names.flash_man_c2),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.quick_man_c1),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.metal_man_c2),
|
||||
lambda state: state.has_any([names.item_1, names.item_2], world.player))
|
||||
add_rule(world.get_location(names.metal_man_c3),
|
||||
lambda state: state.has_any([names.item_1, names.item_2], world.player))
|
||||
add_rule(world.get_location(names.crash_man_c3),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.wily_2_c5),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_2_c6),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_3_c2),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
if world.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
add_rule(world.get_location(names.flash_man_c3),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.flash_man_c4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_3_c1),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
|
@ -0,0 +1,861 @@
|
|||
norom
|
||||
!headersize = 16
|
||||
|
||||
!controller_mirror = $23
|
||||
!controller_flip = $27 ; only on first frame of input, used by crash man, etc
|
||||
!current_stage = $2A
|
||||
!received_stages = $8A
|
||||
!completed_stages = $8B
|
||||
!received_item_checks = $8C
|
||||
!last_wily = $8D
|
||||
!deathlink = $8F
|
||||
!energylink_packet = $90
|
||||
!rbm_strobe = $91
|
||||
!received_weapons = $9A
|
||||
!received_items = $9B
|
||||
!current_weapon = $A9
|
||||
|
||||
!stage_completion = $0F70
|
||||
!consumable_checks = $0F80
|
||||
|
||||
!CONTROLLER_SELECT = #$04
|
||||
!CONTROLLER_SELECT_START = #$0C
|
||||
!CONTROLLER_ALL_BUTTON = #$0F
|
||||
|
||||
!PpuControl_2000 = $2000
|
||||
!PpuMask_2001 = $2001
|
||||
!PpuAddr_2006 = $2006
|
||||
!PpuData_2007 = $2007
|
||||
|
||||
!LOAD_BANK = $C000
|
||||
|
||||
macro org(address,bank)
|
||||
if <bank> == $0F
|
||||
org <address>-$C000+($4000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
|
||||
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
|
||||
else
|
||||
org <address>-$8000+($4000*<bank>)+!headersize
|
||||
base <address>
|
||||
endif
|
||||
endmacro
|
||||
|
||||
%org($8400, $08)
|
||||
incbin "mm2font.dat"
|
||||
|
||||
%org($A900, $09)
|
||||
incbin "mm2titlefont.dat"
|
||||
|
||||
%org($807E, $0B)
|
||||
FlashFixes:
|
||||
CMP #$FF
|
||||
BEQ FlashFixTarget1
|
||||
CMP #$FF
|
||||
BNE FlashFixTarget2
|
||||
|
||||
%org($8086, $0B)
|
||||
FlashFixTarget1:
|
||||
|
||||
%org($808D, $0B)
|
||||
FlashFixTarget2:
|
||||
|
||||
%org($8015, $0D)
|
||||
ClearRefreshHook:
|
||||
; if we're already doing a fresh load of the stage select
|
||||
; we don't need to immediately refresh it
|
||||
JSR ClearRefresh
|
||||
NOP
|
||||
|
||||
%org($802B, $0D)
|
||||
PatchFaceTiles:
|
||||
LDA !received_stages
|
||||
|
||||
%org($8072, $0D)
|
||||
PatchFaceSprites:
|
||||
LDA !received_stages
|
||||
|
||||
%org($80CC, $0D)
|
||||
CheckItemsForWily:
|
||||
LDA !received_items
|
||||
CMP #$07
|
||||
|
||||
%org($80D2, $0D)
|
||||
LoadWily:
|
||||
JSR GoToMostRecentWily
|
||||
NOP
|
||||
|
||||
%org($80DC, $0D)
|
||||
CheckAccessCodes:
|
||||
LDA !received_stages
|
||||
|
||||
%org($8312, $0D)
|
||||
HookStageSelect:
|
||||
JSR RefreshRBMTiles
|
||||
NOP
|
||||
|
||||
%org($A315, $0D)
|
||||
RemoveWeaponClear:
|
||||
NOP
|
||||
NOP
|
||||
NOP
|
||||
NOP
|
||||
|
||||
;Adjust Password select flasher
|
||||
%org($A32A, $0D)
|
||||
LDX #$68
|
||||
|
||||
;Block password input
|
||||
%org($A346, $0D)
|
||||
EOR #$00
|
||||
|
||||
;Remove password text
|
||||
%org($AF3A, $0D)
|
||||
StartHeight:
|
||||
db $AC ; set Start to center
|
||||
|
||||
%org($AF49, $0D)
|
||||
PasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AF6C, $0D)
|
||||
ContinueHeight:
|
||||
db $AB ; split height between 2 remaining options
|
||||
|
||||
%org($AF77, $0D)
|
||||
StageSelectHeight:
|
||||
db $EB ; split between 2 remaining options
|
||||
|
||||
%org($AF88, $0D)
|
||||
GameOverPasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AFA5, $0D)
|
||||
GetEquippedPasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AFAE, $0D)
|
||||
GetEquippedStageSelect:
|
||||
db $26, $EA
|
||||
|
||||
%org($B195, $0D)
|
||||
GameOverPasswordUp:
|
||||
LDA #$01 ; originally 02, removing last option
|
||||
|
||||
%org($B19F, $0D)
|
||||
GameOverPassword:
|
||||
CMP #$02 ; originally 03, remove the last option
|
||||
|
||||
%org($B1ED, $0D)
|
||||
FixupGameOverArrows:
|
||||
db $68, $78
|
||||
|
||||
%org($BB74, $0D)
|
||||
GetEquippedStage:
|
||||
JSR StageGetEquipped
|
||||
NOP #13
|
||||
|
||||
%org($BBD9, $0D)
|
||||
GetEquippedDefault:
|
||||
LDA #$01
|
||||
|
||||
%org($BC01, $0D)
|
||||
GetEquippedPasswordRemove:
|
||||
ORA #$01 ; originally EOR #$01, we always want 1 here
|
||||
|
||||
%org($BCF1, $0D)
|
||||
GetEquippedItem:
|
||||
ADC #$07
|
||||
JSR ItemGetEquipped
|
||||
JSR LoadItemsColor
|
||||
NOP ; !!!! This is a load-bearing NOP. It gets branched to later in the function
|
||||
LDX $FF
|
||||
|
||||
|
||||
%org($BB08, $0D)
|
||||
WilyProgress:
|
||||
JSR StoreWilyProgress
|
||||
NOP
|
||||
|
||||
%org($BF6F, $0D)
|
||||
GetEquippedStageSelectHeight:
|
||||
db $B8
|
||||
|
||||
%org($805B, $0E)
|
||||
InitalizeStartingRBM:
|
||||
LDA #$FF ; this does two things
|
||||
STA !received_stages ; we're overwriting clearing e-tanks and setting RBM available to none
|
||||
|
||||
%org($8066, $0E)
|
||||
BlockStartupAutoWily:
|
||||
; presumably this would be called from password?
|
||||
LDA #$00
|
||||
|
||||
%org($80A7, $0E)
|
||||
StageLoad:
|
||||
JMP CleanWily5
|
||||
NOP
|
||||
|
||||
%org($8178, $0E)
|
||||
Main1:
|
||||
JSR MainLoopHook
|
||||
NOP
|
||||
|
||||
%org($81DE, $0E)
|
||||
Wily5Teleporter:
|
||||
LDA $99
|
||||
CMP #$01
|
||||
BCC SkipSpawn
|
||||
|
||||
%org($81F9, $0E)
|
||||
SkipSpawn:
|
||||
; just present to fix the branch, if we try to branch raw it'll get confused
|
||||
|
||||
%org($822D, $0E)
|
||||
Main2:
|
||||
; believe used in the wily 5 refights?
|
||||
JSR MainLoopHook
|
||||
NOP
|
||||
|
||||
%org($842F, $0E)
|
||||
Wily5Hook:
|
||||
JMP Wily5Requirement
|
||||
NOP
|
||||
|
||||
%org($C10D, $0F)
|
||||
Deathlink:
|
||||
JSR KillMegaMan
|
||||
|
||||
%org($C1BC, $0F)
|
||||
RemoveETankLoss:
|
||||
NOP
|
||||
NOP
|
||||
|
||||
%org($C23C, $0F)
|
||||
WriteStageComplete:
|
||||
ORA !completed_stages
|
||||
STA !completed_stages
|
||||
|
||||
%org($C243, $0F)
|
||||
WriteReceiveItem:
|
||||
ORA !received_item_checks
|
||||
STA !received_item_checks
|
||||
|
||||
%org($C254, $0F)
|
||||
BlockAutoWily:
|
||||
; and this one is on return from stage?
|
||||
LDA #$00
|
||||
|
||||
%org($C261, $0F)
|
||||
WilyStageCompletion:
|
||||
JSR StoreWilyStageCompletion
|
||||
NOP
|
||||
|
||||
%org($E5AC, $0F)
|
||||
NullDeathlink:
|
||||
STA $8F ; we null his HP later in the process
|
||||
NOP
|
||||
|
||||
%org($E5D1, $0F)
|
||||
EnergylinkHook:
|
||||
JSR Energylink
|
||||
NOP #2 ; comment this out to enable item giving their usual reward alongside EL
|
||||
|
||||
%org($E5E8, $0F)
|
||||
ConsumableHook:
|
||||
JSR CheckConsumable
|
||||
|
||||
%org($F2E3, $0F)
|
||||
|
||||
CheckConsumable:
|
||||
STA $0140, Y
|
||||
TXA
|
||||
PHA
|
||||
LDA $AD ; the consumable value
|
||||
CMP #$7C
|
||||
BPL .Store
|
||||
print "Consumables (replace 7a): ", hex(realbase())
|
||||
CMP #$76
|
||||
BMI .Store
|
||||
LDA #$00
|
||||
.Store:
|
||||
STA $AD
|
||||
LDA $2A
|
||||
ASL
|
||||
ASL
|
||||
TAX
|
||||
TYA
|
||||
.LoopHead:
|
||||
CMP #$08
|
||||
BMI .GetFlag
|
||||
INX
|
||||
SBC #$08
|
||||
BNE .LoopHead
|
||||
.GetFlag:
|
||||
TAY
|
||||
LDA #$01
|
||||
.Loop2Head:
|
||||
CPY #$00
|
||||
BEQ .Apply
|
||||
ASL
|
||||
DEY
|
||||
BNE .Loop2Head
|
||||
.Apply:
|
||||
ORA !consumable_checks, X
|
||||
STA !consumable_checks, X
|
||||
PLA
|
||||
TAX
|
||||
RTS
|
||||
|
||||
GoToMostRecentWily:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_SELECT_START
|
||||
BEQ .Default
|
||||
LDA !last_wily
|
||||
BNE .Store
|
||||
.Default:
|
||||
LDA #$08 ; wily stage 1
|
||||
.Store:
|
||||
STA !current_stage
|
||||
RTS
|
||||
|
||||
StoreWilyStageCompletion:
|
||||
LDA #$01
|
||||
STA !stage_completion, X
|
||||
INC !current_stage
|
||||
LDA !current_stage
|
||||
STA !last_wily
|
||||
RTS
|
||||
|
||||
ReturnToGameOver:
|
||||
LDA #$10
|
||||
STA !PpuControl_2000
|
||||
LDA #$06
|
||||
STA !PpuMask_2001
|
||||
JMP $C1BE ; specific code that loads game over
|
||||
|
||||
MainLoopHook:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_ALL_BUTTON
|
||||
BNE .Next
|
||||
JMP ReturnToGameOver
|
||||
.Next:
|
||||
LDA !deathlink
|
||||
CMP #$01
|
||||
BNE .Next2
|
||||
JMP $E5A8 ; this kills the Mega Man
|
||||
.Next2:
|
||||
print "Quickswap:", hex(realbase())
|
||||
LDA #$00 ; slot data, write in enable for quickswap
|
||||
CMP #$01
|
||||
BNE .Finally
|
||||
LDA !controller_flip
|
||||
AND !CONTROLLER_SELECT
|
||||
BEQ .Finally
|
||||
JMP Quickswap
|
||||
.Finally:
|
||||
LDA !controller_flip
|
||||
AND #$08 ; this is checking for menu
|
||||
RTS
|
||||
|
||||
StoreWilyProgress:
|
||||
STA !current_stage
|
||||
TXA
|
||||
PHA
|
||||
LDX !current_stage
|
||||
LDA #$01
|
||||
STA !stage_completion, X
|
||||
PLA
|
||||
TAX
|
||||
print "Get Equipped Music: ", hex(realbase())
|
||||
LDA #$17
|
||||
RTS
|
||||
|
||||
KillMegaMan:
|
||||
JSR $C051 ; this kills the mega man
|
||||
LDA #$00
|
||||
STA $06C0 ; set HP to zero so client can actually detect he died
|
||||
RTS
|
||||
|
||||
Wily5Requirement:
|
||||
LDA #$01
|
||||
LDX #$08
|
||||
LDY #$00
|
||||
.LoopHead:
|
||||
BIT $BC
|
||||
BEQ .Skip
|
||||
INY
|
||||
.Skip:
|
||||
DEX
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .LoopHead
|
||||
print "Wily 5 Requirement:", hex(realbase())
|
||||
CPY #$08
|
||||
BCS .SpawnTeleporter
|
||||
JMP $8450
|
||||
.SpawnTeleporter:
|
||||
LDA #$FF
|
||||
STA $BC
|
||||
LDA #$01
|
||||
STA $99
|
||||
JMP $8433
|
||||
|
||||
CleanWily5:
|
||||
LDA #$00
|
||||
STA $BC
|
||||
STA $99
|
||||
JMP $80AB
|
||||
|
||||
LoadString:
|
||||
STY $00
|
||||
ASL
|
||||
ASL
|
||||
ASL
|
||||
ASL
|
||||
TAY
|
||||
LDA $DB
|
||||
ADC #$00
|
||||
STA $C8
|
||||
LDA #$40
|
||||
STA $C9
|
||||
LDA #$F6
|
||||
CLC
|
||||
ADC $C8
|
||||
STA $CA
|
||||
LDA ($C9), Y
|
||||
STA $03B6
|
||||
TYA
|
||||
CLC
|
||||
ADC #$01
|
||||
TAY
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
LDA ($C9), Y
|
||||
STA $03B7
|
||||
TYA
|
||||
CLC
|
||||
ADC #$01
|
||||
TAY
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
STY $FE
|
||||
LDA #$0E
|
||||
STA $FD
|
||||
.LoopHead:
|
||||
JSR $BD34
|
||||
LDY $FE
|
||||
CPY #$40
|
||||
BNE .NotEqual
|
||||
LDA $0420
|
||||
BNE .Skip
|
||||
.NotEqual:
|
||||
LDA ($C9), Y
|
||||
.Skip:
|
||||
STA $03B8
|
||||
INC $47
|
||||
INC $03B7
|
||||
LDA $FE
|
||||
CLC
|
||||
ADC #$01
|
||||
STA $FE
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
DEC $FD
|
||||
BNE .LoopHead
|
||||
LDY $00
|
||||
JSR $C0AB
|
||||
RTS
|
||||
|
||||
StageGetEquipped:
|
||||
LDA !current_stage
|
||||
LDX #$00
|
||||
BCS LoadGetEquipped
|
||||
ItemGetEquipped:
|
||||
LDX #$02
|
||||
LoadGetEquipped:
|
||||
STX $DB
|
||||
ASL
|
||||
ASL
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
PLA
|
||||
ADC #$00
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
PLA
|
||||
ADC #$00
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
LDA #$00
|
||||
SEC
|
||||
JSR $BD3E
|
||||
PLA
|
||||
ADC #$00
|
||||
SEC
|
||||
JSR LoadString
|
||||
RTS
|
||||
|
||||
LoadItemsColor:
|
||||
LDA #$7D
|
||||
STA $FD
|
||||
LDA $0420
|
||||
AND #$0F
|
||||
ASL
|
||||
SEC
|
||||
ADC #$1A
|
||||
STA $FF
|
||||
RTS
|
||||
|
||||
Energylink:
|
||||
LSR $0420, X
|
||||
print "Energylink: ", hex(realbase())
|
||||
LDA #$00
|
||||
BEQ .ApplyDrop
|
||||
LDA $04E0, X
|
||||
BEQ .ApplyDrop ; This is a stage pickup, and not an enemy drop
|
||||
STY !energylink_packet
|
||||
SEC
|
||||
BCS .Return
|
||||
.ApplyDrop:
|
||||
STY $AD
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
|
||||
Quickswap:
|
||||
LDX #$0F
|
||||
.LoopHead:
|
||||
LDA $0420, X
|
||||
BMI .Return1 ; return if we have any weapon entities spawned
|
||||
DEX
|
||||
CPX #$01
|
||||
BNE .LoopHead
|
||||
LDX !current_weapon
|
||||
BNE .DoQuickswap
|
||||
LDX #$00
|
||||
.DoQuickswap:
|
||||
TYA
|
||||
PHA
|
||||
LDX !current_weapon
|
||||
INX
|
||||
CPX #$09
|
||||
BPL .Items
|
||||
LDA #$01
|
||||
.Loop2Head:
|
||||
DEX
|
||||
BEQ .FoundTarget
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .Loop2Head
|
||||
.FoundTarget:
|
||||
LDX !current_weapon
|
||||
INX
|
||||
.Loop3Head:
|
||||
PHA
|
||||
AND !received_weapons
|
||||
BNE .CanSwap
|
||||
PLA
|
||||
INX
|
||||
CPX #$09
|
||||
BPL .Items
|
||||
ASL
|
||||
BNE .Loop3Head
|
||||
.CanSwap:
|
||||
PLA
|
||||
SEC
|
||||
BCS .ApplySwap
|
||||
.Items:
|
||||
TXA
|
||||
PHA
|
||||
SEC
|
||||
SBC #$08
|
||||
TAX
|
||||
LDA #$01
|
||||
.Loop4Head:
|
||||
DEX
|
||||
BEQ .CheckItem
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .Loop4Head
|
||||
.CheckItem:
|
||||
TAY
|
||||
PLA
|
||||
TAX
|
||||
TYA
|
||||
.Loop5Head:
|
||||
PHA
|
||||
AND !received_items
|
||||
BNE .CanSwap
|
||||
PLA
|
||||
INX
|
||||
ASL
|
||||
BNE .Loop5Head
|
||||
LDX #$00
|
||||
SEC
|
||||
BCS .ApplySwap
|
||||
.Return1:
|
||||
RTS
|
||||
.ApplySwap: ; $F408 on old rom
|
||||
LDA #$0D
|
||||
JSR !LOAD_BANK
|
||||
; this is a bunch of boiler plate to make the swap work
|
||||
LDA $B5
|
||||
PHA
|
||||
LDA $B6
|
||||
PHA
|
||||
LDA $B7
|
||||
PHA
|
||||
LDA $B8
|
||||
PHA
|
||||
LDA $B9
|
||||
PHA
|
||||
LDA $20
|
||||
PHA
|
||||
LDA $1F
|
||||
PHA
|
||||
;but wait, there's more
|
||||
STX !current_weapon
|
||||
JSR $CC6C
|
||||
LDA $1A
|
||||
PHA
|
||||
LDX #$00
|
||||
.Loop6Head:
|
||||
STX $FD
|
||||
CLC
|
||||
LDA $52
|
||||
ADC $957F, X
|
||||
STA $08
|
||||
LDA $53
|
||||
ADC #$00
|
||||
STA $09
|
||||
LDA $08
|
||||
LSR $09
|
||||
ROR
|
||||
LSR $09
|
||||
ROR
|
||||
STA $08
|
||||
AND #$3F
|
||||
STA $1A
|
||||
CLC
|
||||
LDA $09
|
||||
ADC #$85
|
||||
STA $09
|
||||
LDA #$00
|
||||
STA $1B
|
||||
LDA $FD
|
||||
CMP #$08
|
||||
BCS .Past8
|
||||
LDX $A9
|
||||
LDA $9664, X
|
||||
TAY
|
||||
CPX #$09
|
||||
BCC .LessThanNine
|
||||
LDX #$00
|
||||
BEQ .Apply
|
||||
.LessThanNine:
|
||||
LDX #$05
|
||||
BNE .Apply
|
||||
.Past8:
|
||||
LDY #$90
|
||||
LDX #$00
|
||||
.Apply:
|
||||
JSR $C760
|
||||
JSR $C0AB ; iirc this is loading graphics?
|
||||
LDX $FD
|
||||
INX
|
||||
CPX #$0F
|
||||
BNE .Loop6Head
|
||||
STX $FD
|
||||
LDY #$90
|
||||
LDX #$00
|
||||
JSR $C760
|
||||
JSR $D2ED
|
||||
; two sections redacted here, might need to look at what they actually do?
|
||||
PLA
|
||||
STA $1A
|
||||
PLA
|
||||
STA $1F
|
||||
PLA
|
||||
STA $20
|
||||
PLA
|
||||
STA $B9
|
||||
PLA
|
||||
STA $B8
|
||||
PLA
|
||||
STA $B7
|
||||
PLA
|
||||
STA $B6
|
||||
PLA
|
||||
STA $B5
|
||||
LDA #$00
|
||||
STA $AC
|
||||
STA $2C
|
||||
STA $0680
|
||||
STA $06A0
|
||||
LDA #$1A
|
||||
STA $0400
|
||||
LDA #$03
|
||||
STA $AA
|
||||
LDA #$30
|
||||
JSR $C051
|
||||
.Finally:
|
||||
LDA #$0E
|
||||
JSR !LOAD_BANK
|
||||
PLA
|
||||
TAY
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
RefreshRBMTiles:
|
||||
; primarily just a copy of the startup RBM setup, we just do it again
|
||||
; can't jump to it as it leads into the main loop
|
||||
LDA !rbm_strobe
|
||||
BNE .Update
|
||||
JMP .NoUpdate
|
||||
.Update:
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
LDA #$10
|
||||
STA $F7
|
||||
STA !PpuControl_2000
|
||||
LDA #$06
|
||||
STA $F8
|
||||
STA !PpuMask_2001
|
||||
JSR $847E
|
||||
JSR $843C
|
||||
LDX #$00
|
||||
LDA $8A
|
||||
STA $01
|
||||
.TileLoop:
|
||||
STX $00
|
||||
LSR $01
|
||||
BCC .SkipTile
|
||||
LDA $8531,X
|
||||
STA $09
|
||||
LDA $8539,X
|
||||
STA $08
|
||||
LDX #$04
|
||||
LDA #$00
|
||||
.ClearBody:
|
||||
LDA $09
|
||||
STA !PpuAddr_2006
|
||||
LDA $08
|
||||
STA !PpuAddr_2006
|
||||
LDY #$04
|
||||
LDA #$00
|
||||
.ClearLine:
|
||||
STA !PpuData_2007
|
||||
DEY
|
||||
BNE .ClearLine
|
||||
CLC
|
||||
LDA $08
|
||||
ADC #$20
|
||||
STA $08
|
||||
DEX
|
||||
BNE .ClearBody
|
||||
.SkipTile:
|
||||
LDX $00
|
||||
INX
|
||||
CPX #$08
|
||||
BNE .TileLoop
|
||||
LDX #$1F
|
||||
JSR $829E
|
||||
JSR $8473
|
||||
LDX #$00
|
||||
LDA $8A
|
||||
STA $02
|
||||
LDY #$00
|
||||
.SpriteLoop:
|
||||
STX $01
|
||||
LSR $02
|
||||
BCS .SkipRBM
|
||||
LDA $8605,X
|
||||
STA $00
|
||||
LDA $85FD,X
|
||||
TAX
|
||||
.WriteSprite:
|
||||
LDA $8541,X
|
||||
STA $0200,Y
|
||||
INY
|
||||
INX
|
||||
DEC $00
|
||||
BNE .WriteSprite
|
||||
.SkipRBM:
|
||||
LDX $01
|
||||
INX
|
||||
CPX #$08
|
||||
BNE .SpriteLoop
|
||||
JSR $A51D
|
||||
LDA #$0C
|
||||
JSR $C051
|
||||
LDA #$00
|
||||
STA $2A
|
||||
STA $FD
|
||||
JSR $C0AB
|
||||
.NoUpdate:
|
||||
LDA $1C
|
||||
AND #$08
|
||||
RTS
|
||||
|
||||
ClearRefresh:
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
LDA #$10
|
||||
STA $F7
|
||||
RTS
|
||||
|
||||
assert realbase() <= $03F650 ; This is the start of our text data, and we absolutely cannot go past this point (text takes too much room).
|
||||
|
||||
%org($F640, $0F)
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
|
||||
%org($FFB0, $0F)
|
||||
db "MM2_BASEPATCH_ARCHI "
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MM2TestBase(WorldTestBase):
|
||||
game = "Mega Man 2"
|
|
@ -0,0 +1,47 @@
|
|||
from . import MM2TestBase
|
||||
from ..locations import (quick_man_locations, heat_man_locations, wily_1_locations, wily_2_locations,
|
||||
wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations,
|
||||
energy_pickups, etank_1ups)
|
||||
from ..names import *
|
||||
|
||||
|
||||
class TestAccess(MM2TestBase):
|
||||
options = {
|
||||
"consumables": "all"
|
||||
}
|
||||
|
||||
def test_time_stopper(self) -> None:
|
||||
"""Optional based on Enable Lasers setting, confirm these are the locations affected"""
|
||||
locations = [*quick_man_locations, *energy_pickups["Quick Man Stage"], *etank_1ups["Quick Man Stage"]]
|
||||
items = [["Time Stopper"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def test_item_2(self) -> None:
|
||||
"""Optional based on Yoku Block setting, confirm these are the locations affected"""
|
||||
locations = [*heat_man_locations, *etank_1ups["Heat Man Stage"]]
|
||||
items = [["Item 2 - Rocket"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
|
||||
def test_any_item(self) -> None:
|
||||
locations = [flash_man_c2, quick_man_c1, crash_man_c3]
|
||||
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"], ["Item 3 - Bouncy"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
locations = [metal_man_c2, metal_man_c3]
|
||||
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
|
||||
def test_all_items(self) -> None:
|
||||
locations = [flash_man_c2, quick_man_c1, crash_man_c3, metal_man_c2, metal_man_c3, *heat_man_locations,
|
||||
*etank_1ups["Heat Man Stage"], *wily_1_locations, *wily_2_locations, *wily_3_locations,
|
||||
*wily_4_locations, *wily_5_locations, *wily_6_locations, *etank_1ups["Wily Stage 1"],
|
||||
*etank_1ups["Wily Stage 2"], *etank_1ups["Wily Stage 3"], *etank_1ups["Wily Stage 4"],
|
||||
*energy_pickups["Wily Stage 1"], *energy_pickups["Wily Stage 2"], *energy_pickups["Wily Stage 3"],
|
||||
*energy_pickups["Wily Stage 4"]]
|
||||
items = [["Item 1 - Propeller", "Item 2 - Rocket", "Item 3 - Bouncy"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def test_crash_bomber(self) -> None:
|
||||
locations = [flash_man_c3, flash_man_c4, wily_2_c5, wily_2_c6, wily_3_c1, wily_3_c2,
|
||||
wily_4, wily_stage_4]
|
||||
items = [["Crash Bomber"]]
|
||||
self.assertAccessDependency(locations, items)
|
|
@ -0,0 +1,93 @@
|
|||
from math import ceil
|
||||
|
||||
from . import MM2TestBase
|
||||
from ..options import bosses
|
||||
|
||||
|
||||
# Need to figure out how this test should work
|
||||
def validate_wily_5(base: MM2TestBase) -> None:
|
||||
world = base.multiworld.worlds[base.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
boss_health = {boss: 0x1C for boss in [*list(range(8)), 12]}
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 10,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 0.5,
|
||||
5: 0.125,
|
||||
6: 4,
|
||||
7: 0.25,
|
||||
8: 7,
|
||||
}
|
||||
weapon_energy = {key: float(0x1C * 2) if key == 12 else float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: weapon_damage[weapon][boss] for weapon in weapon_damage}
|
||||
for boss in [*list(range(8)), 12]}
|
||||
flexibility = [(sum(1 if weapon_boss[boss][weapon] > 0 else 0 for weapon in range(9)) *
|
||||
sum(weapon_boss[boss].values()), boss) for boss in weapon_boss if boss != 12]
|
||||
for _, boss in [*sorted(flexibility), (0, 12)]:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon]}
|
||||
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
|
||||
# We get exactly one use of Time Stopper during the rush
|
||||
# So we want to make sure that use is absolutely needed
|
||||
weapon_weight[8] = min(weapon_weight[8], 0.001)
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0]:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
base.fail(f"Ran out of weapon energy to damage "
|
||||
f"{next(name for name in bosses if bosses[name] == boss)}\n"
|
||||
f"Seed: {base.multiworld.seed}\n"
|
||||
f"Damage Table: {weapon_damage}")
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
|
||||
|
||||
class StrictWeaknessTests(MM2TestBase):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"yoku_jumps": True,
|
||||
"enable_lasers": True
|
||||
}
|
||||
|
||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||
world = self.multiworld.worlds[self.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
for boss in range(14):
|
||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_5(self) -> None:
|
||||
validate_wily_5(self)
|
||||
|
||||
|
||||
class RandomStrictWeaknessTests(MM2TestBase):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "randomized",
|
||||
"yoku_jumps": True,
|
||||
"enable_lasers": True
|
||||
}
|
||||
|
||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||
world = self.multiworld.worlds[self.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
for boss in range(14):
|
||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_5(self) -> None:
|
||||
validate_wily_5(self)
|
|
@ -0,0 +1,90 @@
|
|||
from typing import DefaultDict
|
||||
from collections import defaultdict
|
||||
|
||||
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, {
|
||||
' ': 0x40,
|
||||
'A': 0x41,
|
||||
'B': 0x42,
|
||||
'C': 0x43,
|
||||
'D': 0x44,
|
||||
'E': 0x45,
|
||||
'F': 0x46,
|
||||
'G': 0x47,
|
||||
'H': 0x48,
|
||||
'I': 0x49,
|
||||
'J': 0x4A,
|
||||
'K': 0x4B,
|
||||
'L': 0x4C,
|
||||
'M': 0x4D,
|
||||
'N': 0x4E,
|
||||
'O': 0x4F,
|
||||
'P': 0x50,
|
||||
'Q': 0x51,
|
||||
'R': 0x52,
|
||||
'S': 0x53,
|
||||
'T': 0x54,
|
||||
'U': 0x55,
|
||||
'V': 0x56,
|
||||
'W': 0x57,
|
||||
'X': 0x58,
|
||||
'Y': 0x59,
|
||||
'Z': 0x5A,
|
||||
# 0x5B is the small r in Dr Light
|
||||
'.': 0x5C,
|
||||
',': 0x5D,
|
||||
'\'': 0x5E,
|
||||
'!': 0x5F,
|
||||
'(': 0x60,
|
||||
')': 0x61,
|
||||
'#': 0x62,
|
||||
'$': 0x63,
|
||||
'%': 0x64,
|
||||
'&': 0x65,
|
||||
'*': 0x66,
|
||||
'+': 0x67,
|
||||
'/': 0x68,
|
||||
'\\': 0x69,
|
||||
':': 0x6A,
|
||||
';': 0x6B,
|
||||
'<': 0x6C,
|
||||
'>': 0x6D,
|
||||
'=': 0x6E,
|
||||
'?': 0x6F,
|
||||
'@': 0x70,
|
||||
'[': 0x71,
|
||||
']': 0x72,
|
||||
'^': 0x73,
|
||||
'_': 0x74,
|
||||
'`': 0x75,
|
||||
'{': 0x76,
|
||||
'}': 0x77,
|
||||
'|': 0x78,
|
||||
'~': 0x79,
|
||||
'\"': 0x92,
|
||||
'-': 0x94,
|
||||
'0': 0xA0,
|
||||
'1': 0xA1,
|
||||
'2': 0xA2,
|
||||
'3': 0xA3,
|
||||
'4': 0xA4,
|
||||
'5': 0xA5,
|
||||
'6': 0xA6,
|
||||
'7': 0xA7,
|
||||
'8': 0xA8,
|
||||
'9': 0xA9,
|
||||
})
|
||||
|
||||
|
||||
class MM2TextEntry:
|
||||
def __init__(self, text: str = "", coords: int = 0x0B):
|
||||
self.target_area: int = 0x25 # don't change
|
||||
self.coords: int = coords # 0xYX, Y can only be increments of 0x20
|
||||
self.text: str = text
|
||||
|
||||
def resolve(self) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(self.target_area)
|
||||
data.append(self.coords)
|
||||
data.extend([MM2_WEAPON_ENCODING[x] for x in self.text.upper()])
|
||||
data.extend([0x40] * (14 - len(self.text)))
|
||||
return bytes(data)
|
Loading…
Reference in New Issue