Skip to content

Commit b7baaed

Browse files
silent-destroyerScipioWrightExempt-Medic
authored
TUNIC: Grass Randomizer (ArchipelagoMW#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>
1 parent 9dac7d9 commit b7baaed

File tree

8 files changed

+8156
-28
lines changed

8 files changed

+8156
-28
lines changed

worlds/tunic/__init__.py

+113-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
1+
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
22
from logging import warning
33
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
44
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
55
combat_items)
6-
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
6+
from .locations import location_table, location_name_groups, standard_location_name_to_id, hexagon_locations, sphere_one
77
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
88
from .er_rules import set_er_location_rules
99
from .regions import tunic_regions
1010
from .er_scripts import create_er_regions
11+
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
1112
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
1213
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
1314
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
1415
from .combat_logic import area_data, CombatState
1516
from worlds.AutoWorld import WebWorld, World
16-
from Options import PlandoConnection
17+
from Options import PlandoConnection, OptionError
1718
from decimal import Decimal, ROUND_HALF_UP
1819
from settings import Group, Bool
1920

@@ -22,7 +23,11 @@ class TunicSettings(Group):
2223
class DisableLocalSpoiler(Bool):
2324
"""Disallows the TUNIC client from creating a local spoiler log."""
2425

26+
class LimitGrassRando(Bool):
27+
"""Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95."""
28+
2529
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
30+
limit_grass_rando: Union[LimitGrassRando, bool] = True
2631

2732

2833
class TunicWeb(WebWorld):
@@ -73,10 +78,13 @@ class TunicWorld(World):
7378
settings: ClassVar[TunicSettings]
7479
item_name_groups = item_name_groups
7580
location_name_groups = location_name_groups
81+
location_name_groups.update(grass_location_name_groups)
7682

7783
item_name_to_id = item_name_to_id
78-
location_name_to_id = location_name_to_id
84+
location_name_to_id = standard_location_name_to_id.copy()
85+
location_name_to_id.update(grass_location_name_to_id)
7986

87+
player_location_table: Dict[str, int]
8088
ability_unlocks: Dict[str, int]
8189
slot_data_items: List[TunicItem]
8290
tunic_portal_pairs: Dict[str, str]
@@ -85,6 +93,11 @@ class TunicWorld(World):
8593
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
8694
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
8795

96+
# for the local_fill option
97+
fill_items: List[TunicItem]
98+
fill_locations: List[TunicLocation]
99+
amount_to_local_fill: int
100+
88101
# so we only loop the multiworld locations once
89102
# if these are locations instead of their info, it gives a memory leak error
90103
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
@@ -132,6 +145,7 @@ def generate_early(self) -> None:
132145
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
133146
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
134147
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
148+
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
135149
self.options.fixed_shop.value = self.options.fixed_shop.option_false
136150
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
137151
self.options.combat_logic.value = self.passthrough["combat_logic"]
@@ -140,6 +154,22 @@ def generate_early(self) -> None:
140154
else:
141155
self.using_ut = False
142156

157+
self.player_location_table = standard_location_name_to_id.copy()
158+
159+
if self.options.local_fill == -1:
160+
if self.options.grass_randomizer:
161+
self.options.local_fill.value = 95
162+
else:
163+
self.options.local_fill.value = 0
164+
165+
if self.options.grass_randomizer:
166+
if self.settings.limit_grass_rando and self.options.local_fill < 95 and self.multiworld.players > 1:
167+
raise OptionError(f"TUNIC: Player {self.player_name} has their Local Fill option set too low. "
168+
f"They must either bring it above 95% or the host needs to disable limit_grass_rando "
169+
f"in their host.yaml settings")
170+
171+
self.player_location_table.update(grass_location_name_to_id)
172+
143173
@classmethod
144174
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
145175
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
@@ -245,6 +275,14 @@ def create_items(self) -> None:
245275
self.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(laurels)
246276
items_to_create["Hero's Laurels"] = 0
247277

278+
if self.options.grass_randomizer:
279+
items_to_create["Grass"] = len(grass_location_table)
280+
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
281+
items_to_create["Glass Cannon"] = 0
282+
for grass_location in excluded_grass_locations:
283+
self.get_location(grass_location).place_locked_item(self.create_item("Grass"))
284+
items_to_create["Grass"] -= len(excluded_grass_locations)
285+
248286
if self.options.keys_behind_bosses:
249287
for rgb_hexagon, location in hexagon_locations.items():
250288
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
@@ -332,8 +370,73 @@ def remove_filler(amount: int) -> None:
332370
if tunic_item.name in slot_data_item_names:
333371
self.slot_data_items.append(tunic_item)
334372

373+
# pull out the filler so that we can place it manually during pre_fill
374+
self.fill_items = []
375+
if self.options.local_fill > 0 and self.multiworld.players > 1:
376+
# skip items marked local or non-local, let fill deal with them in its own way
377+
# discard grass from non_local if it's meant to be limited
378+
if self.settings.limit_grass_rando:
379+
self.options.non_local_items.value.discard("Grass")
380+
all_filler: List[TunicItem] = []
381+
non_filler: List[TunicItem] = []
382+
for tunic_item in tunic_items:
383+
if (tunic_item.excludable
384+
and tunic_item.name not in self.options.local_items
385+
and tunic_item.name not in self.options.non_local_items):
386+
all_filler.append(tunic_item)
387+
else:
388+
non_filler.append(tunic_item)
389+
self.amount_to_local_fill = int(self.options.local_fill.value * len(all_filler) / 100)
390+
self.fill_items += all_filler[:self.amount_to_local_fill]
391+
del all_filler[:self.amount_to_local_fill]
392+
tunic_items = all_filler + non_filler
393+
335394
self.multiworld.itempool += tunic_items
336395

396+
def pre_fill(self) -> None:
397+
self.fill_locations = []
398+
399+
if self.options.local_fill > 0 and self.multiworld.players > 1:
400+
# we need to reserve a couple locations so that we don't fill up every sphere 1 location
401+
reserved_locations: Set[str] = set(self.random.sample(sphere_one, 2))
402+
viable_locations = [loc for loc in self.multiworld.get_unfilled_locations(self.player)
403+
if loc.name not in reserved_locations
404+
and loc.name not in self.options.priority_locations.value]
405+
406+
if len(viable_locations) < self.amount_to_local_fill:
407+
raise OptionError(f"TUNIC: Not enough locations for local_fill option for {self.player_name}. "
408+
f"This is likely due to excess plando or priority locations.")
409+
410+
self.fill_locations += viable_locations
411+
412+
@classmethod
413+
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
414+
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
415+
if world.options.local_fill.value > 0]
416+
if tunic_fill_worlds:
417+
grass_fill: List[TunicItem] = []
418+
non_grass_fill: List[TunicItem] = []
419+
grass_fill_locations: List[Location] = []
420+
non_grass_fill_locations: List[Location] = []
421+
for world in tunic_fill_worlds:
422+
if world.options.grass_randomizer:
423+
grass_fill.extend(world.fill_items)
424+
grass_fill_locations.extend(world.fill_locations)
425+
else:
426+
non_grass_fill.extend(world.fill_items)
427+
non_grass_fill_locations.extend(world.fill_locations)
428+
429+
multiworld.random.shuffle(grass_fill)
430+
multiworld.random.shuffle(non_grass_fill)
431+
multiworld.random.shuffle(grass_fill_locations)
432+
multiworld.random.shuffle(non_grass_fill_locations)
433+
434+
for filler_item in grass_fill:
435+
multiworld.push_item(grass_fill_locations.pop(), filler_item, collect=False)
436+
437+
for filler_item in non_grass_fill:
438+
multiworld.push_item(non_grass_fill_locations.pop(), filler_item, collect=False)
439+
337440
def create_regions(self) -> None:
338441
self.tunic_portal_pairs = {}
339442
self.er_portal_hints = {}
@@ -346,7 +449,8 @@ def create_regions(self) -> None:
346449
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
347450

348451
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
349-
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
452+
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
453+
or self.options.grass_randomizer):
350454
portal_pairs = create_er_regions(self)
351455
if self.options.entrance_rando:
352456
# these get interpreted by the game to tell it which entrances to connect
@@ -362,7 +466,7 @@ def create_regions(self) -> None:
362466
region = self.get_region(region_name)
363467
region.add_exits(exits)
364468

365-
for location_name, location_id in self.location_name_to_id.items():
469+
for location_name, location_id in self.player_location_table.items():
366470
region = self.get_region(location_table[location_name].region)
367471
location = TunicLocation(self.player, location_name, location_id, region)
368472
region.locations.append(location)
@@ -375,7 +479,8 @@ def create_regions(self) -> None:
375479

376480
def set_rules(self) -> None:
377481
# same reason as in create_regions, could probably be put into create_regions
378-
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
482+
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
483+
or self.options.grass_randomizer):
379484
set_er_location_rules(self)
380485
else:
381486
set_region_rules(self)
@@ -463,6 +568,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
463568
"maskless": self.options.maskless.value,
464569
"entrance_rando": int(bool(self.options.entrance_rando.value)),
465570
"shuffle_ladders": self.options.shuffle_ladders.value,
571+
"grass_randomizer": self.options.grass_randomizer.value,
466572
"combat_logic": self.options.combat_logic.value,
467573
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
468574
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],

worlds/tunic/er_data.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,16 @@ class DeadEnd(IntEnum):
629629
"Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
630630
"West Garden before Terry": RegionInfo("Archipelagos Redux"), # the lower entry point, near hero grave
631631
"West Garden after Terry": RegionInfo("Archipelagos Redux"), # after Terry, up until next chompignons
632+
"West Garden West Combat": RegionInfo("Archipelagos Redux"), # for grass rando basically
632633
"West Garden at Dagger House": RegionInfo("Archipelagos Redux"), # just outside magic dagger house
633-
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"),
634+
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"), # the checkpoint and the blue lines area
634635
"Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
635-
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"),
636+
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted,
637+
outlet_region="West Garden by Portal"),
636638
"West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
637639
"West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
638640
"West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
639-
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # main west garden
641+
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # up the ladder before garden knight
640642
"West Garden after Boss": RegionInfo("Archipelagos Redux"),
641643
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden before Terry"),
642644
"Ruined Atoll": RegionInfo("Atoll Redux"),
@@ -1165,8 +1167,10 @@ class DeadEnd(IntEnum):
11651167
"West Garden after Terry": {
11661168
"West Garden before Terry":
11671169
[],
1168-
"West Garden South Checkpoint":
1170+
"West Garden West Combat":
11691171
[],
1172+
"West Garden South Checkpoint":
1173+
[["Hyperdash"]],
11701174
"West Garden Laurels Exit Region":
11711175
[["LS1"]],
11721176
},
@@ -1176,6 +1180,8 @@ class DeadEnd(IntEnum):
11761180
"West Garden at Dagger House":
11771181
[],
11781182
"West Garden after Terry":
1183+
[["Hyperdash"]],
1184+
"West Garden West Combat":
11791185
[],
11801186
},
11811187
"West Garden before Boss": {

worlds/tunic/er_rules.py

+31-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
22
from worlds.generic.Rules import set_rule, add_rule, forbid_item
3+
from BaseClasses import Region, CollectionState
34
from .options import IceGrappling, LadderStorage, CombatLogic
45
from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
56
laurels_zip, bomb_walls)
67
from .er_data import Portal, get_portal_outlet_region
78
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
89
from .combat_logic import has_combat_reqs
9-
from BaseClasses import Region, CollectionState
10+
from .grass import set_grass_location_rules
1011

1112
if TYPE_CHECKING:
1213
from . import TunicWorld
@@ -555,7 +556,6 @@ def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
555556
regions["Dark Tomb Upper"].connect(
556557
connecting_region=regions["Dark Tomb Entry Point"])
557558

558-
# ice grapple through the wall, get the little secret sound to trigger
559559
regions["Dark Tomb Upper"].connect(
560560
connecting_region=regions["Dark Tomb Main"],
561561
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)
@@ -577,11 +577,24 @@ def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
577577
wg_after_to_before_terry = regions["West Garden after Terry"].connect(
578578
connecting_region=regions["West Garden before Terry"])
579579

580-
regions["West Garden after Terry"].connect(
581-
connecting_region=regions["West Garden South Checkpoint"])
582-
wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect(
580+
wg_after_terry_to_west_combat = regions["West Garden after Terry"].connect(
581+
connecting_region=regions["West Garden West Combat"])
582+
regions["West Garden West Combat"].connect(
583583
connecting_region=regions["West Garden after Terry"])
584584

585+
wg_checkpoint_to_west_combat = regions["West Garden South Checkpoint"].connect(
586+
connecting_region=regions["West Garden West Combat"])
587+
regions["West Garden West Combat"].connect(
588+
connecting_region=regions["West Garden South Checkpoint"])
589+
590+
# if not laurels, it goes through the west combat region instead
591+
regions["West Garden after Terry"].connect(
592+
connecting_region=regions["West Garden South Checkpoint"],
593+
rule=lambda state: state.has(laurels, player))
594+
regions["West Garden South Checkpoint"].connect(
595+
connecting_region=regions["West Garden after Terry"],
596+
rule=lambda state: state.has(laurels, player))
597+
585598
wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect(
586599
connecting_region=regions["West Garden at Dagger House"])
587600
regions["West Garden at Dagger House"].connect(
@@ -1402,12 +1415,16 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None:
14021415
set_rule(wg_after_to_before_terry,
14031416
lambda state: state.has_any({laurels, ice_dagger}, player)
14041417
or has_combat_reqs("West Garden", state, player))
1405-
# laurels through, probably to the checkpoint, or just fight
1406-
set_rule(wg_checkpoint_to_after_terry,
1407-
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
1408-
set_rule(wg_checkpoint_to_before_boss,
1418+
1419+
set_rule(wg_after_terry_to_west_combat,
1420+
lambda state: has_combat_reqs("West Garden", state, player))
1421+
set_rule(wg_checkpoint_to_west_combat,
14091422
lambda state: has_combat_reqs("West Garden", state, player))
14101423

1424+
# maybe a little too generous? probably fine though
1425+
set_rule(wg_checkpoint_to_before_boss,
1426+
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
1427+
14111428
add_rule(btv_front_to_main,
14121429
lambda state: has_combat_reqs("Beneath the Vault", state, player))
14131430
add_rule(btv_back_to_main,
@@ -1528,6 +1545,9 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None:
15281545
def set_er_location_rules(world: "TunicWorld") -> None:
15291546
player = world.player
15301547

1548+
if world.options.grass_randomizer:
1549+
set_grass_location_rules(world)
1550+
15311551
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
15321552

15331553
# Ability Shuffle Exclusive Rules
@@ -1852,6 +1872,8 @@ def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool =
18521872
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden")
18531873
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden")
18541874
combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden")
1875+
combat_logic_to_loc("West Garden - [Central Highlands] Holy Cross (Blue Lines)", "West Garden")
1876+
combat_logic_to_loc("West Garden - [Central Highlands] Behind Guard Captain", "West Garden")
18551877

18561878
# with combat logic on, I presume the player will want to be able to see to avoid the spiders
18571879
set_rule(world.get_location("Beneath the Fortress - Bridge"),

0 commit comments

Comments
 (0)