222 lines
10 KiB
Python
222 lines
10 KiB
Python
import os
|
|
import typing
|
|
import settings
|
|
import base64
|
|
import logging
|
|
|
|
from BaseClasses import Item, Region, Tutorial, ItemClassification
|
|
from .items import CVCotMItem, FILLER_ITEM_NAMES, ACTION_CARDS, ATTRIBUTE_CARDS, cvcotm_item_info, \
|
|
get_item_names_to_ids, get_item_counts
|
|
from .locations import CVCotMLocation, get_location_names_to_ids, BASE_ID, get_named_locations_data, \
|
|
get_location_name_groups
|
|
from .options import cvcotm_option_groups, CVCotMOptions, SubWeaponShuffle, IronMaidenBehavior, RequiredSkirmishes, \
|
|
CompletionGoal, EarlyEscapeItem
|
|
from .regions import get_region_info, get_all_region_names
|
|
from .rules import CVCotMRules
|
|
from .data import iname, lname
|
|
from .presets import cvcotm_options_presets
|
|
from worlds.AutoWorld import WebWorld, World
|
|
|
|
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
|
get_start_inventory_data
|
|
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
|
|
CVCOTM_VC_US_HASH
|
|
from .client import CastlevaniaCotMClient
|
|
|
|
|
|
class CVCotMSettings(settings.Group):
|
|
class RomFile(settings.UserFilePath):
|
|
"""File name of the Castlevania CotM US rom"""
|
|
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
|
description = "Castlevania CotM (US) ROM File"
|
|
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
|
|
|
rom_file: RomFile = RomFile(RomFile.copy_to)
|
|
|
|
|
|
class CVCotMWeb(WebWorld):
|
|
theme = "stone"
|
|
options_presets = cvcotm_options_presets
|
|
|
|
tutorials = [Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to setting up the Archipleago Castlevania: Circle of the Moon randomizer on your computer and "
|
|
"connecting it to a multiworld.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Liquid Cat"]
|
|
)]
|
|
|
|
option_groups = cvcotm_option_groups
|
|
|
|
|
|
class CVCotMWorld(World):
|
|
"""
|
|
Castlevania: Circle of the Moon is a launch title for the Game Boy Advance and the first of three Castlevania games
|
|
released for the handheld in the "Metroidvania" format. As Nathan Graves, wielding the Hunter Whip and utilizing the
|
|
Dual Set-Up System for new possibilities, you must battle your way through Camilla's castle and rescue your master
|
|
from a demonic ritual to restore the Count's power...
|
|
"""
|
|
game = "Castlevania - Circle of the Moon"
|
|
item_name_groups = {
|
|
"DSS": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
|
"Card": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
|
"Action": ACTION_CARDS,
|
|
"Action Card": ACTION_CARDS,
|
|
"Attribute": ATTRIBUTE_CARDS,
|
|
"Attribute Card": ATTRIBUTE_CARDS,
|
|
"Freeze": {iname.serpent, iname.cockatrice, iname.mercury, iname.mars},
|
|
"Freeze Action": {iname.mercury, iname.mars},
|
|
"Freeze Attribute": {iname.serpent, iname.cockatrice}
|
|
}
|
|
location_name_groups = get_location_name_groups()
|
|
options_dataclass = CVCotMOptions
|
|
options: CVCotMOptions
|
|
settings: typing.ClassVar[CVCotMSettings]
|
|
origin_region_name = "Catacomb"
|
|
hint_blacklist = frozenset({lname.ba24}) # The Battle Arena reward, if it's put in, will always be a Last Key.
|
|
|
|
item_name_to_id = {name: cvcotm_item_info[name].code + BASE_ID for name in cvcotm_item_info
|
|
if cvcotm_item_info[name].code is not None}
|
|
location_name_to_id = get_location_names_to_ids()
|
|
|
|
# Default values to possibly be updated in generate_early
|
|
total_last_keys: int = 0
|
|
required_last_keys: int = 0
|
|
|
|
auth: bytearray
|
|
|
|
web = CVCotMWeb()
|
|
|
|
def generate_early(self) -> None:
|
|
# Generate the player's unique authentication
|
|
self.auth = bytearray(self.random.getrandbits(8) for _ in range(16))
|
|
|
|
# If Required Skirmishes are on, force the Required and Available Last Keys to 8 or 9 depending on which option
|
|
# was chosen.
|
|
if self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses:
|
|
self.options.required_last_keys.value = 8
|
|
self.options.available_last_keys.value = 8
|
|
elif self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena:
|
|
self.options.required_last_keys.value = 9
|
|
self.options.available_last_keys.value = 9
|
|
|
|
self.total_last_keys = self.options.available_last_keys.value
|
|
self.required_last_keys = self.options.required_last_keys.value
|
|
|
|
# If there are more Last Keys required than there are Last Keys in total, drop the required Last Keys to
|
|
# the total Last Keys.
|
|
if self.required_last_keys > self.total_last_keys:
|
|
self.required_last_keys = self.total_last_keys
|
|
logging.warning(f"[{self.player_name}] The Required Last Keys "
|
|
f"({self.options.required_last_keys.value}) is higher than the Available Last Keys "
|
|
f"({self.options.available_last_keys.value}). Lowering the required number to: "
|
|
f"{self.required_last_keys}")
|
|
self.options.required_last_keys.value = self.required_last_keys
|
|
|
|
# Place the Double or Roc Wing in local_early_items if the Early Escape option is being used.
|
|
if self.options.early_escape_item == EarlyEscapeItem.option_double:
|
|
self.multiworld.local_early_items[self.player][iname.double] = 1
|
|
elif self.options.early_escape_item == EarlyEscapeItem.option_roc_wing:
|
|
self.multiworld.local_early_items[self.player][iname.roc_wing] = 1
|
|
elif self.options.early_escape_item == EarlyEscapeItem.option_double_or_roc_wing:
|
|
self.multiworld.local_early_items[self.player][self.random.choice([iname.double, iname.roc_wing])] = 1
|
|
|
|
def create_regions(self) -> None:
|
|
# Create every Region object.
|
|
created_regions = [Region(name, self.player, self.multiworld) for name in get_all_region_names()]
|
|
|
|
# Attach the Regions to the Multiworld.
|
|
self.multiworld.regions.extend(created_regions)
|
|
|
|
for reg in created_regions:
|
|
|
|
# Add the Entrances to all the Regions.
|
|
ent_destinations_and_names = get_region_info(reg.name, "entrances")
|
|
if ent_destinations_and_names is not None:
|
|
reg.add_exits(ent_destinations_and_names)
|
|
|
|
# Add the Locations to all the Regions.
|
|
loc_names = get_region_info(reg.name, "locations")
|
|
if loc_names is None:
|
|
continue
|
|
locations_with_ids, locked_pairs = get_named_locations_data(loc_names, self.options)
|
|
reg.add_locations(locations_with_ids, CVCotMLocation)
|
|
|
|
# Place locked Items on all of their associated Locations.
|
|
for locked_loc, locked_item in locked_pairs.items():
|
|
self.get_location(locked_loc).place_locked_item(self.create_item(locked_item,
|
|
ItemClassification.progression))
|
|
|
|
def create_item(self, name: str, force_classification: typing.Optional[ItemClassification] = None) -> Item:
|
|
if force_classification is not None:
|
|
classification = force_classification
|
|
else:
|
|
classification = cvcotm_item_info[name].default_classification
|
|
|
|
code = cvcotm_item_info[name].code
|
|
if code is not None:
|
|
code += BASE_ID
|
|
|
|
created_item = CVCotMItem(name, classification, code, self.player)
|
|
|
|
return created_item
|
|
|
|
def create_items(self) -> None:
|
|
item_counts = get_item_counts(self)
|
|
|
|
# Set up the items correctly
|
|
self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
|
|
in item_counts[classification] for _ in range(item_counts[classification][item])]
|
|
|
|
def set_rules(self) -> None:
|
|
# Set all the Entrance and Location rules properly.
|
|
CVCotMRules(self).set_cvcotm_rules()
|
|
|
|
def generate_output(self, output_directory: str) -> None:
|
|
# Get out all the Locations that are not Events. Only take the Iron Maiden switch if the Maiden Detonator is in
|
|
# the item pool.
|
|
active_locations = [loc for loc in self.multiworld.get_locations(self.player) if loc.address is not None and
|
|
(loc.name != lname.ct21 or self.options.iron_maiden_behavior ==
|
|
IronMaidenBehavior.option_detonator_in_pool)]
|
|
|
|
# Location data
|
|
offset_data = get_location_data(self, active_locations)
|
|
# Sub-weapons
|
|
if self.options.sub_weapon_shuffle:
|
|
offset_data.update(shuffle_sub_weapons(self))
|
|
# Item drop randomization
|
|
if self.options.item_drop_randomization:
|
|
offset_data.update(populate_enemy_drops(self))
|
|
# Countdown
|
|
if self.options.countdown:
|
|
offset_data.update(get_countdown_flags(self, active_locations))
|
|
# Start Inventory
|
|
start_inventory_data = get_start_inventory_data(self)
|
|
offset_data.update(start_inventory_data[0])
|
|
|
|
patch = CVCotMProcedurePatch(player=self.player, player_name=self.player_name)
|
|
patch_rom(self, patch, offset_data, start_inventory_data[1])
|
|
|
|
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
|
|
f"{patch.patch_file_ending}")
|
|
|
|
patch.write(rom_path)
|
|
|
|
def fill_slot_data(self) -> dict:
|
|
return {"death_link": self.options.death_link.value,
|
|
"iron_maiden_behavior": self.options.iron_maiden_behavior.value,
|
|
"ignore_cleansing": self.options.ignore_cleansing.value,
|
|
"skip_tutorials": self.options.skip_tutorials.value,
|
|
"required_last_keys": self.required_last_keys,
|
|
"completion_goal": self.options.completion_goal.value}
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
return self.random.choice(FILLER_ITEM_NAMES)
|
|
|
|
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
|
|
# Put the player's unique authentication in connect_names.
|
|
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
|
|
multidata["connect_names"][self.player_name]
|