Files
Grinch-AP/worlds/tunic/options.py
Scipio Wright a3aac3d737 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

467 lines
18 KiB
Python

import logging
from dataclasses import dataclass
from typing import Dict, Any, TYPE_CHECKING
from decimal import Decimal, ROUND_HALF_UP
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
PerGameCommonOptions, OptionGroup, Removed, Visibility, NamedRange)
from .er_data import portal_mapping
if TYPE_CHECKING:
from . import TunicWorld
class SwordProgression(DefaultOnToggle):
"""
Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.
"""
internal_name = "sword_progression"
display_name = "Sword Progression"
class StartWithSword(Toggle):
"""
Start with a sword in the player's inventory. Does not count towards Sword Progression.
"""
internal_name = "start_with_sword"
display_name = "Start With Sword"
class KeysBehindBosses(Toggle):
"""
Places the three hexagon keys behind their respective boss fight in your world.
If playing Hexagon Quest, it will place three gold hexagons at the boss locations.
"""
internal_name = "keys_behind_bosses"
display_name = "Keys Behind Bosses"
class AbilityShuffling(DefaultOnToggle):
"""
Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required
Hexagon goal amount, unless the option is set to have them unlock via pages instead.
* Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
"""
internal_name = "ability_shuffling"
display_name = "Shuffle Abilities"
class Lanternless(Toggle):
"""
Choose whether you require the Lantern for dark areas.
When enabled, the Lantern is marked as Useful instead of Progression.
"""
internal_name = "lanternless"
display_name = "Lanternless"
class Maskless(Toggle):
"""
Choose whether you require the Scavenger's Mask for Lower Quarry.
When enabled, the Scavenger's Mask is marked as Useful instead of Progression.
"""
internal_name = "maskless"
display_name = "Maskless"
class FoolTraps(Choice):
"""
Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.
"""
internal_name = "fool_traps"
display_name = "Fool Traps"
option_off = 0
option_normal = 1
option_double = 2
option_onslaught = 3
default = 1
class HexagonQuest(Toggle):
"""
An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.
"""
internal_name = "hexagon_quest"
display_name = "Hexagon Quest"
class HexagonGoal(Range):
"""
How many Gold Questagons are required to complete the game on Hexagon Quest.
"""
internal_name = "hexagon_goal"
display_name = "Gold Hexagons Required"
range_start = 1
range_end = 100
default = 20
class ExtraHexagonPercentage(Range):
"""
How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
The max number of Gold Questagons that can be in the item pool is 100, so this option may be overridden and/or
reduced if the Hexagon Goal amount is greater than 50.
"""
internal_name = "extra_hexagon_percentage"
display_name = "Percentage of Extra Gold Hexagons"
range_start = 0
range_end = 100
default = 50
class HexagonQuestAbilityUnlockType(Choice):
"""
Determines how abilities are unlocked when playing Hexagon Quest with Shuffled Abilities enabled.
Hexagons: A new ability is randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount. Requires at least 3 Gold Hexagons in the item pool, or 15 if Keys Behind Bosses is enabled.
Pages: Abilities are unlocked by finding specific pages in the manual.
This option does nothing if Shuffled Abilities is not enabled.
"""
internal_name = "hexagon_quest_ability_type"
display_name = "Hexagon Quest Ability Unlocks"
option_hexagons = 0
option_pages = 1
default = 0
class EntranceRando(TextChoice):
"""
Randomize the connections between scenes.
A small, very lost fox on a big adventure.
If you set this option's value to a string, it will be used as a custom seed.
Every player who uses the same custom seed will have the same entrances, choosing the most restrictive settings among these players for the purpose of pairing entrances.
"""
internal_name = "entrance_rando"
display_name = "Entrance Rando"
alias_false = 0
alias_off = 0
option_no = 0
alias_true = 1
alias_on = 1
option_yes = 1
default = 0
class FixedShop(Toggle):
"""
This option has been superseded by the Entrance Layout option.
If enabled, it will override the Entrance Layout option.
This is kept to keep older yamls working, and will be removed at a later date.
"""
visibility = Visibility.none
internal_name = "fixed_shop"
display_name = "Fewer Shops in Entrance Rando"
class EntranceLayout(Choice):
"""
Decide how the Entrance Randomizer chooses how to pair the entrances.
Standard: Entrances are randomly connected. There are 6 shops in the pool with this option.
Fixed Shop: Forces the Windmill entrance to lead to a shop, and removes the other shops from the pool.
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
Direction Pairs: Entrances facing opposite directions are paired together. There are 8 shops in the pool with this option.
Note: For seed groups, if one player in a group chooses Fixed Shop and another chooses Direction Pairs, it will error out.
Either of these options will override Standard within a seed group.
"""
internal_name = "entrance_layout"
display_name = "Entrance Layout"
option_standard = 0
option_fixed_shop = 1
option_direction_pairs = 2
default = 0
class Decoupled(Toggle):
"""
Decouple the entrances, so that when you go from one entrance to another, the return trip won't necessarily bring you back to the same place.
Note: For seed groups, all players in the group must have this option enabled or disabled.
"""
internal_name = "decoupled"
display_name = "Decoupled Entrances"
class LaurelsLocation(Choice):
"""
Force the Hero's Laurels to be placed at a location in your world.
For if you want to avoid or specify early or late Laurels.
"""
internal_name = "laurels_location"
display_name = "Laurels Location"
option_anywhere = 0
option_6_coins = 1
option_10_coins = 2
option_10_fairies = 3
default = 0
class ShuffleLadders(Toggle):
"""
Turns several ladders in the game into items that must be found before they can be climbed on.
Adds more layers of progression to the game by blocking access to many areas early on.
"Ladders were a mistake."
—Andrew Shouldice
"""
internal_name = "shuffle_ladders"
display_name = "Shuffle Ladders"
class GrassRandomizer(Toggle):
"""
Turns over 6,000 blades of grass and bushes in the game into checks.
"""
internal_name = "grass_randomizer"
display_name = "Grass Randomizer"
class LocalFill(NamedRange):
"""
Choose the percentage of your filler/trap items that will be kept local or distributed to other TUNIC players with this option enabled.
If you have Grass Randomizer enabled, this option must be set to 95% or higher to avoid flooding the item pool. The host can remove this restriction by turning off the limit_grass_rando setting in host.yaml.
This option defaults to 95% if you have Grass Randomizer enabled, and to 0% otherwise.
This option ignores items placed in your local_items or non_local_items.
This option does nothing in single player games.
"""
internal_name = "local_fill"
display_name = "Local Fill Percent"
range_start = 0
range_end = 98
special_range_names = {
"default": -1
}
default = -1
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
class TunicPlandoConnections(PlandoConnections):
"""
Generic connection plando. Format is:
- entrance: Entrance Name
exit: Exit Name
direction: Direction
percentage: 100
Direction must be one of entrance, exit, or both, and defaults to both if omitted.
Direction entrance means the entrance leads to the exit. Direction exit means the exit leads to the entrance.
If you do not have Decoupled enabled, you do not need the direction line, as it will only use both.
Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted.
If the Entrance Layout option is set to Standard or Fixed Shop, you can plando multiple shops.
If the Entrance Layout option is set to Direction Pairs, your plando connections must be facing opposite directions.
Shop Portal 1-6 are South portals, and Shop Portal 7-8 are West portals.
This option does nothing if Entrance Rando is disabled.
"""
shops = {f"Shop Portal {i + 1}" for i in range(500)}
entrances = {portal.name for portal in portal_mapping}.union(shops)
exits = {portal.name for portal in portal_mapping}.union(shops)
duplicate_exits = True
class CombatLogic(Choice):
"""
If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty.
The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks.
This option marks many more items as progression and may force weapons much earlier than normal.
Bosses Only makes it so that additional combat logic is only added to the boss fights and the Gauntlet.
If disabled, the standard, looser logic is used. The standard logic does not include stat upgrades, just minimal weapon requirements, such as requiring a Sword or Magic Wand for Quarry, or not requiring a weapon for Swamp.
"""
internal_name = "combat_logic"
display_name = "More Combat Logic"
option_off = 0
option_bosses_only = 1
option_on = 2
default = 0
class LaurelsZips(Toggle):
"""
Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots.
Notable inclusions are the Monastery gate, Ruined Passage door, Old House gate, Forest Grave Path gate, and getting from the Back of Swamp to the Middle of Swamp.
"""
internal_name = "laurels_zips"
display_name = "Laurels Zips Logic"
class IceGrappling(Choice):
"""
Choose whether grappling frozen enemies is in logic.
Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain.
Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it.
Hard includes luring or grappling enemies to get to where you want to go.
Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic.
Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off.
"""
internal_name = "ice_grappling"
display_name = "Ice Grapple Logic"
option_off = 0
option_easy = 1
option_medium = 2
option_hard = 3
default = 0
class LadderStorage(Choice):
"""
Choose whether Ladder Storage is in logic.
Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.).
Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS.
Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS.
Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic.
Opening individual chests while doing ladder storage is excluded due to tedium.
Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic.
"""
internal_name = "ladder_storage"
display_name = "Ladder Storage Logic"
option_off = 0
option_easy = 1
option_medium = 2
option_hard = 3
default = 0
class LadderStorageWithoutItems(Toggle):
"""
If disabled, you logically require Stick, Sword, Magic Orb, or Shield to perform Ladder Storage.
If enabled, you will be expected to perform Ladder Storage without progression items.
This can be done with the plushie code, a Golden Coin, Prayer, and many other options.
This option has no effect if you do not have Ladder Storage Logic enabled.
"""
internal_name = "ladder_storage_without_items"
display_name = "Ladder Storage without Items"
class LogicRules(Choice):
"""
This option has been superseded by the individual trick options.
If set to nmg, it will set Ice Grappling to medium and Laurels Zips on.
If set to ur, it will do nmg as well as set Ladder Storage to medium.
It is here to avoid breaking old yamls, and will be removed at a later date.
"""
visibility = Visibility.none
internal_name = "logic_rules"
display_name = "Logic Rules"
option_restricted = 0
option_no_major_glitches = 1
alias_nmg = 1
option_unrestricted = 2
alias_ur = 2
default = 0
class BreakableShuffle(Toggle):
"""
Turns approximately 250 breakable objects in the game into checks.
"""
internal_name = "breakable_shuffle"
display_name = "Breakable Shuffle"
@dataclass
class TunicOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
sword_progression: SwordProgression
start_with_sword: StartWithSword
keys_behind_bosses: KeysBehindBosses
ability_shuffling: AbilityShuffling
fool_traps: FoolTraps
laurels_location: LaurelsLocation
hexagon_quest: HexagonQuest
hexagon_goal: HexagonGoal
extra_hexagon_percentage: ExtraHexagonPercentage
hexagon_quest_ability_type: HexagonQuestAbilityUnlockType
shuffle_ladders: ShuffleLadders
grass_randomizer: GrassRandomizer
breakable_shuffle: BreakableShuffle
local_fill: LocalFill
entrance_rando: EntranceRando
entrance_layout: EntranceLayout
decoupled: Decoupled
plando_connections: TunicPlandoConnections
combat_logic: CombatLogic
lanternless: Lanternless
maskless: Maskless
laurels_zips: LaurelsZips
ice_grappling: IceGrappling
ladder_storage: LadderStorage
ladder_storage_without_items: LadderStorageWithoutItems
fixed_shop: FixedShop # will be removed at a later date
logic_rules: Removed # fully removed in the direction pairs update
tunic_option_groups = [
OptionGroup("Hexagon Quest Options", [
HexagonQuest,
HexagonGoal,
ExtraHexagonPercentage,
HexagonQuestAbilityUnlockType
]),
OptionGroup("Logic Options", [
CombatLogic,
Lanternless,
Maskless,
LaurelsZips,
IceGrappling,
LadderStorage,
LadderStorageWithoutItems,
]),
OptionGroup("Entrance Randomizer", [
EntranceRando,
EntranceLayout,
Decoupled,
TunicPlandoConnections,
]),
]
tunic_option_presets: Dict[str, Dict[str, Any]] = {
"Sync": {
"ability_shuffling": True,
},
"Async": {
"progression_balancing": 0,
"ability_shuffling": True,
"shuffle_ladders": True,
"laurels_location": "10_fairies",
},
"Glace Mode": {
"accessibility": "minimal",
"ability_shuffling": True,
"entrance_rando": True,
"fool_traps": "onslaught",
"laurels_zips": True,
"ice_grappling": "hard",
"ladder_storage": "hard",
"ladder_storage_without_items": True,
"maskless": True,
"lanternless": True,
},
}
def check_options(world: "TunicWorld"):
options = world.options
if options.hexagon_quest and options.ability_shuffling and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
total_hexes = get_hexagons_in_pool(world)
min_hexes = 3
if options.keys_behind_bosses:
min_hexes = 15
if total_hexes < min_hexes:
logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.")
options.hexagon_quest_ability_type.value = HexagonQuestAbilityUnlockType.option_pages
def get_hexagons_in_pool(world: "TunicWorld"):
# Calculate number of hexagons in item pool
options = world.options
return min(int((Decimal(100 + options.extra_hexagon_percentage) / 100 * options.hexagon_goal)
.to_integral_value(rounding=ROUND_HALF_UP)), 100)