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>
This commit is contained in:
@@ -7,12 +7,12 @@ from .locations import location_table, location_name_groups, standard_location_n
|
||||
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
|
||||
from .er_rules import set_er_location_rules
|
||||
from .regions import tunic_regions
|
||||
from .er_scripts import create_er_regions
|
||||
from .er_scripts import create_er_regions, verify_plando_directions
|
||||
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
|
||||
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
||||
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options,
|
||||
get_hexagons_in_pool, HexagonQuestAbilityUnlockType)
|
||||
get_hexagons_in_pool, HexagonQuestAbilityUnlockType, EntranceLayout)
|
||||
from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table
|
||||
from .combat_logic import area_data, CombatState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
@@ -61,8 +61,9 @@ class SeedGroup(TypedDict):
|
||||
ice_grappling: int # ice_grappling value
|
||||
ladder_storage: int # ls value
|
||||
laurels_at_10_fairies: bool # laurels location value
|
||||
fixed_shop: bool # fixed shop value
|
||||
plando: TunicPlandoConnections # consolidated plando connections for the seed group
|
||||
entrance_layout: int # entrance layout value
|
||||
has_decoupled_enabled: bool # for checking that players don't have conflicting options
|
||||
plando: List[PlandoConnection] # consolidated plando connections for the seed group
|
||||
|
||||
|
||||
class TunicWorld(World):
|
||||
@@ -95,7 +96,7 @@ class TunicWorld(World):
|
||||
tunic_portal_pairs: Dict[str, str]
|
||||
er_portal_hints: Dict[int, str]
|
||||
seed_groups: Dict[str, SeedGroup] = {}
|
||||
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
|
||||
used_shop_numbers: Set[int]
|
||||
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
|
||||
|
||||
# for the local_fill option
|
||||
@@ -122,24 +123,35 @@ class TunicWorld(World):
|
||||
|
||||
check_options(self)
|
||||
|
||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||
self.options.ice_grappling.value = IceGrappling.option_medium
|
||||
if self.options.logic_rules.value == LogicRules.option_unrestricted:
|
||||
self.options.ladder_storage.value = LadderStorage.option_medium
|
||||
|
||||
self.er_regions = tunic_er_regions.copy()
|
||||
if self.options.plando_connections and not self.options.entrance_rando:
|
||||
self.options.plando_connections.value = ()
|
||||
if self.options.plando_connections:
|
||||
def replace_connection(old_cxn: PlandoConnection, new_cxn: PlandoConnection, index: int) -> None:
|
||||
self.options.plando_connections.value.remove(old_cxn)
|
||||
self.options.plando_connections.value.insert(index, new_cxn)
|
||||
|
||||
for index, cxn in enumerate(self.options.plando_connections):
|
||||
# making shops second to simplify other things later
|
||||
if cxn.entrance.startswith("Shop"):
|
||||
replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
|
||||
self.options.plando_connections.value.remove(cxn)
|
||||
self.options.plando_connections.value.insert(index, replacement)
|
||||
elif cxn.exit.startswith("Shop"):
|
||||
replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
|
||||
self.options.plando_connections.value.remove(cxn)
|
||||
self.options.plando_connections.value.insert(index, replacement)
|
||||
replacement = None
|
||||
if self.options.decoupled:
|
||||
# flip any that are pointing to exit to point to entrance so that I don't have to deal with it
|
||||
if cxn.direction == "exit":
|
||||
replacement = PlandoConnection(cxn.exit, cxn.entrance, "entrance", cxn.percentage)
|
||||
# if decoupled is on and you plando'd an entrance to itself but left the direction as both
|
||||
if cxn.direction == "both" and cxn.entrance == cxn.exit:
|
||||
replacement = PlandoConnection(cxn.entrance, cxn.exit, "entrance")
|
||||
# if decoupled is off, just convert these to both
|
||||
elif cxn.direction != "both":
|
||||
replacement = PlandoConnection(cxn.entrance, cxn.exit, "both", cxn.percentage)
|
||||
|
||||
if replacement:
|
||||
replace_connection(cxn, replacement, index)
|
||||
|
||||
if (self.options.entrance_layout == EntranceLayout.option_direction_pairs
|
||||
and not verify_plando_directions(cxn)):
|
||||
raise OptionError(f"TUNIC: Player {self.player_name} has invalid plando connections. "
|
||||
f"They have Direction Pairs enabled and the connection "
|
||||
f"{cxn.entrance} --> {cxn.exit} does not abide by this option.")
|
||||
|
||||
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
@@ -160,16 +172,16 @@ class TunicWorld(World):
|
||||
self.options.hexagon_quest_ability_type.value = self.passthrough.get("hexagon_quest_ability_type", 0)
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.entrance_layout.value = EntranceLayout.option_standard
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
|
||||
self.options.entrance_layout.value = EntranceLayout.option_fixed_shop
|
||||
self.options.decoupled = self.passthrough.get("decoupled", 0)
|
||||
self.options.laurels_location.value = LaurelsLocation.option_anywhere
|
||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||
self.options.breakable_shuffle.value = self.passthrough.get("breakable_shuffle", 0)
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_true
|
||||
|
||||
self.options.combat_logic.value = self.passthrough.get("combat_logic", 0)
|
||||
else:
|
||||
self.using_ut = False
|
||||
else:
|
||||
@@ -227,10 +239,14 @@ class TunicWorld(World):
|
||||
ice_grappling=tunic.options.ice_grappling.value,
|
||||
ladder_storage=tunic.options.ladder_storage.value,
|
||||
laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies,
|
||||
fixed_shop=bool(tunic.options.fixed_shop),
|
||||
plando=tunic.options.plando_connections)
|
||||
entrance_layout=tunic.options.entrance_layout.value,
|
||||
has_decoupled_enabled=bool(tunic.options.decoupled),
|
||||
plando=tunic.options.plando_connections.value.copy())
|
||||
continue
|
||||
|
||||
# I feel that syncing this one is worse than erroring out
|
||||
if bool(tunic.options.decoupled) != cls.seed_groups[group]["has_decoupled_enabled"]:
|
||||
raise OptionError(f"TUNIC: All players in the seed group {group} must "
|
||||
f"have Decoupled either enabled or disabled.")
|
||||
# off is more restrictive
|
||||
if not tunic.options.laurels_zips:
|
||||
cls.seed_groups[group]["laurels_zips"] = False
|
||||
@@ -243,34 +259,52 @@ class TunicWorld(World):
|
||||
# laurels at 10 fairies changes logic for secret gathering place placement
|
||||
if tunic.options.laurels_location == 3:
|
||||
cls.seed_groups[group]["laurels_at_10_fairies"] = True
|
||||
# more restrictive, overrides the option for others in the same group, which is better than failing imo
|
||||
if tunic.options.fixed_shop:
|
||||
cls.seed_groups[group]["fixed_shop"] = True
|
||||
|
||||
# fixed shop and direction pairs override standard, but conflict with each other
|
||||
if tunic.options.entrance_layout:
|
||||
if cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_standard:
|
||||
cls.seed_groups[group]["entrance_layout"] = tunic.options.entrance_layout.value
|
||||
elif cls.seed_groups[group]["entrance_layout"] != tunic.options.entrance_layout.value:
|
||||
raise OptionError(f"TUNIC: Conflict between seed group {group}'s Entrance Layout options. "
|
||||
f"Seed group cannot have both Fixed Shop and Direction Pairs enabled.")
|
||||
if tunic.options.plando_connections:
|
||||
# loop through the connections in the player's yaml
|
||||
for cxn in tunic.options.plando_connections:
|
||||
for index, player_cxn in enumerate(tunic.options.plando_connections):
|
||||
new_cxn = True
|
||||
for group_cxn in cls.seed_groups[group]["plando"]:
|
||||
# if neither entrance nor exit match anything in the group, add to group
|
||||
if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit)
|
||||
or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)):
|
||||
new_cxn = False
|
||||
break
|
||||
|
||||
# verify that it abides by direction pairs if enabled
|
||||
if (cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_direction_pairs
|
||||
and not verify_plando_directions(player_cxn)):
|
||||
player_dir = "<->" if player_cxn.direction == "both" else "-->"
|
||||
raise Exception(f"TUNIC: Conflict between Entrance Layout option and Plando Connection: "
|
||||
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
|
||||
# check if this pair is the same as a pair in the group already
|
||||
if ((player_cxn.entrance == group_cxn.entrance and player_cxn.exit == group_cxn.exit)
|
||||
or (player_cxn.entrance == group_cxn.exit and player_cxn.exit == group_cxn.entrance
|
||||
and "both" in [player_cxn.direction, group_cxn.direction])):
|
||||
new_cxn = False
|
||||
# if the group's was one-way and the player's was two-way, we replace the group's now
|
||||
if player_cxn.direction == "both" and group_cxn.direction == "entrance":
|
||||
cls.seed_groups[group]["plando"].remove(group_cxn)
|
||||
cls.seed_groups[group]["plando"].insert(index, player_cxn)
|
||||
break
|
||||
is_mismatched = (
|
||||
cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit
|
||||
or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance
|
||||
or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit
|
||||
or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance
|
||||
player_cxn.entrance == group_cxn.entrance and player_cxn.exit != group_cxn.exit
|
||||
or player_cxn.exit == group_cxn.exit and player_cxn.entrance != group_cxn.entrance
|
||||
)
|
||||
if not tunic.options.decoupled:
|
||||
is_mismatched = is_mismatched or (
|
||||
player_cxn.entrance == group_cxn.exit and player_cxn.exit != group_cxn.entrance
|
||||
or player_cxn.exit == group_cxn.entrance and player_cxn.entrance != group_cxn.exit
|
||||
)
|
||||
if is_mismatched:
|
||||
raise Exception(f"TUNIC: Conflict between seed group {group}'s plando "
|
||||
f"connection {group_cxn.entrance} <-> {group_cxn.exit} and "
|
||||
f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}")
|
||||
group_dir = "<->" if group_cxn.direction == "both" else "-->"
|
||||
player_dir = "<->" if player_cxn.direction == "both" else "-->"
|
||||
raise OptionError(f"TUNIC: Conflict between seed group {group}'s plando "
|
||||
f"connection {group_cxn.entrance} {group_dir} {group_cxn.exit} and "
|
||||
f"{tunic.player_name}'s plando connection "
|
||||
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
|
||||
if new_cxn:
|
||||
cls.seed_groups[group]["plando"].value.append(cxn)
|
||||
cls.seed_groups[group]["plando"].append(player_cxn)
|
||||
|
||||
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
|
||||
item_data = item_table[name]
|
||||
@@ -571,7 +605,7 @@ class TunicWorld(World):
|
||||
all_state = self.multiworld.get_all_state(True)
|
||||
all_state.update_reachable_regions(self.player)
|
||||
paths = all_state.path
|
||||
portal_names = [portal.name for portal in portal_mapping]
|
||||
portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)})
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
# skipping event locations
|
||||
if not location.address:
|
||||
@@ -630,6 +664,7 @@ class TunicWorld(World):
|
||||
"lanternless": self.options.lanternless.value,
|
||||
"maskless": self.options.maskless.value,
|
||||
"entrance_rando": int(bool(self.options.entrance_rando.value)),
|
||||
"decoupled": self.options.decoupled.value if self.options.entrance_rando else 0,
|
||||
"shuffle_ladders": self.options.shuffle_ladders.value,
|
||||
"grass_randomizer": self.options.grass_randomizer.value,
|
||||
"combat_logic": self.options.combat_logic.value,
|
||||
|
||||
Reference in New Issue
Block a user