diff --git a/worlds/hk_rework/Charms.py b/worlds/hk_rework/Charms.py index d810322c82dc..fb33c9bdd74f 100644 --- a/worlds/hk_rework/Charms.py +++ b/worlds/hk_rework/Charms.py @@ -45,3 +45,6 @@ "Weaversong", "Grimmchild" ] + +charm_name_to_id = {"_".join(name.split(" ")): index for index, name in enumerate(names)} + # TODO >:( diff --git a/worlds/hk_rework/__init__.py b/worlds/hk_rework/__init__.py index ca588057cafa..02a3574e6df3 100644 --- a/worlds/hk_rework/__init__.py +++ b/worlds/hk_rework/__init__.py @@ -16,7 +16,7 @@ from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ shop_to_option, HKOptions from .Rules import cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance -from .Charms import names as charm_names +from .Charms import names as charm_names, charm_name_to_id from .ExtractedData import pool_options, logic_options, items as extracted_items, logic_items, item_effects, multi_locations from .Items import item_name_groups, item_name_to_id, location_name_to_id # item_table, lookup_type_to_names, item_name_groups @@ -67,6 +67,9 @@ class HK_state_diff(NamedTuple): twister_required: bool # shortcut logic if any step in a spell skip needs 4 casts + add_charms: Dict[str, int] + # dict of charms that need to be in state and their costs + class HKClause(NamedTuple): # Dict of item: count for state.has_all_counts() @@ -90,7 +93,7 @@ class HKClause(NamedTuple): default_hk_rule = [HKClause( hk_item_requirements={"True": 1}, hk_region_requirements=[], - hk_state_requirements=HK_state_diff(shadeskip=0, twister_required=False), + hk_state_requirements=HK_state_diff(shadeskip=0, twister_required=False, add_charms={}), hk_before_resets=[], hk_after_resets=[], hk_state_modifiers={}, @@ -372,7 +375,6 @@ def create_location(location: Tuple[str, Optional[int]], rule: Any, item: Option loc.costs = costs.pop() loc.sort_costs() loc.vanilla = True - # TODO remove/rework based on how sort_shops_by_cost ends up # setting vanilla=True so shop sorting doesn't shuffle the price around self.created_multi_locations[shop].append(loc) loc.basename = shop # for costsanity pool checking @@ -508,6 +510,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: before = False after_resets = [] add_dict = Counter() + add_charms = {} valid_keys = {key for key in ("ROOMSOUL", "AREASOUL", "MAPAREASOUL")} # TODO with ER change this logic if self.options.RandomizeCharms: # TODO confirm @@ -518,6 +521,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: if req == "$BENCHRESET": after_resets.append("DAMAGE") after_resets.append("SPENTSHADE") + after_resets.append("SPENTNOTCHES") # reset charms elif req == "$FLOWERGET": after_resets.append("NOFLOWER") @@ -531,13 +535,19 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: after_resets.append("SPENTSHADE") add_dict["NOFLOWER"] += 1 - if req.startswith("$EQUIPPEDCHARM"): + elif req.startswith("$EQUIPPEDCHARM"): charm = re.search(r"\$EQUIPPEDCHARM\[(.*)\]", req).group(1) if charm == "Kingsoul": - charm = "WHITEFRAGMENT" - item_requirements.append(charm) + item_requirements.append("WHITEFRAGMENT>1") + add_charms[charm] = self.charm_costs[charm_name_to_id[charm]] + elif charm == "Void_Heart": + item_requirements.append("WHITEFRAGMENT>2") + add_charms[charm] = 0 + else: + item_requirements.append(charm) + add_charms[charm] = self.charm_costs[charm_name_to_id[charm]] - if req.startswith("$SHADESKIP"): + elif req.startswith("$SHADESKIP"): if not self.options.ShadeSkips: skip_clause = True else: @@ -548,7 +558,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: else: shadeskips.append(int(search.groups(1)[0])) - if req.startswith("$CASTSPELL"): + elif req.startswith("$CASTSPELL"): # any skips will be marked, this covers dive uses too c, b, a = parse_cast_logic(req, valid_keys) casts += c @@ -556,7 +566,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: before = b if b else before if a: after_resets.append("CASTSUSED") - if req.startswith("$SHRIEKPOGO"): + elif req.startswith("$SHRIEKPOGO"): if True or not self.options.ShriekPogo: # currently unsupported skip_clause = True else: @@ -567,7 +577,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: if a: after_resets.append("CASTSUSED") item_requirements.append("SCREAM>1") - if req.startswith("$SLOPEBALL"): + elif req.startswith("$SLOPEBALL"): if True or not self.options.SlopeBall: # currently unsupported skip_clause = True else: @@ -580,7 +590,7 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: item_requirements.append("FIREBALL") # can roll back to vs in mod - if req.startswith("$TAKEDAMAGE"): + elif req.startswith("$TAKEDAMAGE"): # no skip_clause because damageboosts are tracked differently search = re.search(r".*\[(.*)HITS\]", req) if not search: @@ -591,10 +601,18 @@ def parse_cast_logic(req: str, valid_keys) -> Tuple[List[int], bool, bool]: # checking for a False condidtion before and after item parsing for short circuiting if skip_clause: continue + if any(cast > 3 for cast in casts): + # twister required + charm = "Spell_Twister" + item_requirements.append(charm) + add_charms[charm] = self.charm_costs[charm_name_to_id[charm]] + state_requirements = HK_state_diff( shadeskip=max(*shadeskips, 0), # highest health shadeskip needed for this clause - twister_required=any(cast > 3 for cast in casts), + twister_required=None, # any(cast > 3 for cast in casts), + add_charms=add_charms, ) + skip_clause, items = parse_item_logic(item_requirements) if skip_clause: continue @@ -769,7 +787,7 @@ def set_victory(self) -> None: multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) def get_item_classification(self, name: str) -> ItemClassification: - item_type = extracted_items[name] + item_type = extracted_items.get(name, None) progression_charms = { # Baldur Killers @@ -985,6 +1003,9 @@ def collect(self, state, item: HKItem) -> bool: if item.name == "Mask_Shard": prog_items["TOTAL_HEALTH"] = 4 + (4 * int(prog_items["Mask_Shard"] / 4)) prog_items["SHADE_HEALTH"] = max(int(prog_items["TOTAL_HEALTH"]/2), 1) + if item.name == "Charm_Notch": + # TODO consider switching to += 1 + prog_items["TOTAL_NOTCHES"] = 3 + prog_items["Charm_Notch"] return change def remove(self, state, item: HKItem) -> bool: @@ -1015,6 +1036,9 @@ def remove(self, state, item: HKItem) -> bool: if item.name == "Mask_Shard": prog_items["TOTAL_HEALTH"] = 4 + (4 * int(prog_items["Mask_Shard"] / 4)) prog_items["SHADE_HEALTH"] = max(int(prog_items["TOTAL_HEALTH"]/2), 1) + if item.name == "Charm_Notch": + # TODO consider switching to -= 1 + prog_items["TOTAL_NOTCHES"] = 3 + prog_items["Charm_Notch"] return change def fill_slot_data(self): diff --git a/worlds/hk_rework/state_mixin.py b/worlds/hk_rework/state_mixin.py index 05363a2c6119..49634c7708e8 100644 --- a/worlds/hk_rework/state_mixin.py +++ b/worlds/hk_rework/state_mixin.py @@ -1,19 +1,18 @@ from BaseClasses import MultiWorld, CollectionState, Region from typing import TYPE_CHECKING, Tuple, NamedTuple, Dict, Set, Any, List from worlds.AutoWorld import LogicMixin -from .Charms import names as charm_names +from .Charms import names as charm_names, charm_name_to_id from collections import Counter from copy import deepcopy +from Utils import KeyedDefaultDict if TYPE_CHECKING: from . import HKWorld, HKClause -default_state = ([], Counter({"DAMAGE": 0, "SPENTSHADE": 0, "SPENTSOUL": 0, "NOFLOWER": 0, "SPENTNOTCHES": 0})) +default_state = ({}, Counter({"DAMAGE": 0, "SPENTSHADE": 0, "SPENTSOUL": 0, "NOFLOWER": 0, "SPENTNOTCHES": 0})) BASE_SOUL = 12 BASE_NOTCHES = 3 BASE_HEALTH = 4 -charm_name_to_id = {"_".join(name.split(" ")): index for index, name in enumerate(charm_names)} # if name in ("Spell_Twister", "Fragile_Heart")} - # TODO >:( class HKLogicMixin(LogicMixin): @@ -34,7 +33,7 @@ class HKLogicMixin(LogicMixin): def init_mixin(self, multiworld) -> None: from . import HKWorld as cls players = multiworld.get_game_players(cls.game) - self._hk_per_player_resource_states = {player: {"Menu": [default_state]} for player in players} # {player: {init_state: [start_region]} for player in players} + self._hk_per_player_resource_states = {player: KeyedDefaultDict(lambda region: [default_state] if region == "Menu" else []) for player in players} # {player: {init_state: [start_region]} for player in players} self._hk_per_player_sweepable_entrances = {player: set() for player in players} self._hk_free_entrances = {player: set() for player in players} self._hk_entrance_clause_cache = {player: {} for player in players} @@ -42,6 +41,7 @@ def init_mixin(self, multiworld) -> None: self.prog_items[player]["TOTAL_SOUL"] = BASE_SOUL self.prog_items[player]["TOTAL_HEALTH"] = BASE_HEALTH self.prog_items[player]["SHADE_HEALTH"] = max(int(BASE_HEALTH/2), 1) + self.prog_items[player]["TOTAL_NOTCHES"] = BASE_NOTCHES def copy_mixin(self, other) -> CollectionState: other._hk_per_player_resource_states = self._hk_per_player_resource_states @@ -51,22 +51,24 @@ def copy_mixin(self, other) -> CollectionState: def _hk_apply_and_validate_state(self, clause: "HKClause", region, target_region=None) -> bool: player = region.player - avaliable_states = self._hk_per_player_resource_states[player].get(region.name, None) + # avaliable_states = self._hk_per_player_resource_states[player].get(region.name, None) - if avaliable_states is None: - region.can_reach(self) - avaliable_states = self._hk_per_player_resource_states[player].get(region.name, []) + # if avaliable_states is None: + # region.can_reach(self) + # avaliable_states = self._hk_per_player_resource_states[player].get(region.name, []) + avaliable_states = self._hk_per_player_resource_states[player][region.name] + # loses the can_reach parent call, potentially re-add it? + + if clause.hk_state_requirements.shadeskip: + avaliable_states = [state for state in avaliable_states if not state[1]["SPENTSHADE"]] + # if False and clause.hk_state_requirements.twister_required: + # # TODO do black magic here + # self.has("Spell_Twister", player) if not avaliable_states: # no valid parent states return False - if clause.hk_state_requirements.shadeskip: - avaliable_states = [state for state in avaliable_states if not state[1]["SPENTSHADE"]] - if False and clause.hk_state_requirements.twister_required: - # TODO do black magic here - self.has("Spell_Twister", player) - any_true = False if target_region: target_states = self._hk_per_player_resource_states[player].get(target_region.name, []) @@ -75,28 +77,47 @@ def _hk_apply_and_validate_state(self, clause: "HKClause", region, target_region else: persist = False - for state_tuple in avaliable_states: - resource_state = state_tuple[1] + for charms, resource_state in avaliable_states: + # resource_state = state_tuple[1] + # charms = state_tuple[0] # TODO see if we can remove the charm list for reset_key in clause.hk_before_resets: resource_state[reset_key] = 0 for key, value in clause.hk_state_modifiers.items(): resource_state[key] += value + # if a resource state requirement cannot be sufficed, blindly add charms to fix it + # if the charm validation fails because we are missing the charm or not enough notches + # then the state would have been skipped anyways if not self.prog_items[player]["TOTAL_HEALTH"] > resource_state["DAMAGE"]: continue if not self.prog_items[player]["SHADE_HEALTH"] >= resource_state["SPENTSHADE"]: continue - if not self.prog_items[player]["TOTAL_SOUL"] >= resource_state["SPENTCASTS"] * (3 if "Spell_Twister" in state_tuple[0] else 4): + if not self.prog_items[player]["TOTAL_SOUL"] >= resource_state["SPENTCASTS"] * (3 if "Spell_Twister" in charms else 4): + continue + # first check if we have spots for overcharming + if not self.prog_items[player]["TOTAL_NOTCHES"] >= sum(charms.values()): continue + + # TODO implement overcharming and its effects on DAMAGE + # if we can take off one charm and have one notch free we can overcharm + # if self.prog_items[player]["TOTAL_NOTCHES"] > sum(charms.values()) - max(charms.values()): + # resource_state["OVERCHARMED"] = 1 + # # recheck damage with overcharm effects + # if not self.prog_items[player]["TOTAL_HEALTH"] > 2 * resource_state["DAMAGE"]: + # continue + # continue + # TODO charm+notch calcs for reset_key in clause.hk_after_resets: resource_state[reset_key] = 0 + if "SPENTNOTCHES" in clause.hk_after_resets: + charms = {} if persist: any_true = True - self._hk_per_player_resource_states[player][target_region.name].append(([], resource_state)) + self._hk_per_player_resource_states[player][target_region.name].append((charms, resource_state)) else: # we only need one success return True