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:
parent
b68be7360c
commit
26188230b7
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue