mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 17:33:20 -07:00
Mega Man 3: Implement new game (#5237)
This commit is contained in:
@@ -85,6 +85,7 @@ Currently, the following games are supported:
|
||||
* APQuest
|
||||
* Satisfactory
|
||||
* EarthBound
|
||||
* Mega Man 3
|
||||
|
||||
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
|
||||
|
||||
@@ -134,6 +134,9 @@
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
# Mega Man 3
|
||||
/worlds/mm3/ @Silvris
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
|
||||
@@ -213,6 +213,11 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
1
worlds/mm3/.apignore
Normal file
1
worlds/mm3/.apignore
Normal file
@@ -0,0 +1 @@
|
||||
/src/*
|
||||
275
worlds/mm3/__init__.py
Normal file
275
worlds/mm3/__init__.py
Normal file
@@ -0,0 +1,275 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Sequence, ClassVar
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .names import (gamma, gemini_man_stage, needle_man_stage, hard_man_stage, magnet_man_stage, top_man_stage,
|
||||
snake_man_stage, spark_man_stage, shadow_man_stage, rush_marine, rush_jet, rush_coil)
|
||||
from .items import (item_table, item_names, MM3Item, filler_item_weights, robot_master_weapon_table,
|
||||
stage_access_table, rush_item_table, lookup_item_to_id)
|
||||
from .locations import (MM3Location, mm3_regions, MM3Region, lookup_location_to_id,
|
||||
location_groups)
|
||||
from .rom import patch_rom, MM3ProcedurePatch, MM3LCHASH, MM3VCHASH, PROTEUSHASH, MM3NESHASH
|
||||
from .options import MM3Options, Consumables
|
||||
from .client import MegaMan3Client
|
||||
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 3")
|
||||
|
||||
|
||||
class MM3Settings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the MM3 EN rom"""
|
||||
description = "Mega Man 3 ROM File"
|
||||
copy_to: str | None = "Mega Man 3 (USA).nes"
|
||||
md5s = [MM3NESHASH, MM3LCHASH, PROTEUSHASH, MM3VCHASH]
|
||||
|
||||
def browse(self: settings.T,
|
||||
filetypes: Sequence[tuple[str, Sequence[str]]] | None = None,
|
||||
**kwargs: Any) -> settings.T | None:
|
||||
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 MM3WebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
|
||||
Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Mega Man 3 randomizer connected to an Archipelago Multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Silvris"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class MM3World(World):
|
||||
"""
|
||||
Following his second defeat by Mega Man, Dr. Wily has finally come to his senses. He and Dr. Light begin work on
|
||||
Gamma, a giant peacekeeping robot. However, Gamma's power source, the Energy Elements, are being guarded by the
|
||||
Robot Masters sent to retrieve them. It's up to Mega Man to retrieve the Energy Elements and defeat the mastermind
|
||||
behind the Robot Masters' betrayal.
|
||||
"""
|
||||
|
||||
game = "Mega Man 3"
|
||||
settings: ClassVar[MM3Settings]
|
||||
options_dataclass = MM3Options
|
||||
options: MM3Options
|
||||
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 = MM3WebWorld()
|
||||
rom_name: bytearray
|
||||
|
||||
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_4_weapons: dict[int, list[int]] = {}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu = MM3Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu)
|
||||
location: MM3Location
|
||||
for name, region in mm3_regions.items():
|
||||
stage = MM3Region(name, self.player, self.multiworld)
|
||||
if not region.parent:
|
||||
menu.connect(stage, f"To {name}",
|
||||
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
|
||||
else:
|
||||
old_stage = self.get_region(region.parent)
|
||||
old_stage.connect(stage, f"To {name}",
|
||||
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
|
||||
stage.add_locations({loc: data.location_id for loc, data in region.locations.items()
|
||||
if (not data.energy or self.options.consumables.value in (Consumables.option_weapon_health, Consumables.option_all))
|
||||
and (not data.oneup_tank or self.options.consumables.value in (Consumables.option_1up_etank, Consumables.option_all))})
|
||||
for location in stage.get_locations():
|
||||
if location.address is None and location.name != gamma:
|
||||
location.place_locked_item(MM3Item(location.name, ItemClassification.progression,
|
||||
None, self.player))
|
||||
self.multiworld.regions.append(stage)
|
||||
goal_location = self.get_location(gamma)
|
||||
goal_location.place_locked_item(MM3Item("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def create_item(self, name: str, force_non_progression: bool = False) -> MM3Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression and not force_non_progression:
|
||||
classification = ItemClassification.progression_skip_balancing \
|
||||
if item.skip_balancing else ItemClassification.progression
|
||||
if item.useful:
|
||||
classification |= ItemClassification.useful
|
||||
return MM3Item(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[0x0101 + 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 rush_item_table.keys()])
|
||||
total_checks = 31
|
||||
if self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
total_checks += 33
|
||||
if self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
total_checks += 106
|
||||
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 (self.options.starting_robot_master.current_key == "gemini_man"
|
||||
and not any(item in self.options.start_inventory for item in rush_item_table.keys())) or \
|
||||
(self.options.starting_robot_master.current_key == "hard_man"
|
||||
and not any(item in self.options.start_inventory for item in [rush_coil, rush_jet])):
|
||||
robot_master_pool = [0, 1, 4, 5, 6, 7, ]
|
||||
if rush_marine in self.options.start_inventory:
|
||||
robot_master_pool.append(2)
|
||||
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
|
||||
logger.warning(
|
||||
f"Incompatible starting Robot Master, changing to "
|
||||
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
|
||||
|
||||
def fill_hook(self,
|
||||
prog_item_pool: list["Item"],
|
||||
useful_item_pool: list["Item"],
|
||||
filler_item_pool: 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
|
||||
# MM3 is worse than MM2 here, some of the RBMs can also require Rush
|
||||
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: needle_man_stage,
|
||||
1: magnet_man_stage,
|
||||
2: gemini_man_stage,
|
||||
3: hard_man_stage,
|
||||
4: top_man_stage,
|
||||
5: snake_man_stage,
|
||||
6: spark_man_stage,
|
||||
7: shadow_man_stage
|
||||
}
|
||||
affected_rbm = [2, 3] # Gemini and Hard will always have this happen
|
||||
possible_rbm = [0, 7] # Needle and Shadow are always valid targets, due to Rush Marine/Jet receive
|
||||
if self.options.consumables:
|
||||
possible_rbm.extend([4, 5]) # every stage has at least one of each consumable
|
||||
if self.options.consumables in (Consumables.option_weapon_health, Consumables.option_all):
|
||||
possible_rbm.extend([1, 6])
|
||||
else:
|
||||
affected_rbm.extend([1, 6])
|
||||
else:
|
||||
affected_rbm.extend([1, 4, 5, 6]) # only two checks on non consumables
|
||||
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 prog_item_pool
|
||||
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)
|
||||
prog_item_pool.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 prog_item_pool
|
||||
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 == 0x0101 + self.options.starting_robot_master.value)
|
||||
weapon_location = self.get_location(weapon_name)
|
||||
weapon_location.place_locked_item(placed_weapon)
|
||||
prog_item_pool.remove(placed_weapon)
|
||||
fill_locations.remove(weapon_location)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
patch = MM3ProcedurePatch(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_4_weapons": self.wily_4_weapons
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def interpret_slot_data(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_4_weapons"].items()}
|
||||
return {"weapon_damage": local_weapon, "wily_4_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]
|
||||
6
worlds/mm3/archipelago.json
Normal file
6
worlds/mm3/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Mega Man 3",
|
||||
"authors": ["Silvris"],
|
||||
"world_version": "0.1.7",
|
||||
"minimum_ap_version": "0.6.4"
|
||||
}
|
||||
783
worlds/mm3/client.py
Normal file
783
worlds/mm3/client.py
Normal file
@@ -0,0 +1,783 @@
|
||||
import logging
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING, 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")
|
||||
|
||||
MM3_CURRENT_STAGE = 0x22
|
||||
MM3_MEGAMAN_STATE = 0x30
|
||||
MM3_PROG_STATE = 0x60
|
||||
MM3_ROBOT_MASTERS_DEFEATED = 0x61
|
||||
MM3_DOC_STATUS = 0x62
|
||||
MM3_HEALTH = 0xA2
|
||||
MM3_WEAPON_ENERGY = 0xA3
|
||||
MM3_WEAPONS = {
|
||||
1: 1,
|
||||
2: 3,
|
||||
3: 0,
|
||||
4: 2,
|
||||
5: 4,
|
||||
6: 5,
|
||||
7: 7,
|
||||
8: 9,
|
||||
0x11: 6,
|
||||
0x12: 8,
|
||||
0x13: 10,
|
||||
}
|
||||
|
||||
MM3_DOC_REMAP = {
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 6,
|
||||
5: 7,
|
||||
6: 4,
|
||||
7: 5
|
||||
}
|
||||
MM3_LIVES = 0xAE
|
||||
MM3_E_TANKS = 0xAF
|
||||
MM3_ENERGY_BAR = 0xB2
|
||||
MM3_CONSUMABLES = 0x150
|
||||
MM3_ROBOT_MASTERS_UNLOCKED = 0x680
|
||||
MM3_DOC_ROBOT_UNLOCKED = 0x681
|
||||
MM3_ENERGYLINK = 0x682
|
||||
MM3_LAST_WILY = 0x683
|
||||
MM3_RBM_STROBE = 0x684
|
||||
MM3_SFX_QUEUE = 0x685
|
||||
MM3_DOC_ROBOT_DEFEATED = 0x686
|
||||
MM3_COMPLETED_STAGES = 0x687
|
||||
MM3_RECEIVED_ITEMS = 0x688
|
||||
MM3_RUSH_RECEIVED = 0x689
|
||||
|
||||
MM3_CONSUMABLE_TABLE: dict[int, dict[int, tuple[int, int]]] = {
|
||||
# Stage:
|
||||
# Item: (byte offset, bit mask)
|
||||
0: {
|
||||
0x0200: (0, 5),
|
||||
0x0201: (3, 2),
|
||||
},
|
||||
1: {
|
||||
0x0202: (2, 6),
|
||||
0x0203: (2, 5),
|
||||
0x0204: (2, 4),
|
||||
0x0205: (2, 3),
|
||||
0x0206: (3, 6),
|
||||
0x0207: (3, 5),
|
||||
0x0208: (3, 7),
|
||||
0x0209: (4, 0)
|
||||
},
|
||||
2: {
|
||||
0x020A: (2, 7),
|
||||
0x020B: (3, 0),
|
||||
0x020C: (3, 1),
|
||||
0x020D: (3, 2),
|
||||
0x020E: (4, 2),
|
||||
0x020F: (4, 3),
|
||||
0x0210: (4, 7),
|
||||
0x0211: (5, 1),
|
||||
0x0212: (6, 1),
|
||||
0x0213: (7, 0)
|
||||
},
|
||||
3: {
|
||||
0x0214: (0, 6),
|
||||
0x0215: (1, 5),
|
||||
0x0216: (2, 3),
|
||||
0x0217: (2, 7),
|
||||
0x0218: (2, 6),
|
||||
0x0219: (2, 5),
|
||||
0x021A: (4, 5),
|
||||
},
|
||||
4: {
|
||||
0x021B: (1, 3),
|
||||
0x021C: (1, 5),
|
||||
0x021D: (1, 7),
|
||||
0x021E: (2, 0),
|
||||
0x021F: (1, 6),
|
||||
0x0220: (2, 4),
|
||||
0x0221: (2, 5),
|
||||
0x0222: (4, 5)
|
||||
},
|
||||
5: {
|
||||
0x0223: (3, 0),
|
||||
0x0224: (3, 2),
|
||||
0x0225: (4, 5),
|
||||
0x0226: (4, 6),
|
||||
0x0227: (6, 4),
|
||||
},
|
||||
6: {
|
||||
0x0228: (2, 0),
|
||||
0x0229: (2, 1),
|
||||
0x022A: (3, 1),
|
||||
0x022B: (3, 2),
|
||||
0x022C: (3, 3),
|
||||
0x022D: (3, 4),
|
||||
},
|
||||
7: {
|
||||
0x022E: (3, 5),
|
||||
0x022F: (3, 4),
|
||||
0x0230: (3, 3),
|
||||
0x0231: (3, 2),
|
||||
},
|
||||
8: {
|
||||
0x0232: (1, 4),
|
||||
0x0233: (2, 1),
|
||||
0x0234: (2, 2),
|
||||
0x0235: (2, 5),
|
||||
0x0236: (3, 5),
|
||||
0x0237: (4, 2),
|
||||
0x0238: (4, 4),
|
||||
0x0239: (5, 3),
|
||||
0x023A: (6, 0),
|
||||
0x023B: (6, 1),
|
||||
0x023C: (7, 5),
|
||||
|
||||
},
|
||||
9: {
|
||||
0x023D: (3, 2),
|
||||
0x023E: (3, 6),
|
||||
0x023F: (4, 5),
|
||||
0x0240: (5, 4),
|
||||
},
|
||||
10: {
|
||||
0x0241: (0, 2),
|
||||
0x0242: (2, 4)
|
||||
},
|
||||
11: {
|
||||
0x0243: (4, 1),
|
||||
0x0244: (6, 0),
|
||||
0x0245: (6, 1),
|
||||
0x0246: (6, 2),
|
||||
0x0247: (6, 3),
|
||||
},
|
||||
12: {
|
||||
0x0248: (0, 0),
|
||||
0x0249: (0, 3),
|
||||
0x024A: (0, 5),
|
||||
0x024B: (1, 6),
|
||||
0x024C: (2, 7),
|
||||
0x024D: (2, 3),
|
||||
0x024E: (2, 1),
|
||||
0x024F: (2, 2),
|
||||
0x0250: (3, 5),
|
||||
0x0251: (3, 4),
|
||||
0x0252: (3, 6),
|
||||
0x0253: (3, 7)
|
||||
},
|
||||
13: {
|
||||
0x0254: (0, 3),
|
||||
0x0255: (0, 6),
|
||||
0x0256: (1, 0),
|
||||
0x0257: (3, 0),
|
||||
0x0258: (3, 2),
|
||||
0x0259: (3, 3),
|
||||
0x025A: (3, 4),
|
||||
0x025B: (3, 5),
|
||||
0x025C: (3, 6),
|
||||
0x025D: (4, 0),
|
||||
0x025E: (3, 7),
|
||||
0x025F: (4, 1),
|
||||
0x0260: (4, 2),
|
||||
},
|
||||
14: {
|
||||
0x0261: (0, 3),
|
||||
0x0262: (0, 2),
|
||||
0x0263: (0, 6),
|
||||
0x0264: (1, 2),
|
||||
0x0265: (1, 7),
|
||||
0x0266: (2, 0),
|
||||
0x0267: (2, 1),
|
||||
0x0268: (2, 2),
|
||||
0x0269: (2, 3),
|
||||
0x026A: (5, 2),
|
||||
0x026B: (5, 3),
|
||||
},
|
||||
15: {
|
||||
0x026C: (0, 0),
|
||||
0x026D: (0, 1),
|
||||
0x026E: (0, 2),
|
||||
0x026F: (0, 3),
|
||||
0x0270: (0, 4),
|
||||
0x0271: (0, 6),
|
||||
0x0272: (1, 0),
|
||||
0x0273: (1, 2),
|
||||
0x0274: (1, 3),
|
||||
0x0275: (1, 1),
|
||||
0x0276: (0, 7),
|
||||
0x0277: (3, 2),
|
||||
0x0278: (2, 2),
|
||||
0x0279: (2, 3),
|
||||
0x027A: (2, 4),
|
||||
0x027B: (2, 5),
|
||||
0x027C: (3, 1),
|
||||
0x027D: (3, 0),
|
||||
0x027E: (2, 7),
|
||||
0x027F: (2, 6),
|
||||
},
|
||||
16: {
|
||||
0x0280: (0, 0),
|
||||
0x0281: (0, 3),
|
||||
0x0282: (0, 1),
|
||||
0x0283: (0, 2),
|
||||
},
|
||||
17: {
|
||||
0x0284: (0, 2),
|
||||
0x0285: (0, 6),
|
||||
0x0286: (0, 1),
|
||||
0x0287: (0, 5),
|
||||
0x0288: (0, 3),
|
||||
0x0289: (0, 0),
|
||||
0x028A: (0, 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def to_oneup_format(val: int) -> int:
|
||||
return ((val // 10) * 0x10) + val % 10
|
||||
|
||||
|
||||
def from_oneup_format(val: int) -> int:
|
||||
return ((val // 0x10) * 10) + val % 0x10
|
||||
|
||||
|
||||
class MM3EnergyLinkType(IntEnum):
|
||||
Life = 0
|
||||
NeedleCannon = 1
|
||||
MagnetMissile = 2
|
||||
GeminiLaser = 3
|
||||
HardKnuckle = 4
|
||||
TopSpin = 5
|
||||
SearchSnake = 6
|
||||
SparkShot = 7
|
||||
ShadowBlade = 8
|
||||
OneUP = 12
|
||||
RushCoil = 0x11
|
||||
RushMarine = 0x12
|
||||
RushJet = 0x13
|
||||
|
||||
|
||||
request_to_name: dict[str, str] = {
|
||||
"HP": "health",
|
||||
"NE": "Needle Cannon energy",
|
||||
"MA": "Magnet Missile energy",
|
||||
"GE": "Gemini Laser energy",
|
||||
"HA": "Hard Knuckle energy",
|
||||
"TO": "Top Spin energy",
|
||||
"SN": "Search Snake energy",
|
||||
"SP": "Spark Shot energy",
|
||||
"SH": "Shadow Blade energy",
|
||||
"RC": "Rush Coil energy",
|
||||
"RM": "Rush Marine energy",
|
||||
"RJ": "Rush Jet 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 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
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:
|
||||
"""Request a refill from EnergyLink."""
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
if self.ctx.game != "Mega Man 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
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, MM3EnergyLinkType] = {
|
||||
"HP": MM3EnergyLinkType.Life,
|
||||
"NE": MM3EnergyLinkType.NeedleCannon,
|
||||
"MA": MM3EnergyLinkType.MagnetMissile,
|
||||
"GE": MM3EnergyLinkType.GeminiLaser,
|
||||
"HA": MM3EnergyLinkType.HardKnuckle,
|
||||
"TO": MM3EnergyLinkType.TopSpin,
|
||||
"SN": MM3EnergyLinkType.SearchSnake,
|
||||
"SP": MM3EnergyLinkType.SparkShot,
|
||||
"SH": MM3EnergyLinkType.ShadowBlade,
|
||||
"RC": MM3EnergyLinkType.RushCoil,
|
||||
"RM": MM3EnergyLinkType.RushMarine,
|
||||
"RJ": MM3EnergyLinkType.RushJet,
|
||||
"1U": MM3EnergyLinkType.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, MegaMan3Client)
|
||||
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
|
||||
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
|
||||
|
||||
|
||||
def cmd_autoheal(self: "BizHawkClientCommandProcessor") -> None:
|
||||
"""Enable auto heal from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 3":
|
||||
logger.warning("This command can only be used when playing Mega Man 3.")
|
||||
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, MegaMan3Client)
|
||||
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[int, bytes, str]:
|
||||
return MM3_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"
|
||||
|
||||
|
||||
class MegaMan3Client(BizHawkClient):
|
||||
game = "Mega Man 3"
|
||||
system = "NES"
|
||||
patch_suffix = ".apmm3"
|
||||
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: bytes | None = None
|
||||
weapon_energy: int = 0
|
||||
health_energy: int = 0
|
||||
auto_heal: bool = False
|
||||
refill_queue: list[tuple[MM3EnergyLinkType, int]] = []
|
||||
last_wily: int | None = None # default to wily 1
|
||||
doc_status: int | None = None # default to no doc progress
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from worlds._bizhawk import RequestFailedError, read, get_memory_size
|
||||
from . import MM3World
|
||||
|
||||
try:
|
||||
|
||||
if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0:
|
||||
# not the entire size, but enough to check validation
|
||||
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
|
||||
|
||||
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3F320, 21, "PRG ROM"),
|
||||
(0x3F33C, 3, "PRG ROM")]))
|
||||
if game_name[:3] != b"MM3" or version != bytes(MM3World.world_version):
|
||||
if game_name[:3] == b"MM3":
|
||||
# I think this is an easier check than the other?
|
||||
older_version = f"{version[0]}.{version[1]}.{version[2]}"
|
||||
logger.warning(f"This Mega Man 3 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 MM3World.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, [(0x3F336, 1, "PRG ROM")]))[0][0]
|
||||
if deathlink & 0x01:
|
||||
self.death_link = True
|
||||
await ctx.update_death_link(self.death_link)
|
||||
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"MM3_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.last_wily = args["keys"][f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]
|
||||
if f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.doc_status = args["keys"][f"MM3_DOC_STATUS_{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
|
||||
(prog_state, robot_masters_unlocked, robot_masters_defeated, doc_status, doc_robo_unlocked, doc_robo_defeated,
|
||||
rush_acquired, received_items, completed_stages, consumable_checks,
|
||||
e_tanks, lives, weapon_energy, health, state, bar_state, current_stage,
|
||||
energy_link_packet, last_wily) = await read(ctx.bizhawk_ctx, [
|
||||
(MM3_PROG_STATE, 1, "RAM"),
|
||||
(MM3_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
|
||||
(MM3_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
|
||||
(MM3_DOC_STATUS, 1, "RAM"),
|
||||
(MM3_DOC_ROBOT_UNLOCKED, 1, "RAM"),
|
||||
(MM3_DOC_ROBOT_DEFEATED, 1, "RAM"),
|
||||
(MM3_RUSH_RECEIVED, 1, "RAM"),
|
||||
(MM3_RECEIVED_ITEMS, 1, "RAM"),
|
||||
(MM3_COMPLETED_STAGES, 0x1, "RAM"),
|
||||
(MM3_CONSUMABLES, 16, "RAM"), # Could be more but 16 definitely catches all current
|
||||
(MM3_E_TANKS, 1, "RAM"),
|
||||
(MM3_LIVES, 1, "RAM"),
|
||||
(MM3_WEAPON_ENERGY, 11, "RAM"),
|
||||
(MM3_HEALTH, 1, "RAM"),
|
||||
(MM3_MEGAMAN_STATE, 1, "RAM"),
|
||||
(MM3_ENERGY_BAR, 2, "RAM"),
|
||||
(MM3_CURRENT_STAGE, 1, "RAM"),
|
||||
(MM3_ENERGYLINK, 1, "RAM"),
|
||||
(MM3_LAST_WILY, 1, "RAM"),
|
||||
])
|
||||
|
||||
if bar_state[0] not in (0x00, 0x80):
|
||||
return # Game is not initialized
|
||||
# Bit of a trick here, bar state can only be 0x00 or 0x80 (display health bar, or don't)
|
||||
# This means it can double as init guard and in-stage tracker
|
||||
|
||||
if not ctx.finished_game and completed_stages[0] & 0x20:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
writes = []
|
||||
|
||||
# deathlink
|
||||
# only handle deathlink in bar state 0x80 (in stage)
|
||||
if bar_state[0] == 0x80:
|
||||
if self.pending_death_link:
|
||||
writes.append((MM3_MEGAMAN_STATE, bytes([0x0E]), "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 state[0] == 0x0E and not self.sending_death_link:
|
||||
await self.send_deathlink(ctx)
|
||||
elif state[0] != 0x0E:
|
||||
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"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 0xC}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
|
||||
elif last_wily[0] == 0:
|
||||
writes.append((MM3_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"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.last_wily}
|
||||
]}])
|
||||
|
||||
if self.doc_status != doc_status[0]:
|
||||
if self.doc_status is None:
|
||||
# revalidate doc status from data storage
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 0}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]}])
|
||||
elif doc_status[0] == 0:
|
||||
writes.append((MM3_DOC_STATUS, self.doc_status.to_bytes(1, "little"), "RAM"))
|
||||
else:
|
||||
# correct our setting
|
||||
# shouldn't be possible to desync, but we'll account for it anyways
|
||||
self.doc_status |= doc_status[0]
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.doc_status}
|
||||
]}])
|
||||
|
||||
weapon_energy = bytearray(weapon_energy)
|
||||
# handle receiving items
|
||||
recv_amount = received_items[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_slot(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 & 0x120 == 0:
|
||||
# Robot Master Weapon, or Rush
|
||||
new_weapons = item.item & 0xFF
|
||||
weapon_energy[MM3_WEAPONS[new_weapons]] |= 0x9C
|
||||
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
writes.append(get_sfx_writes(0x32))
|
||||
elif item.item & 0x20 == 0:
|
||||
# Robot Master Stage Access
|
||||
# Catch the Doc Robo here
|
||||
if item.item & 0x10:
|
||||
ptr = MM3_DOC_ROBOT_UNLOCKED
|
||||
unlocked = doc_robo_unlocked
|
||||
else:
|
||||
ptr = MM3_ROBOT_MASTERS_UNLOCKED
|
||||
unlocked = robot_masters_unlocked
|
||||
new_stages = unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
print(new_stages)
|
||||
writes.append((ptr, new_stages.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x34))
|
||||
writes.append((MM3_RBM_STROBE, b"\x01", "RAM"))
|
||||
else:
|
||||
# append to the queue, so we handle it later
|
||||
self.item_queue.append(item)
|
||||
recv_amount += 1
|
||||
writes.append((MM3_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if energy_link_packet[0]:
|
||||
pickup = energy_link_packet[0]
|
||||
if pickup in (0x64, 0x65):
|
||||
# Health pickups
|
||||
if pickup == 0x65:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif pickup in (0x66, 0x67):
|
||||
# Weapon Energy
|
||||
if pickup == 0x67:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
elif pickup == 0x69:
|
||||
# 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((MM3_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
|
||||
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
|
||||
if weapon & 0x80 and (weapon & 0x7F) < 0x1C:
|
||||
missing = 0x1C - (weapon & 0x7F)
|
||||
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((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
break
|
||||
else:
|
||||
if current_energy != self.weapon_energy:
|
||||
writes.append((MM3_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 & 0x7F) < 0x1C:
|
||||
health_diff = 0x1C - (current_health & 0x7F)
|
||||
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((MM3_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if self.refill_queue:
|
||||
refill_type, refill_amount = self.refill_queue.pop()
|
||||
if refill_type == MM3EnergyLinkType.Life:
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif refill_type == MM3EnergyLinkType.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 == MM3EnergyLinkType.Life:
|
||||
refill_ptr = MM3_HEALTH
|
||||
elif refill_type == MM3EnergyLinkType.OneUP:
|
||||
refill_ptr = MM3_LIVES
|
||||
else:
|
||||
refill_ptr = MM3_WEAPON_ENERGY + MM3_WEAPONS[refill_type]
|
||||
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
|
||||
if refill_type == MM3EnergyLinkType.OneUP:
|
||||
current_value = from_oneup_format(current_value)
|
||||
new_value = min(0x9C if refill_type != MM3EnergyLinkType.OneUP else 99, current_value + refill_amount)
|
||||
if refill_type == MM3EnergyLinkType.OneUP:
|
||||
new_value = to_oneup_format(new_value)
|
||||
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 = from_oneup_format(lives[0])
|
||||
if current_lives > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_lives += 1
|
||||
current_lives = to_oneup_format(current_lives)
|
||||
writes.append((MM3_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x14))
|
||||
elif idx == 1:
|
||||
self.weapon_energy += 0xE
|
||||
writes.append(get_sfx_writes(0x1C))
|
||||
elif idx == 2:
|
||||
self.health_energy += 0xE
|
||||
writes.append(get_sfx_writes(0x1C))
|
||||
elif idx == 3:
|
||||
current_tanks = from_oneup_format(e_tanks[0])
|
||||
if current_tanks > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_tanks += 1
|
||||
current_tanks = to_oneup_format(current_tanks)
|
||||
writes.append((MM3_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
|
||||
writes.append(get_sfx_writes(0x14))
|
||||
|
||||
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:
|
||||
rbm_id = 0x0001 + i
|
||||
if rbm_id not in ctx.checked_locations:
|
||||
new_checks.append(rbm_id)
|
||||
wep_id = 0x0101 + i
|
||||
if wep_id not in ctx.checked_locations:
|
||||
new_checks.append(wep_id)
|
||||
if doc_robo_defeated[0] & flag:
|
||||
doc_id = 0x0010 + MM3_DOC_REMAP[i]
|
||||
if doc_id not in ctx.checked_locations:
|
||||
new_checks.append(doc_id)
|
||||
|
||||
for i in range(2):
|
||||
flag = 1 << i
|
||||
if rush_acquired[0] & flag:
|
||||
itm_id = 0x0111 + i
|
||||
if itm_id not in ctx.checked_locations:
|
||||
new_checks.append(itm_id)
|
||||
|
||||
for i in (0, 1, 2, 4):
|
||||
# Wily 4 does not have a boss check
|
||||
boss_id = 0x0009 + i
|
||||
if completed_stages[0] & (1 << i) != 0:
|
||||
if boss_id not in ctx.checked_locations:
|
||||
new_checks.append(boss_id)
|
||||
|
||||
if completed_stages[0] & 0x80 and 0x000F not in ctx.checked_locations:
|
||||
new_checks.append(0x000F)
|
||||
|
||||
if bar_state[0] == 0x80: # currently in stage
|
||||
if (prog_state[0] > 0x00 and current_stage[0] >= 8) or prog_state[0] == 0x00:
|
||||
# need to block the specific state of Break Man prog=0x12 stage=0x5
|
||||
# it doesn't clean the consumable table and he doesn't have any anyways
|
||||
for consumable in MM3_CONSUMABLE_TABLE[current_stage[0]]:
|
||||
consumable_info = MM3_CONSUMABLE_TABLE[current_stage[0]][consumable]
|
||||
if consumable not in ctx.checked_locations:
|
||||
is_checked = consumable_checks[consumable_info[0]] & (1 << consumable_info[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]}])
|
||||
331
worlds/mm3/color.py
Normal file
331
worlds/mm3/color.py
Normal file
@@ -0,0 +1,331 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
from . import names
|
||||
from zlib import crc32
|
||||
import struct
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
from .rom import MM3ProcedurePatch
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
MM3_COLORS: dict[str, tuple[int, int]] = {
|
||||
names.gemini_laser: (0x30, 0x21),
|
||||
names.needle_cannon: (0x30, 0x17),
|
||||
names.hard_knuckle: (0x10, 0x01),
|
||||
names.magnet_missile: (0x10, 0x16),
|
||||
names.top_spin: (0x36, 0x00),
|
||||
names.search_snake: (0x30, 0x19),
|
||||
names.rush_coil: (0x30, 0x15),
|
||||
names.spark_shock: (0x30, 0x26),
|
||||
names.rush_marine: (0x30, 0x15),
|
||||
names.shadow_blade: (0x34, 0x14),
|
||||
names.rush_jet: (0x30, 0x15),
|
||||
names.needle_man_stage: (0x3C, 0x11),
|
||||
names.magnet_man_stage: (0x30, 0x15),
|
||||
names.gemini_man_stage: (0x30, 0x21),
|
||||
names.hard_man_stage: (0x10, 0xC),
|
||||
names.top_man_stage: (0x30, 0x26),
|
||||
names.snake_man_stage: (0x30, 0x29),
|
||||
names.spark_man_stage: (0x30, 0x26),
|
||||
names.shadow_man_stage: (0x30, 0x11),
|
||||
names.doc_needle_stage: (0x27, 0x15),
|
||||
names.doc_gemini_stage: (0x27, 0x15),
|
||||
names.doc_spark_stage: (0x27, 0x15),
|
||||
names.doc_shadow_stage: (0x27, 0x15),
|
||||
}
|
||||
|
||||
MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = {
|
||||
**MM3_COLORS,
|
||||
# Metroid series
|
||||
"Varia Suit": (0x27, 0x16),
|
||||
"Gravity Suit": (0x14, 0x16),
|
||||
"Phazon Suit": (0x06, 0x1D),
|
||||
# Street Fighter, technically
|
||||
"Hadouken": (0x3C, 0x11),
|
||||
"Shoryuken": (0x38, 0x16),
|
||||
# X Series
|
||||
"Z-Saber": (0x20, 0x16),
|
||||
"Helmet Upgrade": (0x20, 0x01),
|
||||
"Body Upgrade": (0x20, 0x01),
|
||||
"Arms Upgrade": (0x20, 0x01),
|
||||
"Plasma Shot Upgrade": (0x20, 0x01),
|
||||
"Stock Charge Upgrade": (0x20, 0x01),
|
||||
"Legs Upgrade": (0x20, 0x01),
|
||||
# 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),
|
||||
# X4
|
||||
"Lightning Web": (0x3D, 0x28),
|
||||
"Aiming Laser": (0x2C, 0x14),
|
||||
"Double Cyclone": (0x28, 0x1A),
|
||||
"Rising Fire": (0x20, 0x16),
|
||||
"Ground Hunter": (0x2C, 0x15),
|
||||
"Soul Body": (0x37, 0x27),
|
||||
"Twin Slasher": (0x28, 0x00),
|
||||
"Frost Tower": (0x3D, 0x2C),
|
||||
}
|
||||
|
||||
if "worlds.mm2" in sys.modules:
|
||||
# is this the proper way to do this? who knows!
|
||||
try:
|
||||
mm2 = sys.modules["worlds.mm2"]
|
||||
MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS)
|
||||
for item in MM3_COLORS:
|
||||
mm2.color.add_color_to_mm2(item, MM3_COLORS[item])
|
||||
except AttributeError:
|
||||
# pass through if an old MM2 is found
|
||||
pass
|
||||
|
||||
palette_pointers: dict[str, list[int]] = {
|
||||
"Mega Buster": [0x7C8A8, 0x4650],
|
||||
"Gemini Laser": [0x4654],
|
||||
"Needle Cannon": [0x4658],
|
||||
"Hard Knuckle": [0x465C],
|
||||
"Magnet Missile": [0x4660],
|
||||
"Top Spin": [0x4664],
|
||||
"Search Snake": [0x4668],
|
||||
"Rush Coil": [0x466C],
|
||||
"Spark Shock": [0x4670],
|
||||
"Rush Marine": [0x4674],
|
||||
"Shadow Blade": [0x4678],
|
||||
"Rush Jet": [0x467C],
|
||||
"Needle Man": [0x216C],
|
||||
"Magnet Man": [0x215C],
|
||||
"Gemini Man": [0x217C],
|
||||
"Hard Man": [0x2164],
|
||||
"Top Man": [0x2194],
|
||||
"Snake Man": [0x2174],
|
||||
"Spark Man": [0x2184],
|
||||
"Shadow Man": [0x218C],
|
||||
"Doc Robot": [0x20B8]
|
||||
}
|
||||
|
||||
|
||||
def add_color_to_mm3(name: str, color: tuple[int, int]) -> None:
|
||||
"""
|
||||
Add a color combo for Mega Man 3 to recognize as the color to display for a given item.
|
||||
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
|
||||
"""
|
||||
MM3_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 expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
|
||||
if color_2 >= 0x30:
|
||||
color_a = color_b = color_2
|
||||
else:
|
||||
color_a = color_2 + 0x10
|
||||
color_b = color_2
|
||||
|
||||
if color_1 < 0x10:
|
||||
color_c = color_1 + 0x10
|
||||
color_d = color_1
|
||||
color_e = color_1 + 0x20
|
||||
elif color_1 >= 0x30:
|
||||
color_c = color_1 - 0x10
|
||||
color_d = color_1 - 0x20
|
||||
color_e = color_1
|
||||
else:
|
||||
color_c = color_1
|
||||
color_d = color_1 - 0x10
|
||||
color_e = color_1 + 0x10
|
||||
|
||||
return (0x30, color_a, color_b), (color_d, color_e, color_c)
|
||||
|
||||
|
||||
def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
|
||||
if name in MM3_KNOWN_COLORS:
|
||||
return expand_colors(*MM3_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 expand_colors(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: "MM3World", rom: "MM3ProcedurePatch") -> None:
|
||||
palette_shuffle: 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.player_name} "
|
||||
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 != 0:
|
||||
if palette_shuffle > 1:
|
||||
if palette_shuffle == 3:
|
||||
# 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
|
||||
else:
|
||||
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:
|
||||
shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot
|
||||
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()
|
||||
|
||||
for character in palettes_to_write:
|
||||
for pointer in palette_pointers[character]:
|
||||
rom.write_bytes(pointer + 2, bytes(palettes_to_write[character]))
|
||||
BIN
worlds/mm3/data/mm3_basepatch.bsdiff4
Normal file
BIN
worlds/mm3/data/mm3_basepatch.bsdiff4
Normal file
Binary file not shown.
131
worlds/mm3/docs/en_Mega Man 3.md
Normal file
131
worlds/mm3/docs/en_Mega Man 3.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Mega Man 3
|
||||
|
||||
## 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 (including Doc Robot stages), and Items from Dr. Light are randomized
|
||||
into the multiworld. Access to the Wily Stages is locked behind clearing the 4 Doc Robot stages and defeating Break Man. The game is complete upon
|
||||
viewing the ending sequence after defeating Gamma.
|
||||
|
||||
## What Mega Man 3 items can appear in other players' worlds?
|
||||
- Robot Master weapons
|
||||
- Robot Master Access Codes (stage access)
|
||||
- Doc Robot Access Codes (stage access)
|
||||
- Rush Coil/Jet/Marine
|
||||
- 1-Ups
|
||||
- E-Tanks
|
||||
- Health Energy (L)
|
||||
- Weapon Energy (L)
|
||||
|
||||
## What is considered a location check in Mega Man 3?
|
||||
- The defeat of a Robot Master, Doc Robot, or Wily Boss
|
||||
- Receiving a weapon or Rush 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 remaining are withheld until they can be applied.
|
||||
|
||||
## How do I access the Doc Robot stages?
|
||||
By pressing Select on the Robot Master screen, the screen will transition between Robot Masters and
|
||||
Doc Robots.
|
||||
|
||||
## Useful Information
|
||||
* **NesHawk is the recommended core for this game!** Players using QuickNes (or QuickerNes) will experience graphical
|
||||
glitches while in Gemini Man's stage and fighting Gamma.
|
||||
* Pressing A+B+Start+Select while in a stage will take you to the Game Over screen, allowing you to leave the stage.
|
||||
Your E-Tanks will be preserved.
|
||||
* Your current progress through the Wily stages is saved to the multiworld, allowing you to return to the last stage you
|
||||
reached should you need to leave and enter a Robot Master stage. If you need to return to an earlier Wily stage, holding
|
||||
Select while entering Break Man's stage will take you to Wily 1.
|
||||
* When Random Weaknesses are enabled, Break Man's weakness will be changed from Mega Buster to one random weapon.
|
||||
|
||||
|
||||
## What is EnergyLink?
|
||||
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
|
||||
3, 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 `/pool` 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
|
||||
- Gemini Laser
|
||||
- Needle Cannon
|
||||
- Hard Knuckle
|
||||
- Magnet Missile
|
||||
- Top Spin
|
||||
- Search Snake
|
||||
- Spark Shot
|
||||
- Shadow Blade
|
||||
- Rush Coil
|
||||
- Rush Jet
|
||||
- Rush Marine
|
||||
- Needle Man
|
||||
- Magnet Man
|
||||
- Gemini Man
|
||||
- Hard Man
|
||||
- Top Man
|
||||
- Snake Man
|
||||
- Spark Man
|
||||
- Shadow Man
|
||||
- Doc Robot
|
||||
|
||||
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:
|
||||
Needle Man:
|
||||
Top Spin: 0
|
||||
Hard Knuckle: 4
|
||||
```
|
||||
This would cause Air Man to take 4 damage from Hard Knuckle, and 0 from Top Spin.
|
||||
|
||||
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
|
||||
- `NE` Needle Cannon
|
||||
- `MA` Magnet Missile
|
||||
- `GE` Gemini Laser
|
||||
- `HA` Hard Knuckle
|
||||
- `TO` Top Spin
|
||||
- `SN` Search Snake
|
||||
- `SP` Spark Shot
|
||||
- `SH` Shadow Blade
|
||||
- `RC` Rush Coil
|
||||
- `RM` Rush Marine
|
||||
- `RJ` Rush Jet
|
||||
- `1U` Lives
|
||||
53
worlds/mm3/docs/setup_en.md
Normal file
53
worlds/mm3/docs/setup_en.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Mega Man 3 Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- An English Mega Man 3 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. Bizhawk 2.10
|
||||
|
||||
### 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 3 options page](../../../games/Mega%20Man%203/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 `.apmm3` 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 3 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 3.
|
||||
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.
|
||||
80
worlds/mm3/items.py
Normal file
80
worlds/mm3/items.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from BaseClasses import Item
|
||||
from typing import NamedTuple
|
||||
from .names import (needle_cannon, magnet_missile, gemini_laser, hard_knuckle, top_spin, search_snake, spark_shock,
|
||||
shadow_blade, rush_coil, rush_marine, rush_jet, needle_man_stage, magnet_man_stage,
|
||||
gemini_man_stage, hard_man_stage, top_man_stage, snake_man_stage, spark_man_stage, shadow_man_stage,
|
||||
doc_needle_stage, doc_gemini_stage, doc_spark_stage, doc_shadow_stage, e_tank, weapon_energy,
|
||||
health_energy, one_up)
|
||||
|
||||
|
||||
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 MM3Item(Item):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
robot_master_weapon_table = {
|
||||
needle_cannon: ItemData(0x0001, True),
|
||||
magnet_missile: ItemData(0x0002, True, True),
|
||||
gemini_laser: ItemData(0x0003, True),
|
||||
hard_knuckle: ItemData(0x0004, True),
|
||||
top_spin: ItemData(0x0005, True, True),
|
||||
search_snake: ItemData(0x0006, True),
|
||||
spark_shock: ItemData(0x0007, True),
|
||||
shadow_blade: ItemData(0x0008, True, True),
|
||||
}
|
||||
|
||||
stage_access_table = {
|
||||
needle_man_stage: ItemData(0x0101, True),
|
||||
magnet_man_stage: ItemData(0x0102, True),
|
||||
gemini_man_stage: ItemData(0x0103, True),
|
||||
hard_man_stage: ItemData(0x0104, True),
|
||||
top_man_stage: ItemData(0x0105, True),
|
||||
snake_man_stage: ItemData(0x0106, True),
|
||||
spark_man_stage: ItemData(0x0107, True),
|
||||
shadow_man_stage: ItemData(0x0108, True),
|
||||
doc_needle_stage: ItemData(0x0111, True, True),
|
||||
doc_gemini_stage: ItemData(0x0113, True, True),
|
||||
doc_spark_stage: ItemData(0x0117, True, True),
|
||||
doc_shadow_stage: ItemData(0x0118, True, True),
|
||||
}
|
||||
|
||||
rush_item_table = {
|
||||
rush_coil: ItemData(0x0011, True, True),
|
||||
rush_marine: ItemData(0x0012, True),
|
||||
rush_jet: ItemData(0x0013, True, True),
|
||||
}
|
||||
|
||||
filler_item_table = {
|
||||
one_up: ItemData(0x0020, False),
|
||||
weapon_energy: ItemData(0x0021, False),
|
||||
health_energy: ItemData(0x0022, False),
|
||||
e_tank: ItemData(0x0023, False, True),
|
||||
}
|
||||
|
||||
filler_item_weights = {
|
||||
one_up: 1,
|
||||
weapon_energy: 4,
|
||||
health_energy: 1,
|
||||
e_tank: 2,
|
||||
}
|
||||
|
||||
item_table = {
|
||||
**robot_master_weapon_table,
|
||||
**stage_access_table,
|
||||
**rush_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()},
|
||||
"Rush": {name for name in rush_item_table.keys()}
|
||||
}
|
||||
|
||||
lookup_item_to_id: dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
|
||||
312
worlds/mm3/locations.py
Normal file
312
worlds/mm3/locations.py
Normal file
@@ -0,0 +1,312 @@
|
||||
from BaseClasses import Location, Region
|
||||
from typing import NamedTuple
|
||||
from . import names
|
||||
|
||||
|
||||
class MM3Location(Location):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
class MM3Region(Region):
|
||||
game = "Mega Man 3"
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
location_id: int | None
|
||||
energy: bool = False
|
||||
oneup_tank: bool = False
|
||||
|
||||
|
||||
class RegionData(NamedTuple):
|
||||
locations: dict[str, LocationData]
|
||||
required_items: list[str]
|
||||
parent: str = ""
|
||||
|
||||
mm3_regions: dict[str, RegionData] = {
|
||||
"Needle Man Stage": RegionData({
|
||||
names.needle_man: LocationData(0x0001),
|
||||
names.get_needle_cannon: LocationData(0x0101),
|
||||
names.get_rush_jet: LocationData(0x0111),
|
||||
names.needle_man_c1: LocationData(0x0200, energy=True),
|
||||
names.needle_man_c2: LocationData(0x0201, oneup_tank=True),
|
||||
}, [names.needle_man_stage]),
|
||||
|
||||
"Magnet Man Stage": RegionData({
|
||||
names.magnet_man: LocationData(0x0002),
|
||||
names.get_magnet_missile: LocationData(0x0102),
|
||||
names.magnet_man_c1: LocationData(0x0202, energy=True),
|
||||
names.magnet_man_c2: LocationData(0x0203, energy=True),
|
||||
names.magnet_man_c3: LocationData(0x0204, energy=True),
|
||||
names.magnet_man_c4: LocationData(0x0205, energy=True),
|
||||
names.magnet_man_c5: LocationData(0x0206, energy=True),
|
||||
names.magnet_man_c6: LocationData(0x0207, energy=True),
|
||||
names.magnet_man_c7: LocationData(0x0208, energy=True),
|
||||
names.magnet_man_c8: LocationData(0x0209, energy=True),
|
||||
}, [names.magnet_man_stage]),
|
||||
|
||||
"Gemini Man Stage": RegionData({
|
||||
names.gemini_man: LocationData(0x0003),
|
||||
names.get_gemini_laser: LocationData(0x0103),
|
||||
names.gemini_man_c1: LocationData(0x020A, oneup_tank=True),
|
||||
names.gemini_man_c2: LocationData(0x020B, energy=True),
|
||||
names.gemini_man_c3: LocationData(0x020C, oneup_tank=True),
|
||||
names.gemini_man_c4: LocationData(0x020D, energy=True),
|
||||
names.gemini_man_c5: LocationData(0x020E, energy=True),
|
||||
names.gemini_man_c6: LocationData(0x020F, oneup_tank=True),
|
||||
names.gemini_man_c7: LocationData(0x0210, oneup_tank=True),
|
||||
names.gemini_man_c8: LocationData(0x0211, energy=True),
|
||||
names.gemini_man_c9: LocationData(0x0212, energy=True),
|
||||
names.gemini_man_c10: LocationData(0x0213, oneup_tank=True),
|
||||
}, [names.gemini_man_stage]),
|
||||
|
||||
"Hard Man Stage": RegionData({
|
||||
names.hard_man: LocationData(0x0004),
|
||||
names.get_hard_knuckle: LocationData(0x0104),
|
||||
names.hard_man_c1: LocationData(0x0214, energy=True),
|
||||
names.hard_man_c2: LocationData(0x0215, energy=True),
|
||||
names.hard_man_c3: LocationData(0x0216, oneup_tank=True),
|
||||
names.hard_man_c4: LocationData(0x0217, energy=True),
|
||||
names.hard_man_c5: LocationData(0x0218, energy=True),
|
||||
names.hard_man_c6: LocationData(0x0219, energy=True),
|
||||
names.hard_man_c7: LocationData(0x021A, energy=True),
|
||||
}, [names.hard_man_stage]),
|
||||
|
||||
"Top Man Stage": RegionData({
|
||||
names.top_man: LocationData(0x0005),
|
||||
names.get_top_spin: LocationData(0x0105),
|
||||
names.top_man_c1: LocationData(0x021B, energy=True),
|
||||
names.top_man_c2: LocationData(0x021C, energy=True),
|
||||
names.top_man_c3: LocationData(0x021D, energy=True),
|
||||
names.top_man_c4: LocationData(0x021E, energy=True),
|
||||
names.top_man_c5: LocationData(0x021F, energy=True),
|
||||
names.top_man_c6: LocationData(0x0220, oneup_tank=True),
|
||||
names.top_man_c7: LocationData(0x0221, energy=True),
|
||||
names.top_man_c8: LocationData(0x0222, energy=True),
|
||||
}, [names.top_man_stage]),
|
||||
|
||||
"Snake Man Stage": RegionData({
|
||||
names.snake_man: LocationData(0x0006),
|
||||
names.get_search_snake: LocationData(0x0106),
|
||||
names.snake_man_c1: LocationData(0x0223, energy=True),
|
||||
names.snake_man_c2: LocationData(0x0224, energy=True),
|
||||
names.snake_man_c3: LocationData(0x0225, oneup_tank=True),
|
||||
names.snake_man_c4: LocationData(0x0226, oneup_tank=True),
|
||||
names.snake_man_c5: LocationData(0x0227, energy=True),
|
||||
}, [names.snake_man_stage]),
|
||||
|
||||
"Spark Man Stage": RegionData({
|
||||
names.spark_man: LocationData(0x0007),
|
||||
names.get_spark_shock: LocationData(0x0107),
|
||||
names.spark_man_c1: LocationData(0x0228, energy=True),
|
||||
names.spark_man_c2: LocationData(0x0229, energy=True),
|
||||
names.spark_man_c3: LocationData(0x022A, energy=True),
|
||||
names.spark_man_c4: LocationData(0x022B, energy=True),
|
||||
names.spark_man_c5: LocationData(0x022C, energy=True),
|
||||
names.spark_man_c6: LocationData(0x022D, energy=True),
|
||||
}, [names.spark_man_stage]),
|
||||
|
||||
"Shadow Man Stage": RegionData({
|
||||
names.shadow_man: LocationData(0x0008),
|
||||
names.get_shadow_blade: LocationData(0x0108),
|
||||
names.get_rush_marine: LocationData(0x0112),
|
||||
names.shadow_man_c1: LocationData(0x022E, energy=True),
|
||||
names.shadow_man_c2: LocationData(0x022F, energy=True),
|
||||
names.shadow_man_c3: LocationData(0x0230, energy=True),
|
||||
names.shadow_man_c4: LocationData(0x0231, energy=True),
|
||||
}, [names.shadow_man_stage]),
|
||||
|
||||
"Doc Robot (Needle) - Air": RegionData({
|
||||
names.doc_air: LocationData(0x0010),
|
||||
names.doc_needle_c1: LocationData(0x0232, energy=True),
|
||||
names.doc_needle_c2: LocationData(0x0233, oneup_tank=True),
|
||||
names.doc_needle_c3: LocationData(0x0234, oneup_tank=True),
|
||||
}, [names.doc_needle_stage]),
|
||||
|
||||
"Doc Robot (Needle) - Crash": RegionData({
|
||||
names.doc_crash: LocationData(0x0011),
|
||||
names.doc_needle: LocationData(None),
|
||||
names.doc_needle_c4: LocationData(0x0235, energy=True),
|
||||
names.doc_needle_c5: LocationData(0x0236, energy=True),
|
||||
names.doc_needle_c6: LocationData(0x0237, energy=True),
|
||||
names.doc_needle_c7: LocationData(0x0238, energy=True),
|
||||
names.doc_needle_c8: LocationData(0x0239, energy=True),
|
||||
names.doc_needle_c9: LocationData(0x023A, energy=True),
|
||||
names.doc_needle_c10: LocationData(0x023B, energy=True),
|
||||
names.doc_needle_c11: LocationData(0x023C, energy=True),
|
||||
}, [], parent="Doc Robot (Needle) - Air"),
|
||||
|
||||
"Doc Robot (Gemini) - Flash": RegionData({
|
||||
names.doc_flash: LocationData(0x0012),
|
||||
names.doc_gemini_c1: LocationData(0x023D, oneup_tank=True),
|
||||
names.doc_gemini_c2: LocationData(0x023E, oneup_tank=True),
|
||||
}, [names.doc_gemini_stage]),
|
||||
|
||||
"Doc Robot (Gemini) - Bubble": RegionData({
|
||||
names.doc_bubble: LocationData(0x0013),
|
||||
names.doc_gemini: LocationData(None),
|
||||
names.doc_gemini_c3: LocationData(0x023F, energy=True),
|
||||
names.doc_gemini_c4: LocationData(0x0240, energy=True),
|
||||
}, [], parent="Doc Robot (Gemini) - Flash"),
|
||||
|
||||
"Doc Robot (Shadow) - Wood": RegionData({
|
||||
names.doc_wood: LocationData(0x0014),
|
||||
}, [names.doc_shadow_stage]),
|
||||
|
||||
"Doc Robot (Shadow) - Heat": RegionData({
|
||||
names.doc_heat: LocationData(0x0015),
|
||||
names.doc_shadow: LocationData(None),
|
||||
names.doc_shadow_c1: LocationData(0x0243, energy=True),
|
||||
names.doc_shadow_c2: LocationData(0x0244, energy=True),
|
||||
names.doc_shadow_c3: LocationData(0x0245, energy=True),
|
||||
names.doc_shadow_c4: LocationData(0x0246, energy=True),
|
||||
names.doc_shadow_c5: LocationData(0x0247, energy=True),
|
||||
}, [], parent="Doc Robot (Shadow) - Wood"),
|
||||
|
||||
"Doc Robot (Spark) - Metal": RegionData({
|
||||
names.doc_metal: LocationData(0x0016),
|
||||
names.doc_spark_c1: LocationData(0x0241, energy=True),
|
||||
}, [names.doc_spark_stage]),
|
||||
|
||||
"Doc Robot (Spark) - Quick": RegionData({
|
||||
names.doc_quick: LocationData(0x0017),
|
||||
names.doc_spark: LocationData(None),
|
||||
names.doc_spark_c2: LocationData(0x0242, energy=True),
|
||||
}, [], parent="Doc Robot (Spark) - Metal"),
|
||||
|
||||
"Break Man": RegionData({
|
||||
names.break_man: LocationData(0x000F),
|
||||
names.break_stage: LocationData(None),
|
||||
}, [names.doc_needle, names.doc_gemini, names.doc_spark, names.doc_shadow]),
|
||||
|
||||
"Wily Stage 1": RegionData({
|
||||
names.wily_1_boss: LocationData(0x0009),
|
||||
names.wily_stage_1: LocationData(None),
|
||||
names.wily_1_c1: LocationData(0x0248, oneup_tank=True),
|
||||
names.wily_1_c2: LocationData(0x0249, oneup_tank=True),
|
||||
names.wily_1_c3: LocationData(0x024A, energy=True),
|
||||
names.wily_1_c4: LocationData(0x024B, oneup_tank=True),
|
||||
names.wily_1_c5: LocationData(0x024C, energy=True),
|
||||
names.wily_1_c6: LocationData(0x024D, energy=True),
|
||||
names.wily_1_c7: LocationData(0x024E, energy=True),
|
||||
names.wily_1_c8: LocationData(0x024F, oneup_tank=True),
|
||||
names.wily_1_c9: LocationData(0x0250, energy=True),
|
||||
names.wily_1_c10: LocationData(0x0251, energy=True),
|
||||
names.wily_1_c11: LocationData(0x0252, energy=True),
|
||||
names.wily_1_c12: LocationData(0x0253, energy=True),
|
||||
}, [names.break_stage], parent="Break Man"),
|
||||
|
||||
"Wily Stage 2": RegionData({
|
||||
names.wily_2_boss: LocationData(0x000A),
|
||||
names.wily_stage_2: LocationData(None),
|
||||
names.wily_2_c1: LocationData(0x0254, energy=True),
|
||||
names.wily_2_c2: LocationData(0x0255, energy=True),
|
||||
names.wily_2_c3: LocationData(0x0256, oneup_tank=True),
|
||||
names.wily_2_c4: LocationData(0x0257, energy=True),
|
||||
names.wily_2_c5: LocationData(0x0258, energy=True),
|
||||
names.wily_2_c6: LocationData(0x0259, energy=True),
|
||||
names.wily_2_c7: LocationData(0x025A, energy=True),
|
||||
names.wily_2_c8: LocationData(0x025B, energy=True),
|
||||
names.wily_2_c9: LocationData(0x025C, oneup_tank=True),
|
||||
names.wily_2_c10: LocationData(0x025D, energy=True),
|
||||
names.wily_2_c11: LocationData(0x025E, oneup_tank=True),
|
||||
names.wily_2_c12: LocationData(0x025F, energy=True),
|
||||
names.wily_2_c13: LocationData(0x0260, energy=True),
|
||||
}, [names.wily_stage_1], parent="Wily Stage 1"),
|
||||
|
||||
"Wily Stage 3": RegionData({
|
||||
names.wily_3_boss: LocationData(0x000B),
|
||||
names.wily_stage_3: LocationData(None),
|
||||
names.wily_3_c1: LocationData(0x0261, energy=True),
|
||||
names.wily_3_c2: LocationData(0x0262, energy=True),
|
||||
names.wily_3_c3: LocationData(0x0263, oneup_tank=True),
|
||||
names.wily_3_c4: LocationData(0x0264, oneup_tank=True),
|
||||
names.wily_3_c5: LocationData(0x0265, energy=True),
|
||||
names.wily_3_c6: LocationData(0x0266, energy=True),
|
||||
names.wily_3_c7: LocationData(0x0267, energy=True),
|
||||
names.wily_3_c8: LocationData(0x0268, energy=True),
|
||||
names.wily_3_c9: LocationData(0x0269, energy=True),
|
||||
names.wily_3_c10: LocationData(0x026A, oneup_tank=True),
|
||||
names.wily_3_c11: LocationData(0x026B, oneup_tank=True)
|
||||
}, [names.wily_stage_2], parent="Wily Stage 2"),
|
||||
|
||||
"Wily Stage 4": RegionData({
|
||||
names.wily_stage_4: LocationData(None),
|
||||
names.wily_4_c1: LocationData(0x026C, energy=True),
|
||||
names.wily_4_c2: LocationData(0x026D, energy=True),
|
||||
names.wily_4_c3: LocationData(0x026E, energy=True),
|
||||
names.wily_4_c4: LocationData(0x026F, energy=True),
|
||||
names.wily_4_c5: LocationData(0x0270, energy=True),
|
||||
names.wily_4_c6: LocationData(0x0271, energy=True),
|
||||
names.wily_4_c7: LocationData(0x0272, energy=True),
|
||||
names.wily_4_c8: LocationData(0x0273, energy=True),
|
||||
names.wily_4_c9: LocationData(0x0274, energy=True),
|
||||
names.wily_4_c10: LocationData(0x0275, oneup_tank=True),
|
||||
names.wily_4_c11: LocationData(0x0276, energy=True),
|
||||
names.wily_4_c12: LocationData(0x0277, oneup_tank=True),
|
||||
names.wily_4_c13: LocationData(0x0278, energy=True),
|
||||
names.wily_4_c14: LocationData(0x0279, energy=True),
|
||||
names.wily_4_c15: LocationData(0x027A, energy=True),
|
||||
names.wily_4_c16: LocationData(0x027B, energy=True),
|
||||
names.wily_4_c17: LocationData(0x027C, energy=True),
|
||||
names.wily_4_c18: LocationData(0x027D, energy=True),
|
||||
names.wily_4_c19: LocationData(0x027E, energy=True),
|
||||
names.wily_4_c20: LocationData(0x027F, energy=True),
|
||||
}, [names.wily_stage_3], parent="Wily Stage 3"),
|
||||
|
||||
"Wily Stage 5": RegionData({
|
||||
names.wily_5_boss: LocationData(0x000D),
|
||||
names.wily_stage_5: LocationData(None),
|
||||
names.wily_5_c1: LocationData(0x0280, energy=True),
|
||||
names.wily_5_c2: LocationData(0x0281, energy=True),
|
||||
names.wily_5_c3: LocationData(0x0282, oneup_tank=True),
|
||||
names.wily_5_c4: LocationData(0x0283, oneup_tank=True),
|
||||
}, [names.wily_stage_4], parent="Wily Stage 4"),
|
||||
|
||||
"Wily Stage 6": RegionData({
|
||||
names.gamma: LocationData(None),
|
||||
names.wily_6_c1: LocationData(0x0284, oneup_tank=True),
|
||||
names.wily_6_c2: LocationData(0x0285, oneup_tank=True),
|
||||
names.wily_6_c3: LocationData(0x0286, energy=True),
|
||||
names.wily_6_c4: LocationData(0x0287, energy=True),
|
||||
names.wily_6_c5: LocationData(0x0288, oneup_tank=True),
|
||||
names.wily_6_c6: LocationData(0x0289, oneup_tank=True),
|
||||
names.wily_6_c7: LocationData(0x028A, energy=True),
|
||||
}, [names.wily_stage_5], parent="Wily Stage 5"),
|
||||
}
|
||||
|
||||
|
||||
def get_boss_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items()
|
||||
if not data.energy and not data.oneup_tank]
|
||||
|
||||
|
||||
def get_energy_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items() if data.energy]
|
||||
|
||||
|
||||
def get_oneup_locations(region: str) -> list[str]:
|
||||
return [location for location, data in mm3_regions[region].locations.items() if data.oneup_tank]
|
||||
|
||||
|
||||
location_table: dict[str, int | None] = {
|
||||
location: data.location_id for region in mm3_regions.values() for location, data in region.locations.items()
|
||||
}
|
||||
|
||||
|
||||
location_groups = {
|
||||
"Get Equipped": {
|
||||
names.get_needle_cannon,
|
||||
names.get_magnet_missile,
|
||||
names.get_gemini_laser,
|
||||
names.get_hard_knuckle,
|
||||
names.get_top_spin,
|
||||
names.get_search_snake,
|
||||
names.get_spark_shock,
|
||||
names.get_shadow_blade,
|
||||
names.get_rush_marine,
|
||||
names.get_rush_jet,
|
||||
},
|
||||
**{name: {location for location, data in region.locations.items() if data.location_id} for name, region in mm3_regions.items()}
|
||||
}
|
||||
|
||||
lookup_location_to_id: dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}
|
||||
221
worlds/mm3/names.py
Normal file
221
worlds/mm3/names.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# Robot Master Weapons
|
||||
gemini_laser = "Gemini Laser"
|
||||
needle_cannon = "Needle Cannon"
|
||||
hard_knuckle = "Hard Knuckle"
|
||||
magnet_missile = "Magnet Missile"
|
||||
top_spin = "Top Spin"
|
||||
search_snake = "Search Snake"
|
||||
spark_shock = "Spark Shock"
|
||||
shadow_blade = "Shadow Blade"
|
||||
|
||||
# Rush
|
||||
rush_coil = "Rush Coil"
|
||||
rush_jet = "Rush Jet"
|
||||
rush_marine = "Rush Marine"
|
||||
|
||||
# Access Codes
|
||||
needle_man_stage = "Needle Man Access Codes"
|
||||
magnet_man_stage = "Magnet Man Access Codes"
|
||||
gemini_man_stage = "Gemini Man Access Codes"
|
||||
hard_man_stage = "Hard Man Access Codes"
|
||||
top_man_stage = "Top Man Access Codes"
|
||||
snake_man_stage = "Snake Man Access Codes"
|
||||
spark_man_stage = "Spark Man Access Codes"
|
||||
shadow_man_stage = "Shadow Man Access Codes"
|
||||
doc_needle_stage = "Doc Robot (Needle) Access Codes"
|
||||
doc_gemini_stage = "Doc Robot (Gemini) Access Codes"
|
||||
doc_spark_stage = "Doc Robot (Spark) Access Codes"
|
||||
doc_shadow_stage = "Doc Robot (Shadow) Access Codes"
|
||||
|
||||
# Misc. Items
|
||||
one_up = "1-Up"
|
||||
weapon_energy = "Weapon Energy (L)"
|
||||
health_energy = "Health Energy (L)"
|
||||
e_tank = "E-Tank"
|
||||
|
||||
needle_man = "Needle Man - Defeated"
|
||||
magnet_man = "Magnet Man - Defeated"
|
||||
gemini_man = "Gemini Man - Defeated"
|
||||
hard_man = "Hard Man - Defeated"
|
||||
top_man = "Top Man - Defeated"
|
||||
snake_man = "Snake Man - Defeated"
|
||||
spark_man = "Spark Man - Defeated"
|
||||
shadow_man = "Shadow Man - Defeated"
|
||||
doc_air = "Doc Robot (Air) - Defeated"
|
||||
doc_crash = "Doc Robot (Crash) - Defeated"
|
||||
doc_flash = "Doc Robot (Flash) - Defeated"
|
||||
doc_bubble = "Doc Robot (Bubble) - Defeated"
|
||||
doc_wood = "Doc Robot (Wood) - Defeated"
|
||||
doc_heat = "Doc Robot (Heat) - Defeated"
|
||||
doc_metal = "Doc Robot (Metal) - Defeated"
|
||||
doc_quick = "Doc Robot (Quick) - Defeated"
|
||||
break_man = "Break Man - Defeated"
|
||||
wily_1_boss = "Kamegoro Maker - Defeated"
|
||||
wily_2_boss = "Yellow Devil MK-II - Defeated"
|
||||
wily_3_boss = "Holograph Mega Man - Defeated"
|
||||
wily_5_boss = "Wily Machine 3 - Defeated"
|
||||
gamma = "Gamma - Defeated"
|
||||
|
||||
get_gemini_laser = "Gemini Laser - Received"
|
||||
get_needle_cannon = "Needle Cannon - Received"
|
||||
get_hard_knuckle = "Hard Knuckle - Received"
|
||||
get_magnet_missile = "Magnet Missile - Received"
|
||||
get_top_spin = "Top Spin - Received"
|
||||
get_search_snake = "Search Snake - Received"
|
||||
get_spark_shock = "Spark Shock - Received"
|
||||
get_shadow_blade = "Shadow Blade - Received"
|
||||
get_rush_jet = "Rush Jet - Received"
|
||||
get_rush_marine = "Rush Marine - Received"
|
||||
|
||||
# Wily Stage Event Items
|
||||
doc_needle = "Doc Robot (Needle) - Completed"
|
||||
doc_gemini = "Doc Robot (Gemini) - Completed"
|
||||
doc_spark = "Doc Robot (Spark) - Completed"
|
||||
doc_shadow = "Doc Robot (Shadow) - Completed"
|
||||
break_stage = "Break Man"
|
||||
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
|
||||
needle_man_c1 = "Needle Man Stage - Weapon Energy 1"
|
||||
needle_man_c2 = "Needle Man Stage - E-Tank"
|
||||
magnet_man_c1 = "Magnet Man Stage - Health Energy 1"
|
||||
magnet_man_c2 = "Magnet Man Stage - Health Energy 2"
|
||||
magnet_man_c3 = "Magnet Man Stage - Health Energy 3"
|
||||
magnet_man_c4 = "Magnet Man Stage - Health Energy 4"
|
||||
magnet_man_c5 = "Magnet Man Stage - Weapon Energy 1"
|
||||
magnet_man_c6 = "Magnet Man Stage - Weapon Energy 2"
|
||||
magnet_man_c7 = "Magnet Man Stage - Weapon Energy 3"
|
||||
magnet_man_c8 = "Magnet Man Stage - Health Energy 5"
|
||||
gemini_man_c1 = "Gemini Man Stage - 1-Up 1"
|
||||
gemini_man_c2 = "Gemini Man Stage - Health Energy 1"
|
||||
gemini_man_c3 = "Gemini Man Stage - Mystery Tank"
|
||||
gemini_man_c4 = "Gemini Man Stage - Weapon Energy 1"
|
||||
gemini_man_c5 = "Gemini Man Stage - Health Energy 2"
|
||||
gemini_man_c6 = "Gemini Man Stage - 1-Up 2"
|
||||
gemini_man_c7 = "Gemini Man Stage - E-Tank 1"
|
||||
gemini_man_c8 = "Gemini Man Stage - Weapon Energy 2"
|
||||
gemini_man_c9 = "Gemini Man Stage - Weapon Energy 3"
|
||||
gemini_man_c10 = "Gemini Man Stage - E-Tank 2"
|
||||
hard_man_c1 = "Hard Man Stage - Health Energy 1"
|
||||
hard_man_c2 = "Hard Man Stage - Health Energy 2"
|
||||
hard_man_c3 = "Hard Man Stage - E-Tank"
|
||||
hard_man_c4 = "Hard Man Stage - Health Energy 3"
|
||||
hard_man_c5 = "Hard Man Stage - Health Energy 4"
|
||||
hard_man_c6 = "Hard Man Stage - Health Energy 5"
|
||||
hard_man_c7 = "Hard Man Stage - Health Energy 6"
|
||||
top_man_c1 = "Top Man Stage - Health Energy 1"
|
||||
top_man_c2 = "Top Man Stage - Health Energy 2"
|
||||
top_man_c3 = "Top Man Stage - Health Energy 3"
|
||||
top_man_c4 = "Top Man Stage - Health Energy 4"
|
||||
top_man_c5 = "Top Man Stage - Weapon Energy 1"
|
||||
top_man_c6 = "Top Man Stage - 1-Up"
|
||||
top_man_c7 = "Top Man Stage - Health Energy 5"
|
||||
top_man_c8 = "Top Man Stage - Health Energy 6"
|
||||
snake_man_c1 = "Snake Man Stage - Health Energy 1"
|
||||
snake_man_c2 = "Snake Man Stage - Health Energy 2"
|
||||
snake_man_c3 = "Snake Man Stage - Mystery Tank 1"
|
||||
snake_man_c4 = "Snake Man Stage - Mystery Tank 2"
|
||||
snake_man_c5 = "Snake Man Stage - Health Energy 3"
|
||||
spark_man_c1 = "Spark Man Stage - Health Energy 1"
|
||||
spark_man_c2 = "Spark Man Stage - Weapon Energy 1"
|
||||
spark_man_c3 = "Spark Man Stage - Weapon Energy 2"
|
||||
spark_man_c4 = "Spark Man Stage - Weapon Energy 3"
|
||||
spark_man_c5 = "Spark Man Stage - Weapon Energy 4"
|
||||
spark_man_c6 = "Spark Man Stage - Weapon Energy 5"
|
||||
shadow_man_c1 = "Shadow Man Stage - Weapon Energy 1"
|
||||
shadow_man_c2 = "Shadow Man Stage - Weapon Energy 2"
|
||||
shadow_man_c3 = "Shadow Man Stage - Weapon Energy 3"
|
||||
shadow_man_c4 = "Shadow Man Stage - Weapon Energy 4"
|
||||
doc_needle_c1 = "Doc Robot (Needle) - Health Energy 1"
|
||||
doc_needle_c2 = "Doc Robot (Needle) - 1-Up 1"
|
||||
doc_needle_c3 = "Doc Robot (Needle) - E-Tank 1"
|
||||
doc_needle_c4 = "Doc Robot (Needle) - Weapon Energy 1"
|
||||
doc_needle_c5 = "Doc Robot (Needle) - Weapon Energy 2"
|
||||
doc_needle_c6 = "Doc Robot (Needle) - Weapon Energy 3"
|
||||
doc_needle_c7 = "Doc Robot (Needle) - Weapon Energy 4"
|
||||
doc_needle_c8 = "Doc Robot (Needle) - Weapon Energy 5"
|
||||
doc_needle_c9 = "Doc Robot (Needle) - Weapon Energy 6"
|
||||
doc_needle_c10 = "Doc Robot (Needle) - Weapon Energy 7"
|
||||
doc_needle_c11 = "Doc Robot (Needle) - Health Energy 2"
|
||||
doc_gemini_c1 = "Doc Robot (Gemini) - Mystery Tank 1"
|
||||
doc_gemini_c2 = "Doc Robot (Gemini) - Mystery Tank 2"
|
||||
doc_gemini_c3 = "Doc Robot (Gemini) - Weapon Energy 1"
|
||||
doc_gemini_c4 = "Doc Robot (Gemini) - Weapon Energy 2"
|
||||
doc_spark_c1 = "Doc Robot (Spark) - Health Energy 1"
|
||||
doc_spark_c2 = "Doc Robot (Spark) - Health Energy 2"
|
||||
doc_shadow_c1 = "Doc Robot (Shadow) - Health Energy 1"
|
||||
doc_shadow_c2 = "Doc Robot (Shadow) - Weapon Energy 1"
|
||||
doc_shadow_c3 = "Doc Robot (Shadow) - Weapon Energy 2"
|
||||
doc_shadow_c4 = "Doc Robot (Shadow) - Weapon Energy 3"
|
||||
doc_shadow_c5 = "Doc Robot (Shadow) - Weapon Energy 4"
|
||||
wily_1_c1 = "Wily Stage 1 - 1-Up 1"
|
||||
wily_1_c2 = "Wily Stage 1 - E-Tank 1"
|
||||
wily_1_c3 = "Wily Stage 1 - Weapon Energy 1"
|
||||
wily_1_c4 = "Wily Stage 1 - 1-Up 2" # Hard Knuckle
|
||||
wily_1_c5 = "Wily Stage 1 - Health Energy 1" # Hard Knuckle
|
||||
wily_1_c6 = "Wily Stage 1 - Weapon Energy 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c7 = "Wily Stage 1 - Health Energy 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c8 = "Wily Stage 1 - E-Tank 2" # Hard Knuckle & Rush Vertical
|
||||
wily_1_c9 = "Wily Stage 1 - Health Energy 3"
|
||||
wily_1_c10 = "Wily Stage 1 - Health Energy 4"
|
||||
wily_1_c11 = "Wily Stage 1 - Weapon Energy 3" # Rush Vertical
|
||||
wily_1_c12 = "Wily Stage 1 - Weapon Energy 4" # Rush Vertical
|
||||
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1"
|
||||
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2"
|
||||
wily_2_c3 = "Wily Stage 2 - 1-Up 1"
|
||||
wily_2_c4 = "Wily Stage 2 - Weapon Energy 3"
|
||||
wily_2_c5 = "Wily Stage 2 - Health Energy 1"
|
||||
wily_2_c6 = "Wily Stage 2 - Health Energy 2"
|
||||
wily_2_c7 = "Wily Stage 2 - Health Energy 3"
|
||||
wily_2_c8 = "Wily Stage 2 - Weapon Energy 4"
|
||||
wily_2_c9 = "Wily Stage 2 - E-Tank 1"
|
||||
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5"
|
||||
wily_2_c11 = "Wily Stage 2 - E-Tank 2"
|
||||
wily_2_c12 = "Wily Stage 2 - Weapon Energy 6"
|
||||
wily_2_c13 = "Wily Stage 2 - Weapon Energy 7"
|
||||
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # Hard Knuckle
|
||||
wily_3_c2 = "Wily Stage 3 - Weapon Energy 2" # Hard Knuckle
|
||||
wily_3_c3 = "Wily Stage 3 - E-Tank 1"
|
||||
wily_3_c4 = "Wily Stage 3 - 1-Up 1"
|
||||
wily_3_c5 = "Wily Stage 3 - Health Energy 1"
|
||||
wily_3_c6 = "Wily Stage 3 - Health Energy 2"
|
||||
wily_3_c7 = "Wily Stage 3 - Health Energy 3"
|
||||
wily_3_c8 = "Wily Stage 3 - Health Energy 4"
|
||||
wily_3_c9 = "Wily Stage 3 - Weapon Energy 3"
|
||||
wily_3_c10 = "Wily Stage 3 - Mystery Tank 1" # Hard Knuckle
|
||||
wily_3_c11 = "Wily Stage 3 - Mystery Tank 2" # Hard Knuckle
|
||||
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1"
|
||||
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2"
|
||||
wily_4_c3 = "Wily Stage 4 - Weapon Energy 3"
|
||||
wily_4_c4 = "Wily Stage 4 - Weapon Energy 4"
|
||||
wily_4_c5 = "Wily Stage 4 - Weapon Energy 5"
|
||||
wily_4_c6 = "Wily Stage 4 - Health Energy 1"
|
||||
wily_4_c7 = "Wily Stage 4 - Health Energy 2"
|
||||
wily_4_c8 = "Wily Stage 4 - Health Energy 3"
|
||||
wily_4_c9 = "Wily Stage 4 - Health Energy 4"
|
||||
wily_4_c10 = "Wily Stage 4 - Mystery Tank"
|
||||
wily_4_c11 = "Wily Stage 4 - Weapon Energy 6"
|
||||
wily_4_c12 = "Wily Stage 4 - 1-Up"
|
||||
wily_4_c13 = "Wily Stage 4 - Weapon Energy 7"
|
||||
wily_4_c14 = "Wily Stage 4 - Weapon Energy 8"
|
||||
wily_4_c15 = "Wily Stage 4 - Weapon Energy 9"
|
||||
wily_4_c16 = "Wily Stage 4 - Weapon Energy 10"
|
||||
wily_4_c17 = "Wily Stage 4 - Weapon Energy 11"
|
||||
wily_4_c18 = "Wily Stage 4 - Weapon Energy 12"
|
||||
wily_4_c19 = "Wily Stage 4 - Weapon Energy 13"
|
||||
wily_4_c20 = "Wily Stage 4 - Weapon Energy 14"
|
||||
wily_5_c1 = "Wily Stage 5 - Weapon Energy 1"
|
||||
wily_5_c2 = "Wily Stage 5 - Weapon Energy 2"
|
||||
wily_5_c3 = "Wily Stage 5 - Mystery Tank 1"
|
||||
wily_5_c4 = "Wily Stage 5 - Mystery Tank 2"
|
||||
wily_6_c1 = "Wily Stage 6 - Mystery Tank 1"
|
||||
wily_6_c2 = "Wily Stage 6 - Mystery Tank 2"
|
||||
wily_6_c3 = "Wily Stage 6 - Weapon Energy 1"
|
||||
wily_6_c4 = "Wily Stage 6 - Weapon Energy 2"
|
||||
wily_6_c5 = "Wily Stage 6 - 1-Up"
|
||||
wily_6_c6 = "Wily Stage 6 - E-Tank"
|
||||
wily_6_c7 = "Wily Stage 6 - Health Energy"
|
||||
164
worlds/mm3/options.py
Normal file
164
worlds/mm3/options.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Toggle, DeathLink, TextChoice, Range, OptionDict, PerGameCommonOptions
|
||||
from schema import Schema, And, Use, Optional
|
||||
from .rules import bosses, weapons_to_id
|
||||
|
||||
|
||||
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_needle_man = 0
|
||||
option_magnet_man = 1
|
||||
option_gemini_man = 2
|
||||
option_hard_man = 3
|
||||
option_top_man = 4
|
||||
option_snake_man = 5
|
||||
option_spark_man = 6
|
||||
option_shadow_man = 7
|
||||
default = "random"
|
||||
|
||||
|
||||
class Consumables(Choice):
|
||||
"""
|
||||
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
|
||||
"""
|
||||
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"
|
||||
elif value == 2:
|
||||
return "Weapon/Health Energy"
|
||||
return super().get_option_name(value)
|
||||
|
||||
|
||||
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. Certain enemies 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 Wily/Gamma).
|
||||
"""
|
||||
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 Shadow Blade may do Top Spin damage.
|
||||
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 Wily4Requirement(Range):
|
||||
"""
|
||||
Change the amount of Robot Masters that are required to be defeated for
|
||||
the door to the Wily Machine to open.
|
||||
"""
|
||||
display_name = "Wily 4 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(0, 14))
|
||||
}
|
||||
})
|
||||
default = {}
|
||||
|
||||
|
||||
class ReduceFlashing(Toggle):
|
||||
"""
|
||||
Reduce flashing seen in gameplay, such as in stages and when defeating certain bosses.
|
||||
"""
|
||||
display_name = "Reduce Flashing"
|
||||
|
||||
|
||||
class MusicShuffle(Choice):
|
||||
"""
|
||||
Shuffle the music that plays in every stage
|
||||
"""
|
||||
display_name = "Music Shuffle"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_no_music = 3
|
||||
default = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MM3Options(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
starting_robot_master: StartingRobotMaster
|
||||
consumables: Consumables
|
||||
enemy_weakness: EnemyWeaknesses
|
||||
strict_weakness: StrictWeaknesses
|
||||
random_weakness: RandomWeaknesses
|
||||
wily_4_requirement: Wily4Requirement
|
||||
plando_weakness: WeaknessPlando
|
||||
palette_shuffle: PaletteShuffle
|
||||
reduce_flashing: ReduceFlashing
|
||||
music_shuffle: MusicShuffle
|
||||
374
worlds/mm3/rom.py
Normal file
374
worlds/mm3/rom.py
Normal file
@@ -0,0 +1,374 @@
|
||||
import pkgutil
|
||||
from typing import TYPE_CHECKING, Iterable
|
||||
import hashlib
|
||||
import Utils
|
||||
import os
|
||||
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from . import names
|
||||
from .rules import bosses
|
||||
|
||||
from .text import MM3TextEntry
|
||||
from .color import get_colors_for_item, write_palette_shuffle
|
||||
from .options import Consumables
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
|
||||
MM3LCHASH = "5266687de215e790b2008284402f3917"
|
||||
PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
|
||||
MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c"
|
||||
MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5"
|
||||
|
||||
enemy_weakness_ptrs: dict[int, int] = {
|
||||
0: 0x14100,
|
||||
1: 0x14200,
|
||||
2: 0x14300,
|
||||
3: 0x14400,
|
||||
4: 0x14500,
|
||||
5: 0x14600,
|
||||
6: 0x14700,
|
||||
7: 0x14800,
|
||||
8: 0x14900,
|
||||
}
|
||||
|
||||
enemy_addresses: dict[str, int] = {
|
||||
"Dada": 0x12,
|
||||
"Potton": 0x13,
|
||||
"New Shotman": 0x15,
|
||||
"Hammer Joe": 0x16,
|
||||
"Peterchy": 0x17,
|
||||
"Bubukan": 0x18,
|
||||
"Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole?
|
||||
"Bomb Flier": 0x1A,
|
||||
"Yambow": 0x1D,
|
||||
"Metall 2": 0x1E,
|
||||
"Cannon": 0x22,
|
||||
"Jamacy": 0x25,
|
||||
"Jamacy 2": 0x26, # dunno what this is, but I won't question
|
||||
"Jamacy 3": 0x27,
|
||||
"Jamacy 4": 0x28, # tf is this Capcom
|
||||
"Mag Fly": 0x2A,
|
||||
"Egg": 0x2D,
|
||||
"Gyoraibo 2": 0x2E,
|
||||
"Junk Golem": 0x2F,
|
||||
"Pickelman Bull": 0x30,
|
||||
"Nitron": 0x35,
|
||||
"Pole": 0x37,
|
||||
"Gyoraibo": 0x38,
|
||||
"Hari Harry": 0x3A,
|
||||
"Penpen Maker": 0x3B,
|
||||
"Returning Monking": 0x3C,
|
||||
"Have 'Su' Bee": 0x3E,
|
||||
"Hive": 0x3F,
|
||||
"Bolton-Nutton": 0x40,
|
||||
"Walking Bomb": 0x44,
|
||||
"Elec'n": 0x45,
|
||||
"Mechakkero": 0x47,
|
||||
"Chibee": 0x4B,
|
||||
"Swimming Penpen": 0x4D,
|
||||
"Top": 0x52,
|
||||
"Penpen": 0x56,
|
||||
"Komasaburo": 0x57,
|
||||
"Parasyu": 0x59,
|
||||
"Hologran (Static)": 0x5A,
|
||||
"Hologran (Moving)": 0x5B,
|
||||
"Bomber Pepe": 0x5C,
|
||||
"Metall DX": 0x5D,
|
||||
"Petit Snakey": 0x5E,
|
||||
"Proto Man": 0x62,
|
||||
"Break Man": 0x63,
|
||||
"Metall": 0x7D,
|
||||
"Giant Springer": 0x83,
|
||||
"Springer Missile": 0x85,
|
||||
"Giant Snakey": 0x99,
|
||||
"Tama": 0x9A,
|
||||
"Doc Robot (Flash)": 0xB0,
|
||||
"Doc Robot (Wood)": 0xB1,
|
||||
"Doc Robot (Crash)": 0xB2,
|
||||
"Doc Robot (Metal)": 0xB3,
|
||||
"Doc Robot (Bubble)": 0xC0,
|
||||
"Doc Robot (Heat)": 0xC1,
|
||||
"Doc Robot (Quick)": 0xC2,
|
||||
"Doc Robot (Air)": 0xC3,
|
||||
"Snake": 0xCA,
|
||||
"Needle Man": 0xD0,
|
||||
"Magnet Man": 0xD1,
|
||||
"Top Man": 0xD2,
|
||||
"Shadow Man": 0xD3,
|
||||
"Top Man's Top": 0xD5,
|
||||
"Shadow Man (Sliding)": 0xD8, # Capcom I swear
|
||||
"Hard Man": 0xE0,
|
||||
"Spark Man": 0xE2,
|
||||
"Snake Man": 0xE4,
|
||||
"Gemini Man": 0xE6,
|
||||
"Gemini Man (Clone)": 0xE7, # Capcom why
|
||||
"Yellow Devil MK-II": 0xF1,
|
||||
"Wily Machine 3": 0xF3,
|
||||
"Gamma": 0xF8,
|
||||
"Kamegoro": 0x101,
|
||||
"Kamegoro Shell": 0x102,
|
||||
"Holograph Mega Man": 0x105,
|
||||
"Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header
|
||||
}
|
||||
|
||||
# addresses printed when assembling basepatch
|
||||
wily_4_ptr: int = 0x7F570
|
||||
consumables_ptr: int = 0x7FDEA
|
||||
energylink_ptr: int = 0x7FDF9
|
||||
|
||||
|
||||
class MM3ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH]
|
||||
game = "Mega Man 3"
|
||||
patch_file_ending = ".apmm3"
|
||||
result_file_ending = ".nes"
|
||||
name: bytearray
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["mm3_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: "MM3World", patch: MM3ProcedurePatch) -> None:
|
||||
patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4")))
|
||||
# text writing
|
||||
|
||||
base_address = 0x3C000
|
||||
color_address = 0x31BC7
|
||||
for i, offset, location in zip([0, 8, 1, 2,
|
||||
3, 4, 5, 6,
|
||||
7, 9],
|
||||
[0x10, 0x50, 0x91, 0xD2,
|
||||
0x113, 0x154, 0x195, 0x1D6,
|
||||
0x217, 0x257],
|
||||
[
|
||||
names.get_needle_cannon,
|
||||
names.get_rush_jet,
|
||||
names.get_magnet_missile,
|
||||
names.get_gemini_laser,
|
||||
names.get_hard_knuckle,
|
||||
names.get_top_spin,
|
||||
names.get_search_snake,
|
||||
names.get_spark_shock,
|
||||
names.get_shadow_blade,
|
||||
names.get_rush_marine,
|
||||
]):
|
||||
item = world.get_location(location).item
|
||||
if item:
|
||||
if len(item.name) <= 13:
|
||||
# we want to just place it in the center
|
||||
first_str = ""
|
||||
second_str = item.name
|
||||
third_str = ""
|
||||
elif len(item.name) <= 26:
|
||||
# spread across second and third
|
||||
first_str = ""
|
||||
second_str = item.name[:13]
|
||||
third_str = item.name[13:]
|
||||
else:
|
||||
# all three
|
||||
first_str = item.name[:13]
|
||||
second_str = item.name[13:26]
|
||||
third_str = item.name[26:]
|
||||
if len(third_str) > 13:
|
||||
third_str = third_str[:13]
|
||||
player_str = world.multiworld.get_player_name(item.player)
|
||||
if len(player_str) > 13:
|
||||
player_str = player_str[:13]
|
||||
y_coords = 0xA5
|
||||
row = 0x21
|
||||
if location in [names.get_rush_marine, names.get_rush_jet]:
|
||||
y_coords = 0x45
|
||||
row = 0x22
|
||||
patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve())
|
||||
patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve())
|
||||
patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve())
|
||||
if y_coords + 0x60 > 0xFF:
|
||||
row += 1
|
||||
y_coords = 0x01
|
||||
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve())
|
||||
colors_high, colors_low = get_colors_for_item(item.name)
|
||||
patch.write_bytes(color_address + (i * 8) + 1, colors_high)
|
||||
patch.write_bytes(color_address + (i * 8) + 5, colors_low)
|
||||
else:
|
||||
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve())
|
||||
|
||||
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
|
||||
for boss in bosses:
|
||||
if boss == "Kamegoro Maker":
|
||||
enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
elif boss == "Gemini Man":
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
elif boss == "Shadow Man":
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]]
|
||||
for i in world.weapon_damage}
|
||||
else:
|
||||
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
|
||||
|
||||
if world.options.enemy_weakness:
|
||||
for enemy in enemy_addresses:
|
||||
if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]:
|
||||
continue
|
||||
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
|
||||
if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0:
|
||||
enemy_weaknesses[enemy][0] = 1
|
||||
elif enemy == "Jamacy 2":
|
||||
# bruh
|
||||
if not enemy_weaknesses[enemy][8] > 0:
|
||||
enemy_weaknesses[enemy][8] = 1
|
||||
if not enemy_weaknesses[enemy][3] > 0:
|
||||
enemy_weaknesses[enemy][3] = 1
|
||||
|
||||
for enemy, damage in enemy_weaknesses.items():
|
||||
for weapon in enemy_weakness_ptrs:
|
||||
if damage[weapon] < 0:
|
||||
damage[weapon] = 256 + damage[weapon]
|
||||
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon])
|
||||
|
||||
if world.options.consumables != Consumables.option_all:
|
||||
value_a = 0x64
|
||||
value_b = 0x6A
|
||||
if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank):
|
||||
value_a = 0x68
|
||||
if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health):
|
||||
value_b = 0x67
|
||||
patch.write_byte(consumables_ptr - 3, value_a)
|
||||
patch.write_byte(consumables_ptr + 1, value_b)
|
||||
|
||||
patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value)
|
||||
|
||||
patch.write_byte(energylink_ptr + 1, world.options.energy_link.value)
|
||||
|
||||
if world.options.reduce_flashing:
|
||||
# Spark Man
|
||||
patch.write_byte(0x12649, 8)
|
||||
patch.write_byte(0x1264E, 8)
|
||||
patch.write_byte(0x12653, 8)
|
||||
# Shadow Man
|
||||
patch.write_byte(0x12658, 0x10)
|
||||
# Gemini Man
|
||||
patch.write_byte(0x12637, 0x20)
|
||||
patch.write_byte(0x1263D, 0x20)
|
||||
patch.write_byte(0x12643, 0x20)
|
||||
# Gamma
|
||||
patch.write_byte(0x7DA4A, 0xF)
|
||||
|
||||
if world.options.music_shuffle:
|
||||
if world.options.music_shuffle.current_key == "no_music":
|
||||
pool = [0xF0] * 18
|
||||
elif world.options.music_shuffle.current_key == "randomized":
|
||||
pool = world.random.choices(range(1, 0xC), k=18)
|
||||
else:
|
||||
pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11]
|
||||
world.random.shuffle(pool)
|
||||
patch.write_bytes(0x7CD1C, pool)
|
||||
|
||||
from Utils import __version__
|
||||
patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
patch.name.extend([0] * (21 - len(patch.name)))
|
||||
patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid!
|
||||
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
|
||||
patch.write_byte(0x3F346, deathlink_byte)
|
||||
|
||||
patch.write_bytes(0x3F34C, world.world_version)
|
||||
|
||||
version_map = {
|
||||
"0": 0x00,
|
||||
"1": 0x01,
|
||||
"2": 0x02,
|
||||
"3": 0x03,
|
||||
"4": 0x04,
|
||||
"5": 0x05,
|
||||
"6": 0x06,
|
||||
"7": 0x07,
|
||||
"8": 0x08,
|
||||
"9": 0x09,
|
||||
".": 0x26
|
||||
}
|
||||
patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25))
|
||||
patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25))
|
||||
|
||||
# BY SILVRIS
|
||||
patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C])
|
||||
# ARCHIPELAGO x.x.x
|
||||
patch.write_bytes(0x654D,
|
||||
[0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18])
|
||||
patch.write_bytes(0x6559, 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\x10\x40\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: bytes | None = 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_mm3(base_rom_bytes)
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}:
|
||||
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:
|
||||
from . import MM3World
|
||||
if not file_name:
|
||||
file_name = MM3World.settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
prg_offset = 0xCF1B0
|
||||
prg_size = 0x40000
|
||||
chr_offset = 0x10F1B0
|
||||
chr_size = 0x20000
|
||||
|
||||
|
||||
def extract_mm3(proteus: bytes) -> bytes:
|
||||
mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size])
|
||||
mm3.extend(proteus[chr_offset:chr_offset + chr_size])
|
||||
return bytes(mm3)
|
||||
388
worlds/mm3/rules.py
Normal file
388
worlds/mm3/rules.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
from . import names
|
||||
from .locations import get_boss_locations, get_oneup_locations, get_energy_locations
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM3World
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
bosses: dict[str, int] = {
|
||||
"Needle Man": 0,
|
||||
"Magnet Man": 1,
|
||||
"Gemini Man": 2,
|
||||
"Hard Man": 3,
|
||||
"Top Man": 4,
|
||||
"Snake Man": 5,
|
||||
"Spark Man": 6,
|
||||
"Shadow Man": 7,
|
||||
"Doc Robot (Metal)": 8,
|
||||
"Doc Robot (Quick)": 9,
|
||||
"Doc Robot (Air)": 10,
|
||||
"Doc Robot (Crash)": 11,
|
||||
"Doc Robot (Flash)": 12,
|
||||
"Doc Robot (Bubble)": 13,
|
||||
"Doc Robot (Wood)": 14,
|
||||
"Doc Robot (Heat)": 15,
|
||||
"Break Man": 16,
|
||||
"Kamegoro Maker": 17,
|
||||
"Yellow Devil MK-II": 18,
|
||||
"Holograph Mega Man": 19,
|
||||
"Wily Machine 3": 20,
|
||||
"Gamma": 21
|
||||
}
|
||||
|
||||
weapons_to_id: dict[str, int] = {
|
||||
"Mega Buster": 0,
|
||||
"Needle Cannon": 1,
|
||||
"Magnet Missile": 2,
|
||||
"Gemini Laser": 3,
|
||||
"Hard Knuckle": 4,
|
||||
"Top Spin": 5,
|
||||
"Search Snake": 6,
|
||||
"Spark Shot": 7,
|
||||
"Shadow Blade": 8,
|
||||
}
|
||||
|
||||
weapon_damage: dict[int, list[int]] = {
|
||||
0: [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 0, ], # Mega Buster
|
||||
1: [4, 1, 1, 0, 2, 4, 2, 1, 0, 1, 1, 2, 4, 2, 4, 2, 0, 3, 1, 1, 1, 0, ], # Needle Cannon
|
||||
2: [1, 4, 2, 4, 1, 0, 0, 1, 4, 2, 4, 1, 1, 0, 0, 1, 0, 3, 1, 0, 1, 0, ], # Magnet Missile
|
||||
3: [7, 2, 4, 1, 0, 1, 1, 1, 1, 4, 2, 0, 4, 1, 1, 1, 0, 3, 1, 1, 1, 0, ], # Gemini Laser
|
||||
4: [0, 2, 2, 4, 7, 2, 2, 2, 4, 1, 2, 7, 0, 2, 2, 2, 0, 1, 5, 4, 7, 4, ], # Hard Knuckle
|
||||
5: [1, 1, 2, 0, 4, 2, 1, 7, 0, 1, 1, 4, 1, 1, 2, 7, 0, 1, 0, 7, 0, 2, ], # Top Spin
|
||||
6: [1, 1, 5, 0, 1, 4, 0, 1, 0, 4, 1, 1, 1, 0, 4, 1, 0, 1, 0, 7, 4, 2, ], # Search Snake
|
||||
7: [0, 7, 1, 0, 1, 1, 4, 1, 2, 1, 4, 1, 0, 4, 1, 1, 0, 0, 0, 0, 7, 0, ], # Spark Shot
|
||||
8: [2, 7, 2, 0, 1, 2, 4, 4, 2, 2, 0, 1, 2, 4, 2, 4, 0, 1, 3, 2, 2, 2, ], # Shadow Blade
|
||||
}
|
||||
|
||||
weapons_to_name: dict[int, str] = {
|
||||
1: names.needle_cannon,
|
||||
2: names.magnet_missile,
|
||||
3: names.gemini_laser,
|
||||
4: names.hard_knuckle,
|
||||
5: names.top_spin,
|
||||
6: names.search_snake,
|
||||
7: names.spark_shock,
|
||||
8: names.shadow_blade
|
||||
}
|
||||
|
||||
minimum_weakness_requirement: dict[int, int] = {
|
||||
0: 1, # Mega Buster is free
|
||||
1: 1, # 112 shots of Needle Cannon
|
||||
2: 2, # 14 shots of Magnet Missile
|
||||
3: 2, # 14 shots of Gemini Laser
|
||||
4: 2, # 14 uses of Hard Knuckle
|
||||
5: 4, # an unknown amount of Top Spin (4 means you should be able to be fine)
|
||||
6: 1, # 56 uses of Search Snake
|
||||
7: 2, # 14 functional uses of Spark Shot (fires in twos)
|
||||
8: 1, # 56 uses of Shadow Blade
|
||||
}
|
||||
|
||||
robot_masters: dict[int, str] = {
|
||||
0: "Needle Man Defeated",
|
||||
1: "Magnet Man Defeated",
|
||||
2: "Gemini Man Defeated",
|
||||
3: "Hard Man Defeated",
|
||||
4: "Top Man Defeated",
|
||||
5: "Snake Man Defeated",
|
||||
6: "Spark Man Defeated",
|
||||
7: "Shadow Man Defeated"
|
||||
}
|
||||
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 0.25,
|
||||
2: 2,
|
||||
3: 2,
|
||||
4: 2,
|
||||
5: 7, # Not really, but we can really only rely on Top for one RBM
|
||||
6: 0.5,
|
||||
7: 2,
|
||||
8: 0.5,
|
||||
}
|
||||
|
||||
|
||||
def can_defeat_enough_rbms(state: "CollectionState", player: int,
|
||||
required: int, boss_requirements: dict[int, list[int]]) -> bool:
|
||||
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 has_rush_vertical(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_coil, names.rush_jet], player)
|
||||
|
||||
|
||||
def can_traverse_long_water(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_marine, names.rush_jet], player)
|
||||
|
||||
|
||||
def has_any_rush(state: "CollectionState", player: int) -> bool:
|
||||
return state.has_any([names.rush_coil, names.rush_jet, names.rush_marine], player)
|
||||
|
||||
|
||||
def has_rush_jet(state: "CollectionState", player: int) -> bool:
|
||||
return state.has(names.rush_jet, player)
|
||||
|
||||
|
||||
def set_rules(world: "MM3World") -> 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"):
|
||||
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 3"]
|
||||
world.weapon_damage = slot_data["weapon_damage"]
|
||||
else:
|
||||
if world.options.random_weakness == world.options.random_weakness.option_shuffled:
|
||||
weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon != 0]
|
||||
world.random.shuffle(weapon_tables)
|
||||
for i in range(1, 9):
|
||||
world.weapon_damage[i] = weapon_tables.pop()
|
||||
elif world.options.random_weakness == world.options.random_weakness.option_randomized:
|
||||
world.weapon_damage = {i: [] for i in range(9)}
|
||||
for boss in range(22):
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(min(14, max(0, int(world.random.normalvariate(3, 3)))))
|
||||
if not any([world.weapon_damage[weapon][boss] >= 4
|
||||
for weapon in range(1, 9)]):
|
||||
# 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(4, 14) # Force weakness
|
||||
# handle Break Man
|
||||
boss = 16
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
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(22):
|
||||
if i == 16:
|
||||
continue # Break is only weak to buster on non-random, and minimal damage on random
|
||||
elif weapon == 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif i in (20, 21) and not world.options.random_weakness:
|
||||
continue
|
||||
# Gamma and Wily Machine need all weaknesses present, so allow
|
||||
elif not world.options.random_weakness == world.options.random_weakness.option_randomized \
|
||||
and i == 17:
|
||||
if 3 > world.weapon_damage[weapon][i] > 0:
|
||||
# Kamegoros take 3 max from weapons on non-random
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif 4 > world.weapon_damage[weapon][i] > 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
if not any(w for w in world.weapon_damage
|
||||
if w != weapons_to_id[p_weapon]
|
||||
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]):
|
||||
# we need to replace this weakness
|
||||
weakness = world.random.choice([key for key in world.weapon_damage
|
||||
if key != weapons_to_id[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]
|
||||
|
||||
# handle special cases
|
||||
for boss in range(22):
|
||||
for weapon in range(1, 9):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
|
||||
for i in range(1, 8) if i != weapon)):
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = 1
|
||||
|
||||
# weakness validation, it is better to confirm a completable seed than respect plando
|
||||
boss_health = {boss: 0x1C for boss in range(8)}
|
||||
|
||||
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)}
|
||||
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()
|
||||
}
|
||||
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
|
||||
for boss in boss_flexibility:
|
||||
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}
|
||||
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]
|
||||
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
|
||||
used_weapons[boss].add(wp)
|
||||
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
|
||||
max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], 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)
|
||||
used_weapons[boss].add(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)
|
||||
used_weapons[boss].add(wp)
|
||||
|
||||
world.wily_4_weapons = {boss: sorted(weapons) for boss, weapons in used_weapons.items()}
|
||||
|
||||
for i, boss_locations in zip(range(22), [
|
||||
get_boss_locations("Needle Man Stage"),
|
||||
get_boss_locations("Magnet Man Stage"),
|
||||
get_boss_locations("Gemini Man Stage"),
|
||||
get_boss_locations("Hard Man Stage"),
|
||||
get_boss_locations("Top Man Stage"),
|
||||
get_boss_locations("Snake Man Stage"),
|
||||
get_boss_locations("Spark Man Stage"),
|
||||
get_boss_locations("Shadow Man Stage"),
|
||||
get_boss_locations("Doc Robot (Spark) - Metal"),
|
||||
get_boss_locations("Doc Robot (Spark) - Quick"),
|
||||
get_boss_locations("Doc Robot (Needle) - Air"),
|
||||
get_boss_locations("Doc Robot (Needle) - Crash"),
|
||||
get_boss_locations("Doc Robot (Gemini) - Flash"),
|
||||
get_boss_locations("Doc Robot (Gemini) - Bubble"),
|
||||
get_boss_locations("Doc Robot (Shadow) - Wood"),
|
||||
get_boss_locations("Doc Robot (Shadow) - Heat"),
|
||||
get_boss_locations("Break Man"),
|
||||
get_boss_locations("Wily Stage 1"),
|
||||
get_boss_locations("Wily Stage 2"),
|
||||
get_boss_locations("Wily Stage 3"),
|
||||
get_boss_locations("Wily Stage 5"),
|
||||
get_boss_locations("Wily Stage 6")
|
||||
]):
|
||||
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
|
||||
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 in (20, 21):
|
||||
# multi-phase fights, get all potential weaknesses
|
||||
# we should probably do this smarter, but this works for now
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
|
||||
else:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
|
||||
|
||||
# Need to defeat x amount of robot masters for Wily 4
|
||||
add_rule(world.get_location(names.wily_stage_4),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_4_requirement.value,
|
||||
world.wily_4_weapons))
|
||||
|
||||
# Handle Doc Robo stage connections
|
||||
for entrance, location in (("To Doc Robot (Needle) - Crash", names.doc_air),
|
||||
("To Doc Robot (Gemini) - Bubble", names.doc_flash),
|
||||
("To Doc Robot (Shadow) - Heat", names.doc_wood),
|
||||
("To Doc Robot (Spark) - Quick", names.doc_metal)):
|
||||
entrance_object = world.get_entrance(entrance)
|
||||
add_rule(entrance_object, lambda state, loc=location: state.can_reach(loc, "Location", world.player))
|
||||
|
||||
# finally, real logic
|
||||
for location in get_boss_locations("Hard Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Gemini Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
|
||||
add_rule(world.get_entrance("To Doc Robot (Spark) - Metal"),
|
||||
lambda state: has_rush_vertical(state, world.player) and
|
||||
state.has_any([names.shadow_blade, names.gemini_laser], world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Needle) - Air"),
|
||||
lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Needle) - Crash"),
|
||||
lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_entrance("To Doc Robot (Gemini) - Bubble"),
|
||||
lambda state: has_rush_vertical(state, world.player) and can_traverse_long_water(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Wily Stage 1"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
|
||||
for location in get_boss_locations("Wily Stage 2"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
|
||||
# Wily 3 technically needs vertical
|
||||
# However, Wily 3 requires beating Wily 2, and Wily 2 explicitly needs Jet
|
||||
# So we can skip the additional rule on Wily 3
|
||||
|
||||
if world.options.consumables in (world.options.consumables.option_1up_etank,
|
||||
world.options.consumables.option_all):
|
||||
add_rule(world.get_location(names.needle_man_c2), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c1), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c3),
|
||||
lambda state: has_rush_vertical(state, world.player)
|
||||
or state.has_any([names.gemini_laser, names.shadow_blade], world.player))
|
||||
for location in (names.gemini_man_c6, names.gemini_man_c7, names.gemini_man_c10):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
for location in get_oneup_locations("Hard Man Stage"):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.top_man_c6), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.doc_needle_c2), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.doc_needle_c3), lambda state: has_rush_jet(state, world.player))
|
||||
add_rule(world.get_location(names.doc_gemini_c1), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.doc_gemini_c2), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.wily_1_c8), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.wily_1_c4, names.wily_1_c8]:
|
||||
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
|
||||
for location in get_oneup_locations("Wily Stage 2"):
|
||||
if location == names.wily_2_c3:
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
if world.options.consumables in (world.options.consumables.option_weapon_health,
|
||||
world.options.consumables.option_all):
|
||||
add_rule(world.get_location(names.gemini_man_c2), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c4), lambda state: has_rush_vertical(state, world.player))
|
||||
add_rule(world.get_location(names.gemini_man_c5), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in (names.gemini_man_c8, names.gemini_man_c9):
|
||||
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
|
||||
for location in get_energy_locations("Hard Man Stage"):
|
||||
if location == names.hard_man_c1:
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in (names.spark_man_c1, names.spark_man_c2):
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.top_man_c2, names.top_man_c3, names.top_man_c4, names.top_man_c7]:
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in [names.wily_1_c5, names.wily_1_c6, names.wily_1_c7]:
|
||||
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
|
||||
for location in [names.wily_1_c6, names.wily_1_c7, names.wily_1_c11, names.wily_1_c12]:
|
||||
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
|
||||
for location in get_energy_locations("Wily Stage 2"):
|
||||
if location in (names.wily_2_c1, names.wily_2_c2, names.wily_2_c4):
|
||||
continue
|
||||
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
|
||||
0
worlds/mm3/src/__init__.py
Normal file
0
worlds/mm3/src/__init__.py
Normal file
781
worlds/mm3/src/mm3_basepatch.asm
Normal file
781
worlds/mm3/src/mm3_basepatch.asm
Normal file
@@ -0,0 +1,781 @@
|
||||
norom
|
||||
!headersize = 16
|
||||
|
||||
!controller_flip = $14 ; only on first frame of input, used by crash man, etc
|
||||
!controller_mirror = $16
|
||||
!current_stage = $22
|
||||
!current_state = $60
|
||||
!completed_rbm_stages = $61
|
||||
!completed_doc_stages = $62
|
||||
!current_wily = $75
|
||||
!received_rbm_stages = $680
|
||||
!received_doc_stages = $681
|
||||
; !deathlink = $30, set to $0E
|
||||
!energylink_packet = $682
|
||||
!last_wily = $683
|
||||
!rbm_strobe = $684
|
||||
!sound_effect_strobe = $685
|
||||
!doc_robo_kills = $686
|
||||
!wily_stage_completion = $687
|
||||
;!received_items = $688
|
||||
!acquired_rush = $689
|
||||
|
||||
!current_weapon = $A0
|
||||
!current_health = $A2
|
||||
!received_weapons = $A3
|
||||
|
||||
'0' = $00
|
||||
'1' = $01
|
||||
'2' = $02
|
||||
'3' = $03
|
||||
'4' = $04
|
||||
'5' = $05
|
||||
'6' = $06
|
||||
'7' = $07
|
||||
'8' = $08
|
||||
'9' = $09
|
||||
'A' = $0A
|
||||
'B' = $0B
|
||||
'C' = $0C
|
||||
'D' = $0D
|
||||
'E' = $0E
|
||||
'F' = $0F
|
||||
'G' = $10
|
||||
'H' = $11
|
||||
'I' = $12
|
||||
'J' = $13
|
||||
'K' = $14
|
||||
'L' = $15
|
||||
'M' = $16
|
||||
'N' = $17
|
||||
'O' = $18
|
||||
'P' = $19
|
||||
'Q' = $1A
|
||||
'R' = $1B
|
||||
'S' = $1C
|
||||
'T' = $1D
|
||||
'U' = $1E
|
||||
'V' = $1F
|
||||
'W' = $20
|
||||
'X' = $21
|
||||
'Y' = $22
|
||||
'Z' = $23
|
||||
' ' = $25
|
||||
'.' = $26
|
||||
',' = $27
|
||||
'!' = $29
|
||||
'r' = $2A
|
||||
':' = $2B
|
||||
|
||||
; !consumable_checks = $0F80 ; have to find in-stage solutions for this, there's literally not enough ram
|
||||
|
||||
!CONTROLLER_SELECT = #$20
|
||||
!CONTROLLER_SELECT_START = #$30
|
||||
!CONTROLLER_ALL_BUTTON = #$F0
|
||||
|
||||
!PpuControl_2000 = $2000
|
||||
!PpuMask_2001 = $2001
|
||||
!PpuAddr_2006 = $2006
|
||||
!PpuData_2007 = $2007
|
||||
|
||||
;!LOAD_BANK = $C000
|
||||
|
||||
macro org(address,bank)
|
||||
if <bank> == $3E
|
||||
org <address>-$C000+($2000*<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
|
||||
if <bank> == $3F
|
||||
org <address>-$E000+($2000*<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
|
||||
if <address> >= $A000
|
||||
org <address>-$A000+($2000*<bank>)+!headersize
|
||||
base <address>
|
||||
else
|
||||
org <address>-$8000+($2000*<bank>)+!headersize
|
||||
base <address>
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endmacro
|
||||
|
||||
; capcom.....
|
||||
; i can't keep defending you like this
|
||||
|
||||
;P
|
||||
%org($BEBA, $13)
|
||||
RemoveP:
|
||||
db $25
|
||||
;A
|
||||
%org($BD7D, $13)
|
||||
RemoveA:
|
||||
db $25
|
||||
;S
|
||||
%org($BE7D, $13)
|
||||
RemoveS1:
|
||||
db $25
|
||||
;S
|
||||
%org($BDD5, $13)
|
||||
RemoveS2:
|
||||
db $25
|
||||
|
||||
;W
|
||||
%org($BDC7, $13)
|
||||
RemoveW:
|
||||
db $25
|
||||
;O
|
||||
%org($BEC7, $13)
|
||||
RemoveO:
|
||||
db $25
|
||||
;R
|
||||
%org($BDCF, $13)
|
||||
RemoveR:
|
||||
db $25
|
||||
;D
|
||||
%org($BECF, $13)
|
||||
RemoveD:
|
||||
db $25
|
||||
|
||||
%org($A17C, $02)
|
||||
AdjustWeaponRefill:
|
||||
; compare vs unreceived instead. Since the stage ends anyways, this just means you aren't granted the weapon if you don't have it already
|
||||
CMP #$1C
|
||||
BCS WeaponRefillJump
|
||||
|
||||
%org($A18B, $02)
|
||||
WeaponRefillJump:
|
||||
; just as a branch target
|
||||
|
||||
%org($A3BF, $02)
|
||||
FixPseudoSnake:
|
||||
JMP CheckFirstWep
|
||||
NOP
|
||||
|
||||
%org($A3CB, $02)
|
||||
FixPseudoRush:
|
||||
JMP CheckRushWeapon
|
||||
NOP
|
||||
|
||||
%org($BF80, $02)
|
||||
CheckRushWeapon:
|
||||
AND #$01
|
||||
BNE .Rush
|
||||
JMP $A3CF
|
||||
.Rush:
|
||||
LDA $A1
|
||||
CLC
|
||||
ADC $B4
|
||||
TAY
|
||||
LDA $00A2, Y
|
||||
BNE .Skip
|
||||
DEC $A1
|
||||
.Skip:
|
||||
JMP $A477
|
||||
|
||||
; don't even try to go past this point
|
||||
|
||||
%org($802F, $0B)
|
||||
HookBreakMan:
|
||||
JSR SetBreakMan
|
||||
NOP
|
||||
|
||||
%org($90BC, $18)
|
||||
BlockPassword:
|
||||
AND #$08 ; originally 0C, just block down inputs
|
||||
|
||||
%org($9258, $18)
|
||||
HookStageSelect:
|
||||
JSR ChangeStageMode
|
||||
NOP
|
||||
|
||||
%org($92F2, $18)
|
||||
AccessStageTarget:
|
||||
|
||||
%org($9316, $18)
|
||||
AccessStage:
|
||||
JSR RewireDocRobotAccess
|
||||
NOP #2
|
||||
BEQ AccessStageTarget
|
||||
|
||||
%org($9468, $18)
|
||||
HookWeaponGet:
|
||||
JSR WeaponReceived
|
||||
NOP #4
|
||||
|
||||
%org($9917, $18)
|
||||
GameOverStageSelect:
|
||||
; fix it returning to Wily 1
|
||||
CMP #$16
|
||||
|
||||
%org($9966, $18)
|
||||
SwapSelectTiles:
|
||||
; swaps when stage select face tiles should be shown
|
||||
JMP InvertSelectTiles
|
||||
NOP
|
||||
|
||||
%org($9A54, $18)
|
||||
SwapSelectSprites:
|
||||
JMP InvertSelectSprites
|
||||
NOP
|
||||
|
||||
%org($9AFF, $18)
|
||||
BreakManSelect:
|
||||
JSR ApplyLastWily
|
||||
NOP
|
||||
|
||||
%org($BE22, $1D)
|
||||
ConsumableHook:
|
||||
JMP CheckConsumable
|
||||
|
||||
%org($BE32, $1D)
|
||||
EnergyLinkHook:
|
||||
JSR EnergyLink
|
||||
|
||||
%org($A000, $1E)
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P"
|
||||
db $22, $45, $0C, "PLACEHOLDER 1"
|
||||
db $22, $65, $0C, "PLACEHOLDER 2"
|
||||
db $22, $85, $0C, "PLACEHOLDER 3"
|
||||
db $22, $A5, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P", $FF
|
||||
db $21, $A5, $0C, "PLACEHOLDER 1"
|
||||
db $21, $C5, $0C, "PLACEHOLDER 2"
|
||||
db $21, $E5, $0C, "PLACEHOLDER 3"
|
||||
db $22, $05, $0C, "PLACEHOLDER P"
|
||||
db $22, $45, $0C, "PLACEHOLDER 1"
|
||||
db $22, $65, $0C, "PLACEHOLDER 2"
|
||||
db $22, $85, $0C, "PLACEHOLDER 3"
|
||||
db $22, $A5, $0C, "PLACEHOLDER P", $FF
|
||||
|
||||
ShowItemString:
|
||||
STY $04
|
||||
LDA ItemLower,X
|
||||
STA $02
|
||||
LDA ItemUpper,X
|
||||
STA $03
|
||||
LDY #$00
|
||||
.LoadString:
|
||||
LDA ($02),Y
|
||||
ORA $10
|
||||
STA $0780,Y
|
||||
BMI .Return
|
||||
INY
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
INY
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
STA $00
|
||||
INY
|
||||
.LoadCharacters:
|
||||
LDA ($02),Y
|
||||
STA $0780,Y
|
||||
INY
|
||||
DEC $00
|
||||
BPL .LoadCharacters
|
||||
BMI .LoadString
|
||||
.Return:
|
||||
STA $19
|
||||
LDY $04
|
||||
RTS
|
||||
|
||||
ItemUpper:
|
||||
db $A0, $A0, $A0, $A1, $A1, $A1, $A1, $A2, $A2
|
||||
|
||||
ItemLower:
|
||||
db $00, $81, $C2, $03, $44, $85, $C6, $07, $47
|
||||
|
||||
%org($C8F7, $3E)
|
||||
RemoveRushCoil:
|
||||
NOP #4
|
||||
|
||||
%org($CA73, $3E)
|
||||
HookController:
|
||||
JMP ControllerHook
|
||||
NOP
|
||||
|
||||
%org($DA18, $3E)
|
||||
NullWeaponGet:
|
||||
NOP #5 ; TODO: see if I can reroute this write instead for nicer timings
|
||||
|
||||
%org($DB99, $3E)
|
||||
HookMidDoc:
|
||||
JSR SetMidDoc
|
||||
NOP
|
||||
|
||||
%org($DBB0, $3E)
|
||||
HoodEndDoc:
|
||||
JSR SetEndDoc
|
||||
NOP
|
||||
|
||||
%org($DC57, $3E)
|
||||
RerouteStageComplete:
|
||||
LDA $60
|
||||
JSR SetStageComplete
|
||||
NOP #2
|
||||
|
||||
%org($DC6F, $3E)
|
||||
RerouteRushMarine:
|
||||
JMP SetRushMarine
|
||||
NOP
|
||||
|
||||
%org($DC6A, $3E)
|
||||
RerouteRushJet:
|
||||
JMP SetRushJet
|
||||
NOP
|
||||
|
||||
%org($DC78, $3E)
|
||||
RerouteWilyComplete:
|
||||
JMP SetEndWily
|
||||
NOP
|
||||
EndWilyReturn:
|
||||
|
||||
%org($DF81, $3E)
|
||||
NullBreak:
|
||||
NOP #5 ; nop break man giving every weapon
|
||||
|
||||
%org($E15F, $3F)
|
||||
Wily4:
|
||||
JMP Wily4Comparison
|
||||
NOP
|
||||
|
||||
|
||||
%org($F340, $3F)
|
||||
RewireDocRobotAccess:
|
||||
LDA !current_state
|
||||
BNE .DocRobo
|
||||
LDA !received_rbm_stages
|
||||
SEC
|
||||
BCS .Return
|
||||
.DocRobo:
|
||||
LDA !received_doc_stages
|
||||
.Return:
|
||||
AND $9DED,Y
|
||||
RTS
|
||||
|
||||
ChangeStageMode:
|
||||
; also handles hot reload of stage select
|
||||
; kinda broken, sprites don't disappear and palettes go wonky with Break Man access
|
||||
; but like, it functions!
|
||||
LDA !sound_effect_strobe
|
||||
BEQ .Continue
|
||||
JSR $F89A
|
||||
LDA #$00
|
||||
STA !sound_effect_strobe
|
||||
.Continue:
|
||||
LDA $14
|
||||
AND #$20
|
||||
BEQ .Next
|
||||
LDA !current_state
|
||||
BNE .Set
|
||||
LDA !completed_doc_stages
|
||||
CMP #$C5
|
||||
BEQ .BreakMan
|
||||
LDA #$09
|
||||
SEC
|
||||
BCS .Set
|
||||
.EarlyReturn:
|
||||
LDA $14
|
||||
AND #$90
|
||||
RTS
|
||||
.BreakMan:
|
||||
LDA #$12
|
||||
.Set:
|
||||
EOR !current_state
|
||||
STA !current_state
|
||||
LDA #$01
|
||||
STA !rbm_strobe
|
||||
.Next:
|
||||
LDA !rbm_strobe
|
||||
BEQ .EarlyReturn
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
; Clear the sprite buffer
|
||||
LDX #$98
|
||||
.Loop:
|
||||
LDA #$00
|
||||
STA $01FF, X
|
||||
DEX
|
||||
STA $01FF, X
|
||||
DEX
|
||||
STA $01FF, X
|
||||
DEX
|
||||
LDA #$F8
|
||||
STA $01FF, X
|
||||
DEX
|
||||
CPX #$00
|
||||
BNE .Loop
|
||||
; Break Man Sprites
|
||||
LDX #$24
|
||||
.Loop2:
|
||||
LDA #$00
|
||||
STA $02DB, X
|
||||
DEX
|
||||
STA $02DB, X
|
||||
DEX
|
||||
STA $02DB, X
|
||||
DEX
|
||||
LDA #$F8
|
||||
STA $02DB, X
|
||||
DEX
|
||||
CPX #$00
|
||||
BNE .Loop2
|
||||
; Swap out the tilemap and write sprites
|
||||
LDY #$10
|
||||
LDA $11
|
||||
BMI .B1
|
||||
LDA $FD
|
||||
EOR #$01
|
||||
ASL A
|
||||
ASL A
|
||||
STA $10
|
||||
LDA #$01
|
||||
JSR $E8B4
|
||||
LDA #$00
|
||||
STA $70
|
||||
STA $EE
|
||||
.B3:
|
||||
LDA $10
|
||||
PHA
|
||||
JSR $EF8C
|
||||
PLA
|
||||
STA $10
|
||||
JSR $FF21
|
||||
LDA $70
|
||||
BNE .B3
|
||||
JSR $995C
|
||||
LDX #$03
|
||||
JSR $939E
|
||||
JSR $FF21
|
||||
LDX #$04
|
||||
JSR $939E
|
||||
LDA $FD
|
||||
EOR #$01
|
||||
STA $FD
|
||||
LDY #$00
|
||||
LDA #$7E
|
||||
STA $E9
|
||||
JSR $FF3C
|
||||
.B1:
|
||||
LDX #$00
|
||||
; palettes
|
||||
.B2:
|
||||
LDA $9C33,Y
|
||||
STA $0600,X
|
||||
LDA $9C23,Y
|
||||
STA $0610,X
|
||||
INY
|
||||
INX
|
||||
CPX #$10
|
||||
BNE .B2
|
||||
LDA #$FF
|
||||
STA $18
|
||||
LDA #$01
|
||||
STA $12
|
||||
LDA #$03
|
||||
STA $13
|
||||
LDA $11
|
||||
JSR $99FA
|
||||
LDA $14
|
||||
AND #$90
|
||||
RTS
|
||||
|
||||
InvertSelectTiles:
|
||||
LDY !current_state
|
||||
BNE .DocRobo
|
||||
AND !received_rbm_stages
|
||||
SEC
|
||||
BCS .Compare
|
||||
.DocRobo:
|
||||
AND !received_doc_stages
|
||||
.Compare:
|
||||
BNE .False
|
||||
JMP $996A
|
||||
.False:
|
||||
JMP $99BA
|
||||
|
||||
InvertSelectSprites:
|
||||
LDY !current_state
|
||||
BNE .DocRobo
|
||||
AND !received_rbm_stages
|
||||
SEC
|
||||
BCS .Compare
|
||||
.DocRobo:
|
||||
AND !received_doc_stages
|
||||
.Compare:
|
||||
BNE .False
|
||||
JMP $9A58
|
||||
.False:
|
||||
JMP $9A6D
|
||||
|
||||
SetStageComplete:
|
||||
CMP #$00
|
||||
BNE .DocRobo
|
||||
LDA !completed_rbm_stages
|
||||
ORA $DEC2, Y
|
||||
STA !completed_rbm_stages
|
||||
SEC
|
||||
BCS .Return
|
||||
.DocRobo:
|
||||
LDA !completed_doc_stages
|
||||
ORA $DEC2, Y
|
||||
STA !completed_doc_stages
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
ControllerHook:
|
||||
; Jump in here too for sfx
|
||||
LDA !sound_effect_strobe
|
||||
BEQ .Next
|
||||
JSR $F89A
|
||||
LDA #$00
|
||||
STA !sound_effect_strobe
|
||||
.Next:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_ALL_BUTTON
|
||||
BNE .Continue
|
||||
JMP $CBB1
|
||||
.Continue:
|
||||
LDA !controller_flip
|
||||
AND #$10 ; start
|
||||
JMP $CA77
|
||||
|
||||
SetRushMarine:
|
||||
LDA #$01
|
||||
SEC
|
||||
BCS SetRushAcquire
|
||||
|
||||
SetRushJet:
|
||||
LDA #$02
|
||||
SEC
|
||||
BCS SetRushAcquire
|
||||
|
||||
SetRushAcquire:
|
||||
ORA !acquired_rush
|
||||
STA !acquired_rush
|
||||
RTS
|
||||
|
||||
ApplyLastWily:
|
||||
LDA !controller_mirror
|
||||
AND !CONTROLLER_SELECT
|
||||
BEQ .LastWily
|
||||
.Default:
|
||||
LDA #$00
|
||||
SEC
|
||||
BCS .Set
|
||||
.LastWily:
|
||||
LDA !last_wily
|
||||
BEQ .Default
|
||||
SEC
|
||||
SBC #$0C
|
||||
.Set:
|
||||
STA $75 ; wily index
|
||||
LDA #$03
|
||||
STA !current_stage
|
||||
RTS
|
||||
|
||||
SetMidDoc:
|
||||
LDA !current_stage
|
||||
SEC
|
||||
SBC #$08
|
||||
ASL
|
||||
TAY
|
||||
LDA #$01
|
||||
.Loop:
|
||||
CPY #$00
|
||||
BEQ .Return
|
||||
DEY
|
||||
ASL
|
||||
SEC
|
||||
BCS .Loop
|
||||
.Return:
|
||||
ORA !doc_robo_kills
|
||||
STA !doc_robo_kills
|
||||
LDA #$00
|
||||
STA $30
|
||||
RTS
|
||||
|
||||
SetEndDoc:
|
||||
LDA !current_stage
|
||||
SEC
|
||||
SBC #$08
|
||||
ASL
|
||||
TAY
|
||||
INY
|
||||
LDA #$01
|
||||
.Loop:
|
||||
CPY #$00
|
||||
BEQ .Set
|
||||
DEY
|
||||
ASL
|
||||
SEC
|
||||
BCS .Loop
|
||||
.Set:
|
||||
ORA !doc_robo_kills
|
||||
STA !doc_robo_kills
|
||||
.Return:
|
||||
LDA #$0D
|
||||
STA $30
|
||||
RTS
|
||||
|
||||
SetEndWily:
|
||||
LDA !current_wily
|
||||
PHA
|
||||
CLC
|
||||
ADC #$0C
|
||||
STA !last_wily
|
||||
PLA
|
||||
TAX
|
||||
LDA #$01
|
||||
.WLoop:
|
||||
CPX #$00
|
||||
BEQ .WContinue
|
||||
DEX
|
||||
ASL A
|
||||
SEC
|
||||
BCS .WLoop
|
||||
.WContinue:
|
||||
ORA !wily_stage_completion
|
||||
STA !wily_stage_completion
|
||||
INC !current_wily
|
||||
LDA #$9C
|
||||
JMP EndWilyReturn
|
||||
|
||||
|
||||
SetBreakMan:
|
||||
LDA #$80
|
||||
ORA !wily_stage_completion
|
||||
STA !wily_stage_completion
|
||||
LDA #$16
|
||||
STA $22
|
||||
RTS
|
||||
|
||||
CheckFirstWep:
|
||||
LDA $B4
|
||||
BEQ .SetNone
|
||||
TAY
|
||||
.Loop:
|
||||
LDA $00A2,Y
|
||||
BMI .SetNew
|
||||
INY
|
||||
CPY #$0C
|
||||
BEQ .SetSame
|
||||
BCC .Loop
|
||||
.SetSame:
|
||||
LDA #$80
|
||||
STA $A1
|
||||
JMP $A3A1
|
||||
.SetNew:
|
||||
TYA
|
||||
SEC
|
||||
SBC $B4
|
||||
BCS .Set
|
||||
.SetNone:
|
||||
LDA #$00
|
||||
.Set:
|
||||
STA $A1
|
||||
JMP $A3DE
|
||||
|
||||
Wily4Comparison:
|
||||
TYA
|
||||
PHA
|
||||
TXA
|
||||
PHA
|
||||
LDY #$00
|
||||
LDX #$08
|
||||
LDA #$01
|
||||
.Loop:
|
||||
PHA
|
||||
AND $6E
|
||||
BEQ .Skip
|
||||
INY
|
||||
.Skip:
|
||||
PLA
|
||||
ASL
|
||||
DEX
|
||||
BNE .Loop
|
||||
print "Wily 4 Requirement:", hex(realbase())
|
||||
CPY #$08
|
||||
BCC .Return
|
||||
LDA #$FF
|
||||
STA $6E
|
||||
.Return:
|
||||
PLA
|
||||
TAX
|
||||
PLA
|
||||
TAY
|
||||
LDA #$0C
|
||||
STA $EC
|
||||
RTS
|
||||
|
||||
; out of space here :(
|
||||
|
||||
%org($FDBA, $3F)
|
||||
WeaponReceived:
|
||||
TAX
|
||||
LDA $F5
|
||||
PHA
|
||||
LDA #$1E
|
||||
STA $F5
|
||||
JSR $FF6B
|
||||
TXA
|
||||
JSR ShowItemString
|
||||
PLA
|
||||
STA $F5
|
||||
JSR $FF6B
|
||||
RTS
|
||||
|
||||
CheckConsumable:
|
||||
STA $0150, Y
|
||||
LDA $0320, X
|
||||
CMP #$64
|
||||
BMI .Return
|
||||
print "Consumables (replace 67): ", hex(realbase())
|
||||
CMP #$6A
|
||||
BPL .Return
|
||||
LDA #$00
|
||||
STA $0300, X
|
||||
JMP $BE49
|
||||
.Return:
|
||||
JMP $BE25
|
||||
|
||||
EnergyLink:
|
||||
print "Energylink: ", hex(realbase())
|
||||
LDA #$01
|
||||
BEQ .Return
|
||||
TYA
|
||||
STA !energylink_packet
|
||||
LDA #$49
|
||||
STA $00
|
||||
.Return:
|
||||
LDA $BDEC, Y
|
||||
RTS
|
||||
|
||||
; out of room here :(
|
||||
8
worlds/mm3/src/patch_mm3base.py
Normal file
8
worlds/mm3/src/patch_mm3base.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
mm3 = bytearray(open("Mega Man 3 (USA).nes", 'rb').read())
|
||||
mm3[0x3C010:0x3C010] = [0] * 0x40000
|
||||
mm3[0x4] = 0x20 # have to do it here, because we don't this in the basepatch itself
|
||||
open("mm3_basepatch.nes", 'wb').write(mm3)
|
||||
0
worlds/mm3/test/__init__.py
Normal file
0
worlds/mm3/test/__init__.py
Normal file
5
worlds/mm3/test/bases.py
Normal file
5
worlds/mm3/test/bases.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MM3TestBase(WorldTestBase):
|
||||
game = "Mega Man 3"
|
||||
105
worlds/mm3/test/test_weakness.py
Normal file
105
worlds/mm3/test/test_weakness.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from math import ceil
|
||||
|
||||
from .bases import MM3TestBase
|
||||
from ..rules import minimum_weakness_requirement, bosses
|
||||
|
||||
|
||||
# Need to figure out how this test should work
|
||||
def validate_wily_4(base: MM3TestBase) -> None:
|
||||
world = base.multiworld.worlds[base.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 0.25,
|
||||
2: 2,
|
||||
3: 1,
|
||||
4: 2,
|
||||
5: 7, # Not really, but we can really only rely on Top for one RBM
|
||||
6: 0.5,
|
||||
7: 2,
|
||||
8: 0.5,
|
||||
}
|
||||
boss_health = {boss: 0x1C for boss in range(8)}
|
||||
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)}
|
||||
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()
|
||||
}
|
||||
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
|
||||
for boss in boss_flexibility:
|
||||
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}
|
||||
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
|
||||
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 WeaknessTests(MM3TestBase):
|
||||
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(22):
|
||||
if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_4(self) -> None:
|
||||
validate_wily_4(self)
|
||||
|
||||
|
||||
class StrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
}
|
||||
|
||||
|
||||
class RandomWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"random_weakness": "randomized"
|
||||
}
|
||||
|
||||
|
||||
class ShuffledWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"random_weakness": "shuffled"
|
||||
}
|
||||
|
||||
|
||||
class RandomStrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "randomized",
|
||||
}
|
||||
|
||||
|
||||
class ShuffledStrictWeaknessTests(WeaknessTests):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "shuffled"
|
||||
}
|
||||
63
worlds/mm3/text.py
Normal file
63
worlds/mm3/text.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict
|
||||
|
||||
MM3_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x25, {
|
||||
'0': 0x00,
|
||||
'1': 0x01,
|
||||
'2': 0x02,
|
||||
'3': 0x03,
|
||||
'4': 0x04,
|
||||
'5': 0x05,
|
||||
'6': 0x06,
|
||||
'7': 0x07,
|
||||
'8': 0x08,
|
||||
'9': 0x09,
|
||||
'A': 0x0A,
|
||||
'B': 0x0B,
|
||||
'C': 0x0C,
|
||||
'D': 0x0D,
|
||||
'E': 0x0E,
|
||||
'F': 0x0F,
|
||||
'G': 0x10,
|
||||
'H': 0x11,
|
||||
'I': 0x12,
|
||||
'J': 0x13,
|
||||
'K': 0x14,
|
||||
'L': 0x15,
|
||||
'M': 0x16,
|
||||
'N': 0x17,
|
||||
'O': 0x18,
|
||||
'P': 0x19,
|
||||
'Q': 0x1A,
|
||||
'R': 0x1B,
|
||||
'S': 0x1C,
|
||||
'T': 0x1D,
|
||||
'U': 0x1E,
|
||||
'V': 0x1F,
|
||||
'W': 0x20,
|
||||
'X': 0x21,
|
||||
'Y': 0x22,
|
||||
'Z': 0x23,
|
||||
' ': 0x25,
|
||||
'.': 0x26,
|
||||
',': 0x27,
|
||||
'\'': 0x28,
|
||||
'!': 0x29,
|
||||
':': 0x2B
|
||||
})
|
||||
|
||||
|
||||
class MM3TextEntry:
|
||||
def __init__(self, text: str = "", y_coords: int = 0xA5, row: int = 0x21):
|
||||
self.target_area: int = row # don't change
|
||||
self.coords: int = y_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.append(12)
|
||||
data.extend([MM3_WEAPON_ENCODING[x] for x in self.text.upper()])
|
||||
data.extend([0x25] * (13 - len(self.text)))
|
||||
return bytes(data)
|
||||
Reference in New Issue
Block a user