Skip to content

Commit 1481674

Browse files
authored
TUNIC: Shuffle Ladders option (#2919)
1 parent 30a0aa2 commit 1481674

11 files changed

+1672
-1069
lines changed

docs/CODEOWNERS

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@
177177
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
178178

179179
# TUNIC
180-
/worlds/tunic/ @silent-destroyer
180+
/worlds/tunic/ @silent-destroyer @ScipioWright
181181

182182
# Undertale
183183
/worlds/undertale/ @jonloveslegos

worlds/tunic/__init__.py

+71-25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .er_rules import set_er_location_rules
88
from .regions import tunic_regions
99
from .er_scripts import create_er_regions
10+
from .er_data import portal_mapping
1011
from .options import TunicOptions
1112
from worlds.AutoWorld import WebWorld, World
1213
from decimal import Decimal, ROUND_HALF_UP
@@ -44,7 +45,6 @@ class TunicWorld(World):
4445
game = "TUNIC"
4546
web = TunicWeb()
4647

47-
data_version = 2
4848
options: TunicOptions
4949
options_dataclass = TunicOptions
5050
item_name_groups = item_name_groups
@@ -72,6 +72,7 @@ def generate_early(self) -> None:
7272
self.options.maskless.value = passthrough["maskless"]
7373
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
7474
self.options.entrance_rando.value = passthrough["entrance_rando"]
75+
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
7576

7677
def create_item(self, name: str) -> TunicItem:
7778
item_data = item_table[name]
@@ -119,36 +120,55 @@ def create_items(self) -> None:
119120
items_to_create[rgb_hexagon] = 0
120121
items_to_create[gold_hexagon] -= 3
121122

123+
# Filler items in the item pool
124+
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
125+
item_table[filler].classification == ItemClassification.filler]
126+
127+
# Remove filler to make room for other items
128+
def remove_filler(amount: int):
129+
for _ in range(0, amount):
130+
if not available_filler:
131+
fill = "Fool Trap"
132+
else:
133+
fill = self.random.choice(available_filler)
134+
if items_to_create[fill] == 0:
135+
raise Exception("No filler items left to accommodate options selected. Turn down fool trap amount.")
136+
items_to_create[fill] -= 1
137+
if items_to_create[fill] == 0:
138+
available_filler.remove(fill)
139+
140+
if self.options.shuffle_ladders:
141+
ladder_count = 0
142+
for item_name, item_data in item_table.items():
143+
if item_data.item_group == "ladders":
144+
items_to_create[item_name] = 1
145+
ladder_count += 1
146+
remove_filler(ladder_count)
147+
122148
if hexagon_quest:
123149
# Calculate number of hexagons in item pool
124150
hexagon_goal = self.options.hexagon_goal
125151
extra_hexagons = self.options.extra_hexagon_percentage
126152
items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP))
127-
153+
128154
# Replace pages and normal hexagons with filler
129155
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
130-
items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item]
156+
filler_name = self.get_filler_item_name()
157+
items_to_create[filler_name] += items_to_create[replaced_item]
158+
if items_to_create[filler_name] >= 1 and filler_name not in available_filler:
159+
available_filler.append(filler_name)
131160
items_to_create[replaced_item] = 0
132161

133-
# Filler items that are still in the item pool to swap out
134-
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
135-
item_table[filler].classification == ItemClassification.filler]
136-
137-
# Remove filler to make room for extra hexagons
138-
for i in range(0, items_to_create[gold_hexagon]):
139-
fill = self.random.choice(available_filler)
140-
items_to_create[fill] -= 1
141-
if items_to_create[fill] == 0:
142-
available_filler.remove(fill)
162+
remove_filler(items_to_create[gold_hexagon])
143163

144164
if self.options.maskless:
145165
mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player)
146166
tunic_items.append(mask_item)
147167
items_to_create["Scavenger Mask"] = 0
148168

149169
if self.options.lanternless:
150-
mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player)
151-
tunic_items.append(mask_item)
170+
lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player)
171+
tunic_items.append(lantern_item)
152172
items_to_create["Lantern"] = 0
153173

154174
for item, quantity in items_to_create.items():
@@ -172,15 +192,16 @@ def create_regions(self) -> None:
172192
self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"]
173193
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
174194
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
175-
176-
if self.options.entrance_rando:
177-
portal_pairs, portal_hints = create_er_regions(self)
178-
for portal1, portal2 in portal_pairs.items():
179-
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
180-
181-
self.er_portal_hints = portal_hints
182195

196+
# ladder rando uses ER with vanilla connections, so that we're not managing more rules files
197+
if self.options.entrance_rando or self.options.shuffle_ladders:
198+
portal_pairs = create_er_regions(self)
199+
if self.options.entrance_rando:
200+
# these get interpreted by the game to tell it which entrances to connect
201+
for portal1, portal2 in portal_pairs.items():
202+
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
183203
else:
204+
# for non-ER, non-ladders
184205
for region_name in tunic_regions:
185206
region = Region(region_name, self.player, self.multiworld)
186207
self.multiworld.regions.append(region)
@@ -201,7 +222,7 @@ def create_regions(self) -> None:
201222
victory_region.locations.append(victory_location)
202223

203224
def set_rules(self) -> None:
204-
if self.options.entrance_rando:
225+
if self.options.entrance_rando or self.options.shuffle_ladders:
205226
set_er_location_rules(self, self.ability_unlocks)
206227
else:
207228
set_region_rules(self, self.ability_unlocks)
@@ -212,7 +233,31 @@ def get_filler_item_name(self) -> str:
212233

213234
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
214235
if self.options.entrance_rando:
215-
hint_data[self.player] = self.er_portal_hints
236+
hint_data.update({self.player: {}})
237+
# all state seems to have efficient paths
238+
all_state = self.multiworld.get_all_state(True)
239+
all_state.update_reachable_regions(self.player)
240+
paths = all_state.path
241+
portal_names = [portal.name for portal in portal_mapping]
242+
for location in self.multiworld.get_locations(self.player):
243+
# skipping event locations
244+
if not location.address:
245+
continue
246+
path_to_loc = []
247+
previous_name = "placeholder"
248+
name, connection = paths[location.parent_region]
249+
while connection != ("Menu", None):
250+
name, connection = connection
251+
# for LS entrances, we just want to give the portal name
252+
if "(LS)" in name:
253+
name, _ = name.split(" (LS) ")
254+
# was getting some cases like Library Grave -> Library Grave -> other place
255+
if name in portal_names and name != previous_name:
256+
previous_name = name
257+
path_to_loc.append(name)
258+
hint_text = " -> ".join(reversed(path_to_loc))
259+
if hint_text:
260+
hint_data[self.player][location.address] = hint_text
216261

217262
def fill_slot_data(self) -> Dict[str, Any]:
218263
slot_data: Dict[str, Any] = {
@@ -226,7 +271,8 @@ def fill_slot_data(self) -> Dict[str, Any]:
226271
"logic_rules": self.options.logic_rules.value,
227272
"lanternless": self.options.lanternless.value,
228273
"maskless": self.options.maskless.value,
229-
"entrance_rando": bool(self.options.entrance_rando.value),
274+
"entrance_rando": int(bool(self.options.entrance_rando.value)),
275+
"shuffle_ladders": self.options.shuffle_ladders.value,
230276
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
231277
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
232278
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],

worlds/tunic/docs/en_TUNIC.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ For the Entrance Randomizer:
6767
Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, icebolt, and progressive sword.
6868

6969
## What location groups are there?
70-
Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group.
70+
Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), shop, bosses (for the bosses with checks associated with them), hero relic (for the 6 hero grave checks), and ladders (for the ladder items when you have shuffle ladders enabled).
7171

7272
## Is Connection Plando supported?
7373
Yes. The host needs to enable it in their `host.yaml`, and the player's yaml needs to contain a plando_connections block.

0 commit comments

Comments
 (0)