11from typing import Optional
22from worlds .AutoWorld import World
3- from ..Helpers import clamp , get_items_with_value , is_option_enabled
3+ from ..Helpers import clamp , get_items_with_value
44from BaseClasses import MultiWorld , CollectionState
55
66import re
@@ -27,3 +27,104 @@ def anyClassLevel(world: World, multiworld: MultiWorld, state: CollectionState,
2727def requiresMelee (world : World , multiworld : MultiWorld , state : CollectionState , player : int ):
2828 """Returns a requires string that checks if the player has unlocked the tank."""
2929 return "|Figher Level:15| or |Black Belt Level:15| or |Thief Level:15|"
30+
31+ def ItemValue (world : World , multiworld : MultiWorld , state : CollectionState , player : int , args : str ):
32+ """When passed a string with this format: 'valueName:int',
33+ this function will check if the player has collect at least 'int' valueName worth of items\n
34+ eg. {ItemValue(Coins:12)} will check if the player has collect at least 12 coins worth of items
35+ """
36+
37+ args_list = args .split (":" )
38+ if not len (args_list ) == 2 or not args_list [1 ].isnumeric ():
39+ raise Exception (f"ItemValue needs a number after : so it looks something like 'ItemValue({ args_list [0 ]} :12)'" )
40+ args_list [0 ] = args_list [0 ].lower ().strip ()
41+ args_list [1 ] = int (args_list [1 ].strip ())
42+
43+ if not hasattr (world , 'item_values_cache' ): #Cache made for optimization purposes
44+ world .item_values_cache = {}
45+
46+ if not world .item_values_cache .get (player , {}):
47+ world .item_values_cache [player ] = {
48+ 'state' : {},
49+ 'count' : {},
50+ }
51+
52+ if (args_list [0 ] not in world .item_values_cache [player ].get ('count' , {}).keys ()
53+ or world .item_values_cache [player ].get ('state' ) != dict (state .prog_items [player ])):
54+ #Run First Time or if state changed since last check
55+ existing_item_values = get_items_with_value (world , multiworld , args_list [0 ])
56+ total_Count = 0
57+ for name , value in existing_item_values .items ():
58+ count = state .count (name , player )
59+ if count > 0 :
60+ total_Count += count * value
61+ world .item_values_cache [player ]['count' ][args_list [0 ]] = total_Count
62+ world .item_values_cache [player ]['state' ] = dict (state .prog_items [player ]) #save the current gotten items to check later if its the same
63+ return world .item_values_cache [player ]['count' ][args_list [0 ]] >= args_list [1 ]
64+
65+
66+ # Two useful functions to make require work if an item is disabled instead of making it inaccessible
67+ def OptOne (world : World , multiworld : MultiWorld , state : CollectionState , player : int , item : str , items_counts : Optional [dict ] = None ):
68+ """Check if the passed item (with or without ||) is enabled, then this returns |item:count|
69+ where count is clamped to the maximum number of said item in the itempool.\n
70+ Eg. requires: "{OptOne(|DisabledItem|)} and |other items|" become "|DisabledItem:0| and |other items|" if the item is disabled.
71+ """
72+ if item == "" :
73+ return "" #Skip this function if item is left blank
74+ if not items_counts :
75+ items_counts = world .get_item_counts ()
76+
77+ require_type = 'item'
78+
79+ if '@' in item [:2 ]:
80+ require_type = 'category'
81+
82+ item = item .lstrip ('|@$' ).rstrip ('|' )
83+
84+ item_parts = item .split (":" )
85+ item_name = item
86+ item_count = '1'
87+
88+ if len (item_parts ) > 1 :
89+ item_name = item_parts [0 ]
90+ item_count = item_parts [1 ]
91+
92+ if require_type == 'category' :
93+ if item_count .isnumeric ():
94+ #Only loop if we can use the result to clamp
95+ category_items = [item for item in world .item_name_to_item .values () if "category" in item and item_name in item ["category" ]]
96+ category_items_counts = sum ([items_counts .get (category_item ["name" ], 0 ) for category_item in category_items ])
97+ item_count = clamp (int (item_count ), 0 , category_items_counts )
98+ return f"|@{ item_name } :{ item_count } |"
99+ elif require_type == 'item' :
100+ if item_count .isnumeric ():
101+ item_current_count = items_counts .get (item_name , 0 )
102+ item_count = clamp (int (item_count ), 0 , item_current_count )
103+ return f"|{ item_name } :{ item_count } |"
104+
105+ # OptAll check the passed require string and loop every item to check if they're enabled,
106+ def OptAll (world : World , multiworld : MultiWorld , state : CollectionState , player : int , requires : str ):
107+ """Check the passed require string and loop every item to check if they're enabled,
108+ then returns the require string with items counts adjusted using OptOne\n
109+ eg. requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"
110+ become "|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|" """
111+ requires_list = requires
112+
113+ items_counts = world .get_item_counts ()
114+
115+ functions = {}
116+ if requires_list == "" :
117+ return True
118+ for item in re .findall (r'\{(\w+)\(([^)]*)\)\}' , requires_list ):
119+ #so this function doesn't try to get item from other functions, in theory.
120+ func_name = item [0 ]
121+ functions [func_name ] = item [1 ]
122+ requires_list = requires_list .replace ("{" + func_name + "(" + item [1 ] + ")}" , "{" + func_name + "(temp)}" )
123+ # parse user written statement into list of each item
124+ for item in re .findall (r'\|[^|]+\|' , requires ):
125+ itemScanned = OptOne (world , multiworld , state , player , item , items_counts )
126+ requires_list = requires_list .replace (item , itemScanned )
127+
128+ for function in functions :
129+ requires_list = requires_list .replace ("{" + function + "(temp)}" , "{" + func_name + "(" + functions [func_name ] + ")}" )
130+ return requires_list
0 commit comments