TUNIC: Grass Randomizer (#3913)
* Fix certain items not being added to slot data * Change where items get added to slot data * Add initial grass randomizer stuff * Fix rules * Update grass.py Improve location names * Remove wand and gun from logic * Update __init__.py * Fix logic for two pieces of grass in atoll * Make early bushes only contain grass * Backport changes to grass rando (#20) * Backport changes to grass rando * add_rule instead of set_rule for the special cases, add special cases for back of swamp laurels area cause I should've made a new region for the swamp upper entrance * Remove item name group for grass * Update grass rando option descriptions - Also ignore grass fill for single player games * Ignore grass fill option for solo rando * Update er_rules.py * Fix pre fill issue * Remove duplicate option * Add excluded grass locations back * Hide grass fill option from simple ui options page * Check for start with sword before setting grass rules * Update worlds/tunic/options.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Exclude grass from get_filler_item_name - non-grass rando games were accidentally seeing grass items get shuffled in as filler, which is funny but probably shouldn't happen * Update worlds/tunic/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Scipio Wright <scipiowright@gmail.com> * change the rest of grass_fill to local_fill * Filter out grass from filler_items * remove -> discard * Update worlds/tunic/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * change has_stick to has_melee * Update grass list with combat logic regions * More fixes from combat logic merge * Fix some dumb stuff (#21) * Reorganize pre fill for grass * Update option value passthrough * Update __init__.py * Fix region name * Make separate pools for the grass and non-grass fills (#22) * Make separate pools for the grass and non-grass fills * Update worlds/tunic/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Fix those things in the PR (#23) * Use excludable property Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Scipio Wright <scipiowright@gmail.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
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, CollectionState
|
||||
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
|
||||
combat_items)
|
||||
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 .combat_logic import area_data, CombatState
|
||||
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
|
||||
|
||||
@@ -22,7 +23,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 local_fill percentages below 95."""
|
||||
|
||||
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
|
||||
limit_grass_rando: Union[LimitGrassRando, bool] = True
|
||||
|
||||
|
||||
class TunicWeb(WebWorld):
|
||||
@@ -73,10 +78,13 @@ 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]
|
||||
@@ -85,6 +93,11 @@ class TunicWorld(World):
|
||||
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
|
||||
|
||||
# for the local_fill option
|
||||
fill_items: List[TunicItem]
|
||||
fill_locations: List[TunicLocation]
|
||||
amount_to_local_fill: int
|
||||
|
||||
# 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]]]] = {}
|
||||
@@ -132,6 +145,7 @@ class TunicWorld(World):
|
||||
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
@@ -140,6 +154,22 @@ class TunicWorld(World):
|
||||
else:
|
||||
self.using_ut = False
|
||||
|
||||
self.player_location_table = standard_location_name_to_id.copy()
|
||||
|
||||
if self.options.local_fill == -1:
|
||||
if self.options.grass_randomizer:
|
||||
self.options.local_fill.value = 95
|
||||
else:
|
||||
self.options.local_fill.value = 0
|
||||
|
||||
if self.options.grass_randomizer:
|
||||
if self.settings.limit_grass_rando and self.options.local_fill < 95 and self.multiworld.players > 1:
|
||||
raise OptionError(f"TUNIC: Player {self.player_name} has their Local 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)
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
|
||||
@@ -245,6 +275,14 @@ class TunicWorld(World):
|
||||
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.get_location(grass_location).place_locked_item(self.create_item("Grass"))
|
||||
items_to_create["Grass"] -= len(excluded_grass_locations)
|
||||
|
||||
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)
|
||||
@@ -332,8 +370,73 @@ class TunicWorld(World):
|
||||
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.fill_items = []
|
||||
if self.options.local_fill > 0 and self.multiworld.players > 1:
|
||||
# skip items marked local or non-local, let fill deal with them in its own way
|
||||
# discard grass from non_local if it's meant to be limited
|
||||
if self.settings.limit_grass_rando:
|
||||
self.options.non_local_items.value.discard("Grass")
|
||||
all_filler: List[TunicItem] = []
|
||||
non_filler: List[TunicItem] = []
|
||||
for tunic_item in tunic_items:
|
||||
if (tunic_item.excludable
|
||||
and tunic_item.name not in self.options.local_items
|
||||
and tunic_item.name not in self.options.non_local_items):
|
||||
all_filler.append(tunic_item)
|
||||
else:
|
||||
non_filler.append(tunic_item)
|
||||
self.amount_to_local_fill = int(self.options.local_fill.value * len(all_filler) / 100)
|
||||
self.fill_items += all_filler[:self.amount_to_local_fill]
|
||||
del all_filler[:self.amount_to_local_fill]
|
||||
tunic_items = all_filler + non_filler
|
||||
|
||||
self.multiworld.itempool += tunic_items
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
self.fill_locations = []
|
||||
|
||||
if self.options.local_fill > 0 and self.multiworld.players > 1:
|
||||
# we need to reserve a couple locations so that we don't fill up every sphere 1 location
|
||||
reserved_locations: Set[str] = set(self.random.sample(sphere_one, 2))
|
||||
viable_locations = [loc for loc in self.multiworld.get_unfilled_locations(self.player)
|
||||
if loc.name not in reserved_locations
|
||||
and loc.name not in self.options.priority_locations.value]
|
||||
|
||||
if len(viable_locations) < self.amount_to_local_fill:
|
||||
raise OptionError(f"TUNIC: Not enough locations for local_fill option for {self.player_name}. "
|
||||
f"This is likely due to excess plando or priority locations.")
|
||||
|
||||
self.fill_locations += viable_locations
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
|
||||
if world.options.local_fill.value > 0]
|
||||
if tunic_fill_worlds:
|
||||
grass_fill: List[TunicItem] = []
|
||||
non_grass_fill: List[TunicItem] = []
|
||||
grass_fill_locations: List[Location] = []
|
||||
non_grass_fill_locations: List[Location] = []
|
||||
for world in tunic_fill_worlds:
|
||||
if world.options.grass_randomizer:
|
||||
grass_fill.extend(world.fill_items)
|
||||
grass_fill_locations.extend(world.fill_locations)
|
||||
else:
|
||||
non_grass_fill.extend(world.fill_items)
|
||||
non_grass_fill_locations.extend(world.fill_locations)
|
||||
|
||||
multiworld.random.shuffle(grass_fill)
|
||||
multiworld.random.shuffle(non_grass_fill)
|
||||
multiworld.random.shuffle(grass_fill_locations)
|
||||
multiworld.random.shuffle(non_grass_fill_locations)
|
||||
|
||||
for filler_item in grass_fill:
|
||||
multiworld.push_item(grass_fill_locations.pop(), filler_item, collect=False)
|
||||
|
||||
for filler_item in non_grass_fill:
|
||||
multiworld.push_item(non_grass_fill_locations.pop(), filler_item, collect=False)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
self.tunic_portal_pairs = {}
|
||||
self.er_portal_hints = {}
|
||||
@@ -346,7 +449,8 @@ class TunicWorld(World):
|
||||
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
|
||||
|
||||
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
|
||||
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
|
||||
@@ -362,7 +466,7 @@ class TunicWorld(World):
|
||||
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)
|
||||
@@ -375,7 +479,8 @@ class TunicWorld(World):
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# same reason as in create_regions, could probably be put into create_regions
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
|
||||
or self.options.grass_randomizer):
|
||||
set_er_location_rules(self)
|
||||
else:
|
||||
set_region_rules(self)
|
||||
@@ -463,6 +568,7 @@ class TunicWorld(World):
|
||||
"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,
|
||||
"combat_logic": self.options.combat_logic.value,
|
||||
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
|
||||
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
|
||||
|
||||
Reference in New Issue
Block a user