Archipelago/worlds/mmbn3/__init__.py

500 lines
28 KiB
Python

import os
import settings
import typing
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region, Entrance, \
LocationProgressType
from worlds.AutoWorld import WebWorld, World
from .Rom import MMBN3DeltaPatch, LocalRom, get_base_rom_path
from .Items import MMBN3Item, ItemData, item_table, all_items, item_frequencies, items_by_id, ItemType
from .Locations import Location, MMBN3Location, all_locations, location_table, location_data_table, \
always_excluded_locations, jobs
from .Options import MMBN3Options
from .Regions import regions, RegionName
from .Names.ItemName import ItemName
from .Names.LocationName import LocationName
from worlds.generic.Rules import add_item_rule
class MMBN3Settings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the MMBN3 Blue US rom"""
copy_to = "Mega Man Battle Network 3 - Blue Version (USA).gba"
description = "MMBN3 ROM File"
md5s = [MMBN3DeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: bool = True
class MMBN3Web(WebWorld):
theme = "ice"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the MegaMan Battle Network 3 Randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["digiholic"]
)
tutorials = [setup_en]
class MMBN3World(World):
"""
Play as Lan and MegaMan to stop the evil organization WWW led by the nefarious
Dr. Wily in their plans to take over the Net! Collect BattleChips, Customize your Navi,
and utilize powerful Style Changes to grow strong enough to take on the greatest
threat the Internet has ever faced!
"""
game = "MegaMan Battle Network 3"
options_dataclass = MMBN3Options
options: MMBN3Options
settings: typing.ClassVar[MMBN3Settings]
topology_present = False
data_version = 1
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
excluded_locations: typing.List[str]
item_frequencies: typing.Dict[str, int]
web = MMBN3Web()
def generate_early(self) -> None:
"""
called per player before any items or locations are created. You can set properties on your world here.
Already has access to player options and RNG.
"""
self.item_frequencies = item_frequencies.copy()
if self.options.extra_ranks > 0:
self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.options.extra_ranks
if not self.options.include_jobs:
self.excluded_locations = always_excluded_locations + [job.name for job in jobs]
else:
self.excluded_locations = always_excluded_locations
def create_regions(self) -> None:
"""
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
during generate_early or basic as well.
"""
name_to_region = {}
for region_info in regions:
region = Region(region_info.name, self.player, self.multiworld)
name_to_region[region_info.name] = region
for location in region_info.locations:
loc = MMBN3Location(self.player, location, self.location_name_to_id.get(location, None), region)
if location in self.excluded_locations:
loc.progress_type = LocationProgressType.EXCLUDED
# Do not place any progression items on WWW Island
if region_info.name == RegionName.WWW_Island:
add_item_rule(loc, lambda item: not item.advancement)
region.locations.append(loc)
self.multiworld.regions.append(region)
for region_info in regions:
region = name_to_region[region_info.name]
for connection in region_info.connections:
entrance = region.connect(name_to_region[connection])
# ACDC Pending with Start Randomizer
# if connection == RegionName.ACDC_Overworld:
# entrance.access_rule = lambda state: state.has(ItemName.Parasol, self.player)
if connection == RegionName.SciLab_Overworld:
entrance.access_rule = lambda state: state.has(ItemName.SubPET, self.player)
if connection == RegionName.Yoka_Overworld:
entrance.access_rule = lambda state: state.has(ItemName.Needle, self.player)
if connection == RegionName.Beach_Overworld:
entrance.access_rule = lambda state: state.has(ItemName.PETCase, self.player)
# ACDC Pending with Start Randomizer
# if connection == RegionName.ACDC_Cyberworld:
# entrance.access_rule = lambda state: state.has(ItemName.CACDCPas, self.player)
if connection == RegionName.SciLab_Cyberworld:
entrance.access_rule = lambda state: \
state.has(ItemName.CSciPas, self.player) or \
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player)
if connection == RegionName.Yoka_Cyberworld:
entrance.access_rule = lambda state: \
state.has(ItemName.CYokaPas, self.player) or \
(
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and
state.has(ItemName.Press, self.player)
)
if connection == RegionName.Beach_Cyberworld:
entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
if connection == RegionName.Undernet:
entrance.access_rule = lambda state: self.explore_score(state) > 8 and\
state.has(ItemName.Press, self.player)
if connection == RegionName.Secret_Area:
entrance.access_rule = lambda state: self.explore_score(state) > 12 and\
state.has(ItemName.Hammer, self.player)
if connection == RegionName.WWW_Island:
entrance.access_rule = lambda state:\
state.has(ItemName.Progressive_Undernet_Rank, self.player, 8)
def create_items(self) -> None:
# First add in all progression and useful items
required_items = []
for item in all_items:
if item.progression != ItemClassification.filler:
freq = self.item_frequencies.get(item.itemName, 1)
required_items += [item.itemName for _ in range(freq)]
for itemName in required_items:
self.multiworld.itempool.append(self.create_item(itemName))
# Then, get a random amount of fillers until we have as many items as we have locations
filler_items = []
for item in all_items:
if item.progression == ItemClassification.filler:
freq = self.item_frequencies.get(item.itemName, 1)
filler_items += [item.itemName for _ in range(freq)]
remaining = len(all_locations) - len(required_items)
for i in range(remaining):
filler_item_name = self.random.choice(filler_items)
item = self.create_item(filler_item_name)
self.multiworld.itempool.append(item)
filler_items.remove(filler_item_name)
def set_rules(self) -> None:
"""
called to set access and item rules on locations and entrances.
"""
# Set WWW ID requirements
def has_www_id(state): return state.has(ItemName.WWW_ID, self.player)
self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player).access_rule = has_www_id
self.multiworld.get_location(LocationName.SciLab_1_WWW_BMD, self.player).access_rule = has_www_id
self.multiworld.get_location(LocationName.Yoka_1_WWW_BMD, self.player).access_rule = has_www_id
self.multiworld.get_location(LocationName.Undernet_1_WWW_BMD, self.player).access_rule = has_www_id
# Set Press Program requirements
def has_press(state): return state.has(ItemName.Press, self.player)
self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player).access_rule = has_press
self.multiworld.get_location(LocationName.Yoka_2_Upper_BMD, self.player).access_rule = has_press
self.multiworld.get_location(LocationName.Beach_2_East_BMD, self.player).access_rule = has_press
self.multiworld.get_location(LocationName.Hades_South_BMD, self.player).access_rule = has_press
self.multiworld.get_location(LocationName.Secret_3_BugFrag_BMD, self.player).access_rule = has_press
self.multiworld.get_location(LocationName.Secret_3_Island_BMD, self.player).access_rule = has_press
# Set Job additional area access
self.multiworld.get_location(LocationName.Please_deliver_this, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.My_Navi_is_sick, self.player).access_rule =\
lambda state: \
state.has(ItemName.Recov30_star, self.player)
self.multiworld.get_location(LocationName.Help_me_with_my_son, self.player).access_rule =\
lambda state:\
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Transmission_error, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Chip_Prices, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Im_broke, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Rare_chips_for_cheap, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Be_my_boyfriend, self.player).access_rule =\
lambda state: \
state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Will_you_deliver, self.player).access_rule=\
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Somebody_please_help, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Looking_for_condor, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Help_with_rehab, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Old_Master, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Catching_gang_members, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \
state.has(ItemName.Press, self.player)
self.multiworld.get_location(LocationName.Please_adopt_a_virus, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Legendary_Tomes, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Undernet, "Region", self.player) and \
state.can_reach(RegionName.Deep_Undernet, "Region", self.player) and \
state.has_all({ItemName.Press, ItemName.Magnum1_A}, self.player)
self.multiworld.get_location(LocationName.Legendary_Tomes_Treasure, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \
state.can_reach(LocationName.Legendary_Tomes, "Location", self.player)
self.multiworld.get_location(LocationName.Hide_and_seek_First_Child, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Hide_and_seek_Second_Child, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Hide_and_seek_Third_Child, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Hide_and_seek_Fourth_Child, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Hide_and_seek_Completion, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Finding_the_blue_Navi, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Undernet, "Region", self.player)
self.multiworld.get_location(LocationName.Give_your_support, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player)
self.multiworld.get_location(LocationName.Stamp_collecting, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player)
self.multiworld.get_location(LocationName.Help_with_a_will, self.player).access_rule = \
lambda state: \
state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \
state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \
state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \
state.can_reach(RegionName.Undernet, "Region", self.player)
# Set Trade quests
self.multiworld.get_location(LocationName.ACDC_SonicWav_W_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.SonicWav_W, self.player)
self.multiworld.get_location(LocationName.ACDC_Bubbler_C_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.Bubbler_C, self.player)
self.multiworld.get_location(LocationName.ACDC_Recov120_S_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.Recov120_S, self.player)
self.multiworld.get_location(LocationName.SciLab_Shake1_S_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.Shake1_S, self.player)
self.multiworld.get_location(LocationName.Yoka_FireSwrd_P_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.FireSwrd_P, self.player)
self.multiworld.get_location(LocationName.Hospital_DynaWav_V_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.DynaWave_V, self.player)
self.multiworld.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.WideSwrd_C, self.player)
self.multiworld.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.HoleMetr_H, self.player)
self.multiworld.get_location(LocationName.Beach_DNN_Shadow_J_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.Shadow_J, self.player)
self.multiworld.get_location(LocationName.Hades_GrabBack_K_Trade, self.player).access_rule =\
lambda state: state.has(ItemName.GrabBack_K, self.player)
# Set Number Traders
# The first 8 are considered cheap enough to grind for in ACDC. Protip: Try grinding in the tank
self.multiworld.get_location(LocationName.Numberman_Code_09, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_10, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_11, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_12, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_13, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_14, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_15, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_16, self.player).access_rule = \
lambda state: self.explore_score(state) > 2
self.multiworld.get_location(LocationName.Numberman_Code_17, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_18, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_19, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_20, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_21, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_22, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_23, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_24, self.player).access_rule =\
lambda state: self.explore_score(state) > 4
self.multiworld.get_location(LocationName.Numberman_Code_25, self.player).access_rule =\
lambda state: self.explore_score(state) > 8
self.multiworld.get_location(LocationName.Numberman_Code_26, self.player).access_rule =\
lambda state: self.explore_score(state) > 8
self.multiworld.get_location(LocationName.Numberman_Code_27, self.player).access_rule =\
lambda state: self.explore_score(state) > 8
self.multiworld.get_location(LocationName.Numberman_Code_28, self.player).access_rule =\
lambda state: self.explore_score(state) > 8
self.multiworld.get_location(LocationName.Numberman_Code_29, self.player).access_rule =\
lambda state: self.explore_score(state) > 10
self.multiworld.get_location(LocationName.Numberman_Code_30, self.player).access_rule =\
lambda state: self.explore_score(state) > 10
self.multiworld.get_location(LocationName.Numberman_Code_31, self.player).access_rule =\
lambda state: self.explore_score(state) > 10
def not_undernet(item): return item.code != item_table[ItemName.Progressive_Undernet_Rank].code or item.player != self.player
self.multiworld.get_location(LocationName.WWW_1_Central_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_1_East_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_2_East_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_2_Northwest_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_3_East_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_3_North_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_4_Northwest_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_4_Central_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_Wall_BMD, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_Control_Room_1_Screen, self.player).item_rule = not_undernet
self.multiworld.get_location(LocationName.WWW_Wilys_Desk, self.player).item_rule = not_undernet
# place "Victory" at "Final Boss" and set collection as win condition
self.multiworld.get_location(LocationName.Alpha_Defeated, self.player) \
.place_locked_item(self.create_event(ItemName.Victory))
self.multiworld.completion_condition[self.player] = \
lambda state: state.has(ItemName.Victory, self.player)
def generate_output(self, output_directory: str) -> None:
rompath: str = ""
try:
world = self.multiworld
player = self.player
rom = LocalRom(get_base_rom_path())
for location_name in location_table.keys():
location = world.get_location(location_name, player)
ap_item = location.item
item_id = ap_item.code
if item_id is not None:
if ap_item.player != player or item_id not in items_by_id:
item = ItemData(item_id, ap_item.name, ap_item.classification, ItemType.External)
item = item._replace(recipient=self.multiworld.player_name[ap_item.player])
else:
item = items_by_id[item_id]
location_data = location_data_table[location_name]
# print("Placing item "+item.itemName+" at location "+location_data.name)
rom.replace_item(location_data, item)
if location_data.inject_name:
item_name_text = "Item"
long_item_text = ""
# No item hinting
if self.options.trade_quest_hinting == 0:
item_name_text = "Check"
# Partial item hinting
elif self.options.trade_quest_hinting == 1:
if item.progression == ItemClassification.progression \
or item.progression == ItemClassification.progression_skip_balancing:
item_name_text = "Progress"
elif item.progression == ItemClassification.useful \
or item.progression == ItemClassification.trap:
item_name_text = "Item"
else:
item_name_text = "Garbage"
if item.recipient == 'Myself':
item_name_text = "Your " + item_name_text
else:
item_name_text = item.recipient + "'s " + item_name_text
# Full item hinting
else:
owners_name = "Your" if item.recipient == 'Myself' else item.recipient + "'s"
long_item_text = f"It's {owners_name} \n\"{item.itemName}\"!!"
rom.insert_hint_text(location_data, item_name_text, long_item_text)
rom.inject_name(world.player_name[player])
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gba")
rom.write_changed_rom()
rom.write_to_file(rompath)
patch = MMBN3DeltaPatch(os.path.splitext(rompath)[0]+MMBN3DeltaPatch.patch_file_ending, player=player,
player_name=world.player_name[player], patched_path=rompath)
patch.write()
except:
raise
finally:
if os.path.exists(rompath):
os.unlink(rompath)
@classmethod
def stage_assert_generate(cls, multiworld: "MultiWorld") -> None:
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def create_item(self, name: str) -> "Item":
item = item_table[name]
return MMBN3Item(item.itemName, item.progression, item.code, self.player)
def create_event(self, event: str):
# while we are at it, we can also add a helper to create events
return MMBN3Item(event, ItemClassification.progression, None, self.player)
def fill_slot_data(self):
return self.options.as_dict("extra_ranks", "include_jobs", "trade_quest_hinting")
def explore_score(self, state):
"""
Determine roughly how much of the game you can explore to make certain checks not restrict much movement
"""
score = 0
if state.can_reach(RegionName.WWW_Island, "Region", self.player):
return 999
if state.can_reach(RegionName.SciLab_Overworld, "Region", self.player):
score += 3
if state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player):
score += 1
if state.can_reach(RegionName.Yoka_Overworld, "Region", self.player):
score += 2
if state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player):
score += 1
if state.can_reach(RegionName.Beach_Overworld, "Region", self.player):
score += 3
if state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player):
score += 1
if state.can_reach(RegionName.Undernet, "Region", self.player):
score += 2
if state.can_reach(RegionName.Deep_Undernet, "Region", self.player):
score += 1
if state.can_reach(RegionName.Secret_Area, "Region", self.player):
score += 1
return score