Castlevania: Circle of the Moon - Implement New Game (#3299)
* Add the cotm package with working seed playthrough generation. * Add the proper event flag IDs for the Item codes. * Oooops. Put the world completion condition in! * Adjust the game name and abbreviations. * Implement more settings. * Account for too many start_inventory_from_pool cards with Halve DSS Cards Placed. * Working (albeit very sloooooooooooow) ROM patching. * Screw you, bsdiff! AP Procedure Patch for life! * Nuke stage_assert_generate as the ROM is no longer needed for that. * Working item writing and position adjusting. * Fix the magic item graphics in Locations wherein they can be fixed. * Enable sub-weapon shuffle * Get the seed display working. * Get the enemy item drop randomization working. Phew! * Enemy drop rando and seed display fixes. * Functional Countdown + Early Double setting * Working multiworld (yay!) * Fix item links and demo shenanigans. * Add Wii U VC hash and a docs section explaining the rereleases. * Change all client read/writes to EWRAM instead of Combined WRAM. * Custom text insertion foundations. * Working text converter and word wrap detector. * More refinements to the text wrap system. * Well and truly working sent/received messages. * Add DeathLink and Battle Arena goal options. * Add tracker stuff, unittests, all locations countdown, presets. * Add to README, CODEOWNERS, and inno_setup * Add to README, CODEOWNERS, and inno_setup * Address some suggestions/problems. * Switch the Items and Locations to using dataclasses. * Add note about the alternate classes to the Game Page. * Oooops, typo! * Touch up the Options descriptions. * Fix Battle Arena flag being detected incorrectly on connection and name the locked location/item pairs better. * Implement option groups * Swap the Lizard-man Locations into their correct Regions. * Local start inventory, better DeathLink message handling, handle receiving over 255 of an item. * Update the PopTracker pack links to no longer point to the Releases page. * Add Skip Dialogues option. * Update the presets for the accessibility rework. * Swap the choices in the accessibility preset options. * Uhhhhhhh...just see the apworld v4 changelog for this one. * Ooops, typo! * . * Bunch of small stuff * Correctly change "Fake" to "Breakable" in this comment. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make can_touch_water one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Make broke_iron_maidens one line. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix majors countdown and make can_open_ceremonial_door one line. * Make the Trap AP Item less obvious. * Add Progression + Useful stuff, patcher handling for incompatible versions, and fix some mypy stuff. * Better option groups. * Change Early Double to Early Escape Item. * Update DeathLink description and ditch the Menu region. * Fix the Start Broken choice for Iron Maiden Behavior * Remove the forced option change with Arena goal + required All Bosses and Arena. * Update the Game Page with the removal of the forced option combination change. * Fix client potential to send packets nonstop. * More review addressing. * Fix the new select_drop code. * Fix the new select_drop code for REAL this time. * Send another LocationScout if we send Location checks without having the Location info. --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
221
worlds/cvcotm/__init__.py
Normal file
221
worlds/cvcotm/__init__.py
Normal file
@@ -0,0 +1,221 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user