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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2c463012b75ab5e6a2ad6d2558aaf0d13d4ca815b0092d7384efa44903c124d
4
- data.tar.gz: ffd6889945d22837ba488133f5d364d086352dd945adf95cb4adb6512036963e
3
+ metadata.gz: a4336121be8b520924050e57400f46e24661e9c9dc4156def8b08802e6eec2e2
4
+ data.tar.gz: 5011af8aa6f7f7a044786ca134ca8d4cce0bb3ae076409de70cc69a1adf8d6f9
5
5
  SHA512:
6
- metadata.gz: cf5c5d4011b0febc18953536d8bd94d90fa1df257386685dbce8e6d1dbc745e8ed4afbab2f079be71e1f2d13767802cf8d054ffeb90f2c2342f2dc9093e39c29
7
- data.tar.gz: 2c6626ef2c4222dc002db072403d55bec2c06c7b87a860795b739d1b312419283ab6fdc3b6ace8a31f1da9374131a6045d3fd8ce3b2dbd581c40add167cf1ba5
6
+ metadata.gz: 0b055768a4bad1ebc9b244574d546dd859c4db6f7475d72afd7352d24b3aa9db5a311f6c74b6ce160ae86756ebbbc249df87c8d17b2294bba1a575bec8b6e96d
7
+ data.tar.gz: f5537518eebae1c5f62d63d9daaa0b2ee29d1570940a165e0c7dcee1b033ed728909f6f7f3940edc51d5dc00672363f8639adb8f8a64780ecc3ed9706716ffa5
data/bin/bdoap CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
+ # bin/bdoap
2
3
  # frozen_string_literal: true
3
4
 
4
5
  require_relative '../lib/bdo_alchemy_profits'
5
6
 
6
- profit_calculator = BDOAlchemyProfits.new
7
+ profit_calculator = BDOAP::BDOAlchemyProfits.new
7
8
 
8
9
  profit_calculator.start_cli
@@ -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
- class BDOAlchemyProfits
33
- include Utils
34
-
35
- def start_cli
36
- options = {}
37
-
38
- OptionParser.new do |opt|
39
- opt.on('--silent', '-s') { options[:silent] = true }
40
- end.parse!
41
-
42
- cli = UserCLI.new options
43
-
44
- # option setup
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
- category = cli.choose_category
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
- cli.end_cli if category == 'exit'
138
+ OptionParser.new do |opt|
139
+ opt.on('--silent', '-s') { options[:silent] = true }
140
+ end.parse!
49
141
 
50
- region = cli.choose_region
142
+ # option setup
143
+ cli = UserCLI.new options
51
144
 
52
- cli.end_cli if region == 'exit'
145
+ unless enhancing
146
+ category = cli.choose_category
147
+ cli.end_cli if category == 'exit'
148
+ end
53
149
 
54
- lang = cli.choose_lang
150
+ region = cli.choose_region
151
+ cli.end_cli if region == 'exit'
55
152
 
56
- cli.end_cli if lang == 'exit'
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
- aggression = cli.choose_aggression
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
- cli.end_cli if aggression == 'exit'
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
- if aggression == 'hyperaggressive'
63
- 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.")
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
- # start searching
173
+ market_searcher = MarketSearcher.new(region, cli, free_ingredients)
67
174
 
68
- cli.vipiko("\n♫ let's see if #{cli.yellow category} alchemy items are profitable in #{cli.yellow region}!")
175
+ market_item_list = market_searcher.get_alchemy_market_data category
69
176
 
70
- market_searcher = MarketSearcher.new region, cli
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
- market_item_list = market_searcher.get_alchemy_market_data category
181
+ bdo_codex_searcher = BDOCodexSearcher.new(region, lang, cli, aggression == 'hyperaggressive')
73
182
 
74
- cli.vipiko("I'll look for #{cli.yellow(market_item_list.length)} item#{
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
- bdo_codex_searcher = BDOCodexSearcher.new(region, lang, cli, aggression == 'hyperaggressive')
185
+ recipe_prices = market_searcher.get_all_recipe_prices item_codex_data, category
79
186
 
80
- item_codex_data = bdo_codex_searcher.get_item_codex_data market_item_list
187
+ mapped_prices = recipe_prices.reverse.sort_by { |recipe| recipe[:gain].to_i }.map { |recipe| recipe[:information] }
81
188
 
82
- recipe_prices = market_searcher.get_all_recipe_prices item_codex_data, category
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
- mapped_prices = recipe_prices.sort_by { |recipe| recipe[:silver_per_hour].to_i }.map { |recipe| recipe[:information] }
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
- if mapped_prices.length > 0
87
- cli.vipiko_overwrite "done!"
88
- puts "\n\n"
89
- puts mapped_prices
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', 'type', 'icon', 'total weight of materials'].include? category
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
- result[:element] = element.text.downcase if category == 'title'
142
+ if %w[title id].include? category
143
+ result[:element] = element.text.downcase
144
+ end
127
145
 
128
- result[:element] = element.text.downcase if category == 'id'
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
- # houserecipe_direct_url = "https://bdocodex.com/query.php?a=designs&type=product&item_id=#{item_id}&l=#{@region_lang}"
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 == 'harmony draught'
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
- @cache.write cache_data
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: item[:price_per_one],
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
- # update: ->(data) { { blackStoneResponse: data['list'] } },
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
- # update: ->(data) { { blackStoneResponse: data['list'] } },
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
- # update: ->(data) { { blackStoneResponse: data['list'] } },
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
- # update: ->(data) { { bloodResponse: data['list'] } },
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
- # update: ->(data) { { offensiveResponse: data['marketList'] } },
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
- def get_item_price_info(ingredient_id, is_recipe_ingredient = true)
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(1..3)
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
- ingredient_data = {} if ingredient_data.include? 'use a different browser'
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'][0]
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=0&chooseKey=0&isUp=true&keyType=0&name=#{URI.encode_www_form_component(resolved_data['name'])}"
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 |(recipe_id, recipe)|
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
- next if item_price_info_hash.nil?
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
- if item_price_info[:count].zero? && rand > 0.5
242
- out_of_stock_items.push(item_price_info[:name].downcase)
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
- for_recipe_id: recipe_id,
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
- average_procs = 1 if selected_recipe.find { |a| a[:name].downcase == 'blue reagent' } != nil
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
- { information: results[:recipe_info], max_profit: max_taxed_sell_profit_after_procs, silver_per_hour: results[:silver_per_hour] }
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]
@@ -2,7 +2,20 @@
2
2
 
3
3
  require 'date'
4
4
 
5
- $request_verification_token = '__RequestVerificationToken=aVYGQPovG8EI6bRIagh8tbHJUhZlM-nH3UKVQaV9R9N0vODzmWcB747BHEsHaphwANvzsaNi5TCPlB-72-e1LadqAlL-bdkDkTqVh4gMnu81' # rubocop:disable Layout/LineLength
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: 'blackdesert-tradeweb.playredfox.com',
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
- def self.set_rvt(rvt)
72
- $request_verification_token = rvt
73
- end
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 = '__RequestVerificationToken=aVYGQPovG8EI6bRIagh8tbHJUhZlM-nH3UKVQaV9R9N0vODzmWcB747BHEsHaphwANvzsaNi5TCPlB-72-e1LadqAlL-bdkDkTqVh4gMnu81' # rubocop:disable Layout/LineLength
84
- RVT = '__RequestVerificationToken=yWbqJmiU4wcp2IRQGkbDfqMFs2fjCYx4UqVxg4umK8CvdbLhweMLZ1es-4SFWD8J1UfoqwbaQyo_YuzAkSzHWY8Wjyx4ttwZBvtu_pd--9U1' # rubocop:disable Layout/LineLength
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
@@ -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',
@@ -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
- stock_counts.push "#{formatted_potion_amount} [max: #{formatted_max_potion_amount}] [#{ingredient[:id]}] #{@cli.yellow "#{ingredient[:name].downcase}: #{formatted_stock_count}"} in stock#{formatted_npc_information}. price: #{formatted_price} [for max: #{formatted_max_price}]"
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} (accounting for average #{estimated_craft_time}s / craft due to typical server delay)
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 PriceCalculator.format_price(silver_per_hour) } silver / hour
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
- { recipe_info: information, silver_per_hour: silver_per_hour }
92
+
93
+ { recipe_info: information, gain: max_taxed_sell_profit_after_procs }
69
94
  end
70
95
  end
71
96
  end
@@ -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('finally, what aggression level would you like to search for recipes with?', { cycle: true, filter: true }) do |menu|
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BDOAP
4
+ VERSION = "0.0.4"
5
+ end
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.1
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