mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

* Fix merge conflict * Fix formatting, fix rule for heir access after merge * Writing combat logic helpers * More helpers! * More logic! * Rename has_stick to has_melee, some fixes per Medic's review * Clamp max power from sword upgrades * Wrote the rest of the helpers * Remove unused import * Apply item classifications * Create the combat logic option * Item classification varies based on option * Add the shop sword logic stuff in * Add the rules for the boss-only option * Fix tiny issues * Some early Overworld combat logic * Fill out swamp combat logic * Add note * Bump up Boss Scav and Heir * More revisions to combat logic * Some changes, currently broken * New system for power, kinda jank probably * Revisions to new system, needs more balancing * Cap attack upgrades * Uncap mp power since it's directly related to damage output * Voidlings * Put together a table showing the vanilla-expected stats for each area * Added some info on potion counts * Made new helper functions * Make has_required_stats * Make has_combat_reqs * Update er_rules for new combat reqs * Fix all the broken things ever * Remove outdated todo * Make temp option for testing logic * More flexible choices for combat items * Hard require sword for bosses * Temporarily default combat logic to on * Finish writing overworld combat logic * East Forest combat logic done * Remove a few easy ones * Finish beneath the well * Dark Tomb combat logic * West Garden combat logic * make unit tests checkmark again * Weird west garden dagger house edge case * Try block for that weird west garden edge case * Add quarry combat logic * Update to filter out unreachable regions outside of ER * Fortress Grave Path logic, and a couple fixes to the west garden logic * Fortress east shortcut logic, and rewriting the try except blocks to use finally * Refactor to use a new function cause wow there was a lot of repeated code * Add combat logic to the other two sets of fortress fuses * Add combat rules to beneath the vault * Fix missing cathedral -> elevator connection * Combat logic for cathedral to elevator * Add cathedral main region, rename cathedral -> cathedral entry * Setup cathedral combat logic * Adjust locations' regions for ER * Add laurels zip logic to the chest in the spike room in cathedral * Add combat logic to frog's domain * Move frog's domain locations to regions for combat logic * Add new frog's domain regions for combat logic * Update region name for frog's domain * Fix typo * Add more regions for lower zig * Move around lower zig regions for combat logic * Lower Zig combat logic * Upper zig combat logic * Fix typo * Fix typos * Fix missing world. * Update combat logic description * Add todo * Add todo * Don't make zig skip if er or fixed shop is off * Make it so zig skip is only made with fewer shops and er * Temporarily default combat logic on * Update test to explicitly disable combat logic * Update test_access.py * Slight wording changes * Fix bugs, refactor quarry regions so you can access chests in lower quarry with ice grapples * Run through checks you can do with magic dagger * Run through checks you can do with magic dagger * Add rule for entering town portal of having equipment to deal with enemies * Add rule for atoll near the 6 crabs surrounding a poor defenseless baby slorm * Update the rule for the chest near the 6 crabs surrounding a slorm to also possibly require laurels * Revamp combat logic function to work properly without melee * Add laurels rules to combat logic chests * Modify beneath the vault bridge rule to need a lantern if combat logic is on * Put in money logic * Dagger or combat for swamp big skeleton chest * Remove the 100 moneys from logic * Modify lower zig ls drop region destinations * Remove completed todo * Reword combat logic option description, remove test option * Add combat logic to slot data * Merge Silent's missing slot data bugfix PR #3628 * Remove test combat option * Update combat logic description * Fix secret gathering place issue * Fix secret gathering place issue * Fix lower zig ls rule * Fix accidentally removed librarian rule * Remove redundant rule * Update gauntlet rule to hard-require a sword * Add test for a problematic connection * Adjust combat logic to deal with weird edge cases so it doesn't take stuff out of logic that was previously in logic * Fix create_item classification * Update some comments * Update per exempt's suggestion * Add combat logic to the well boss fight, reorder the combat logic stuff a little to better section them off * Add EntranceLayout option * Add back LogicRules as an invisible option, to not break old yamls * Fix a bug with seed group, continue changing fixed shop to entrance layout * Fix missed fixed shop -> entrance layout spot * Fix bug in seed groups with fixed shop on and off * Add entrance layout to the UT regen stuff * Put direction. in, will add them later * Remove unused elevation from portal class * Got like half of them in * Finish adding all of the directions * Add combat rule for zig front to back * Update per Medic's suggestion * Update ladder storage without items option description * Mess with state with collect and remove to save like 2 seconds (never again) * Save even more time, still never going to do this again on anything else * Add option check for collect and remove * Add directions to shop portals * Update direction in Portal with default * Move Direction above Portal * Add decoupled option, mess with plando connection stuff * Merge, implement verify plando directions * Condense the stuff in change and remove to less lines (thanks medic) * Remove unused thing * Swap to using logicmixin instead of prog_items (thanks Vi) * Fix consistency in stat counters * Add back something that was needed * Fix mistake when adding back * Making the fix better (thanks medic) * Make it actually return false if it gets to the backup lists and fails them * Fix stuff after merge * Add outlet regions, create new regions as needed for them * Put together part of decoupled and direction pairs * make direction pairs work * Make decoupled work * Make fixed shop work again * Fix a few minor bugs * Fix a few minor bugs * Fix plando * god i love programming * Reorder portal list * Update portal sorter for variable shops * Add missing parameter * Some cleanup of prints and functions * Fix typo * it's aliiiiiive * Make seed groups not sync decoupled * Add test with full-shop plando * Fix bug with vanilla portals * Handle plando connections and direction pair errors * Update plando checking for decoupled * Fix typo * Fix exception text to be shorter * Add some more comments * Add todo note * Remove unused safety thing * Remove extra plando connections definition in options * Make seed groups in decoupled with overlapping but not fully overlapped plando connections interact nicely without messing with what the entrances look like in the spoiler log * Fix weird edge case that is technically user error * Add note to fixed shop * Fix parsing shop names in UT * Remove debug print * Actually make UT work * multiworld. to world. * Fix typo from merge * Make it so the shops show up in the entrance hints * Fix bug in ladder storage rules * Remove blank line * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py # worlds/tunic/rules.py # worlds/tunic/test/test_access.py * Fix issues after merge * Update plando connections stuff in docs * Fix library mistake * has_stick -> has_melee * has_stick -> has_melee * Add a failsafe for direction pairing * Fix playthrough crash bug * Remove init from logicmixin * Updates per code review (thanks hesto) * has_stick to has_melee in newer update * has_stick to has_melee in newer update * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/combat_logic.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py * Cleanup more stuff after merge * Revert "Cleanup more stuff after merge" This reverts commit a6ee9a93da8f2fcc4413de6df6927b246017889d. * Revert "# Conflicts:" This reverts commit c74ccd74a45b6ad6b9abe6e339d115a0c98baf30. * Cleanup more stuff after merge * Swap to .get for decoupled so it works with older games probably maybe * Fix after merge * Fix typo * Fix UT support with fixed shop option * Backport plando connections fix * Fix issue with fixed shop + decoupled * Make the error not duplicate the while loop condition * Fix rule for quarry back to monastery * Fix more stuff after merge * Make it not output anything if you set plando connections but not ER * Add obvious note to plando connections description * Fix after merge * add comment to commented out connection --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
442 lines
20 KiB
Python
442 lines
20 KiB
Python
from typing import Dict, List, NamedTuple, Tuple, Optional
|
|
from enum import IntEnum
|
|
from collections import defaultdict
|
|
from BaseClasses import CollectionState
|
|
from .rules import has_sword, has_melee
|
|
from worlds.AutoWorld import LogicMixin
|
|
|
|
|
|
# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla
|
|
class AreaStats(NamedTuple):
|
|
"""Attack, Defense, Potion, HP, SP, MP, Flasks, Equipment, is_boss"""
|
|
att_level: int
|
|
def_level: int
|
|
potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k
|
|
hp_level: int
|
|
sp_level: int
|
|
mp_level: int
|
|
potion_count: int
|
|
equipment: List[str] = []
|
|
is_boss: bool = False
|
|
|
|
|
|
# the vanilla upgrades/equipment you would have
|
|
area_data: Dict[str, AreaStats] = {
|
|
# The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something
|
|
"Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]),
|
|
"East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]),
|
|
"Before Well": AreaStats(1, 1, 1, 1, 1, 1, 3, ["Sword", "Shield"]),
|
|
# learn how to upgrade
|
|
"Beneath the Well": AreaStats(2, 1, 3, 3, 1, 1, 3, ["Sword", "Shield"]),
|
|
"Dark Tomb": AreaStats(2, 2, 3, 3, 1, 1, 3, ["Sword", "Shield"]),
|
|
"West Garden": AreaStats(2, 3, 3, 3, 1, 1, 4, ["Sword", "Shield"]),
|
|
"Garden Knight": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield"], is_boss=True),
|
|
# get the wand here
|
|
"Beneath the Vault": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield", "Magic"]),
|
|
"Eastern Vault Fortress": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"]),
|
|
"Siege Engine": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"], is_boss=True),
|
|
"Frog's Domain": AreaStats(3, 4, 3, 5, 3, 3, 4, ["Sword", "Shield", "Magic"]),
|
|
# the second half of Atoll is the part you need the stats for, so putting it after frogs
|
|
"Ruined Atoll": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]),
|
|
"The Librarian": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"], is_boss=True),
|
|
"Quarry": AreaStats(5, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]),
|
|
"Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]),
|
|
"Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True),
|
|
"Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
|
|
# Cathedral has the same requirements as Swamp
|
|
# marked as boss because the garden knights can't get hurt by stick
|
|
"Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True),
|
|
"The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True),
|
|
}
|
|
|
|
|
|
# these are used for caching which areas can currently be reached in state
|
|
# Gauntlet does not have exclusively higher stat requirements, so it will be checked separately
|
|
boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
|
|
# Swamp does not have exclusively higher stat requirements, so it will be checked separately
|
|
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"]
|
|
|
|
|
|
class CombatState(IntEnum):
|
|
unchecked = 0
|
|
failed = 1
|
|
succeeded = 2
|
|
|
|
|
|
def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool:
|
|
# we're caching whether you've met the combat reqs before if the state didn't change first
|
|
# if the combat state is stale, mark each area's combat state as stale
|
|
if state.tunic_need_to_reset_combat_from_collect[player]:
|
|
state.tunic_need_to_reset_combat_from_collect[player] = False
|
|
for name in area_data.keys():
|
|
if state.tunic_area_combat_state[player][name] == CombatState.failed:
|
|
state.tunic_area_combat_state[player][name] = CombatState.unchecked
|
|
|
|
if state.tunic_need_to_reset_combat_from_remove[player]:
|
|
state.tunic_need_to_reset_combat_from_remove[player] = False
|
|
for name in area_data.keys():
|
|
if state.tunic_area_combat_state[player][name] == CombatState.succeeded:
|
|
state.tunic_area_combat_state[player][name] = CombatState.unchecked
|
|
|
|
if state.tunic_area_combat_state[player][area_name] > CombatState.unchecked:
|
|
return state.tunic_area_combat_state[player][area_name] == CombatState.succeeded
|
|
|
|
met_combat_reqs = check_combat_reqs(area_name, state, player)
|
|
|
|
# we want to skip the "none area" since we don't record its results
|
|
if area_name not in area_data.keys():
|
|
return met_combat_reqs
|
|
|
|
# loop through the lists and set the easier/harder area states accordingly
|
|
if area_name in boss_areas:
|
|
area_list = boss_areas
|
|
elif area_name in non_boss_areas:
|
|
area_list = non_boss_areas
|
|
else:
|
|
# this is to check Swamp and Gauntlet on their own
|
|
area_list = [area_name]
|
|
|
|
if met_combat_reqs:
|
|
# set the state as true for each area until you get to the area we're looking at
|
|
for name in area_list:
|
|
state.tunic_area_combat_state[player][name] = CombatState.succeeded
|
|
if name == area_name:
|
|
break
|
|
else:
|
|
# set the state as false for the area we're looking at and each area after that
|
|
reached_name = False
|
|
for name in area_list:
|
|
if name == area_name:
|
|
reached_name = True
|
|
if reached_name:
|
|
state.tunic_area_combat_state[player][name] = CombatState.failed
|
|
|
|
return met_combat_reqs
|
|
|
|
|
|
def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool:
|
|
data = alt_data or area_data[area_name]
|
|
extra_att_needed = 0
|
|
extra_def_needed = 0
|
|
extra_mp_needed = 0
|
|
has_magic = state.has_any(("Magic Wand", "Gun"), player)
|
|
sword_bool = has_sword(state, player)
|
|
stick_bool = sword_bool or has_melee(state, player)
|
|
equipment = data.equipment.copy()
|
|
for item in data.equipment:
|
|
if item == "Stick":
|
|
if not stick_bool:
|
|
if has_magic:
|
|
equipment.remove("Stick")
|
|
if "Magic" not in equipment:
|
|
equipment.append("Magic")
|
|
# magic can make up for the lack of stick
|
|
extra_mp_needed += 2
|
|
extra_att_needed -= 32
|
|
else:
|
|
return False
|
|
|
|
elif item == "Sword":
|
|
if not sword_bool:
|
|
# need sword for bosses
|
|
if data.is_boss:
|
|
return False
|
|
if stick_bool:
|
|
equipment.remove("Sword")
|
|
equipment.append("Stick")
|
|
# may revise this later based on feedback
|
|
extra_att_needed += 3
|
|
extra_def_needed += 2
|
|
# this is for when it changes over to the magic-only state if it needs to later
|
|
extra_mp_needed += 4
|
|
else:
|
|
return False
|
|
|
|
# just increase the stat requirement, we'll check for shield when calculating defense
|
|
elif item == "Shield":
|
|
equipment.remove("Shield")
|
|
extra_def_needed += 2
|
|
|
|
elif item == "Laurels":
|
|
if not state.has("Hero's Laurels", player):
|
|
# require Laurels for the Heir
|
|
return False
|
|
|
|
elif item == "Magic":
|
|
if not has_magic:
|
|
equipment.remove("Magic")
|
|
extra_att_needed += 2
|
|
extra_def_needed += 2
|
|
extra_mp_needed -= 32
|
|
|
|
modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level,
|
|
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count,
|
|
equipment, data.is_boss)
|
|
if has_required_stats(modified_stats, state, player):
|
|
return True
|
|
else:
|
|
# we may need to check if you would have the required stats if you were missing a weapon
|
|
if sword_bool and "Sword" in equipment and has_magic:
|
|
# we need to check if you would have the required stats if you didn't have the sword
|
|
equip_list = [item for item in equipment if item != "Sword"]
|
|
if "Magic" not in equip_list:
|
|
equip_list.append("Magic")
|
|
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
|
modified_stats.potion_level, modified_stats.hp_level,
|
|
modified_stats.sp_level, modified_stats.mp_level + 4,
|
|
modified_stats.potion_count, equip_list, data.is_boss)
|
|
if check_combat_reqs("none", state, player, more_modified_stats):
|
|
return True
|
|
|
|
elif stick_bool and "Stick" in equipment and has_magic:
|
|
# we need to check if you would have the required stats if you didn't have the stick
|
|
equip_list = [item for item in equipment if item != "Stick"]
|
|
if "Magic" not in equip_list:
|
|
equip_list.append("Magic")
|
|
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
|
modified_stats.potion_level, modified_stats.hp_level,
|
|
modified_stats.sp_level, modified_stats.mp_level + 2,
|
|
modified_stats.potion_count, equip_list, data.is_boss)
|
|
if check_combat_reqs("none", state, player, more_modified_stats):
|
|
return True
|
|
else:
|
|
return False
|
|
return False
|
|
|
|
|
|
# check if you have the required stats, and the money to afford them
|
|
# it may be innaccurate due to poor spending, and it may even require you to "spend poorly"
|
|
# but that's fine -- it's already pretty generous to begin with
|
|
def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool:
|
|
money_required = 0
|
|
att_required = data.att_level
|
|
player_att, att_offerings = get_att_level(state, player)
|
|
|
|
# if you have 2 more attack than needed, we can forego needing mp
|
|
if data.mp_level > 1 and "Magic" in data.equipment:
|
|
if player_att < data.att_level + 2:
|
|
player_mp, mp_offerings = get_mp_level(state, player)
|
|
if player_mp < data.mp_level:
|
|
return False
|
|
else:
|
|
extra_mp = player_mp - data.mp_level
|
|
paid_mp = max(0, mp_offerings - extra_mp)
|
|
# mp costs 300 for the first, +50 for each additional
|
|
money_per_mp = 300
|
|
for _ in range(paid_mp):
|
|
money_required += money_per_mp
|
|
money_per_mp += 50
|
|
else:
|
|
att_required += 2
|
|
|
|
if player_att < att_required:
|
|
return False
|
|
else:
|
|
extra_att = player_att - att_required
|
|
paid_att = max(0, att_offerings - extra_att)
|
|
# attack upgrades cost 100 for the first, +50 for each additional
|
|
money_per_att = 100
|
|
for _ in range(paid_att):
|
|
money_required += money_per_att
|
|
money_per_att += 50
|
|
|
|
# adding defense and sp together since they accomplish similar things: making you take less damage
|
|
if data.def_level + data.sp_level > 2:
|
|
player_def, def_offerings = get_def_level(state, player)
|
|
player_sp, sp_offerings = get_sp_level(state, player)
|
|
req_stats = data.def_level + data.sp_level
|
|
if player_def + player_sp < req_stats:
|
|
return False
|
|
else:
|
|
free_def = player_def - def_offerings
|
|
free_sp = player_sp - sp_offerings
|
|
if free_sp + free_def >= req_stats:
|
|
# you don't need to buy upgrades
|
|
pass
|
|
else:
|
|
# we need to pick the cheapest option that gets us above the stats we need
|
|
# first number is def, second number is sp
|
|
upgrade_options: set[tuple[int, int]] = set()
|
|
stats_to_buy = req_stats - free_def - free_sp
|
|
for paid_def in range(0, min(def_offerings + 1, stats_to_buy + 1)):
|
|
sp_required = stats_to_buy - paid_def
|
|
if sp_offerings >= sp_required:
|
|
if sp_required < 0:
|
|
break
|
|
upgrade_options.add((paid_def, stats_to_buy - paid_def))
|
|
costs = [calc_def_sp_cost(defense, sp) for defense, sp in upgrade_options]
|
|
money_required += min(costs)
|
|
|
|
req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count)
|
|
player_potion, potion_offerings = get_potion_level(state, player)
|
|
player_hp, hp_offerings = get_hp_level(state, player)
|
|
player_potion_count = get_potion_count(state, player)
|
|
player_effective_hp = calc_effective_hp(player_hp, player_potion, player_potion_count)
|
|
if player_effective_hp < req_effective_hp:
|
|
return False
|
|
else:
|
|
# need a way to determine which of potion offerings or hp offerings you can reduce
|
|
free_potion = player_potion - potion_offerings
|
|
free_hp = player_hp - hp_offerings
|
|
if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp:
|
|
# you don't need to buy upgrades
|
|
pass
|
|
else:
|
|
# we need to pick the cheapest option that gets us above the amount of effective HP we need
|
|
# first number is hp, second number is potion
|
|
upgrade_options: set[tuple[int, int]] = set()
|
|
# filter out exclusively worse options
|
|
lowest_hp_added = hp_offerings + 1
|
|
for paid_potion in range(0, potion_offerings + 1):
|
|
# check quantities of hp offerings for each potion offering
|
|
for paid_hp in range(0, lowest_hp_added):
|
|
if (calc_effective_hp(free_hp + paid_hp, free_potion + paid_potion, player_potion_count)
|
|
>= req_effective_hp):
|
|
upgrade_options.add((paid_hp, paid_potion))
|
|
lowest_hp_added = paid_hp
|
|
break
|
|
|
|
costs = [calc_hp_potion_cost(hp, potion) for hp, potion in upgrade_options]
|
|
money_required += min(costs)
|
|
|
|
return get_money_count(state, player) >= money_required
|
|
|
|
|
|
# returns a tuple of your max attack level, the number of attack offerings
|
|
def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
att_offerings = state.count("ATT Offering", player)
|
|
att_upgrades = state.count("Hero Relic - ATT", player)
|
|
sword_level = state.count("Sword Upgrade", player)
|
|
if sword_level >= 3:
|
|
att_upgrades += min(2, sword_level - 2)
|
|
# attack falls off, can just cap it at 8 for simplicity
|
|
return (min(8, 1 + att_offerings + att_upgrades)
|
|
+ (1 if state.has("Hero's Laurels", player) else 0), att_offerings)
|
|
|
|
|
|
# returns a tuple of your max defense level, the number of defense offerings
|
|
def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
def_offerings = state.count("DEF Offering", player)
|
|
# defense falls off, can just cap it at 8 for simplicity
|
|
return (min(8, 1 + def_offerings
|
|
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player))
|
|
+ (2 if state.has("Shield", player) else 0)
|
|
+ (2 if state.has("Hero's Laurels", player) else 0),
|
|
def_offerings)
|
|
|
|
|
|
# returns a tuple of your max potion level, the number of potion offerings
|
|
def get_potion_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
potion_offerings = min(2, state.count("Potion Offering", player))
|
|
# your third potion upgrade (from offerings) costs 1,000 money, reasonable to assume you won't do that
|
|
return (1 + potion_offerings
|
|
+ state.count_from_list({"Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"}, player),
|
|
potion_offerings)
|
|
|
|
|
|
# returns a tuple of your max hp level, the number of hp offerings
|
|
def get_hp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
hp_offerings = state.count("HP Offering", player)
|
|
return 1 + hp_offerings + state.count("Hero Relic - HP", player), hp_offerings
|
|
|
|
|
|
# returns a tuple of your max sp level, the number of sp offerings
|
|
def get_sp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
sp_offerings = state.count("SP Offering", player)
|
|
return (1 + sp_offerings
|
|
+ state.count_from_list({"Hero Relic - SP", "Mr Mayor", "Power Up",
|
|
"Regal Weasel", "Forever Friend"}, player),
|
|
sp_offerings)
|
|
|
|
|
|
def get_mp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
|
mp_offerings = state.count("MP Offering", player)
|
|
return (1 + mp_offerings
|
|
+ state.count_from_list({"Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"}, player),
|
|
mp_offerings)
|
|
|
|
|
|
def get_potion_count(state: CollectionState, player: int) -> int:
|
|
return state.count("Potion Flask", player) + state.count("Flask Shard", player) // 3
|
|
|
|
|
|
def calc_effective_hp(hp_level: int, potion_level: int, potion_count: int) -> int:
|
|
player_hp = 60 + hp_level * 20
|
|
# since you don't tend to use potions efficiently all the time, scale healing by .75
|
|
total_healing = int(.75 * potion_count * min(player_hp, 20 + 10 * potion_level))
|
|
return player_hp + total_healing
|
|
|
|
|
|
# returns the total amount of progression money the player has
|
|
def get_money_count(state: CollectionState, player: int) -> int:
|
|
money: int = 0
|
|
# this could be done with something to parse the money count at the end of the string, but I don't wanna
|
|
money += state.count("Money x255", player) * 255 # 1 in pool
|
|
money += state.count("Money x200", player) * 200 # 1 in pool
|
|
money += state.count("Money x128", player) * 128 # 3 in pool
|
|
# total from regular money: 839
|
|
# first effigy is 8, doubles until it reaches 512 at number 7, after effigy 28 they stop dropping money
|
|
# with the vanilla count of 12, you get 3,576 money from effigies
|
|
effigy_count = min(28, state.count("Effigy", player)) # 12 in pool
|
|
money_per_break = 8
|
|
for _ in range(effigy_count):
|
|
money += money_per_break
|
|
money_per_break = min(512, money_per_break * 2)
|
|
return money
|
|
|
|
|
|
def calc_hp_potion_cost(hp_upgrades: int, potion_upgrades: int) -> int:
|
|
money = 0
|
|
|
|
# hp costs 200 for the first, +50 for each additional
|
|
money_per_hp = 200
|
|
for _ in range(hp_upgrades):
|
|
money += money_per_hp
|
|
money_per_hp += 50
|
|
|
|
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
|
|
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
|
|
money_per_potion = 100
|
|
for _ in range(potion_upgrades):
|
|
money += money_per_potion
|
|
if money_per_potion == 100:
|
|
money_per_potion = 300
|
|
elif money_per_potion == 300:
|
|
money_per_potion = 1000
|
|
else:
|
|
money_per_potion += 200
|
|
|
|
return money
|
|
|
|
|
|
def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int:
|
|
money = 0
|
|
|
|
money_per_def = 100
|
|
for _ in range(def_upgrades):
|
|
money += money_per_def
|
|
money_per_def += 50
|
|
|
|
money_per_sp = 200
|
|
for _ in range(sp_upgrades):
|
|
money += money_per_sp
|
|
money_per_sp += 200
|
|
|
|
return money
|
|
|
|
|
|
class TunicState(LogicMixin):
|
|
tunic_need_to_reset_combat_from_collect: Dict[int, bool]
|
|
tunic_need_to_reset_combat_from_remove: Dict[int, bool]
|
|
tunic_area_combat_state: Dict[int, Dict[str, int]]
|
|
|
|
def init_mixin(self, _):
|
|
# the per-player need to reset the combat state when collecting a combat item
|
|
self.tunic_need_to_reset_combat_from_collect = defaultdict(lambda: False)
|
|
# the per-player need to reset the combat state when removing a combat item
|
|
self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False)
|
|
# the per-player, per-area state of combat checking -- unchecked, failed, or succeeded
|
|
self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked))
|
|
# a copy_mixin was intentionally excluded because the empty state from init_mixin
|
|
# will always be appropriate for recalculating the logic cache
|