Files
Grinch-AP/worlds/cvcotm/rules.py
LiquidCat64 3acbe9ece1 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>
2024-12-12 14:47:47 +01:00

204 lines
11 KiB
Python

from typing import Dict, TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule
from .data import iname, lname
from .options import CompletionGoal, IronMaidenBehavior
if TYPE_CHECKING:
from . import CVCotMWorld
class CVCotMRules:
player: int
world: "CVCotMWorld"
rules: Dict[str, CollectionRule]
required_last_keys: int
iron_maiden_behavior: int
nerf_roc_wing: int
ignore_cleansing: int
completion_goal: int
def __init__(self, world: "CVCotMWorld") -> None:
self.player = world.player
self.world = world
self.required_last_keys = world.required_last_keys
self.iron_maiden_behavior = world.options.iron_maiden_behavior.value
self.nerf_roc_wing = world.options.nerf_roc_wing.value
self.ignore_cleansing = world.options.ignore_cleansing.value
self.completion_goal = world.options.completion_goal.value
self.location_rules = {
# Sealed Room
lname.sr3: self.has_jump_level_5,
# Catacomb
lname.cc1: self.has_push,
lname.cc3: self.has_jump_level_1,
lname.cc3b: lambda state:
(self.has_jump_level_1(state) and self.has_ice_or_stone(state)) or self.has_jump_level_4(state),
lname.cc5: self.has_tackle,
lname.cc8b: lambda state: self.has_jump_level_3(state) or self.has_kick(state),
lname.cc14b: lambda state: self.has_jump_level_1(state) or self.has_kick(state),
lname.cc25: self.has_jump_level_1,
# Abyss Staircase
lname.as4: self.has_jump_level_4,
# Audience Room
lname.ar9: self.has_push,
lname.ar11: self.has_tackle,
lname.ar14b: self.has_jump_level_4,
lname.ar17b: lambda state: self.has_jump_level_2(state) or self.has_kick(state),
lname.ar19: lambda state: self.has_jump_level_2(state) or self.has_kick(state),
lname.ar26: lambda state: self.has_tackle(state) and self.has_jump_level_5(state),
lname.ar27: lambda state: self.has_tackle(state) and self.has_push(state),
lname.ar30: lambda state:
(self.has_jump_level_3(state) and self.has_ice_or_stone(state)) or self.has_jump_level_4(state),
lname.ar30b: lambda state:
(self.has_jump_level_3(state) and self.has_ice_or_stone(state)) or self.has_jump_level_4(state),
# Outer Wall
lname.ow0: self.has_jump_level_4,
lname.ow1: lambda state: self.has_jump_level_5(state) or self.has_ice_or_stone(state),
# Triumph Hallway
lname.th3: lambda state:
(self.has_kick(state) and self.has_ice_or_stone(state)) or self.has_jump_level_2(state),
# Machine Tower
lname.mt3: lambda state: self.has_jump_level_2(state) or self.has_kick(state),
lname.mt6: lambda state: self.has_jump_level_2(state) or self.has_kick(state),
lname.mt14: self.has_tackle,
# Chapel Tower
lname.ct1: lambda state: self.has_jump_level_2(state) or self.has_ice_or_stone(state),
lname.ct4: self.has_push,
lname.ct10: self.has_push,
lname.ct13: lambda state: self.has_jump_level_2(state) or self.has_ice_or_stone(state),
lname.ct22: self.broke_iron_maidens,
lname.ct26: lambda state:
(self.has_jump_level_3(state) and self.has_ice_or_stone(state)) or self.has_jump_level_4(state),
lname.ct26b: lambda state:
(self.has_jump_level_3(state) and self.has_ice_or_stone(state)) or self.has_jump_level_4(state),
# Underground Gallery
lname.ug1: self.has_push,
lname.ug2: self.has_push,
lname.ug3: lambda state: self.has_jump_level_2(state) or self.has_ice_or_stone(state),
lname.ug3b: lambda state: self.has_jump_level_4(state) or self.has_ice_or_stone(state),
lname.ug8: self.has_tackle,
# Underground Warehouse
lname.uw10: lambda state:
(self.has_jump_level_4(state) and self.has_ice_or_stone(state)) or self.has_jump_level_5(state),
lname.uw14: lambda state: self.has_jump_level_2(state) or self.has_ice_or_stone(state),
lname.uw16b: lambda state:
(self.has_jump_level_2(state) and self.has_ice_or_stone(state)) or self.has_jump_level_3(state),
# Underground Waterway
lname.uy5: lambda state: self.has_jump_level_3(state) or self.has_ice_or_stone(state),
lname.uy8: self.has_jump_level_2,
lname.uy12b: self.can_touch_water,
lname.uy17: self.can_touch_water,
lname.uy13: self.has_jump_level_3,
lname.uy18: self.has_jump_level_3,
# Ceremonial Room
lname.cr1: lambda state: self.has_jump_level_2(state) or self.has_kick(state),
lname.dracula: self.has_jump_level_2,
}
self.entrance_rules = {
"Catacomb to Stairway": lambda state: self.has_jump_level_1(state) or self.has_kick(state),
"Stairway to Audience": self.has_jump_level_1,
"Audience to Machine Bottom": self.has_tackle,
"Audience to Machine Top": lambda state: self.has_jump_level_2(state) or self.has_kick(state),
"Audience to Chapel": lambda state:
(self.has_jump_level_2(state) and self.has_ice_or_stone(state)) or self.has_jump_level_3(state)
or self.has_kick(state),
"Audience to Gallery": lambda state: self.broke_iron_maidens(state) and self.has_push(state),
"Audience to Warehouse": self.has_push,
"Audience to Waterway": self.broke_iron_maidens,
"Audience to Observation": self.has_jump_level_5,
"Ceremonial Door": self.can_open_ceremonial_door,
"Corridor to Gallery": self.broke_iron_maidens,
"Escape the Gallery Pit": lambda state: self.has_jump_level_2(state) or self.has_kick(state),
"Climb to Chapel Top": lambda state: self.has_jump_level_3(state) or self.has_kick(state),
"Arena Passage": lambda state: self.has_push(state) and self.has_jump_level_2(state),
"Dip Into Waterway End": self.has_jump_level_3,
"Gallery Upper to Lower": self.has_tackle,
"Gallery Lower to Upper": self.has_tackle,
"Into Warehouse Main": self.has_tackle,
"Into Waterway Main": self.can_touch_water,
}
def has_jump_level_1(self, state: CollectionState) -> bool:
"""Double or Roc Wing, regardless of Roc being nerfed or not."""
return state.has_any([iname.double, iname.roc_wing], self.player)
def has_jump_level_2(self, state: CollectionState) -> bool:
"""Specifically Roc Wing, regardless of Roc being nerfed or not."""
return state.has(iname.roc_wing, self.player)
def has_jump_level_3(self, state: CollectionState) -> bool:
"""Roc Wing and Double OR Kick Boots if Roc is nerfed. Otherwise, just Roc."""
if self.nerf_roc_wing:
return state.has(iname.roc_wing, self.player) and \
state.has_any([iname.double, iname.kick_boots], self.player)
else:
return state.has(iname.roc_wing, self.player)
def has_jump_level_4(self, state: CollectionState) -> bool:
"""Roc Wing and Kick Boots specifically if Roc is nerfed. Otherwise, just Roc."""
if self.nerf_roc_wing:
return state.has_all([iname.roc_wing, iname.kick_boots], self.player)
else:
return state.has(iname.roc_wing, self.player)
def has_jump_level_5(self, state: CollectionState) -> bool:
"""Roc Wing, Double, AND Kick Boots if Roc is nerfed. Otherwise, just Roc."""
if self.nerf_roc_wing:
return state.has_all([iname.roc_wing, iname.double, iname.kick_boots], self.player)
else:
return state.has(iname.roc_wing, self.player)
def has_tackle(self, state: CollectionState) -> bool:
return state.has(iname.tackle, self.player)
def has_push(self, state: CollectionState) -> bool:
return state.has(iname.heavy_ring, self.player)
def has_kick(self, state: CollectionState) -> bool:
return state.has(iname.kick_boots, self.player)
def has_ice_or_stone(self, state: CollectionState) -> bool:
"""Valid DSS combo that allows freezing or petrifying enemies to use as platforms."""
return state.has_any([iname.serpent, iname.cockatrice], self.player) and \
state.has_any([iname.mercury, iname.mars], self.player)
def can_touch_water(self, state: CollectionState) -> bool:
"""Cleansing unless it's ignored, in which case this will always return True."""
return self.ignore_cleansing or state.has(iname.cleansing, self.player)
def broke_iron_maidens(self, state: CollectionState) -> bool:
"""Maiden Detonator unless the Iron Maidens start broken, in which case this will always return True."""
return (self.iron_maiden_behavior == IronMaidenBehavior.option_start_broken
or state.has(iname.ironmaidens, self.player))
def can_open_ceremonial_door(self, state: CollectionState) -> bool:
"""The required number of Last Keys. If 0 keys are required, this should always return True."""
return state.has(iname.last_key, self.player, self.required_last_keys)
def set_cvcotm_rules(self) -> None:
multiworld = self.world.multiworld
for region in multiworld.get_regions(self.player):
# Set each Entrance's rule if it should have one.
for ent in region.entrances:
if ent.name in self.entrance_rules:
ent.access_rule = self.entrance_rules[ent.name]
# Set each Location's rule if it should have one.
for loc in region.locations:
if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name]
# Set the World's completion condition depending on what its Completion Goal option is.
if self.completion_goal == CompletionGoal.option_dracula:
multiworld.completion_condition[self.player] = lambda state: state.has(iname.dracula, self.player)
elif self.completion_goal == CompletionGoal.option_battle_arena:
multiworld.completion_condition[self.player] = lambda state: state.has(iname.shinning_armor, self.player)
else:
multiworld.completion_condition[self.player] = \
lambda state: state.has_all([iname.dracula, iname.shinning_armor], self.player)