TUNIC: Better seed groups for Entrance Rando (#2998)

* Update entrance rando description to discuss seed groups

* Starting off, setting up some names

* It lives

* Some preliminary plando connection handling, probably has errors

* Add missed comma

* if -> elif

* I think this is working properly to handle plando connections

* Update comments

* Fix up shop -> shop portal stuff

* Add back comma that got removed for no reason in the ladder PR

* Remove unnecessary if else

* add back the actually necessary if but not the else

* okay they were both necessary

* Update entrance rando description

* blasphemy

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>

* Rename other instances of tunc -> tunic

* Update per Vi's review (thank you)

* Fix a not that shouldn't have been

* Rearrange, update per Vi's comments (thank you)

* Fix indent

* Add a .value

* Add .values

* Fix bad comparison

* Add a not that was supposed to be there

* Replace another isinstance

* Revise option description

* Fix per Kaito's comment

Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>

---------

Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
Co-authored-by: Kaito Sinclaire <ks@rosenthalcastle.org>
This commit is contained in:
Scipio Wright 2024-05-03 01:21:27 -04:00 committed by GitHub
parent b68be7360c
commit 26188230b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 119 additions and 22 deletions

View File

@ -1,6 +1,6 @@
from typing import Dict, List, Any
from typing import Dict, List, Any, Tuple, TypedDict
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
@ -8,8 +8,9 @@ from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
from .er_data import portal_mapping
from .options import TunicOptions
from .options import TunicOptions, EntranceRando
from worlds.AutoWorld import WebWorld, World
from worlds.generic import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
@ -36,6 +37,13 @@ class TunicLocation(Location):
game: str = "TUNIC"
class SeedGroup(TypedDict):
logic_rules: int # logic rules value
laurels_at_10_fairies: bool # laurels location value
fixed_shop: bool # fixed shop value
plando: List[PlandoConnection] # consolidated list of plando connections for the seed group
class TunicWorld(World):
"""
Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game
@ -57,8 +65,21 @@ class TunicWorld(World):
slot_data_items: List[TunicItem]
tunic_portal_pairs: Dict[str, str]
er_portal_hints: Dict[int, str]
seed_groups: Dict[str, SeedGroup] = {}
def generate_early(self) -> None:
if self.multiworld.plando_connections[self.player]:
for index, cxn in enumerate(self.multiworld.plando_connections[self.player]):
# making shops second to simplify other things later
if cxn.entrance.startswith("Shop"):
replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
self.multiworld.plando_connections[self.player].remove(cxn)
self.multiworld.plando_connections[self.player].insert(index, replacement)
elif cxn.exit.startswith("Shop"):
replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
self.multiworld.plando_connections[self.player].remove(cxn)
self.multiworld.plando_connections[self.player].insert(index, replacement)
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
@ -74,6 +95,58 @@ class TunicWorld(World):
self.options.entrance_rando.value = passthrough["entrance_rando"]
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
for tunic in tunic_worlds:
# if it's one of the options, then it isn't a custom seed group
if tunic.options.entrance_rando.value in EntranceRando.options:
continue
group = tunic.options.entrance_rando.value
# if this is the first world in the group, set the rules equal to its rules
if group not in cls.seed_groups:
cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value,
laurels_at_10_fairies=tunic.options.laurels_location == 3,
fixed_shop=bool(tunic.options.fixed_shop),
plando=multiworld.plando_connections[tunic.player])
continue
# lower value is more restrictive
if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]:
cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value
# 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
# fewer shops, one at windmill
if tunic.options.fixed_shop:
cls.seed_groups[group]["fixed_shop"] = True
if multiworld.plando_connections[tunic.player]:
# loop through the connections in the player's yaml
for cxn in multiworld.plando_connections[tunic.player]:
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
# check if this pair is the same as a pair in the group already
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
)
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.multiworld.get_player_name(tunic.player)}'s plando "
f"connection {cxn.entrance} <-> {cxn.exit}")
if new_cxn:
cls.seed_groups[group]["plando"].append(cxn)
def create_item(self, name: str) -> TunicItem:
item_data = item_table[name]
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player)

View File

@ -4,6 +4,7 @@ from .locations import location_table
from .er_data import Portal, tunic_er_regions, portal_mapping, \
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
from .er_rules import set_er_region_rules
from .options import EntranceRando
from worlds.generic import PlandoConnection
from random import Random
@ -128,12 +129,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal_pairs: Dict[Portal, Portal] = {}
dead_ends: List[Portal] = []
two_plus: List[Portal] = []
logic_rules = world.options.logic_rules.value
player_name = world.multiworld.get_player_name(world.player)
logic_rules = world.options.logic_rules.value
fixed_shop = world.options.fixed_shop
laurels_location = world.options.laurels_location
# if it's not one of the EntranceRando options, it's a custom seed
if world.options.entrance_rando.value not in EntranceRando.options:
seed_group = world.seed_groups[world.options.entrance_rando.value]
logic_rules = seed_group["logic_rules"]
fixed_shop = seed_group["fixed_shop"]
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
shop_scenes: Set[str] = set()
shop_count = 6
if world.options.fixed_shop.value:
if fixed_shop:
shop_count = 1
shop_scenes.add("Overworld Redux")
@ -163,7 +173,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
start_region = "Overworld"
connected_regions.update(add_dependent_regions(start_region, logic_rules))
plando_connections = world.multiworld.plando_connections[world.player]
if world.options.entrance_rando.value in EntranceRando.options:
plando_connections = world.multiworld.plando_connections[world.player]
else:
plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
# universal tracker support stuff, don't need to care about region dependency
if hasattr(world.multiworld, "re_gen_passthrough"):
@ -198,10 +211,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
p_entrance = connection.entrance
p_exit = connection.exit
if p_entrance.startswith("Shop"):
p_entrance = p_exit
p_exit = "Shop Portal"
portal1 = None
portal2 = None
@ -213,7 +222,18 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal2 = portal
# search dead_ends individually since we can't really remove items from two_plus during the loop
if not portal1:
if portal1:
two_plus.remove(portal1)
else:
# if not both, they're both dead ends
if not portal2:
if world.options.entrance_rando.value not in EntranceRando.options:
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
"end to a dead end in their plando connections.")
else:
raise Exception(f"{player_name} paired a dead end to a dead end in their "
"plando connections.")
for portal in dead_ends:
if p_entrance == portal.name:
portal1 = portal
@ -222,16 +242,18 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
raise Exception(f"Could not find entrance named {p_entrance} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal1)
else:
two_plus.remove(portal1)
if not portal2:
if portal2:
two_plus.remove(portal2)
else:
# check if portal2 is a dead end
for portal in dead_ends:
if p_exit == portal.name:
portal2 = portal
break
if p_exit in ["Shop Portal", "Shop"]:
portal2 = Portal(name="Shop Portal", region=f"Shop",
# if it's not a dead end, it might be a shop
if p_exit == "Shop Portal":
portal2 = Portal(name="Shop Portal", region="Shop",
destination="Previous Region", tag="_")
shop_count -= 1
if shop_count < 0:
@ -240,13 +262,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
if p.name == p_entrance:
shop_scenes.add(p.scene())
break
# and if it's neither shop nor dead end, it just isn't correct
else:
if not portal2:
raise Exception(f"Could not find entrance named {p_exit} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal2)
else:
two_plus.remove(portal2)
portal_pairs[portal1] = portal2
@ -270,7 +291,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# need to plando fairy cave, or it could end up laurels locked
# fix this later to be random after adding some item logic to dependent regions
if world.options.laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
portal2 = None
for portal in two_plus:
@ -291,7 +312,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
two_plus.remove(portal1)
dead_ends.remove(portal2)
if world.options.fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
@ -307,7 +328,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
two_plus.remove(portal1)
random_object: Random = world.random
if world.options.entrance_rando.value != 1:
# use the seed given in the options to shuffle the portals
if isinstance(world.options.entrance_rando.value, str):
random_object = Random(world.options.entrance_rando.value)
# we want to start by making sure every region is accessible
random_object.shuffle(two_plus)

View File

@ -103,8 +103,10 @@ class ExtraHexagonPercentage(Range):
class EntranceRando(TextChoice):
"""
Randomize the connections between scenes.
If you set this to a value besides true or false, that value will be used as a custom seed.
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"