bdoap 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/bdoap +2 -1
- data/lib/bdo_alchemy_profits.rb +154 -41
- data/lib/bdo_codex/bdo_codex_searcher.rb +63 -14
- data/lib/central_market/category_search_options.rb +46 -13
- data/lib/central_market/exchange_items.rb +33 -0
- data/lib/central_market/market_searcher.rb +73 -32
- data/lib/utils/constants.rb +46 -14
- data/lib/utils/npc_item_index.rb +27 -0
- data/lib/utils/recipe_logger.rb +30 -5
- data/lib/utils/user_cli.rb +52 -7
- data/lib/version.rb +5 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4336121be8b520924050e57400f46e24661e9c9dc4156def8b08802e6eec2e2
|
4
|
+
data.tar.gz: 5011af8aa6f7f7a044786ca134ca8d4cce0bb3ae076409de70cc69a1adf8d6f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b055768a4bad1ebc9b244574d546dd859c4db6f7475d72afd7352d24b3aa9db5a311f6c74b6ce160ae86756ebbbc249df87c8d17b2294bba1a575bec8b6e96d
|
7
|
+
data.tar.gz: f5537518eebae1c5f62d63d9daaa0b2ee29d1570940a165e0c7dcee1b033ed728909f6f7f3940edc51d5dc00672363f8639adb8f8a64780ecc3ed9706716ffa5
|
data/bin/bdoap
CHANGED
data/lib/bdo_alchemy_profits.rb
CHANGED
@@ -24,71 +24,184 @@
|
|
24
24
|
# SOFTWARE.
|
25
25
|
|
26
26
|
require 'optparse'
|
27
|
+
require 'httparty'
|
27
28
|
|
28
29
|
require_relative './utils/user_cli'
|
29
30
|
require_relative './central_market/market_searcher'
|
30
31
|
require_relative './bdo_codex/bdo_codex_searcher'
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
# main module for bdoap. contains the BDOAlchemyProfits class
|
34
|
+
module BDOAP
|
35
|
+
# used to search for profitable alchemy recipes
|
36
|
+
class BDOAlchemyProfits
|
37
|
+
include Utils
|
38
|
+
|
39
|
+
def start_enhance(search_string, enhance_starting_level, region, cli)
|
40
|
+
items_at_level = []
|
41
|
+
root_url = ENVData.get_root_url region
|
42
|
+
search_url = "#{root_url}#{ENVData::MARKET_SEARCH_LIST}"
|
43
|
+
sub_url = "#{root_url}#{ENVData::MARKET_SUB_LIST}"
|
44
|
+
category_opts = {
|
45
|
+
url: search_url,
|
46
|
+
query_string: "#{ENVData::RVT}&searchText=#{URI.encode_www_form_component search_string}",
|
47
|
+
update: ->(data) { data['list'] }
|
48
|
+
}
|
49
|
+
|
50
|
+
data = HTTParty.post(
|
51
|
+
URI(category_opts[:url].to_s),
|
52
|
+
headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
|
53
|
+
body: category_opts[:query_string],
|
54
|
+
content_type: 'application/x-www-form-urlencoded'
|
55
|
+
)
|
56
|
+
|
57
|
+
resolved = category_opts[:update].call(data) if data
|
58
|
+
|
59
|
+
return unless resolved
|
60
|
+
|
61
|
+
resolved.each do |item|
|
62
|
+
subdata = HTTParty.post(
|
63
|
+
URI(sub_url),
|
64
|
+
headers: ENVData.get_central_market_headers(ENVData.get_incap_cookie(@region_subdomain)),
|
65
|
+
body: "#{ENVData::RVT}&mainKey=#{item['mainKey']}&usingCleint=0",
|
66
|
+
content_type: 'application/x-www-form-urlencoded'
|
67
|
+
)
|
68
|
+
|
69
|
+
sleep rand
|
70
|
+
|
71
|
+
if subdata&.dig('detailList')
|
72
|
+
level_map = {
|
73
|
+
0 => 0,
|
74
|
+
1 => 1,
|
75
|
+
2 => 2,
|
76
|
+
3 => 3,
|
77
|
+
4 => 4,
|
78
|
+
5 => 5,
|
79
|
+
6 => 6,
|
80
|
+
7 => 7,
|
81
|
+
8 => 8,
|
82
|
+
9 => 9,
|
83
|
+
10 => 10,
|
84
|
+
11 => 11,
|
85
|
+
12 => 12,
|
86
|
+
13 => 13,
|
87
|
+
14 => 14,
|
88
|
+
15 => 15,
|
89
|
+
16 => 16,
|
90
|
+
17 => 17,
|
91
|
+
18 => 18,
|
92
|
+
19 => 19,
|
93
|
+
20 => 20,
|
94
|
+
}
|
95
|
+
|
96
|
+
level_map_to_bdo = {
|
97
|
+
0 => 0,
|
98
|
+
1 => 1,
|
99
|
+
2 => 2,
|
100
|
+
3 => 3,
|
101
|
+
4 => 4,
|
102
|
+
5 => 5,
|
103
|
+
6 => 6,
|
104
|
+
7 => 7,
|
105
|
+
8 => 8,
|
106
|
+
9 => 9,
|
107
|
+
10 => 10,
|
108
|
+
11 => 11,
|
109
|
+
12 => 12,
|
110
|
+
13 => 13,
|
111
|
+
14 => 14,
|
112
|
+
15 => 15,
|
113
|
+
16 => 'PRI',
|
114
|
+
17 => 'DUO',
|
115
|
+
18 => 'TRI',
|
116
|
+
19 => 'TET',
|
117
|
+
20 => 'PEN',
|
118
|
+
}
|
119
|
+
|
120
|
+
result = subdata['detailList'] .find { |e| e['subKey'].to_s == level_map[enhance_starting_level.to_i].to_s }
|
121
|
+
if result
|
122
|
+
constructed = " #{result['count']} #{cli.yellow result['name'].downcase} @ #{level_map_to_bdo[result['subKey']]}"
|
123
|
+
items_at_level.push constructed unless result['count'].to_i == 0
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
puts "\nthe results are in!\n"
|
129
|
+
items_at_level.each { |item| puts item }
|
130
|
+
puts
|
131
|
+
end
|
45
132
|
|
46
|
-
|
133
|
+
# begin the configuration and search process
|
134
|
+
def start_cli(search_string = nil, enhance_starting_level = nil, enhancing = false)
|
135
|
+
begin
|
136
|
+
options = {}
|
47
137
|
|
48
|
-
|
138
|
+
OptionParser.new do |opt|
|
139
|
+
opt.on('--silent', '-s') { options[:silent] = true }
|
140
|
+
end.parse!
|
49
141
|
|
50
|
-
|
142
|
+
# option setup
|
143
|
+
cli = UserCLI.new options
|
51
144
|
|
52
|
-
|
145
|
+
unless enhancing
|
146
|
+
category = cli.choose_category
|
147
|
+
cli.end_cli if category == 'exit'
|
148
|
+
end
|
53
149
|
|
54
|
-
|
150
|
+
region = cli.choose_region
|
151
|
+
cli.end_cli if region == 'exit'
|
55
152
|
|
56
|
-
|
153
|
+
# start enhancing
|
154
|
+
if enhancing && search_string && enhance_starting_level
|
155
|
+
start_enhance(search_string, enhance_starting_level, region, cli)
|
156
|
+
return
|
157
|
+
end
|
57
158
|
|
58
|
-
|
159
|
+
lang = cli.choose_lang
|
160
|
+
cli.end_cli if lang == 'exit'
|
161
|
+
aggression = cli.choose_aggression
|
162
|
+
cli.end_cli if aggression == 'exit'
|
163
|
+
free_ingredients = cli.choose_free_ingredients
|
164
|
+
show_out_of_stock = cli.choose_show_out_of_stock
|
59
165
|
|
60
|
-
|
166
|
+
if aggression == 'hyperaggressive'
|
167
|
+
puts cli.orange("\nWARN: hyperagressive mode is RISKY AND SLOW. this will evaluate every substitution for every recipe. hammers apis violently. you will get rate limited. you will get IP blocked. her royal holiness imperva incapsula WILL get you. select if you know what all that stuff means and you are ok with waiting 20 minutes.")
|
168
|
+
end
|
61
169
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
170
|
+
# start searching
|
171
|
+
cli.vipiko("\n♫ let's see if #{cli.yellow category} alchemy items are profitable in #{cli.yellow region}!")
|
65
172
|
|
66
|
-
|
173
|
+
market_searcher = MarketSearcher.new(region, cli, free_ingredients)
|
67
174
|
|
68
|
-
|
175
|
+
market_item_list = market_searcher.get_alchemy_market_data category
|
69
176
|
|
70
|
-
|
177
|
+
cli.vipiko("I'll look for #{cli.yellow(market_item_list.length.to_s)} item#{
|
178
|
+
market_item_list.empty? || market_item_list.length > 1 ? 's' : ''
|
179
|
+
} in #{category == 'all' ? cli.yellow('all categories'): "the #{cli.yellow category} category"}!")
|
71
180
|
|
72
|
-
|
181
|
+
bdo_codex_searcher = BDOCodexSearcher.new(region, lang, cli, aggression == 'hyperaggressive')
|
73
182
|
|
74
|
-
|
75
|
-
market_item_list.empty? || market_item_list.length > 1 ? 's' : ''
|
76
|
-
} in #{category == 'all' ? cli.yellow('all categories'): "the #{cli.yellow category} category"}!")
|
183
|
+
item_codex_data = bdo_codex_searcher.get_item_codex_data market_item_list
|
77
184
|
|
78
|
-
|
185
|
+
recipe_prices = market_searcher.get_all_recipe_prices item_codex_data, category
|
79
186
|
|
80
|
-
|
187
|
+
mapped_prices = recipe_prices.reverse.sort_by { |recipe| recipe[:gain].to_i }.map { |recipe| recipe[:information] }
|
81
188
|
|
82
|
-
|
189
|
+
out_of_stock = recipe_prices.dig(0, :out_of_stock) || []
|
190
|
+
out_of_stock_list = ""
|
191
|
+
out_of_stock.each { |item| out_of_stock_list += "\n\t #{cli.yellow item}" }
|
83
192
|
|
84
|
-
|
193
|
+
if mapped_prices.length > 0
|
194
|
+
cli.vipiko_overwrite "done!"
|
195
|
+
puts "\n\n"
|
196
|
+
puts mapped_prices
|
197
|
+
else
|
198
|
+
cli.vipiko_overwrite "none of those recipes look profitable right now...let's go gathering!\n\n"
|
199
|
+
end
|
85
200
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
else
|
91
|
-
cli.vipiko_overwrite "none of those recipes look profitable right now...let's go gathering!"
|
201
|
+
puts " items that were out of stock: #{out_of_stock_list}" if show_out_of_stock && out_of_stock_list.length > 0
|
202
|
+
rescue Interrupt => e
|
203
|
+
puts "\n\nstopping!"
|
204
|
+
end
|
92
205
|
end
|
93
206
|
end
|
94
207
|
end
|
@@ -24,13 +24,18 @@ class BDO_CODEX_UTILS
|
|
24
24
|
'products',
|
25
25
|
'all ingredients',
|
26
26
|
]
|
27
|
+
def self.acc_number_to_enhance_level(number)
|
28
|
+
%w[PRI DUO TRI TET PEN][number]
|
29
|
+
end
|
27
30
|
end
|
28
31
|
|
29
32
|
# used to retrieve items from BDOCodex
|
30
33
|
class BDOCodexSearcher
|
31
34
|
include Utils
|
32
35
|
|
36
|
+
RECIPE_PRODUCTS_INDEX = 3
|
33
37
|
RECIPE_INGREDIENTS_INDEX = 2
|
38
|
+
RECIPE_NAME_KEY = 1
|
34
39
|
|
35
40
|
def initialize(region, lang, cli, hyper_aggressive = false)
|
36
41
|
@region = region
|
@@ -41,14 +46,15 @@ class BDOCodexSearcher
|
|
41
46
|
@hyper_aggressive = hyper_aggressive
|
42
47
|
end
|
43
48
|
|
44
|
-
def get_recipe_url(url)
|
49
|
+
def get_recipe_url(url, item_name)
|
45
50
|
begin
|
46
51
|
data = HTTParty.get(
|
47
52
|
URI(url),
|
48
53
|
headers: ENVData::REQUEST_OPTS[:bdo_codex_headers],
|
49
|
-
content_type: 'application/x-www-form-urlencoded'
|
50
54
|
)
|
51
55
|
|
56
|
+
return {} unless data.length < 1_000_000
|
57
|
+
|
52
58
|
JSON.parse data unless data.body.nil? or data.body.empty?
|
53
59
|
rescue
|
54
60
|
{}
|
@@ -85,6 +91,11 @@ class BDOCodexSearcher
|
|
85
91
|
permutated_chunks = ArrayUtils.deep_permute chunked_by_substitution_groups, original_recipe_length
|
86
92
|
|
87
93
|
permutated_chunks.each do |id_list|
|
94
|
+
if id_list.uniq.length != id_list.length
|
95
|
+
all_recipe_substitutions.push recipe
|
96
|
+
next
|
97
|
+
end
|
98
|
+
|
88
99
|
recipe_with_new_items = [*recipe]
|
89
100
|
recipe_with_new_items[RECIPE_INGREDIENTS_INDEX] = [*recipe][RECIPE_INGREDIENTS_INDEX].map.with_index do |recipe_list, idx|
|
90
101
|
{ **recipe_list, id: id_list[idx] }
|
@@ -100,6 +111,8 @@ class BDOCodexSearcher
|
|
100
111
|
|
101
112
|
def parse_raw_recipe(recipe_data, item_name)
|
102
113
|
item_with_ingredients = recipe_data.dig(BDO_CODEX_UTILS::BDOCODEX_QUERY_DATA_KEY)
|
114
|
+
has_subs = false
|
115
|
+
|
103
116
|
|
104
117
|
if item_with_ingredients
|
105
118
|
recipe_with_substitute_ids = item_with_ingredients.map do |arr|
|
@@ -109,29 +122,37 @@ class BDOCodexSearcher
|
|
109
122
|
end.first
|
110
123
|
|
111
124
|
all_potion_recipes = item_with_ingredients.map do |arr|
|
125
|
+
is_m_recipe = false
|
112
126
|
mapped_item_data = arr
|
113
127
|
.filter.with_index { |_, idx | !BDO_CODEX_UTILS::RECIPE_COLUMNS[idx].nil? }
|
114
128
|
.map.with_index do |raw_element, idx|
|
115
129
|
category = BDO_CODEX_UTILS::RECIPE_COLUMNS[idx]
|
116
130
|
|
117
|
-
next if ['skill level', 'exp', '
|
131
|
+
next if ['skill level', 'exp', 'icon', 'total weight of materials'].include? category
|
118
132
|
|
119
133
|
element = Nokogiri::HTML5 raw_element.to_s
|
120
134
|
|
135
|
+
has_subs = true if element.to_s.include? 'icon-repeat'
|
136
|
+
|
121
137
|
result = {
|
122
138
|
:category => category,
|
123
139
|
:element => element
|
124
140
|
}
|
125
141
|
|
126
|
-
|
142
|
+
if %w[title id].include? category
|
143
|
+
result[:element] = element.text.downcase
|
144
|
+
end
|
127
145
|
|
128
|
-
|
146
|
+
if category == 'type'
|
147
|
+
is_m_recipe = true unless %w[cooking alchemy].include? element.text.downcase
|
148
|
+
end
|
129
149
|
|
130
150
|
if %w[materials products].include? category
|
131
151
|
quants = element.text.scan(/\](\d+)/im).map { |e| e[0].to_i }
|
152
|
+
enhance_levels = element.text.scan(/](\+?\d)/im).map { |e| e[0].to_s }
|
132
153
|
|
133
154
|
ids = element.to_s.scan(/#{@region_lang}\/item\/(\d+)/).map { |e| e[0].to_i }
|
134
|
-
result[:element] = ids.map.with_index { |id, i| { id: id, quant: quants[i]} }.flatten
|
155
|
+
result[:element] = ids.map.with_index { |id, i| { id: id, quant: quants[i] || 1, enhance_level: enhance_levels[i][0] == '+' ? enhance_levels[i].to_i : 0, is_m_recipe: is_m_recipe } }.flatten
|
135
156
|
end
|
136
157
|
|
137
158
|
result
|
@@ -145,6 +166,8 @@ class BDOCodexSearcher
|
|
145
166
|
filtered_item_data
|
146
167
|
end
|
147
168
|
|
169
|
+
return all_potion_recipes unless has_subs
|
170
|
+
|
148
171
|
get_recipe_substitutions recipe_with_substitute_ids, all_potion_recipes, item_name
|
149
172
|
end
|
150
173
|
end
|
@@ -152,21 +175,21 @@ class BDOCodexSearcher
|
|
152
175
|
def get_item_recipes(item_id, item_name)
|
153
176
|
recipe_direct_url = "https://bdocodex.com/query.php?a=recipes&type=product&item_id=#{item_id}&l=#{@region_lang}"
|
154
177
|
mrecipe_direct_url = "https://bdocodex.com/query.php?a=mrecipes&type=product&item_id=#{item_id}&l=#{@region_lang}"
|
155
|
-
|
178
|
+
houserecipe_direct_url = "https://bdocodex.com/query.php?a=designs&type=product&item_id=#{item_id}&l=#{@region_lang}"
|
156
179
|
|
157
180
|
# TODO: there MUST be a better way to determine which recipe to use, rather than just trying them both.
|
158
181
|
begin
|
159
|
-
direct_data = get_recipe_url recipe_direct_url
|
182
|
+
direct_data = get_recipe_url recipe_direct_url, item_name
|
160
183
|
|
161
184
|
if !direct_data || direct_data.empty?
|
162
|
-
mrecipe_data = get_recipe_url mrecipe_direct_url
|
185
|
+
mrecipe_data = get_recipe_url mrecipe_direct_url, item_name
|
163
186
|
|
164
187
|
return parse_raw_recipe mrecipe_data, item_name
|
165
188
|
else
|
166
189
|
parsed = parse_raw_recipe direct_data, item_name
|
167
190
|
|
168
191
|
if !parsed || parsed.empty?
|
169
|
-
mrecipe_data = get_recipe_url mrecipe_direct_url
|
192
|
+
mrecipe_data = get_recipe_url mrecipe_direct_url, item_name
|
170
193
|
|
171
194
|
return parse_raw_recipe mrecipe_data, item_name
|
172
195
|
end
|
@@ -198,7 +221,7 @@ class BDOCodexSearcher
|
|
198
221
|
cache_data = {}
|
199
222
|
|
200
223
|
# TODO: remove this check
|
201
|
-
# return unless item_name.downcase == '
|
224
|
+
# return unless item_name.downcase == 'essence of dawn - damage reduction'
|
202
225
|
|
203
226
|
unless potential_cached_recipes.to_a.empty?
|
204
227
|
recipe_to_maybe_select = potential_cached_recipes.filter { |elem| elem[1].downcase == item_name.downcase }
|
@@ -206,9 +229,33 @@ class BDOCodexSearcher
|
|
206
229
|
end
|
207
230
|
|
208
231
|
recipes = get_item_recipes item_id, item_name
|
209
|
-
cache_data[item_index] = recipes
|
210
232
|
|
211
|
-
|
233
|
+
if recipes
|
234
|
+
cache_data[item_index] = [*recipes]
|
235
|
+
has_uncacheable = recipes.index do |cache_item|
|
236
|
+
has_enhanced_ingredient = cache_item[RECIPE_INGREDIENTS_INDEX].index { |ingredient| ingredient[:enhance_level] > 0 }
|
237
|
+
|
238
|
+
has_enhanced_ingredient
|
239
|
+
end
|
240
|
+
|
241
|
+
cache_data[item_index] = recipes.map do |cache_item|
|
242
|
+
new_cache_recipes = cache_item[RECIPE_INGREDIENTS_INDEX].map do |ingredient|
|
243
|
+
{ id: ingredient[:id], quant: ingredient[:quant] }
|
244
|
+
end
|
245
|
+
new_cache_ouputs = cache_item[RECIPE_PRODUCTS_INDEX].map do |ingredient|
|
246
|
+
{ id: ingredient[:id], quant: ingredient[:quant] }
|
247
|
+
end
|
248
|
+
|
249
|
+
new_cache_item = [*cache_item]
|
250
|
+
|
251
|
+
new_cache_item[RECIPE_INGREDIENTS_INDEX] = new_cache_recipes
|
252
|
+
new_cache_item[RECIPE_PRODUCTS_INDEX] = new_cache_ouputs
|
253
|
+
|
254
|
+
new_cache_item
|
255
|
+
end
|
256
|
+
|
257
|
+
@cache.write cache_data unless has_uncacheable
|
258
|
+
end
|
212
259
|
|
213
260
|
recipes
|
214
261
|
end
|
@@ -239,10 +286,12 @@ class BDOCodexSearcher
|
|
239
286
|
@cli.vipiko_overwrite "(#{index + 1} / #{item_list.length}) let's read the recipe for #{@cli.yellow "[#{item[:name].downcase}]"}. hmm..."
|
240
287
|
|
241
288
|
stock_count = item[:total_in_stock].to_i.zero? ? item[:count].to_i : item[:total_in_stock].to_i
|
289
|
+
price = item[:price_per_one]
|
290
|
+
|
242
291
|
recipe_hash = {
|
243
292
|
name: item[:name],
|
244
293
|
recipe_list: all_recipes_for_item,
|
245
|
-
price:
|
294
|
+
price: price,
|
246
295
|
id: item[:main_key],
|
247
296
|
total_trade_count: item[:total_trade_count],
|
248
297
|
total_in_stock: stock_count,
|
@@ -10,6 +10,7 @@ module MarketSearchTools
|
|
10
10
|
offensive: 1,
|
11
11
|
defensive: 2,
|
12
12
|
functional: 3,
|
13
|
+
food: 4,
|
13
14
|
potion: 5,
|
14
15
|
other: 8,
|
15
16
|
all: [1, 2, 3, 5, 8]
|
@@ -23,49 +24,48 @@ module MarketSearchTools
|
|
23
24
|
name: 'black stone',
|
24
25
|
url: url,
|
25
26
|
query_string: "#{ENVData::RVT}&mainCategory=30&subCategory=1",
|
26
|
-
|
27
|
-
update: ->(data) { data['list'] }
|
27
|
+
update: ->(data) { data['marketList'] }
|
28
28
|
},
|
29
29
|
{
|
30
30
|
name: 'misc',
|
31
31
|
url: url,
|
32
32
|
query_string: "#{ENVData::RVT}&mainCategory=25&subCategory=8",
|
33
|
-
|
34
|
-
update: ->(data) { data['list'] }
|
33
|
+
update: ->(data) { data['marketList'] }
|
35
34
|
},
|
36
35
|
{
|
37
36
|
name: 'other tools',
|
38
37
|
url: url,
|
39
38
|
query_string: "#{ENVData::RVT}&mainCategory=40&subCategory=10",
|
40
|
-
|
41
|
-
update: ->(data) { data['list'] }
|
39
|
+
update: ->(data) { data['marketList'] }
|
42
40
|
},
|
43
41
|
{
|
44
42
|
name: 'blood',
|
45
43
|
url: search_url,
|
46
44
|
query_string: "#{ENVData::RVT}&searchText='s+blood",
|
47
|
-
|
45
|
+
update: ->(data) { data['list'] }
|
46
|
+
},
|
47
|
+
{
|
48
|
+
name: 'essences of dawn',
|
49
|
+
url: search_url,
|
50
|
+
query_string: "#{ENVData::RVT}&searchText=essence+of+dawn",
|
48
51
|
update: ->(data) { data['list'] }
|
49
52
|
},
|
50
53
|
{
|
51
54
|
name: 'reagent',
|
52
55
|
url: search_url,
|
53
56
|
query_string: "#{ENVData::RVT}&searchText=reagent",
|
54
|
-
# update: ->(data) { { reagentResponse: data['list'] } },
|
55
57
|
update: ->(data) { data['list'] }
|
56
58
|
},
|
57
59
|
{
|
58
60
|
name: 'oil',
|
59
61
|
url: search_url,
|
60
62
|
query_string: "#{ENVData::RVT}&searchText=oil+of",
|
61
|
-
# update: ->(data) { { oilResponse: data['list'] } },
|
62
63
|
update: ->(data) { data['list'] }
|
63
64
|
},
|
64
65
|
{
|
65
66
|
name: 'alchemy stone',
|
66
67
|
url: search_url,
|
67
|
-
query_string: "#{ENVData::RVT}&searchText=stone+of",
|
68
|
-
# update: ->(data) { { alchemyStoneResponse: data['list'] } },
|
68
|
+
query_string: "#{ENVData::RVT}&searchText=imperfect+alchemy+stone+of",
|
69
69
|
update: ->(data) { data['list'].filter { |i| i['grade'] == 0 } }
|
70
70
|
},
|
71
71
|
# {
|
@@ -75,12 +75,24 @@ module MarketSearchTools
|
|
75
75
|
# # update: ->(data) { { magicCrystalResponse: data['list'] } },
|
76
76
|
# update: ->(data) { data['list'] }
|
77
77
|
# },
|
78
|
+
{
|
79
|
+
name: 'combined crystals',
|
80
|
+
url: url,
|
81
|
+
query_string: "#{ENVData::RVT}&mainCategory=50&subCategory=4",
|
82
|
+
update: ->(data) { data['marketList'] }
|
83
|
+
},
|
78
84
|
{
|
79
85
|
name: 'offensive',
|
80
86
|
url: url,
|
81
87
|
query_string:
|
82
88
|
"#{ENVData::RVT}&mainCategory=#{CONSUMABLE_CATEGORY}&subCategory=#{CONSUMABLE_SUBCATEGORIES[:offensive]}",
|
83
|
-
|
89
|
+
update: ->(data) { data['marketList'] }
|
90
|
+
},
|
91
|
+
{
|
92
|
+
name: 'food',
|
93
|
+
url: url,
|
94
|
+
query_string:
|
95
|
+
"#{ENVData::RVT}&mainCategory=#{CONSUMABLE_CATEGORY}&subCategory=#{CONSUMABLE_SUBCATEGORIES[:food]}",
|
84
96
|
update: ->(data) { data['marketList'] }
|
85
97
|
},
|
86
98
|
{
|
@@ -114,7 +126,28 @@ module MarketSearchTools
|
|
114
126
|
"#{ENVData::RVT}&mainCategory=#{CONSUMABLE_CATEGORY}&subCategory=#{CONSUMABLE_SUBCATEGORIES[:other]}",
|
115
127
|
# update: ->(data) { { otherResponse: data['marketList'] } },
|
116
128
|
update: ->(data) { data['marketList'] }
|
117
|
-
}
|
129
|
+
},
|
130
|
+
{
|
131
|
+
name: 'manos',
|
132
|
+
url: search_url,
|
133
|
+
query_string: "#{ENVData::RVT}&searchText=manos",
|
134
|
+
# update: ->(data) { { otherResponse: data['marketList'] } },
|
135
|
+
update: ->(data) { data['list'] }
|
136
|
+
},
|
137
|
+
{
|
138
|
+
name: 'purified lightstone',
|
139
|
+
url: search_url,
|
140
|
+
query_string: "#{ENVData::RVT}&searchText=purified+lightstone+of",
|
141
|
+
# update: ->(data) { { otherResponse: data['marketList'] } },
|
142
|
+
update: ->(data) { data['list'] }
|
143
|
+
},
|
144
|
+
# {
|
145
|
+
# name: 'stuffed animals',
|
146
|
+
# url: search_url,
|
147
|
+
# query_string: "#{ENVData::RVT}&searchText=stuffed",
|
148
|
+
# # update: ->(data) { { otherResponse: data['marketList'] } },
|
149
|
+
# update: ->(data) { data['list'] }
|
150
|
+
# }
|
118
151
|
]
|
119
152
|
end
|
120
153
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExchangeItems
|
4
|
+
EXCHANGE_ITEMS = {
|
5
|
+
# magical lightstone crystal
|
6
|
+
766108 => {
|
7
|
+
# items you can exchange for this item. there are technically way more
|
8
|
+
exchange_with: [766105, 766104, 766107, 766106],
|
9
|
+
exchange_with_names: ['imperfect lightstone'],
|
10
|
+
# how many of this item would you get if you exchanged it for the item ID above
|
11
|
+
exchanging_grants: 6,
|
12
|
+
exchange_with_npc: 'dalishain',
|
13
|
+
count: Float::INFINITY,
|
14
|
+
is_npc_item: true,
|
15
|
+
name: 'Magical Lightstone Crystal',
|
16
|
+
id: 766108,
|
17
|
+
}
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def get_exchange_item_info(item_to_exchange_id, item_to_exchange_for_id, price_of_exchange, quant_required)
|
21
|
+
exchange_info = EXCHANGE_ITEMS[item_to_exchange_for_id]
|
22
|
+
|
23
|
+
if exchange_info
|
24
|
+
if exchange_info[:exchange_with].include? item_to_exchange_id
|
25
|
+
# if a recipe requires 10 magical lightstone crystals, for example, and the exchange rate is
|
26
|
+
# 1 imperfect lightstone for 6 crystals, you'll have to buy 2 imperfect lightstones to fill
|
27
|
+
# the quant. since you can't buy a fraction of an item, we .ceil the number required.
|
28
|
+
mult = (quant_required.to_f / exchange_info[:exchanging_grants].to_f).ceil
|
29
|
+
return { **exchange_info, price: price_of_exchange * mult, must_exchange: mult }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -8,13 +8,15 @@ require_relative '../utils/price_calculator'
|
|
8
8
|
require_relative '../utils/recipe_logger'
|
9
9
|
require_relative '../utils/npc_item_index'
|
10
10
|
require_relative './category_search_options'
|
11
|
+
require_relative './exchange_items'
|
11
12
|
|
12
13
|
# search for information on recipes in given categories
|
13
14
|
class MarketSearcher
|
14
15
|
include Utils
|
16
|
+
include ExchangeItems
|
15
17
|
include MarketSearchTools
|
16
18
|
|
17
|
-
def initialize(region, cli)
|
19
|
+
def initialize(region, cli, free_ingredients)
|
18
20
|
@root_url = ENVData.get_root_url region
|
19
21
|
@region_subdomain = CLIConstants::REGION_DOMAINS[region.to_sym].split('.')[1..].join('.')
|
20
22
|
@market_list_url = "#{@root_url}#{ENVData::WORLD_MARKET_LIST}"
|
@@ -22,7 +24,10 @@ class MarketSearcher
|
|
22
24
|
@market_sub_url = "#{@root_url}#{ENVData::MARKET_SUB_LIST}"
|
23
25
|
@market_sell_buy_url = "#{@root_url}#{ENVData::MARKET_SELL_BUY_INFO}"
|
24
26
|
@cli = cli
|
27
|
+
# cache used to store all ingredients for which the price has already been checked
|
25
28
|
@ingredient_cache = {}
|
29
|
+
@free_ingredients = free_ingredients
|
30
|
+
@out_of_stock_items = []
|
26
31
|
end
|
27
32
|
|
28
33
|
def get_alchemy_market_data(category)
|
@@ -61,6 +66,9 @@ class MarketSearcher
|
|
61
66
|
puts
|
62
67
|
mapped_aggregate = filtered_aggregate.map.with_index do |elem, index|
|
63
68
|
|
69
|
+
# TODO: remove this check
|
70
|
+
# next unless elem['name'].downcase == 'essence of dawn - damage reduction'
|
71
|
+
|
64
72
|
@cli.vipiko_overwrite "(#{index + 1} / #{filtered_aggregate.length}) researching #{@cli.yellow elem['name'].downcase}... (category: #{subcategory})"
|
65
73
|
|
66
74
|
begin
|
@@ -129,7 +137,8 @@ class MarketSearcher
|
|
129
137
|
aggregate_response.flatten
|
130
138
|
end
|
131
139
|
|
132
|
-
|
140
|
+
# set is_recipe_ingredient = true if you're using this function to get the cost of buying an ingredient
|
141
|
+
def get_item_price_info(ingredient_id, is_recipe_ingredient = true, enhance_level = 0)
|
133
142
|
npc_item = NPCItemIndex.get_item(ingredient_id)
|
134
143
|
|
135
144
|
return npc_item if npc_item
|
@@ -145,7 +154,7 @@ class MarketSearcher
|
|
145
154
|
content_type: 'application/x-www-form-urlencoded'
|
146
155
|
)
|
147
156
|
|
148
|
-
sleep rand
|
157
|
+
sleep rand
|
149
158
|
rescue StandardError => error
|
150
159
|
puts @cli.red("this could be a network failure. get_item_price_info broke.")
|
151
160
|
|
@@ -156,12 +165,15 @@ class MarketSearcher
|
|
156
165
|
end
|
157
166
|
end
|
158
167
|
|
159
|
-
|
168
|
+
# TODO: actually implement a normal way of handling malformed results
|
169
|
+
ingredient_data = {} if ingredient_data.to_s.downcase.include? 'use a different browser'
|
170
|
+
ingredient_data = {} if ingredient_data.to_s.downcase.include? 'incapsula incident'
|
160
171
|
|
161
172
|
if ingredient_data.dig('detailList')
|
162
|
-
resolved_data = ingredient_data['detailList'][
|
173
|
+
resolved_data = ingredient_data['detailList'].find { |entry| entry['subKey'].to_i == enhance_level.to_i }
|
174
|
+
resolved_data = ingredient_data['detailList'][0] if resolved_data.nil?
|
163
175
|
unless resolved_data.nil?
|
164
|
-
body_string = "#{ENVData::RVT}&mainKey=#{resolved_data['mainKey']}&subKey
|
176
|
+
body_string = "#{ENVData::RVT}&mainKey=#{resolved_data['mainKey']}&subKey=#{enhance_level}&chooseKey=0&isUp=true&keyType=0&name=#{URI.encode_www_form_component(resolved_data['name'])}"
|
165
177
|
|
166
178
|
detailed_price_list = {}
|
167
179
|
|
@@ -172,6 +184,9 @@ class MarketSearcher
|
|
172
184
|
body: body_string,
|
173
185
|
content_type: 'application/x-www-form-urlencoded'
|
174
186
|
)
|
187
|
+
|
188
|
+
detailed_price_list = {} if detailed_price_list.to_s.downcase.include? 'incapsula incident'
|
189
|
+
|
175
190
|
sleep rand
|
176
191
|
rescue StandardError => error
|
177
192
|
puts @cli.red("this could be a network failure. get_item_price_info broke.")
|
@@ -193,53 +208,65 @@ class MarketSearcher
|
|
193
208
|
|
194
209
|
if optimal_price
|
195
210
|
if optimal_price['pricePerOne'] && optimal_price['sellCount']
|
196
|
-
{ **resolved_data, count: total_stock, pricePerOne: optimal_price['pricePerOne'] }
|
211
|
+
{ **resolved_data, count: total_stock, pricePerOne: optimal_price['pricePerOne'], enhanceLevel: enhance_level }
|
197
212
|
else
|
198
|
-
{ **resolved_data, count: total_stock, pricePerOne: resolved_data['pricePerOne'] }
|
213
|
+
{ **resolved_data, count: total_stock, pricePerOne: resolved_data['pricePerOne'], enhanceLevel: enhance_level }
|
199
214
|
end
|
200
215
|
end
|
201
216
|
end
|
202
217
|
end
|
203
|
-
|
204
218
|
end
|
205
219
|
end
|
206
220
|
|
207
221
|
def get_all_recipe_prices(item_codex_data, subcategory)
|
208
222
|
mapped_recipe_prices = []
|
209
|
-
out_of_stock_items = []
|
210
|
-
# vipiko is about to start writing carriage returns,
|
211
|
-
# so printing newline here
|
212
223
|
|
213
224
|
item_codex_data.each.with_index do |item_with_recipe, index|
|
214
225
|
potential_recipes = []
|
215
226
|
name = item_with_recipe[:name].downcase
|
216
227
|
recipe_list = item_with_recipe[:recipe_list]
|
217
228
|
|
229
|
+
# TODO: remove this line
|
230
|
+
# next unless name == 'essence of dawn - damage reduction'
|
231
|
+
|
218
232
|
@cli.vipiko_overwrite "(#{index + 1} / #{item_codex_data.length}) I'll ask a merchant about the price of ingredients for #{@cli.yellow name}!"
|
219
233
|
|
220
|
-
recipe_list.each do |
|
234
|
+
recipe_list.each do |recipe_id, recipe|
|
221
235
|
potential_recipe = []
|
222
236
|
|
223
|
-
recipe.each do |ingredient|
|
237
|
+
recipe.each.with_index do |ingredient, ing_index|
|
224
238
|
ingredient_id = ingredient['id'] ? ingredient['id'] : ingredient[:id]
|
225
239
|
quant = ingredient['quant'] ? ingredient['quant'] : ingredient[:quant]
|
240
|
+
enhance_level = ingredient['enhance_level'] ? ingredient['enhance_level'] : ingredient[:enhance_level]
|
241
|
+
is_m_recipe = ingredient['is_m_recipe'] ? ingredient['is_m_recipe'] : ingredient[:is_m_recipe]
|
242
|
+
quant = 1 if quant.nil?
|
243
|
+
enhance_level = 0 if enhance_level.nil?
|
226
244
|
|
227
|
-
if @ingredient_cache[ingredient_id]
|
228
|
-
cached_ingredient = { **@ingredient_cache[ingredient_id], quant: quant }
|
245
|
+
if @ingredient_cache[ingredient_id] && EXCHANGE_ITEMS[ingredient_id].nil? == true
|
246
|
+
cached_ingredient = { **@ingredient_cache[ingredient_id], quant: quant, for_recipe_id: recipe_id, is_m_recipe: is_m_recipe }
|
229
247
|
potential_recipe.push cached_ingredient
|
230
248
|
next
|
231
249
|
end
|
232
250
|
|
233
|
-
item_price_info_hash = get_item_price_info ingredient_id, true
|
251
|
+
item_price_info_hash = get_item_price_info ingredient_id, true, enhance_level
|
252
|
+
previous_ingredient_id = recipe.dig(ing_index - 1, :id)
|
253
|
+
previous_ingredient_price = potential_recipe.dig(ing_index - 1, :price)
|
234
254
|
|
235
|
-
|
255
|
+
if item_price_info_hash.nil?
|
256
|
+
next if previous_ingredient_id.nil? || previous_ingredient_price.nil?
|
257
|
+
exchange_info = get_exchange_item_info(previous_ingredient_id, ingredient_id, previous_ingredient_price, quant)
|
258
|
+
next unless exchange_info
|
259
|
+
item_price_info_hash = exchange_info
|
260
|
+
end
|
236
261
|
|
237
262
|
item_price_info = item_price_info_hash.transform_keys { |key|
|
238
263
|
key.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
|
239
264
|
}
|
240
265
|
|
241
|
-
|
242
|
-
|
266
|
+
# gathering out of stock items to show to the user
|
267
|
+
stock_string = "#{quant}x [#{item_price_info[:main_key]}] #{item_price_info[:name].downcase}"
|
268
|
+
if item_price_info[:count] < quant && !@out_of_stock_items.include?(stock_string)
|
269
|
+
@out_of_stock_items.push(stock_string)
|
243
270
|
end
|
244
271
|
|
245
272
|
stock_count = get_stock_count item_price_info
|
@@ -249,6 +276,15 @@ class MarketSearcher
|
|
249
276
|
npc_data = item_price_info if item_price_info[:is_npc_item]
|
250
277
|
price_per_one = npc_data[:price].to_i.zero? ? item_price_info[:price_per_one].to_i : npc_data[:price].to_i
|
251
278
|
|
279
|
+
if @free_ingredients.include?(item_price_info[:main_key].to_s) ||
|
280
|
+
@free_ingredients.include?(item_price_info[:id].to_s) ||
|
281
|
+
(item_price_info[:exchange_with] || []).index { |item| @free_ingredients.include? item.to_s }
|
282
|
+
price_per_one = 0
|
283
|
+
stock_count = Float::INFINITY
|
284
|
+
npc_data[:price] = 0
|
285
|
+
npc_data[:price_per_one] = 0
|
286
|
+
end
|
287
|
+
|
252
288
|
# TODO: this is ridiculous, dedupe the hash values
|
253
289
|
potential_ingredient_hash = {
|
254
290
|
name: item_price_info[:name],
|
@@ -261,13 +297,13 @@ class MarketSearcher
|
|
261
297
|
quant: quant,
|
262
298
|
price_per_one: price_per_one,
|
263
299
|
count: stock_count,
|
264
|
-
|
300
|
+
enhance_level: item_price_info[:enhance_level],
|
265
301
|
**npc_data
|
266
302
|
}
|
267
303
|
|
268
304
|
@ingredient_cache[ingredient_id] = potential_ingredient_hash
|
269
305
|
|
270
|
-
potential_recipe.push potential_ingredient_hash
|
306
|
+
potential_recipe.push({ **potential_ingredient_hash, for_recipe_id: recipe_id, is_m_recipe: is_m_recipe })
|
271
307
|
end
|
272
308
|
|
273
309
|
next unless potential_recipe.length == recipe.length
|
@@ -285,7 +321,7 @@ class MarketSearcher
|
|
285
321
|
average_procs = 1
|
286
322
|
|
287
323
|
if [25, 35].include? item[:main_category]
|
288
|
-
unless /oil of|draught|\[mix\]|\[party\]|immortal:|perfume|indignation/im.match item[:name].downcase
|
324
|
+
unless /oil of|draught|\[mix\]|\[party\]|immortal:|perfume|indignation|flame of|essence of dawn|mystical cleaning oil|whale tendon|leather glaze/im.match item[:name].downcase
|
289
325
|
average_procs = 2.5
|
290
326
|
end
|
291
327
|
|
@@ -299,6 +335,11 @@ class MarketSearcher
|
|
299
335
|
end
|
300
336
|
end
|
301
337
|
|
338
|
+
# 1 in 4 chance to create an imperfect alchemy stone of any specific type with the recipe
|
339
|
+
if /imperfect alchemy stone/.match(item[:name].downcase)
|
340
|
+
average_procs = 0.25
|
341
|
+
end
|
342
|
+
|
302
343
|
filtered_recipes = potential_recipes.filter do |recipe|
|
303
344
|
recipe.all? do |ingredient|
|
304
345
|
if ingredient[:total_in_stock] == Float::INFINITY
|
@@ -317,7 +358,11 @@ class MarketSearcher
|
|
317
358
|
|
318
359
|
return nil if selected_recipe.nil?
|
319
360
|
|
320
|
-
|
361
|
+
# set procs to 1 if 1 blue reagent required
|
362
|
+
average_procs = 1 if selected_recipe.find { |a| a[:name].downcase == 'blue reagent' && a[:quant] == 1 }
|
363
|
+
|
364
|
+
# set procs to 1.5 if using magical lightstone crystals to purify lightstones
|
365
|
+
average_procs = 1.5 if selected_recipe.find { |a| a[:name].downcase == 'magical lightstone crystal' }
|
321
366
|
|
322
367
|
# remove recipes where one ingredient is used twice
|
323
368
|
# this usually happens because of incorrect substitution being
|
@@ -325,8 +370,6 @@ class MarketSearcher
|
|
325
370
|
# are some seriously mysterious alchemy recipes out there...
|
326
371
|
ingredients_already_appeared = []
|
327
372
|
filtered_selected_recipe = selected_recipe.filter do |ingredient|
|
328
|
-
# set procs to 1 if blue reagent required
|
329
|
-
average_procs = 1 if ingredient[:name].downcase == 'blue reagent'
|
330
373
|
return false if ingredients_already_appeared.include? ingredient[:name]
|
331
374
|
ingredients_already_appeared.push(ingredient[:name])
|
332
375
|
true
|
@@ -347,6 +390,8 @@ class MarketSearcher
|
|
347
390
|
content_type: 'application/x-www-form-urlencoded'
|
348
391
|
)
|
349
392
|
|
393
|
+
item_price_data = {} if item_price_data.to_s.downcase.include? 'incapsula incident'
|
394
|
+
|
350
395
|
# assuming we were able to find the item price list
|
351
396
|
if item_price_data&.dig('marketConditionList')
|
352
397
|
item_market_sell_price = item_price_data['marketConditionList']&.last&.dig('pricePerOne').to_i
|
@@ -381,7 +426,6 @@ class MarketSearcher
|
|
381
426
|
# taxed profit on selling max amount of this this recipe, with
|
382
427
|
# average procs accounted for
|
383
428
|
# @type [Integer]
|
384
|
-
#
|
385
429
|
max_taxed_sell_profit_after_procs = (PriceCalculator.calculate_taxed_price(item_market_sell_price * max_potion_count) - total_max_ingredient_cost) * average_procs
|
386
430
|
|
387
431
|
return nil if max_taxed_sell_profit_after_procs.to_s.downcase == 'nan'
|
@@ -389,7 +433,8 @@ class MarketSearcher
|
|
389
433
|
|
390
434
|
recipe_logger = RecipeLogger.new @cli
|
391
435
|
results = recipe_logger.log_recipe_data(item, selected_recipe, max_potion_count, item_market_sell_price, total_ingredient_cost, average_procs, total_max_ingredient_cost, raw_max_market_sell_price, max_taxed_sell_profit_after_procs, raw_profit_with_procs, taxed_sell_profit_after_procs)
|
392
|
-
|
436
|
+
|
437
|
+
{ information: results[:recipe_info], max_profit: max_taxed_sell_profit_after_procs, gain: results[:gain], out_of_stock: @out_of_stock_items }
|
393
438
|
end
|
394
439
|
end
|
395
440
|
|
@@ -403,10 +448,6 @@ class MarketSearcher
|
|
403
448
|
item_info[:total_in_stock].to_i.zero? ? item_info[:count].to_i : item_info[:total_in_stock].to_i
|
404
449
|
end
|
405
450
|
|
406
|
-
# async def do_if_category_matches(options, &procedure)
|
407
|
-
# procedure.call.wait if options[:subcategory] == options[:subcat_to_match] || options[:all_subcategories]
|
408
|
-
# end
|
409
|
-
|
410
451
|
def do_if_category_matches(options, &procedure)
|
411
452
|
procedure.call if options[:all_subcategories]
|
412
453
|
procedure.call if options[:subcategory] == options[:subcat_to_match]
|
data/lib/utils/constants.rb
CHANGED
@@ -2,7 +2,20 @@
|
|
2
2
|
|
3
3
|
require 'date'
|
4
4
|
|
5
|
-
|
5
|
+
require_relative '../../environment'
|
6
|
+
|
7
|
+
if ENV['rvt_dom'].length == 0
|
8
|
+
puts "empty DOM environment variable! set ENV['rvt_dom'] in environment.rb."
|
9
|
+
end
|
10
|
+
|
11
|
+
if ENV['rvt_cookie'].length == 0
|
12
|
+
puts "empty cookie environment variable! set ENV['rvt_cookie'] in environment.rb."
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV['rvt_dom'].length == 0 || ENV['rvt_cookie'].length == 0
|
16
|
+
puts ''
|
17
|
+
exit
|
18
|
+
end
|
6
19
|
|
7
20
|
module Utils
|
8
21
|
# constants to use when user is configuring the tool
|
@@ -16,13 +29,19 @@ module Utils
|
|
16
29
|
other: 'category 35, subcategory 8',
|
17
30
|
blood: 'searches "\'s blood"',
|
18
31
|
oil: "searches 'oil of'",
|
19
|
-
'alchemy stone': "searches 'stone of'",
|
32
|
+
'alchemy stone': "searches 'imperfect alchemy stone of'",
|
20
33
|
reagent: "searches 'reagent'",
|
21
34
|
'black stone': 'category 30, subcategory 1',
|
35
|
+
'food': 'category 35, subcategory 4',
|
22
36
|
'misc': 'category 25, subcategory 8',
|
23
37
|
'other tools': 'category 40, subcategory 10',
|
38
|
+
'manos': "searches 'manos'",
|
39
|
+
'purified lightstone': "searches 'purified lightstone of' (requires guru 1 alchemy)",
|
40
|
+
'combined crystals': 'category 50, subcategory 4',
|
41
|
+
'essences of dawn': "searches 'essence of dawn'",
|
42
|
+
# 'stuffed animals': "searches 'stuffed'",
|
24
43
|
# 'magic crystal': "searches 'magic crystal'",
|
25
|
-
exit: 'stops the search'
|
44
|
+
exit: 'stops the search',
|
26
45
|
}.freeze
|
27
46
|
|
28
47
|
REGION_DOMAINS = {
|
@@ -38,8 +57,8 @@ module Utils
|
|
38
57
|
jp: 'trade.jp.playblackdesert.com',
|
39
58
|
th: 'trade.th.playblackdesert.com',
|
40
59
|
tw: 'trade.tw.playblackdesert.com',
|
41
|
-
sa: '
|
42
|
-
exit: 'stops the search'
|
60
|
+
sa: 'trade.sa.playblackdesert.com',
|
61
|
+
exit: 'stops the search',
|
43
62
|
}.freeze
|
44
63
|
|
45
64
|
REGION_LANGUAGES = {
|
@@ -59,18 +78,19 @@ module Utils
|
|
59
78
|
id: 'basa indonesia',
|
60
79
|
se: 'sea english',
|
61
80
|
gl: 'global lab',
|
62
|
-
exit: 'stops the search'
|
81
|
+
exit: 'stops the search',
|
63
82
|
}.freeze
|
64
83
|
|
65
84
|
AGGRESSION_LEVELS = {
|
66
85
|
normal: 'evaluate one permutation of each recipe',
|
67
86
|
hyperaggressive: 'evaluate every substitution for every recipe',
|
68
|
-
exit: 'stops the search'
|
87
|
+
exit: 'stops the search',
|
69
88
|
}.freeze
|
70
89
|
|
71
|
-
|
72
|
-
|
73
|
-
|
90
|
+
YES_OR_NO = {
|
91
|
+
no: false,
|
92
|
+
yes: true,
|
93
|
+
}.freeze
|
74
94
|
end
|
75
95
|
|
76
96
|
# constants that will be used in by search / scraping scripts
|
@@ -80,8 +100,8 @@ module Utils
|
|
80
100
|
ERROR_LOG = './error.log'
|
81
101
|
# note that the following two tokens are different!! the first one is from a request cookie
|
82
102
|
# the second is from the dom of the actual BDO central market interface
|
83
|
-
COOKIE = '
|
84
|
-
RVT = '
|
103
|
+
COOKIE = ENV['rvt_cookie']
|
104
|
+
RVT = ENV['rvt_dom']
|
85
105
|
WORLD_MARKET_LIST = '/GetWorldMarketList'
|
86
106
|
MARKET_SUB_LIST = '/GetWorldMarketSubList'
|
87
107
|
MARKET_SEARCH_LIST = '/GetWorldMarketSearchList'
|
@@ -99,13 +119,25 @@ module Utils
|
|
99
119
|
'x-cdn': 'Imperva'
|
100
120
|
},
|
101
121
|
bdo_codex_headers: {
|
102
|
-
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
103
122
|
'User-Agent':
|
104
123
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
105
|
-
Dnt: '1'
|
124
|
+
Dnt: '1',
|
125
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
|
106
126
|
}
|
107
127
|
}.freeze
|
108
128
|
|
129
|
+
# TODO: wip...
|
130
|
+
def self.get_bdo_codex_headers(item_id, region)
|
131
|
+
item_url = "https://bdocodex.com/#{region}/item/#{item_id}"
|
132
|
+
cookie_string = "bddatabaselang=#{region}"
|
133
|
+
if rand > 0.5
|
134
|
+
REQUEST_OPTS[:central_market_headers]
|
135
|
+
{ **REQUEST_OPTS[:bdo_codex_headers], 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', Cookie: cookie_string }
|
136
|
+
else
|
137
|
+
{ **REQUEST_OPTS[:bdo_codex_headers], 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', Cookie: cookie_string }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
109
141
|
def self.get_central_market_headers(incap_cookie = '')
|
110
142
|
original_cookie = REQUEST_OPTS[:central_market_headers][:Cookie]
|
111
143
|
if rand > 0.5
|
data/lib/utils/npc_item_index.rb
CHANGED
@@ -155,6 +155,33 @@ module Utils
|
|
155
155
|
count: Float::INFINITY,
|
156
156
|
is_npc_item: true,
|
157
157
|
npc_type: 'blacksmith',
|
158
|
+
},
|
159
|
+
820020 => {
|
160
|
+
name: 'Fused Crystal of Emotions',
|
161
|
+
id: 820020,
|
162
|
+
price: 50_000_000,
|
163
|
+
price_per_one: 50_000_000,
|
164
|
+
count: Float::INFINITY,
|
165
|
+
is_npc_item: true,
|
166
|
+
npc_type: 'blacksmith',
|
167
|
+
},
|
168
|
+
4929 => {
|
169
|
+
name: 'Restoration Stone',
|
170
|
+
id: 4929,
|
171
|
+
price: 1_000,
|
172
|
+
price_per_one: 1_000,
|
173
|
+
count: Float::INFINITY,
|
174
|
+
is_npc_item: true,
|
175
|
+
npc_type: 'blacksmith',
|
176
|
+
},
|
177
|
+
8172 => {
|
178
|
+
name: "Combatant's Restoration Stone",
|
179
|
+
id: 8172,
|
180
|
+
price: 5_000_000,
|
181
|
+
price_per_one: 5_000_000,
|
182
|
+
count: Float::INFINITY,
|
183
|
+
is_npc_item: true,
|
184
|
+
npc_type: 'blacksmith',
|
158
185
|
},
|
159
186
|
6656 => {
|
160
187
|
name: 'Purified Water',
|
data/lib/utils/recipe_logger.rb
CHANGED
@@ -31,41 +31,66 @@ module Utils
|
|
31
31
|
formatted_price = @cli.yellow PriceCalculator.format_price ingredient_market_sell_price
|
32
32
|
formatted_max_price = @cli.yellow PriceCalculator.format_price raw_max_ingredient_sell_price
|
33
33
|
formatted_potion_amount = @cli.yellow PriceCalculator.format_num(ingredient[:quant]).rjust(4, ' ')
|
34
|
+
formatted_ingredient_id = "[#{ingredient[:id]}]".rjust(7, ' ')
|
34
35
|
formatted_max_potion_amount = @cli.yellow PriceCalculator.format_num max_potion_count * ingredient[:quant]
|
35
36
|
formatted_stock_count = @cli.yellow PriceCalculator.format_num ingredient[:total_in_stock]
|
36
37
|
formatted_npc_information = ingredient[:is_npc_item] ? @cli.yellow(" (sold by #{ingredient[:npc_type]} npcs)") : ''
|
38
|
+
if ingredient[:exchange_with_npc]
|
39
|
+
formatted_npc_information = @cli.yellow(" (exchange at least #{ingredient[:must_exchange]} #{ingredient[:exchange_with_names].join(' / ')} for #{ingredient[:exchanging_grants] * ingredient[:must_exchange]} with #{ingredient[:exchange_with_npc]})")
|
40
|
+
end
|
37
41
|
|
38
|
-
|
42
|
+
formatted_enhance_level = ['0', ''].include?(ingredient[:enhance_level].to_s) ? ' ' : " +#{ingredient[:enhance_level]} "
|
43
|
+
|
44
|
+
stock_counts.push "#{formatted_potion_amount} [max: #{formatted_max_potion_amount}] #{formatted_ingredient_id}#{@cli.yellow "#{formatted_enhance_level}#{ingredient[:name].downcase}: #{formatted_stock_count}"} in stock#{formatted_npc_information}. price: #{formatted_price} [for max: #{formatted_max_price}]"
|
39
45
|
end
|
40
46
|
|
41
47
|
market_stock_string = item[:total_in_stock] > 2000 ? @cli.red(PriceCalculator.format_num(item[:total_in_stock])) : @cli.green(PriceCalculator.format_num(item[:total_in_stock]))
|
42
48
|
|
43
49
|
trade_count_string = item[:total_trade_count] < 10_000_000 ? @cli.red(PriceCalculator.format_num(item[:total_trade_count])) : @cli.green(PriceCalculator.format_num(item[:total_trade_count]))
|
44
50
|
|
51
|
+
ratio_string = if item[:total_trade_count] / (item[:total_in_stock].zero? ? 1 : item[:total_in_stock]) < 50_000
|
52
|
+
@cli.red('bad')
|
53
|
+
else
|
54
|
+
@cli.green('good')
|
55
|
+
end
|
56
|
+
|
45
57
|
estimated_craft_time = 1.2
|
46
58
|
calculated_time = max_potion_count * estimated_craft_time
|
47
59
|
crafting_time_string = calculated_time > 21600 ? @cli.red((seconds_to_str(calculated_time))) : @cli.green((seconds_to_str(calculated_time)))
|
48
|
-
silver_per_hour = max_taxed_sell_profit_after_procs.to_f / (calculated_time.to_f / 3600)
|
60
|
+
silver_per_hour = PriceCalculator.format_price(max_taxed_sell_profit_after_procs.to_f / (calculated_time.to_f / 3600))
|
61
|
+
estimated_craft_time_string = "(accounting for average #{estimated_craft_time}s / craft due to typical server delay)"
|
62
|
+
|
63
|
+
if selected_recipe.find { |ingredient| ingredient[:is_m_recipe] == true }
|
64
|
+
crafting_time_string = @cli.yellow "unknown: this is a processing recipe!"
|
65
|
+
estimated_craft_time_string = ''
|
66
|
+
silver_per_hour = 'unknown'
|
67
|
+
end
|
68
|
+
|
69
|
+
return_on_investment = max_taxed_sell_profit_after_procs.to_f / total_max_ingredient_cost.to_f
|
70
|
+
formatted_roi = return_on_investment > 1 ? @cli.green("#{return_on_investment.round(2)}x") : @cli.red("#{return_on_investment.round(2)}x" )
|
49
71
|
|
50
72
|
information = " #{@cli.yellow "[#{item[:id]}] [#{item[:name].downcase}], recipe id: #{selected_recipe[0][:for_recipe_id]}"}
|
51
73
|
|
52
74
|
#{padstr("market price of item")}#{@cli.yellow PriceCalculator.format_price item_market_sell_price}
|
53
75
|
#{padstr("market stock of item")}#{market_stock_string}
|
54
76
|
#{padstr("total trades of item")}#{trade_count_string}
|
77
|
+
#{padstr("ratio")}#{ratio_string}
|
55
78
|
#{padstr("you can craft")}#{@cli.yellow PriceCalculator.format_num max_potion_count}
|
56
79
|
#{padstr("theoretical max output")}#{@cli.yellow PriceCalculator.format_num(max_potion_count * average_procs)}
|
57
80
|
#{padstr("cost of ingredients")}#{@cli.yellow PriceCalculator.format_price total_ingredient_cost} (accounting for average #{average_procs} / craft)
|
58
81
|
#{padstr("max cost of ingredients")}#{@cli.yellow PriceCalculator.format_price total_max_ingredient_cost} (accounting for average #{average_procs} / craft)
|
59
|
-
#{padstr("time to craft max")}#{crafting_time_string}
|
82
|
+
#{padstr("time to craft max")}#{crafting_time_string} #{estimated_craft_time_string}
|
60
83
|
|
61
84
|
\t#{stock_counts.join "\n\t"}
|
62
85
|
|
63
86
|
total income for max items before cost subtraction #{@cli.green PriceCalculator.format_price raw_max_market_sell_price}
|
64
|
-
#{padstr("total taxed silver per hour")}#{@cli.green
|
87
|
+
#{padstr("total taxed silver per hour")}#{@cli.green silver_per_hour } silver / hour
|
65
88
|
#{padstr("total untaxed profit")}#{@cli.green PriceCalculator.format_price raw_profit_with_procs} [max: #{@cli.green PriceCalculator.format_price(raw_profit_with_procs * max_potion_count)}]
|
66
89
|
#{padstr("total taxed profit")}#{@cli.green PriceCalculator.format_price taxed_sell_profit_after_procs} [max: #{@cli.green PriceCalculator.format_price(max_taxed_sell_profit_after_procs)}]
|
90
|
+
#{padstr("multiply initial investment by")}#{formatted_roi}
|
67
91
|
"
|
68
|
-
|
92
|
+
|
93
|
+
{ recipe_info: information, gain: max_taxed_sell_profit_after_procs }
|
69
94
|
end
|
70
95
|
end
|
71
96
|
end
|
data/lib/utils/user_cli.rb
CHANGED
@@ -8,51 +8,68 @@ module Utils
|
|
8
8
|
class UserCLI
|
9
9
|
attr_accessor :silent
|
10
10
|
|
11
|
+
# @param [Hash] options options from ARGV
|
11
12
|
def initialize(options)
|
12
13
|
@silent = options[:silent]
|
13
14
|
@prompt = TTY::Prompt.new
|
14
15
|
end
|
15
16
|
|
17
|
+
# uses Rainbow to make a string yellow
|
18
|
+
# @param [String] string the string to print
|
19
|
+
# @return [String]
|
16
20
|
def yellow(string)
|
17
21
|
Rainbow(string).yellow
|
18
22
|
end
|
19
23
|
|
24
|
+
# uses Rainbow to make a string orange
|
25
|
+
# @param [String] string the string to print
|
26
|
+
# @return [String]
|
20
27
|
def orange(string)
|
21
28
|
Rainbow(string).orange
|
22
29
|
end
|
23
30
|
|
31
|
+
# uses Rainbow to make a string red
|
32
|
+
# @param [String] string the string to print
|
24
33
|
def red(string)
|
25
34
|
Rainbow(string).red
|
26
35
|
end
|
27
36
|
|
37
|
+
# uses Rainbow to make a string green
|
38
|
+
# @param [String] string the string to print
|
28
39
|
def green(string)
|
29
40
|
Rainbow(string).green
|
30
41
|
end
|
31
42
|
|
32
|
-
def cyan(string)
|
33
|
-
Rainbow(string).cyan
|
34
|
-
end
|
35
|
-
|
36
43
|
# log, but cutely
|
44
|
+
# @param [String] string the string to print
|
37
45
|
def vipiko(string)
|
38
46
|
puts string unless @silent
|
39
47
|
end
|
40
48
|
|
49
|
+
# calls print on a string preceded by a line erase and carriage return
|
50
|
+
# also calls $stdout.flush
|
51
|
+
# @param [String] string the string to print
|
41
52
|
def vipiko_overwrite(string)
|
42
53
|
print "\033[2K\r#{string}"
|
43
54
|
$stdout.flush
|
44
55
|
end
|
45
56
|
|
46
57
|
# log for other stuff
|
58
|
+
# @param [String] string the string to print
|
47
59
|
def log(string)
|
48
60
|
puts string
|
49
61
|
end
|
50
62
|
|
63
|
+
# adds usage info to a tty-prompt menu
|
64
|
+
# @param menu the tty-prompt menu object in the select() block
|
65
|
+
# @param [Hash] options the hash of options
|
51
66
|
def add_menu_info(menu, options)
|
52
67
|
menu.enum '.'
|
53
68
|
menu.help "use arrow keys or numbers 1-#{options.length} to navigate, press enter to select. type to search."
|
54
69
|
end
|
55
70
|
|
71
|
+
# prompt the user to choose the category to search through
|
72
|
+
# @return [String] the selected category
|
56
73
|
def choose_category
|
57
74
|
vipiko "\n♫ hello! oh? you want to sell #{yellow 'potions'} today? that sounds like fun!\n\n"
|
58
75
|
|
@@ -66,6 +83,8 @@ module Utils
|
|
66
83
|
option.to_s
|
67
84
|
end
|
68
85
|
|
86
|
+
# prompt the user to choose the region to search within
|
87
|
+
# @return [String] the selected region
|
69
88
|
def choose_region
|
70
89
|
option = @prompt.select('and where are you in the world?', { cycle: true, filter: true }) do |menu|
|
71
90
|
add_menu_info(menu, CLIConstants::REGION_DOMAINS)
|
@@ -77,6 +96,8 @@ module Utils
|
|
77
96
|
option.to_s
|
78
97
|
end
|
79
98
|
|
99
|
+
# prompt the user to choose the language to show results in
|
100
|
+
# @return [String] the selected language
|
80
101
|
def choose_lang
|
81
102
|
option = @prompt.select('what language would you like to search bdocodex with?', { cycle: true, filter: true }) do |menu|
|
82
103
|
add_menu_info(menu, CLIConstants::REGION_LANGUAGES)
|
@@ -88,8 +109,11 @@ module Utils
|
|
88
109
|
option.to_s
|
89
110
|
end
|
90
111
|
|
112
|
+
# prompt the user to choose the aggression to search with
|
113
|
+
# higher aggression will cause all recipe permutations / item substitutions to be considered in profit calculation
|
114
|
+
# @return [String] the selected aggression level
|
91
115
|
def choose_aggression
|
92
|
-
option = @prompt.select('
|
116
|
+
option = @prompt.select('what aggression level would you like to search for recipes with?', { cycle: true, filter: true }) do |menu|
|
93
117
|
add_menu_info(menu, CLIConstants::AGGRESSION_LEVELS)
|
94
118
|
CLIConstants::AGGRESSION_LEVELS.each do |k, v|
|
95
119
|
menu.choice "#{k} #{Rainbow(v).faint.white}", k
|
@@ -99,11 +123,32 @@ module Utils
|
|
99
123
|
option.to_s
|
100
124
|
end
|
101
125
|
|
126
|
+
# prompt the user to input item ids that they already have a lot of
|
127
|
+
# the script will consider these as "free" and set their cost to zero and stock to infinite
|
128
|
+
# @return [Array] the selected item ids
|
129
|
+
def choose_free_ingredients
|
130
|
+
option = @prompt.ask('what item ids do you have a lot of already? we can consider that in the final cost calculations.', convert: :list, default: [])
|
131
|
+
|
132
|
+
option&.to_a if option&.to_a.is_a? Array
|
133
|
+
end
|
134
|
+
|
135
|
+
# prompt the user to decide whether to show or hide a list of out of stock items
|
136
|
+
# @return [Boolean] true to show out of stock items, false to hide
|
137
|
+
def choose_show_out_of_stock
|
138
|
+
option = @prompt.select('finally, would you like to see a list the ingredients that were out of stock at the end?', { cycle: true, filter: true }) do |menu|
|
139
|
+
add_menu_info(menu, CLIConstants::YES_OR_NO)
|
140
|
+
CLIConstants::YES_OR_NO.each do |k, v|
|
141
|
+
menu.choice "#{k}", v
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
!!option
|
146
|
+
end
|
147
|
+
|
148
|
+
# ends the program
|
102
149
|
def end_cli
|
103
150
|
vipiko "\nnever mind, let's do some #{yellow 'cooking'} together instead! ♫\n\n"
|
104
151
|
|
105
|
-
@prompt.keypress('(press any key to exit)')
|
106
|
-
|
107
152
|
exit
|
108
153
|
end
|
109
154
|
end
|
data/lib/version.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bdoap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jpegzilla
|
@@ -22,6 +22,7 @@ files:
|
|
22
22
|
- lib/bdo_alchemy_profits.rb
|
23
23
|
- lib/bdo_codex/bdo_codex_searcher.rb
|
24
24
|
- lib/central_market/category_search_options.rb
|
25
|
+
- lib/central_market/exchange_items.rb
|
25
26
|
- lib/central_market/market_searcher.rb
|
26
27
|
- lib/utils/array_utils.rb
|
27
28
|
- lib/utils/constants.rb
|
@@ -30,6 +31,7 @@ files:
|
|
30
31
|
- lib/utils/price_calculator.rb
|
31
32
|
- lib/utils/recipe_logger.rb
|
32
33
|
- lib/utils/user_cli.rb
|
34
|
+
- lib/version.rb
|
33
35
|
homepage: https://github.com/jpegzilla/bdo-alchemy-profits
|
34
36
|
licenses:
|
35
37
|
- MIT
|