Files
Grinch-AP/worlds/tunic/combat_logic.py
Scipio Wright b0b3e3668f TUNIC: The Big Refactor (#5195)
* 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

* Make early bushes only contain grass

* Fix library mistake

* Backport changes to grass rando (#20)

* Backport changes to grass rando

* add_rule instead of set_rule for the special cases, add special cases for back of swamp laurels area cause I should've made a new region for the swamp upper entrance

* Remove item name group for grass

* Update grass rando option descriptions

- Also ignore grass fill for single player games

* Ignore grass fill option for solo rando

* Update er_rules.py

* Fix pre fill issue

* Remove duplicate option

* Add excluded grass locations back

* Hide grass fill option from simple ui options page

* Check for start with sword before setting grass rules

* Update worlds/tunic/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* 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

* Exclude grass from get_filler_item_name

- non-grass rando games were accidentally seeing grass items get shuffled in as filler, which is funny but probably shouldn't happen

* Update worlds/tunic/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* change the rest of grass_fill to local_fill

* Filter out grass from filler_items

* remove -> discard

* Update worlds/tunic/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Starting out

* Rules for breakable regions

* # 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

* change has_stick to has_melee

* Update grass list with combat logic regions

* More fixes from combat logic merge

* Fix some dumb stuff (#21)

* Reorganize pre fill for grass

* make the rest of it work, it's pr ready, boom

* Make it work in not pot shuffle

* Merge grass rando

* multiworld -> world get_location, use has_any

* Swap out region for West Garden Before Terry grass

* Adjust west garden rules to add west combat region

* Adjust grass regions for south checkpoint grass

* Adjust grass regions for after terry grass

* Adjust grass regions for west combat grass

* Adjust grass regions for dagger house grass

* Adjust grass regions for south checkpoint grass, adjust regions and rules for some related locations

* Finish the remainder of the west garden grass, reformat ruined atoll a little

* More hex quest updates

- Implement page ability shuffle for hex quest
- Fix keys behind bosses if hex goal is less than 3
- Added check to fix conflicting hex quest options
- Add option to slot data

* Change option comparison

* Change option checking and fix some stuff

- also keep prayer first on low hex counts

* Update option defaulting

* Update option checking

* Fix option assignment again

* Merge in hex hunt

* Merge in changes

* Clean up imports

* Add ability type to UT stuff

* merge it all

* Make local fill work across pot and grass (to be adjusted later)

* Make separate pools for the grass and non-grass fills

* Fix id overlap

* Update option description

* Fix default

* Reorder localfill option desc

* Load the purgatory ones in

* Adjustments after merge

* Fully remove logicrules

* Fix UT support with fixed shop option

* Add breakable shuffle to the ut stuff

* Make it load in a specific number of locations

* Add Silent's spoiler log ability thing

* Fix for groups

* Fix for groups

* Fix typo

* Fix hex quest UT support

* Use .get

* UT fixes, classification fixes

* Rename some locations

* Adjust guard house names

* Adjust guard house names

* Rework create_item

* Fix for plando connections

* Rename, add new breakables

* Rename more stuff

* Time to rename them again

* Fix issue with fixed shop + decoupled

* Put in an exception to catch that error in the future

* Update create_item to match main

* Update spoiler log lines for hex abilities

* Burn the signs down

* Bring over the combat logic fix

* Merge in combat logic fix

* Silly static method thing

* Move a few areas to before well instead of east forest

* Add an all_random hidden option for dev stuff

* Port over changes from main

* Fix west courtyard pot regions

* Remove debug prints

* Fix fortress courtyard and beneath the fortress loc groups again

* Add exception handling to deal with duplicate apworlds

* Fix typo

* More missing loc group conversions

* Initial fuse shuffle stuff

* Fix gun missing from combat_items, add new for combat logic cache, very slight refactor of check_combat_reqs to let it do the changeover in a less complicated fashion, fix area being a boss area rather than non-boss area for a check

* Add fuse shuffle logic

* reorder atoll statue rule

* Update traversal reqs

* Remove fuse shuffle from temple door

* Combine rules and option checking

* Add bell shuffle; fix fuse location groups

* Fix portal rules not requiring prayer

* Merge the grass laurels exit grass PR

* Merge in fortress bridge PR

* Do a little clean up

* Fix a regression

* Update after merge

* Some more stuff

* More Silent changes

* Update more info section in game info page

* Fix rules for atoll and swamp fuses

* Precollect cathedral fuse in ER

* actually just make the fuse useful instead of progression

* Add it to the swamp and cath rules too

* Fix cath fuse name

* Minor fixes and edits

* Some UT stuff

* Fix a couple more groups

* Move a bunch of UT stuff to its own file

* Fix up a couple UT things

* Couple minor ER fixes

* Formatting change

* UT poptracker stuff enabled since it's optional in one of the releases

* Add author string to world class

* Adjust local fill option name

* Update ut_stuff to match the PR

* Add exception handling for UT with old apworld

* Fix missing tracker_world

* Remove extra entrance from cath main -> elevator

Entry <-> Elev exists,
Entry <-> Main exists
So no connection is needed between Main and Elev

* Fix so that decoupled doesn't incorrectly use get_portal_info and get_paired_portal

* Fix so that decoupled doesn't incorrectly use get_portal_info and get_paired_portal

* Update for breakables poptracker

* Backup and warnings instead

* Update typing

* Delete old regions and rules, move stuff to logic_helpers and constants

* Delete now much less useful tests

* Fix breakables map tracking

* Add more comments to init

* Add todo to grass.py

* Fix up tests

* Pull out fuse and bell shuffle

* Pull out fuse and bell shuffle

* Update worlds/tunic/options.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Update worlds/tunic/logic_helpers.py

Co-authored-by: qwint <qwint.42@gmail.com>

* {} -> () in state functions

* {} -> () in state functions

* Change {} -> () in state functions, use constant for gun

* Remove floating constants in er_data

* Finish hard deprecating FixedShop

* Finish hard deprecating FixedShop

* Fix zig skip showing up in decoupled fixed shop

---------

Co-authored-by: silent-destroyer <osilentdestroyer@gmail.com>
Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: qwint <qwint.42@gmail.com>
2025-09-05 00:44:32 +02:00

444 lines
20 KiB
Python

from collections import defaultdict
from enum import IntEnum
from typing import NamedTuple
from BaseClasses import CollectionState
from worlds.AutoWorld import LogicMixin
from .logic_helpers import has_sword, has_melee
# 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: AreaStats | None = 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