1
- from typing import Dict , List , Any , Tuple , TypedDict , ClassVar , Union
1
+ from typing import Dict , List , Any , Tuple , TypedDict , ClassVar , Union , Set
2
2
from logging import warning
3
3
from BaseClasses import Region , Location , Item , Tutorial , ItemClassification , MultiWorld , CollectionState
4
4
from .items import (item_name_to_id , item_table , item_name_groups , fool_tiers , filler_items , slot_data_item_names ,
5
5
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
7
7
from .rules import set_location_rules , set_region_rules , randomize_ability_unlocks , gold_hexagon
8
8
from .er_rules import set_er_location_rules
9
9
from .regions import tunic_regions
10
10
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
11
12
from .er_data import portal_mapping , RegionInfo , tunic_er_regions
12
13
from .options import (TunicOptions , EntranceRando , tunic_option_groups , tunic_option_presets , TunicPlandoConnections ,
13
14
LaurelsLocation , LogicRules , LaurelsZips , IceGrappling , LadderStorage )
14
15
from .combat_logic import area_data , CombatState
15
16
from worlds .AutoWorld import WebWorld , World
16
- from Options import PlandoConnection
17
+ from Options import PlandoConnection , OptionError
17
18
from decimal import Decimal , ROUND_HALF_UP
18
19
from settings import Group , Bool
19
20
@@ -22,7 +23,11 @@ class TunicSettings(Group):
22
23
class DisableLocalSpoiler (Bool ):
23
24
"""Disallows the TUNIC client from creating a local spoiler log."""
24
25
26
+ class LimitGrassRando (Bool ):
27
+ """Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95."""
28
+
25
29
disable_local_spoiler : Union [DisableLocalSpoiler , bool ] = False
30
+ limit_grass_rando : Union [LimitGrassRando , bool ] = True
26
31
27
32
28
33
class TunicWeb (WebWorld ):
@@ -73,10 +78,13 @@ class TunicWorld(World):
73
78
settings : ClassVar [TunicSettings ]
74
79
item_name_groups = item_name_groups
75
80
location_name_groups = location_name_groups
81
+ location_name_groups .update (grass_location_name_groups )
76
82
77
83
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 )
79
86
87
+ player_location_table : Dict [str , int ]
80
88
ability_unlocks : Dict [str , int ]
81
89
slot_data_items : List [TunicItem ]
82
90
tunic_portal_pairs : Dict [str , str ]
@@ -85,6 +93,11 @@ class TunicWorld(World):
85
93
shop_num : int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
86
94
er_regions : Dict [str , RegionInfo ] # absolutely needed so outlet regions work
87
95
96
+ # for the local_fill option
97
+ fill_items : List [TunicItem ]
98
+ fill_locations : List [TunicLocation ]
99
+ amount_to_local_fill : int
100
+
88
101
# so we only loop the multiworld locations once
89
102
# if these are locations instead of their info, it gives a memory leak error
90
103
item_link_locations : Dict [int , Dict [str , List [Tuple [int , str ]]]] = {}
@@ -132,6 +145,7 @@ def generate_early(self) -> None:
132
145
self .options .hexagon_quest .value = self .passthrough ["hexagon_quest" ]
133
146
self .options .entrance_rando .value = self .passthrough ["entrance_rando" ]
134
147
self .options .shuffle_ladders .value = self .passthrough ["shuffle_ladders" ]
148
+ self .options .grass_randomizer .value = self .passthrough .get ("grass_randomizer" , 0 )
135
149
self .options .fixed_shop .value = self .options .fixed_shop .option_false
136
150
self .options .laurels_location .value = self .options .laurels_location .option_anywhere
137
151
self .options .combat_logic .value = self .passthrough ["combat_logic" ]
@@ -140,6 +154,22 @@ def generate_early(self) -> None:
140
154
else :
141
155
self .using_ut = False
142
156
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
+
143
173
@classmethod
144
174
def stage_generate_early (cls , multiworld : MultiWorld ) -> None :
145
175
tunic_worlds : Tuple [TunicWorld ] = multiworld .get_game_worlds ("TUNIC" )
@@ -245,6 +275,14 @@ def create_items(self) -> None:
245
275
self .get_location ("Secret Gathering Place - 10 Fairy Reward" ).place_locked_item (laurels )
246
276
items_to_create ["Hero's Laurels" ] = 0
247
277
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
+
248
286
if self .options .keys_behind_bosses :
249
287
for rgb_hexagon , location in hexagon_locations .items ():
250
288
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:
332
370
if tunic_item .name in slot_data_item_names :
333
371
self .slot_data_items .append (tunic_item )
334
372
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
+
335
394
self .multiworld .itempool += tunic_items
336
395
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
+
337
440
def create_regions (self ) -> None :
338
441
self .tunic_portal_pairs = {}
339
442
self .er_portal_hints = {}
@@ -346,7 +449,8 @@ def create_regions(self) -> None:
346
449
self .ability_unlocks ["Pages 52-53 (Icebolt)" ] = self .passthrough ["Hexagon Quest Icebolt" ]
347
450
348
451
# 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 ):
350
454
portal_pairs = create_er_regions (self )
351
455
if self .options .entrance_rando :
352
456
# these get interpreted by the game to tell it which entrances to connect
@@ -362,7 +466,7 @@ def create_regions(self) -> None:
362
466
region = self .get_region (region_name )
363
467
region .add_exits (exits )
364
468
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 ():
366
470
region = self .get_region (location_table [location_name ].region )
367
471
location = TunicLocation (self .player , location_name , location_id , region )
368
472
region .locations .append (location )
@@ -375,7 +479,8 @@ def create_regions(self) -> None:
375
479
376
480
def set_rules (self ) -> None :
377
481
# 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 ):
379
484
set_er_location_rules (self )
380
485
else :
381
486
set_region_rules (self )
@@ -463,6 +568,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
463
568
"maskless" : self .options .maskless .value ,
464
569
"entrance_rando" : int (bool (self .options .entrance_rando .value )),
465
570
"shuffle_ladders" : self .options .shuffle_ladders .value ,
571
+ "grass_randomizer" : self .options .grass_randomizer .value ,
466
572
"combat_logic" : self .options .combat_logic .value ,
467
573
"Hexagon Quest Prayer" : self .ability_unlocks ["Pages 24-25 (Prayer)" ],
468
574
"Hexagon Quest Holy Cross" : self .ability_unlocks ["Pages 42-43 (Holy Cross)" ],
0 commit comments