Skip to content

Commit

Permalink
Merge branch 'main' into mlss
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesbrq authored May 3, 2024
2 parents f42dc0b + 298c9fc commit 1f2c353
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 30 deletions.
14 changes: 7 additions & 7 deletions worlds/sm64ex/docs/en_Super Mario 64.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ The player options page for this game contains all the options you need to confi
options page link: [SM64EX Player Options Page](../player-options).

## What does randomization do to this game?
All 120 Stars, the 3 Cap Switches, the Basement and Secound Floor Key are now Location Checks and may contain Items for different games as well
as different Items from within SM64.
All 120 Stars, the 3 Cap Switches, the Basement and Second Floor Key are now location checks and may contain items for different games as well
as different items from within SM64.


## What is the goal of SM64EX when randomized?
As in most Mario Games, save the Princess!
As in most Mario games, save the Princess!

## Which items can be in another player's world?
Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
when someone collects the corresponding Cap Switch Item.
when someone collects the corresponding Cap Switch item.

## What does another world's item look like in SM64EX?
The Items are visually unchanged, though after collecting a Message will pop up to inform you what you collected,
The items are visually unchanged, though after collecting a message will pop up to inform you what you collected,
and who will receive it.

## When the player receives an item, what happens?
When you receive an Item, a Message will pop up to inform you where you received the Item from,
When you receive an item, a message will pop up to inform you where you received the item from,
and which one it is.

NOTE: The Secret Star count in the Menu is broken.
NOTE: The Secret Star count in the menu is broken.
79 changes: 76 additions & 3 deletions worlds/tunic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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
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


Expand All @@ -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
Expand All @@ -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:
Expand All @@ -74,6 +95,58 @@ def generate_early(self) -> None:
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)
Expand Down
60 changes: 41 additions & 19 deletions worlds/tunic/er_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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:
Expand All @@ -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_":
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion worlds/tunic/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 1f2c353

Please sign in to comment.