Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TUNIC: Grass Randomizer #3913

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9a6e17e
Fix certain items not being added to slot data
silent-destroyer Jul 8, 2024
436b3b3
Change where items get added to slot data
silent-destroyer Jul 8, 2024
10bdbb1
Add initial grass randomizer stuff
silent-destroyer Jul 14, 2024
4d00e92
Fix rules
silent-destroyer Jul 14, 2024
486e8a5
Update grass.py
silent-destroyer Jul 20, 2024
44ea69e
Remove wand and gun from logic
silent-destroyer Jul 20, 2024
82629d2
Merge remote-tracking branch 'upstream/main' into tunic/grass-rando
silent-destroyer Jul 23, 2024
651029f
Update __init__.py
silent-destroyer Jul 26, 2024
415cd48
Fix logic for two pieces of grass in atoll
silent-destroyer Jul 29, 2024
c4ec49d
Make early bushes only contain grass
silent-destroyer Sep 4, 2024
6fc52b4
Merge remote-tracking branch 'upstream/main' into tunic/grass-rando
silent-destroyer Sep 4, 2024
4991ca5
Backport changes to grass rando (#20)
ScipioWright Sep 8, 2024
f74d5bf
Update grass rando option descriptions
silent-destroyer Sep 8, 2024
dd31302
Ignore grass fill option for solo rando
silent-destroyer Sep 9, 2024
abb2ecc
Merge remote-tracking branch 'upstream/main' into tunic/grass-rando
silent-destroyer Sep 9, 2024
ac39fb1
Update er_rules.py
silent-destroyer Sep 9, 2024
d8e5a01
Fix pre fill issue
silent-destroyer Sep 9, 2024
05fb8fa
Remove duplicate option
silent-destroyer Sep 9, 2024
63d6639
Add excluded grass locations back
silent-destroyer Sep 9, 2024
0cfba94
Hide grass fill option from simple ui options page
silent-destroyer Sep 9, 2024
4a07006
Check for start with sword before setting grass rules
silent-destroyer Sep 9, 2024
031c1b0
Merge branch 'main' into tunic/grass-rando
silent-destroyer Sep 9, 2024
d19da8c
Update worlds/tunic/options.py
silent-destroyer Sep 10, 2024
5b062ba
Merge branch 'ArchipelagoMW:main' into tunic/grass-rando
silent-destroyer Oct 20, 2024
ed6fada
Exclude grass from get_filler_item_name
silent-destroyer Oct 20, 2024
b8c20a1
Merge remote-tracking branch 'upstream/main' into tunic/grass-rando
silent-destroyer Oct 23, 2024
f47d56d
Merge branch 'main' into tunic/grass-rando
silent-destroyer Nov 8, 2024
5b0faf7
Update worlds/tunic/__init__.py
silent-destroyer Nov 8, 2024
7bb3971
Merge branch 'main' into tunic/grass-rando
silent-destroyer Nov 27, 2024
3974c5d
Merge branch 'main' into tunic/grass-rando
silent-destroyer Dec 10, 2024
9c5ca73
Apply suggestions from code review
silent-destroyer Dec 10, 2024
129f588
change the rest of grass_fill to local_fill
silent-destroyer Dec 10, 2024
5aac5d5
Filter out grass from filler_items
silent-destroyer Dec 10, 2024
bdcaf4a
remove -> discard
silent-destroyer Dec 10, 2024
b5f05aa
Update worlds/tunic/__init__.py
silent-destroyer Dec 11, 2024
56be193
Merge remote-tracking branch 'upstream/main' into tunic/grass-rando
silent-destroyer Dec 15, 2024
717abeb
change has_stick to has_melee
silent-destroyer Dec 15, 2024
7239ea5
Update grass list with combat logic regions
silent-destroyer Dec 15, 2024
62b6e86
More fixes from combat logic merge
silent-destroyer Dec 15, 2024
d74eedc
Fix some dumb stuff (#21)
ScipioWright Dec 17, 2024
92d7e58
Reorganize pre fill for grass
silent-destroyer Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 79 additions & 11 deletions worlds/tunic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, LocationProgressType
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, standard_location_name_to_id, hexagon_locations, sphere_one
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 .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)
from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection
from Options import PlandoConnection, OptionError
from decimal import Decimal, ROUND_HALF_UP
from settings import Group, Bool

Expand All @@ -20,7 +21,11 @@ class TunicSettings(Group):
class DisableLocalSpoiler(Bool):
"""Disallows the TUNIC client from creating a local spoiler log."""

class LimitGrassRando(Bool):
"""Limits the impact of Grass Randomizer on the multiworld by disallowing grass_fill percentages below 95."""

disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
limit_grass_rando: Union[LimitGrassRando, bool] = True


class TunicWeb(WebWorld):
Expand Down Expand Up @@ -71,18 +76,21 @@ class TunicWorld(World):
settings: ClassVar[TunicSettings]
item_name_groups = item_name_groups
location_name_groups = location_name_groups
location_name_groups.update(grass_location_name_groups)

item_name_to_id = item_name_to_id
location_name_to_id = location_name_to_id
location_name_to_id = standard_location_name_to_id.copy()
location_name_to_id.update(grass_location_name_to_id)

player_location_table: Dict[str, int]
ability_unlocks: Dict[str, int]
slot_data_items: List[TunicItem]
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
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work

local_filler: List[TunicItem]
# so we only loop the multiworld locations once
# if these are locations instead of their info, it gives a memory leak error
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
Expand Down Expand Up @@ -125,9 +133,19 @@ def generate_early(self) -> None:
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
self.options.entrance_rando.value = passthrough["entrance_rando"]
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.options.grass_randomizer.value = passthrough["grass_randomizer"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere

self.player_location_table = standard_location_name_to_id.copy()

if self.options.grass_randomizer:
if self.settings.limit_grass_rando and self.options.grass_fill < 95 and self.multiworld.players > 1:
raise OptionError(f"TUNIC: Player {self.player_name} has their Grass Fill option set too low. "
f"They must either bring it above 95% or the host needs to disable limit_grass_rando "
f"in their host.yaml settings")
self.player_location_table.update(grass_location_name_to_id)

silent-destroyer marked this conversation as resolved.
Show resolved Hide resolved
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
Expand Down Expand Up @@ -193,7 +211,6 @@ def create_item(self, name: str, classification: ItemClassification = None) -> T
return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)

def create_items(self) -> None:

tunic_items: List[TunicItem] = []
self.slot_data_items = []

Expand Down Expand Up @@ -222,6 +239,14 @@ def create_items(self) -> None:
self.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(laurels)
items_to_create["Hero's Laurels"] = 0

if self.options.grass_randomizer:
items_to_create["Grass"] = len(grass_location_table)
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
items_to_create["Glass Cannon"] = 0
for grass_location in excluded_grass_locations:
self.multiworld.get_location(grass_location, self.player).place_locked_item(self.create_item("Grass"))
items_to_create["Grass"] -= len(excluded_grass_locations)
silent-destroyer marked this conversation as resolved.
Show resolved Hide resolved

if self.options.keys_behind_bosses:
for rgb_hexagon, location in hexagon_locations.items():
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
Expand Down Expand Up @@ -302,8 +327,50 @@ def remove_filler(amount: int) -> None:
if tunic_item.name in slot_data_item_names:
self.slot_data_items.append(tunic_item)

# pull out the filler so that we can place it manually during pre_fill
self.local_filler = []
if self.options.grass_randomizer and self.options.grass_fill > 0 and self.multiworld.players > 1:
# skip items marked local or non-local, let fill deal with them in its own way
all_filler = []
silent-destroyer marked this conversation as resolved.
Show resolved Hide resolved
non_filler = []
amount_to_local_fill = int(self.options.grass_fill.value * len(all_filler) / 100)
for item in tunic_items:
if item.classification in [ItemClassification.filler, ItemClassification.trap] and item.name not in self.options.local_items and item.name not in self.options.non_local_items:
if len(self.local_filler) < amount_to_local_fill:
self.local_filler.append(item)
else:
all_filler.append(item)
else:
non_filler.append(item)

tunic_items = all_filler + non_filler

self.multiworld.itempool += tunic_items

@classmethod
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
tunic_grass_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
if world.options.grass_randomizer]
tunic_players_with_grass: List[int] = [world.player for world in tunic_grass_worlds]
# we need to reserve a couple locations so that we don't fill up every sphere 1 location
reserved_locations: Set[str] = set(multiworld.random.sample(sphere_one, 2))
unfilled_locations = [loc for loc in multiworld.get_unfilled_locations_for_players(
silent-destroyer marked this conversation as resolved.
Show resolved Hide resolved
location_names=[], players=tunic_players_with_grass) if loc.progress_type != LocationProgressType.PRIORITY
and loc.name not in reserved_locations]
grass_fill: List[TunicItem] = []
for world in tunic_grass_worlds:
grass_fill.extend(world.local_filler)

grass_filler_count = len(grass_fill)
# in case you plando or priority a bunch of locations
if len(unfilled_locations) < grass_filler_count:
raise Exception("Not enough locations for TUNIC grass randomizer players to place grass fill into TUNIC "
"locations. This is likely due to excessive priority locations or plando.")

locations_to_grass_fill = multiworld.random.sample(unfilled_locations, grass_filler_count)
for loc in locations_to_grass_fill:
multiworld.push_item(loc, grass_fill.pop(), collect=False)

def create_regions(self) -> None:
self.tunic_portal_pairs = {}
self.er_portal_hints = {}
Expand All @@ -318,7 +385,7 @@ def create_regions(self) -> None:
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]

# ladder rando uses ER with vanilla connections, so that we're not managing more rules files
if self.options.entrance_rando or self.options.shuffle_ladders:
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.grass_randomizer:
portal_pairs = create_er_regions(self)
if self.options.entrance_rando:
# these get interpreted by the game to tell it which entrances to connect
Expand All @@ -334,7 +401,7 @@ def create_regions(self) -> None:
region = self.get_region(region_name)
region.add_exits(exits)

for location_name, location_id in self.location_name_to_id.items():
for location_name, location_id in self.player_location_table.items():
region = self.get_region(location_table[location_name].region)
location = TunicLocation(self.player, location_name, location_id, region)
region.locations.append(location)
Expand All @@ -346,14 +413,14 @@ def create_regions(self) -> None:
victory_region.locations.append(victory_location)

def set_rules(self) -> None:
if self.options.entrance_rando or self.options.shuffle_ladders:
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.grass_randomizer:
set_er_location_rules(self)
else:
set_region_rules(self)
set_location_rules(self)

def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
return self.random.choice([item for item in filler_items if item != "Grass"])

silent-destroyer marked this conversation as resolved.
Show resolved Hide resolved
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
Expand Down Expand Up @@ -421,6 +488,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
"maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)),
"shuffle_ladders": self.options.shuffle_ladders.value,
"grass_randomizer": self.options.grass_randomizer.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],
Expand Down
5 changes: 5 additions & 0 deletions worlds/tunic/er_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .er_data import Portal, get_portal_outlet_region
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
from BaseClasses import Region, CollectionState
from .grass import set_grass_location_rules

if TYPE_CHECKING:
from . import TunicWorld
Expand Down Expand Up @@ -1222,6 +1223,10 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None:

def set_er_location_rules(world: "TunicWorld") -> None:
player = world.player
options = world.options

if options.grass_randomizer:
set_grass_location_rules(world)

forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)

Expand Down
6 changes: 3 additions & 3 deletions worlds/tunic/er_scripts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
from BaseClasses import Region, ItemClassification, Item, Location
from .locations import location_table
from .locations import all_locations
from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo
from .er_rules import set_er_region_rules
from Options import PlandoConnection
Expand Down Expand Up @@ -37,8 +37,8 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:

set_er_region_rules(world, regions, portal_pairs)

for location_name, location_id in world.location_name_to_id.items():
region = regions[location_table[location_name].er_region]
for location_name, location_id in world.player_location_table.items():
region = regions[all_locations[location_name].er_region]
location = TunicERLocation(world.player, location_name, location_id, region)
region.locations.append(location)

Expand Down
Loading
Loading