7
7
from .er_rules import set_er_location_rules
8
8
from .regions import tunic_regions
9
9
from .er_scripts import create_er_regions
10
+ from .er_data import portal_mapping
10
11
from .options import TunicOptions
11
12
from worlds .AutoWorld import WebWorld , World
12
13
from decimal import Decimal , ROUND_HALF_UP
@@ -44,7 +45,6 @@ class TunicWorld(World):
44
45
game = "TUNIC"
45
46
web = TunicWeb ()
46
47
47
- data_version = 2
48
48
options : TunicOptions
49
49
options_dataclass = TunicOptions
50
50
item_name_groups = item_name_groups
@@ -72,6 +72,7 @@ def generate_early(self) -> None:
72
72
self .options .maskless .value = passthrough ["maskless" ]
73
73
self .options .hexagon_quest .value = passthrough ["hexagon_quest" ]
74
74
self .options .entrance_rando .value = passthrough ["entrance_rando" ]
75
+ self .options .shuffle_ladders .value = passthrough ["shuffle_ladders" ]
75
76
76
77
def create_item (self , name : str ) -> TunicItem :
77
78
item_data = item_table [name ]
@@ -119,36 +120,55 @@ def create_items(self) -> None:
119
120
items_to_create [rgb_hexagon ] = 0
120
121
items_to_create [gold_hexagon ] -= 3
121
122
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
+
122
148
if hexagon_quest :
123
149
# Calculate number of hexagons in item pool
124
150
hexagon_goal = self .options .hexagon_goal
125
151
extra_hexagons = self .options .extra_hexagon_percentage
126
152
items_to_create [gold_hexagon ] += int ((Decimal (100 + extra_hexagons ) / 100 * hexagon_goal ).to_integral_value (rounding = ROUND_HALF_UP ))
127
-
153
+
128
154
# Replace pages and normal hexagons with filler
129
155
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 )
131
160
items_to_create [replaced_item ] = 0
132
161
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 ])
143
163
144
164
if self .options .maskless :
145
165
mask_item = TunicItem ("Scavenger Mask" , ItemClassification .useful , self .item_name_to_id ["Scavenger Mask" ], self .player )
146
166
tunic_items .append (mask_item )
147
167
items_to_create ["Scavenger Mask" ] = 0
148
168
149
169
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 )
152
172
items_to_create ["Lantern" ] = 0
153
173
154
174
for item , quantity in items_to_create .items ():
@@ -172,15 +192,16 @@ def create_regions(self) -> None:
172
192
self .ability_unlocks ["Pages 24-25 (Prayer)" ] = passthrough ["Hexagon Quest Prayer" ]
173
193
self .ability_unlocks ["Pages 42-43 (Holy Cross)" ] = passthrough ["Hexagon Quest Holy Cross" ]
174
194
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
182
195
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 ()
183
203
else :
204
+ # for non-ER, non-ladders
184
205
for region_name in tunic_regions :
185
206
region = Region (region_name , self .player , self .multiworld )
186
207
self .multiworld .regions .append (region )
@@ -201,7 +222,7 @@ def create_regions(self) -> None:
201
222
victory_region .locations .append (victory_location )
202
223
203
224
def set_rules (self ) -> None :
204
- if self .options .entrance_rando :
225
+ if self .options .entrance_rando or self . options . shuffle_ladders :
205
226
set_er_location_rules (self , self .ability_unlocks )
206
227
else :
207
228
set_region_rules (self , self .ability_unlocks )
@@ -212,7 +233,31 @@ def get_filler_item_name(self) -> str:
212
233
213
234
def extend_hint_information (self , hint_data : Dict [int , Dict [int , str ]]):
214
235
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
216
261
217
262
def fill_slot_data (self ) -> Dict [str , Any ]:
218
263
slot_data : Dict [str , Any ] = {
@@ -226,7 +271,8 @@ def fill_slot_data(self) -> Dict[str, Any]:
226
271
"logic_rules" : self .options .logic_rules .value ,
227
272
"lanternless" : self .options .lanternless .value ,
228
273
"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 ,
230
276
"Hexagon Quest Prayer" : self .ability_unlocks ["Pages 24-25 (Prayer)" ],
231
277
"Hexagon Quest Holy Cross" : self .ability_unlocks ["Pages 42-43 (Holy Cross)" ],
232
278
"Hexagon Quest Icebolt" : self .ability_unlocks ["Pages 52-53 (Icebolt)" ],
0 commit comments