Skip to content

Commit

Permalink
first pass at charm cost logic
Browse files Browse the repository at this point in the history
  • Loading branch information
qwint committed Jul 28, 2024
1 parent f5bf03e commit 8826f7a
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 32 deletions.
3 changes: 3 additions & 0 deletions worlds/hk_rework/Charms.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@
"Weaversong",
"Grimmchild"
]

charm_name_to_id = {"_".join(name.split(" ")): index for index, name in enumerate(names)}
# TODO >:(
50 changes: 37 additions & 13 deletions worlds/hk_rework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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={},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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:
Expand All @@ -548,15 +558,15 @@ 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
add_dict["CASTSUSED"] += sum(c)
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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
59 changes: 40 additions & 19 deletions worlds/hk_rework/state_mixin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -34,14 +33,15 @@ 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}
for player in players:
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
Expand All @@ -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, [])
Expand All @@ -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
Expand Down

0 comments on commit 8826f7a

Please sign in to comment.