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 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 .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 .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
|
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 .regions import tunic_regions
|
||||||
from .er_scripts import create_er_regions
|
from .er_scripts import create_er_regions
|
||||||
from .er_data import portal_mapping
|
from .er_data import portal_mapping
|
||||||
from .options import TunicOptions
|
from .options import TunicOptions, EntranceRando
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
|
from worlds.generic import PlandoConnection
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +37,13 @@ class TunicLocation(Location):
|
||||||
game: str = "TUNIC"
|
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):
|
class TunicWorld(World):
|
||||||
"""
|
"""
|
||||||
Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game
|
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]
|
slot_data_items: List[TunicItem]
|
||||||
tunic_portal_pairs: Dict[str, str]
|
tunic_portal_pairs: Dict[str, str]
|
||||||
er_portal_hints: Dict[int, str]
|
er_portal_hints: Dict[int, str]
|
||||||
|
seed_groups: Dict[str, SeedGroup] = {}
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
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
|
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
if "TUNIC" in 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.entrance_rando.value = passthrough["entrance_rando"]
|
||||||
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
|
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:
|
def create_item(self, name: str) -> TunicItem:
|
||||||
item_data = item_table[name]
|
item_data = item_table[name]
|
||||||
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player)
|
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, \
|
from .er_data import Portal, tunic_er_regions, portal_mapping, \
|
||||||
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
|
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
|
||||||
from .er_rules import set_er_region_rules
|
from .er_rules import set_er_region_rules
|
||||||
|
from .options import EntranceRando
|
||||||
from worlds.generic import PlandoConnection
|
from worlds.generic import PlandoConnection
|
||||||
from random import Random
|
from random import Random
|
||||||
|
|
||||||
|
@ -128,12 +129,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
portal_pairs: Dict[Portal, Portal] = {}
|
portal_pairs: Dict[Portal, Portal] = {}
|
||||||
dead_ends: List[Portal] = []
|
dead_ends: List[Portal] = []
|
||||||
two_plus: List[Portal] = []
|
two_plus: List[Portal] = []
|
||||||
logic_rules = world.options.logic_rules.value
|
|
||||||
player_name = world.multiworld.get_player_name(world.player)
|
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_scenes: Set[str] = set()
|
||||||
shop_count = 6
|
shop_count = 6
|
||||||
if world.options.fixed_shop.value:
|
if fixed_shop:
|
||||||
shop_count = 1
|
shop_count = 1
|
||||||
shop_scenes.add("Overworld Redux")
|
shop_scenes.add("Overworld Redux")
|
||||||
|
|
||||||
|
@ -163,7 +173,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
start_region = "Overworld"
|
start_region = "Overworld"
|
||||||
connected_regions.update(add_dependent_regions(start_region, logic_rules))
|
connected_regions.update(add_dependent_regions(start_region, logic_rules))
|
||||||
|
|
||||||
|
if world.options.entrance_rando.value in EntranceRando.options:
|
||||||
plando_connections = world.multiworld.plando_connections[world.player]
|
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
|
# universal tracker support stuff, don't need to care about region dependency
|
||||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
|
@ -198,10 +211,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
p_entrance = connection.entrance
|
p_entrance = connection.entrance
|
||||||
p_exit = connection.exit
|
p_exit = connection.exit
|
||||||
|
|
||||||
if p_entrance.startswith("Shop"):
|
|
||||||
p_entrance = p_exit
|
|
||||||
p_exit = "Shop Portal"
|
|
||||||
|
|
||||||
portal1 = None
|
portal1 = None
|
||||||
portal2 = None
|
portal2 = None
|
||||||
|
|
||||||
|
@ -213,7 +222,18 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
|
|
||||||
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
# 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:
|
for portal in dead_ends:
|
||||||
if p_entrance == portal.name:
|
if p_entrance == portal.name:
|
||||||
portal1 = portal
|
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 "
|
raise Exception(f"Could not find entrance named {p_entrance} for "
|
||||||
f"plando connections in {player_name}'s YAML.")
|
f"plando connections in {player_name}'s YAML.")
|
||||||
dead_ends.remove(portal1)
|
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:
|
for portal in dead_ends:
|
||||||
if p_exit == portal.name:
|
if p_exit == portal.name:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
break
|
break
|
||||||
if p_exit in ["Shop Portal", "Shop"]:
|
# if it's not a dead end, it might be a shop
|
||||||
portal2 = Portal(name="Shop Portal", region=f"Shop",
|
if p_exit == "Shop Portal":
|
||||||
|
portal2 = Portal(name="Shop Portal", region="Shop",
|
||||||
destination="Previous Region", tag="_")
|
destination="Previous Region", tag="_")
|
||||||
shop_count -= 1
|
shop_count -= 1
|
||||||
if shop_count < 0:
|
if shop_count < 0:
|
||||||
|
@ -240,13 +262,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
if p.name == p_entrance:
|
if p.name == p_entrance:
|
||||||
shop_scenes.add(p.scene())
|
shop_scenes.add(p.scene())
|
||||||
break
|
break
|
||||||
|
# and if it's neither shop nor dead end, it just isn't correct
|
||||||
else:
|
else:
|
||||||
if not portal2:
|
if not portal2:
|
||||||
raise Exception(f"Could not find entrance named {p_exit} for "
|
raise Exception(f"Could not find entrance named {p_exit} for "
|
||||||
f"plando connections in {player_name}'s YAML.")
|
f"plando connections in {player_name}'s YAML.")
|
||||||
dead_ends.remove(portal2)
|
dead_ends.remove(portal2)
|
||||||
else:
|
|
||||||
two_plus.remove(portal2)
|
|
||||||
|
|
||||||
portal_pairs[portal1] = 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
|
# 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
|
# 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
|
portal1 = None
|
||||||
portal2 = None
|
portal2 = None
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
|
@ -291,7 +312,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
two_plus.remove(portal1)
|
two_plus.remove(portal1)
|
||||||
dead_ends.remove(portal2)
|
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
|
portal1 = None
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
||||||
|
@ -307,7 +328,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
two_plus.remove(portal1)
|
two_plus.remove(portal1)
|
||||||
|
|
||||||
random_object: Random = world.random
|
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)
|
random_object = Random(world.options.entrance_rando.value)
|
||||||
# we want to start by making sure every region is accessible
|
# we want to start by making sure every region is accessible
|
||||||
random_object.shuffle(two_plus)
|
random_object.shuffle(two_plus)
|
||||||
|
|
|
@ -103,8 +103,10 @@ class ExtraHexagonPercentage(Range):
|
||||||
class EntranceRando(TextChoice):
|
class EntranceRando(TextChoice):
|
||||||
"""
|
"""
|
||||||
Randomize the connections between scenes.
|
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.
|
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"
|
internal_name = "entrance_rando"
|
||||||
display_name = "Entrance Rando"
|
display_name = "Entrance Rando"
|
||||||
|
|
Loading…
Reference in New Issue