| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 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): | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     """Attack, Defense, Potion, HP, SP, MP, Flasks, Equipment, is_boss""" | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |     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] = { | 
					
						
							| 
									
										
											  
											
												TUNIC: Entrance rando Direction Pairs + Decoupled (#3761)
* 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>
											
										 
											2025-05-06 12:33:21 -04:00
										 |  |  |     # The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |     "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"]), | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     # Cathedral has the same requirements as Swamp | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |     # 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 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  | # Gauntlet does not have exclusively higher stat requirements, so it will be checked separately | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  | # 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"] | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         # this is to check Swamp and Gauntlet on their own | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     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() | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |     for item in data.equipment: | 
					
						
							|  |  |  |         if item == "Stick": | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             if not stick_bool: | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                 if has_magic: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                     equipment.remove("Stick") | 
					
						
							|  |  |  |                     if "Magic" not in equipment: | 
					
						
							|  |  |  |                         equipment.append("Magic") | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                     # magic can make up for the lack of stick | 
					
						
							|  |  |  |                     extra_mp_needed += 2 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                     extra_att_needed -= 32 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         elif item == "Sword": | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             if not sword_bool: | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                 # need sword for bosses | 
					
						
							|  |  |  |                 if data.is_boss: | 
					
						
							|  |  |  |                     return False | 
					
						
							| 
									
										
										
										
											2025-02-16 19:30:40 -05:00
										 |  |  |                 if stick_bool: | 
					
						
							|  |  |  |                     equipment.remove("Sword") | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                     equipment.append("Stick") | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                     # may revise this later based on feedback | 
					
						
							|  |  |  |                     extra_att_needed += 3 | 
					
						
							|  |  |  |                     extra_def_needed += 2 | 
					
						
							| 
									
										
										
										
											2025-02-16 19:30:40 -05:00
										 |  |  |                     # this is for when it changes over to the magic-only state if it needs to later | 
					
						
							|  |  |  |                     extra_mp_needed += 4 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         # just increase the stat requirement, we'll check for shield when calculating defense | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         elif item == "Shield": | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             equipment.remove("Shield") | 
					
						
							|  |  |  |             extra_def_needed += 2 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         elif item == "Laurels": | 
					
						
							|  |  |  |             if not state.has("Hero's Laurels", player): | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                 # require Laurels for the Heir | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         elif item == "Magic": | 
					
						
							|  |  |  |             if not has_magic: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                 equipment.remove("Magic") | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                 extra_att_needed += 2 | 
					
						
							|  |  |  |                 extra_def_needed += 2 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                 extra_mp_needed -= 32 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |     modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level, | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                                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: | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         # we may need to check if you would have the required stats if you were missing a weapon | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             if check_combat_reqs("none", state, player, more_modified_stats): | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         elif stick_bool and "Stick" in equipment and has_magic: | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             # we need to check if you would have the required stats if you didn't have the stick | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             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, | 
					
						
							| 
									
										
										
										
											2025-02-16 19:30:40 -05:00
										 |  |  |                                             modified_stats.sp_level, modified_stats.mp_level + 2, | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                                             modified_stats.potion_count, equip_list, data.is_boss) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             if check_combat_reqs("none", state, player, more_modified_stats): | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # 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 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     att_required = data.att_level | 
					
						
							|  |  |  |     player_att, att_offerings = get_att_level(state, player) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     # if you have 2 more attack than needed, we can forego needing mp | 
					
						
							| 
									
										
										
										
											2025-02-16 19:30:40 -05:00
										 |  |  |     if data.mp_level > 1 and "Magic" in data.equipment: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             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 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # 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) | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         req_stats = data.def_level + data.sp_level | 
					
						
							|  |  |  |         if player_def + player_sp < req_stats: | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             return False | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             free_def = player_def - def_offerings | 
					
						
							|  |  |  |             free_sp = player_sp - sp_offerings | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             if free_sp + free_def >= req_stats: | 
					
						
							|  |  |  |                 # you don't need to buy upgrades | 
					
						
							|  |  |  |                 pass | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                 # 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) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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: | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             # 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 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |                         break | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |             costs = [calc_hp_potion_cost(hp, potion) for hp, potion in upgrade_options] | 
					
						
							|  |  |  |             money_required += min(costs) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     return get_money_count(state, player) >= money_required | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # 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 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |     return (min(8, 1 + att_offerings + att_upgrades) | 
					
						
							|  |  |  |             + (1 if state.has("Hero's Laurels", player) else 0), att_offerings) | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # 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 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |                 + 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), | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  |             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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  | 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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 16:40:36 -05:00
										 |  |  | 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)) | 
					
						
							| 
									
										
										
										
											2025-02-09 13:12:17 -05:00
										 |  |  |         # a copy_mixin was intentionally excluded because the empty state from init_mixin | 
					
						
							|  |  |  |         # will always be appropriate for recalculating the logic cache |