bdoap 0.0.1 → 0.0.3

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: 2fabb7eadd3b5446aa42832f5ce1164777c8ed9669c1679a16d61138e9be9de7
4
+ data.tar.gz: 20232aec55bb79082273f1dcdadcc04347d83948b9f3cd2977143672a247d19e
5
5
  SHA512:
6
- metadata.gz: cf5c5d4011b0febc18953536d8bd94d90fa1df257386685dbce8e6d1dbc745e8ed4afbab2f079be71e1f2d13767802cf8d054ffeb90f2c2342f2dc9093e39c29
7
- data.tar.gz: 2c6626ef2c4222dc002db072403d55bec2c06c7b87a860795b739d1b312419283ab6fdc3b6ace8a31f1da9374131a6045d3fd8ce3b2dbd581c40add167cf1ba5
6
+ metadata.gz: d7ce743bbc6e663ae0967e4a4a1117611ed87d83c3a6b793a76b5c635aafc8270652e4fc489644f87bb70cc0d5b5a73865314c56a6d0a4150a42baa08e2ccdfd
7
+ data.tar.gz: 1af804e8da23ec52a1551e1e0db2da5fc2b975a7da9ada2ef1ef0b51ed2df46f4fbf4251226aa0002e4e25c1d198e2ac8e78d873332c7238eb6fb072b567e979
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
@@ -29,66 +29,74 @@ require_relative './utils/user_cli'
29
29
  require_relative './central_market/market_searcher'
30
30
  require_relative './bdo_codex/bdo_codex_searcher'
31
31
 
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
45
-
46
- category = cli.choose_category
47
-
48
- cli.end_cli if category == 'exit'
49
-
50
- region = cli.choose_region
51
-
52
- cli.end_cli if region == 'exit'
53
-
54
- lang = cli.choose_lang
55
-
56
- cli.end_cli if lang == 'exit'
57
-
58
- aggression = cli.choose_aggression
59
-
60
- cli.end_cli if aggression == 'exit'
61
-
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
65
-
66
- # start searching
67
-
68
- cli.vipiko("\n♫ let's see if #{cli.yellow category} alchemy items are profitable in #{cli.yellow region}!")
69
-
70
- market_searcher = MarketSearcher.new region, cli
71
-
72
- market_item_list = market_searcher.get_alchemy_market_data category
73
-
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"}!")
77
-
78
- bdo_codex_searcher = BDOCodexSearcher.new(region, lang, cli, aggression == 'hyperaggressive')
79
-
80
- item_codex_data = bdo_codex_searcher.get_item_codex_data market_item_list
81
-
82
- recipe_prices = market_searcher.get_all_recipe_prices item_codex_data, category
83
-
84
- mapped_prices = recipe_prices.sort_by { |recipe| recipe[:silver_per_hour].to_i }.map { |recipe| recipe[:information] }
85
-
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!"
32
+ # main module for bdoap. contains the BDOAlchemyProfits class
33
+ module BDOAP
34
+ # used to search for profitable alchemy recipes
35
+ class BDOAlchemyProfits
36
+ include Utils
37
+
38
+ # begin the configuration and search process
39
+ def start_cli
40
+ begin
41
+ options = {}
42
+
43
+ OptionParser.new do |opt|
44
+ opt.on('--silent', '-s') { options[:silent] = true }
45
+ end.parse!
46
+
47
+ # option setup
48
+ cli = UserCLI.new options
49
+
50
+ category = cli.choose_category
51
+ cli.end_cli if category == 'exit'
52
+ region = cli.choose_region
53
+ cli.end_cli if region == 'exit'
54
+ lang = cli.choose_lang
55
+ cli.end_cli if lang == 'exit'
56
+ aggression = cli.choose_aggression
57
+ cli.end_cli if aggression == 'exit'
58
+ free_ingredients = cli.choose_free_ingredients
59
+ show_out_of_stock = cli.choose_show_out_of_stock
60
+
61
+ if aggression == 'hyperaggressive'
62
+ 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.")
63
+ end
64
+
65
+ # start searching
66
+ cli.vipiko("\n♫ let's see if #{cli.yellow category} alchemy items are profitable in #{cli.yellow region}!")
67
+
68
+ market_searcher = MarketSearcher.new(region, cli, free_ingredients)
69
+
70
+ market_item_list = market_searcher.get_alchemy_market_data category
71
+
72
+ cli.vipiko("I'll look for #{cli.yellow(market_item_list.length.to_s)} item#{
73
+ market_item_list.empty? || market_item_list.length > 1 ? 's' : ''
74
+ } in #{category == 'all' ? cli.yellow('all categories'): "the #{cli.yellow category} category"}!")
75
+
76
+ bdo_codex_searcher = BDOCodexSearcher.new(region, lang, cli, aggression == 'hyperaggressive')
77
+
78
+ item_codex_data = bdo_codex_searcher.get_item_codex_data market_item_list
79
+
80
+ recipe_prices = market_searcher.get_all_recipe_prices item_codex_data, category
81
+
82
+ mapped_prices = recipe_prices.reverse.sort_by { |recipe| recipe[:gain].to_i }.map { |recipe| recipe[:information] }
83
+
84
+ out_of_stock = recipe_prices.dig(0, :out_of_stock) || []
85
+ out_of_stock_list = ""
86
+ out_of_stock.each { |item| out_of_stock_list += "\n\t #{cli.yellow item}" }
87
+
88
+ if mapped_prices.length > 0
89
+ cli.vipiko_overwrite "done!"
90
+ puts "\n\n"
91
+ puts mapped_prices
92
+ else
93
+ cli.vipiko_overwrite "none of those recipes look profitable right now...let's go gathering!\n\n"
94
+ end
95
+
96
+ puts " items that were out of stock: #{out_of_stock_list}" if show_out_of_stock && out_of_stock_list.length > 0
97
+ rescue Interrupt => e
98
+ puts "\n\nstopping!"
99
+ end
92
100
  end
93
101
  end
94
102
  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,
@@ -23,49 +23,48 @@ module MarketSearchTools
23
23
  name: 'black stone',
24
24
  url: url,
25
25
  query_string: "#{ENVData::RVT}&mainCategory=30&subCategory=1",
26
- # update: ->(data) { { blackStoneResponse: data['list'] } },
27
- update: ->(data) { data['list'] }
26
+ update: ->(data) { data['marketList'] }
28
27
  },
29
28
  {
30
29
  name: 'misc',
31
30
  url: url,
32
31
  query_string: "#{ENVData::RVT}&mainCategory=25&subCategory=8",
33
- # update: ->(data) { { blackStoneResponse: data['list'] } },
34
- update: ->(data) { data['list'] }
32
+ update: ->(data) { data['marketList'] }
35
33
  },
36
34
  {
37
35
  name: 'other tools',
38
36
  url: url,
39
37
  query_string: "#{ENVData::RVT}&mainCategory=40&subCategory=10",
40
- # update: ->(data) { { blackStoneResponse: data['list'] } },
41
- update: ->(data) { data['list'] }
38
+ update: ->(data) { data['marketList'] }
42
39
  },
43
40
  {
44
41
  name: 'blood',
45
42
  url: search_url,
46
43
  query_string: "#{ENVData::RVT}&searchText='s+blood",
47
- # update: ->(data) { { bloodResponse: data['list'] } },
44
+ update: ->(data) { data['list'] }
45
+ },
46
+ {
47
+ name: 'essences of dawn',
48
+ url: search_url,
49
+ query_string: "#{ENVData::RVT}&searchText=essence+of+dawn",
48
50
  update: ->(data) { data['list'] }
49
51
  },
50
52
  {
51
53
  name: 'reagent',
52
54
  url: search_url,
53
55
  query_string: "#{ENVData::RVT}&searchText=reagent",
54
- # update: ->(data) { { reagentResponse: data['list'] } },
55
56
  update: ->(data) { data['list'] }
56
57
  },
57
58
  {
58
59
  name: 'oil',
59
60
  url: search_url,
60
61
  query_string: "#{ENVData::RVT}&searchText=oil+of",
61
- # update: ->(data) { { oilResponse: data['list'] } },
62
62
  update: ->(data) { data['list'] }
63
63
  },
64
64
  {
65
65
  name: 'alchemy stone',
66
66
  url: search_url,
67
- query_string: "#{ENVData::RVT}&searchText=stone+of",
68
- # update: ->(data) { { alchemyStoneResponse: data['list'] } },
67
+ query_string: "#{ENVData::RVT}&searchText=imperfect+alchemy+stone+of",
69
68
  update: ->(data) { data['list'].filter { |i| i['grade'] == 0 } }
70
69
  },
71
70
  # {
@@ -75,12 +74,17 @@ module MarketSearchTools
75
74
  # # update: ->(data) { { magicCrystalResponse: data['list'] } },
76
75
  # update: ->(data) { data['list'] }
77
76
  # },
77
+ {
78
+ name: 'combined crystals',
79
+ url: url,
80
+ query_string: "#{ENVData::RVT}&mainCategory=50&subCategory=4",
81
+ update: ->(data) { data['marketList'] }
82
+ },
78
83
  {
79
84
  name: 'offensive',
80
85
  url: url,
81
86
  query_string:
82
87
  "#{ENVData::RVT}&mainCategory=#{CONSUMABLE_CATEGORY}&subCategory=#{CONSUMABLE_SUBCATEGORIES[:offensive]}",
83
- # update: ->(data) { { offensiveResponse: data['marketList'] } },
84
88
  update: ->(data) { data['marketList'] }
85
89
  },
86
90
  {
@@ -114,6 +118,20 @@ module MarketSearchTools
114
118
  "#{ENVData::RVT}&mainCategory=#{CONSUMABLE_CATEGORY}&subCategory=#{CONSUMABLE_SUBCATEGORIES[:other]}",
115
119
  # update: ->(data) { { otherResponse: data['marketList'] } },
116
120
  update: ->(data) { data['marketList'] }
121
+ },
122
+ {
123
+ name: 'manos',
124
+ url: search_url,
125
+ query_string: "#{ENVData::RVT}&searchText=manos",
126
+ # update: ->(data) { { otherResponse: data['marketList'] } },
127
+ update: ->(data) { data['list'] }
128
+ },
129
+ {
130
+ name: 'purified lightstone',
131
+ url: search_url,
132
+ query_string: "#{ENVData::RVT}&searchText=purified+lightstone+of",
133
+ # update: ->(data) { { otherResponse: data['marketList'] } },
134
+ update: ->(data) { data['list'] }
117
135
  }
118
136
  ]
119
137
  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}"
@@ -23,6 +25,8 @@ class MarketSearcher
23
25
  @market_sell_buy_url = "#{@root_url}#{ENVData::MARKET_SELL_BUY_INFO}"
24
26
  @cli = cli
25
27
  @ingredient_cache = {}
28
+ @free_ingredients = free_ingredients
29
+ @out_of_stock_items = []
26
30
  end
27
31
 
28
32
  def get_alchemy_market_data(category)
@@ -61,6 +65,9 @@ class MarketSearcher
61
65
  puts
62
66
  mapped_aggregate = filtered_aggregate.map.with_index do |elem, index|
63
67
 
68
+ # TODO: remove this check
69
+ # next unless elem['name'].downcase == 'essence of dawn - damage reduction'
70
+
64
71
  @cli.vipiko_overwrite "(#{index + 1} / #{filtered_aggregate.length}) researching #{@cli.yellow elem['name'].downcase}... (category: #{subcategory})"
65
72
 
66
73
  begin
@@ -129,7 +136,8 @@ class MarketSearcher
129
136
  aggregate_response.flatten
130
137
  end
131
138
 
132
- def get_item_price_info(ingredient_id, is_recipe_ingredient = true)
139
+ # set is_recipe_ingredient = true if you're using this function to get the cost of buying an ingredient
140
+ def get_item_price_info(ingredient_id, is_recipe_ingredient = true, enhance_level = 0)
133
141
  npc_item = NPCItemIndex.get_item(ingredient_id)
134
142
 
135
143
  return npc_item if npc_item
@@ -145,7 +153,7 @@ class MarketSearcher
145
153
  content_type: 'application/x-www-form-urlencoded'
146
154
  )
147
155
 
148
- sleep rand(1..3)
156
+ sleep rand
149
157
  rescue StandardError => error
150
158
  puts @cli.red("this could be a network failure. get_item_price_info broke.")
151
159
 
@@ -156,12 +164,15 @@ class MarketSearcher
156
164
  end
157
165
  end
158
166
 
159
- ingredient_data = {} if ingredient_data.include? 'use a different browser'
167
+ # TODO: actually implement a normal way of handling malformed results
168
+ ingredient_data = {} if ingredient_data.to_s.downcase.include? 'use a different browser'
169
+ ingredient_data = {} if ingredient_data.to_s.downcase.include? 'incapsula incident'
160
170
 
161
171
  if ingredient_data.dig('detailList')
162
- resolved_data = ingredient_data['detailList'][0]
172
+ resolved_data = ingredient_data['detailList'].find { |entry| entry['subKey'].to_i == enhance_level.to_i }
173
+ resolved_data = ingredient_data['detailList'][0] if resolved_data.nil?
163
174
  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'])}"
175
+ 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
176
 
166
177
  detailed_price_list = {}
167
178
 
@@ -172,6 +183,9 @@ class MarketSearcher
172
183
  body: body_string,
173
184
  content_type: 'application/x-www-form-urlencoded'
174
185
  )
186
+
187
+ detailed_price_list = {} if detailed_price_list.to_s.downcase.include? 'incapsula incident'
188
+
175
189
  sleep rand
176
190
  rescue StandardError => error
177
191
  puts @cli.red("this could be a network failure. get_item_price_info broke.")
@@ -193,53 +207,65 @@ class MarketSearcher
193
207
 
194
208
  if optimal_price
195
209
  if optimal_price['pricePerOne'] && optimal_price['sellCount']
196
- { **resolved_data, count: total_stock, pricePerOne: optimal_price['pricePerOne'] }
210
+ { **resolved_data, count: total_stock, pricePerOne: optimal_price['pricePerOne'], enhanceLevel: enhance_level }
197
211
  else
198
- { **resolved_data, count: total_stock, pricePerOne: resolved_data['pricePerOne'] }
212
+ { **resolved_data, count: total_stock, pricePerOne: resolved_data['pricePerOne'], enhanceLevel: enhance_level }
199
213
  end
200
214
  end
201
215
  end
202
216
  end
203
-
204
217
  end
205
218
  end
206
219
 
207
220
  def get_all_recipe_prices(item_codex_data, subcategory)
208
221
  mapped_recipe_prices = []
209
- out_of_stock_items = []
210
- # vipiko is about to start writing carriage returns,
211
- # so printing newline here
212
222
 
213
223
  item_codex_data.each.with_index do |item_with_recipe, index|
214
224
  potential_recipes = []
215
225
  name = item_with_recipe[:name].downcase
216
226
  recipe_list = item_with_recipe[:recipe_list]
217
227
 
228
+ # TODO: remove this line
229
+ # next unless name == 'essence of dawn - damage reduction'
230
+
218
231
  @cli.vipiko_overwrite "(#{index + 1} / #{item_codex_data.length}) I'll ask a merchant about the price of ingredients for #{@cli.yellow name}!"
219
232
 
220
- recipe_list.each do |(recipe_id, recipe)|
233
+ recipe_list.each do |recipe_id, recipe|
221
234
  potential_recipe = []
222
235
 
223
- recipe.each do |ingredient|
236
+ recipe.each.with_index do |ingredient, ing_index|
224
237
  ingredient_id = ingredient['id'] ? ingredient['id'] : ingredient[:id]
225
238
  quant = ingredient['quant'] ? ingredient['quant'] : ingredient[:quant]
239
+ enhance_level = ingredient['enhance_level'] ? ingredient['enhance_level'] : ingredient[:enhance_level]
240
+ is_m_recipe = ingredient['is_m_recipe'] ? ingredient['is_m_recipe'] : ingredient[:is_m_recipe]
241
+ quant = 1 if quant.nil?
242
+ enhance_level = 0 if enhance_level.nil?
226
243
 
227
- if @ingredient_cache[ingredient_id]
228
- cached_ingredient = { **@ingredient_cache[ingredient_id], quant: quant }
244
+ if @ingredient_cache[ingredient_id] && EXCHANGE_ITEMS[ingredient_id].nil? == true
245
+ cached_ingredient = { **@ingredient_cache[ingredient_id], quant: quant, for_recipe_id: recipe_id, is_m_recipe: is_m_recipe }
229
246
  potential_recipe.push cached_ingredient
230
247
  next
231
248
  end
232
249
 
233
- item_price_info_hash = get_item_price_info ingredient_id, true
250
+ item_price_info_hash = get_item_price_info ingredient_id, true, enhance_level
251
+ previous_ingredient_id = recipe.dig(ing_index - 1, :id)
252
+ previous_ingredient_price = potential_recipe.dig(ing_index - 1, :price)
234
253
 
235
- next if item_price_info_hash.nil?
254
+ if item_price_info_hash.nil?
255
+ next if previous_ingredient_id.nil? || previous_ingredient_price.nil?
256
+ exchange_info = get_exchange_item_info(previous_ingredient_id, ingredient_id, previous_ingredient_price, quant)
257
+ next unless exchange_info
258
+ item_price_info_hash = exchange_info
259
+ end
236
260
 
237
261
  item_price_info = item_price_info_hash.transform_keys { |key|
238
262
  key.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
239
263
  }
240
264
 
241
- if item_price_info[:count].zero? && rand > 0.5
242
- out_of_stock_items.push(item_price_info[:name].downcase)
265
+ # gathering out of stock items to show to the user
266
+ stock_string = "#{quant}x [#{item_price_info[:main_key]}] #{item_price_info[:name].downcase}"
267
+ if item_price_info[:count] < quant && !@out_of_stock_items.include?(stock_string)
268
+ @out_of_stock_items.push(stock_string)
243
269
  end
244
270
 
245
271
  stock_count = get_stock_count item_price_info
@@ -249,6 +275,15 @@ class MarketSearcher
249
275
  npc_data = item_price_info if item_price_info[:is_npc_item]
250
276
  price_per_one = npc_data[:price].to_i.zero? ? item_price_info[:price_per_one].to_i : npc_data[:price].to_i
251
277
 
278
+ if @free_ingredients.include?(item_price_info[:main_key].to_s) ||
279
+ @free_ingredients.include?(item_price_info[:id].to_s) ||
280
+ (item_price_info[:exchange_with] || []).index { |item| @free_ingredients.include? item.to_s }
281
+ price_per_one = 0
282
+ stock_count = Float::INFINITY
283
+ npc_data[:price] = 0
284
+ npc_data[:price_per_one] = 0
285
+ end
286
+
252
287
  # TODO: this is ridiculous, dedupe the hash values
253
288
  potential_ingredient_hash = {
254
289
  name: item_price_info[:name],
@@ -261,13 +296,13 @@ class MarketSearcher
261
296
  quant: quant,
262
297
  price_per_one: price_per_one,
263
298
  count: stock_count,
264
- for_recipe_id: recipe_id,
299
+ enhance_level: item_price_info[:enhance_level],
265
300
  **npc_data
266
301
  }
267
302
 
268
303
  @ingredient_cache[ingredient_id] = potential_ingredient_hash
269
304
 
270
- potential_recipe.push potential_ingredient_hash
305
+ potential_recipe.push({ **potential_ingredient_hash, for_recipe_id: recipe_id, is_m_recipe: is_m_recipe })
271
306
  end
272
307
 
273
308
  next unless potential_recipe.length == recipe.length
@@ -285,7 +320,7 @@ class MarketSearcher
285
320
  average_procs = 1
286
321
 
287
322
  if [25, 35].include? item[:main_category]
288
- unless /oil of|draught|\[mix\]|\[party\]|immortal:|perfume|indignation/im.match item[:name].downcase
323
+ unless /oil of|draught|\[mix\]|\[party\]|immortal:|perfume|indignation|flame of|essence of dawn/im.match item[:name].downcase
289
324
  average_procs = 2.5
290
325
  end
291
326
 
@@ -299,6 +334,11 @@ class MarketSearcher
299
334
  end
300
335
  end
301
336
 
337
+ # 1 in 4 chance to create an imperfect alchemy stone of any specific type with the recipe
338
+ if /imperfect alchemy stone/.match(item[:name].downcase)
339
+ average_procs = 0.25
340
+ end
341
+
302
342
  filtered_recipes = potential_recipes.filter do |recipe|
303
343
  recipe.all? do |ingredient|
304
344
  if ingredient[:total_in_stock] == Float::INFINITY
@@ -317,7 +357,11 @@ class MarketSearcher
317
357
 
318
358
  return nil if selected_recipe.nil?
319
359
 
320
- average_procs = 1 if selected_recipe.find { |a| a[:name].downcase == 'blue reagent' } != nil
360
+ # set procs to 1 if 1 blue reagent required
361
+ average_procs = 1 if selected_recipe.find { |a| a[:name].downcase == 'blue reagent' && a[:quant] == 1 }
362
+
363
+ # set procs to 1.5 if using magical lightstone crystals to purify lightstones
364
+ average_procs = 1.5 if selected_recipe.find { |a| a[:name].downcase == 'magical lightstone crystal' }
321
365
 
322
366
  # remove recipes where one ingredient is used twice
323
367
  # this usually happens because of incorrect substitution being
@@ -325,8 +369,6 @@ class MarketSearcher
325
369
  # are some seriously mysterious alchemy recipes out there...
326
370
  ingredients_already_appeared = []
327
371
  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
372
  return false if ingredients_already_appeared.include? ingredient[:name]
331
373
  ingredients_already_appeared.push(ingredient[:name])
332
374
  true
@@ -347,6 +389,8 @@ class MarketSearcher
347
389
  content_type: 'application/x-www-form-urlencoded'
348
390
  )
349
391
 
392
+ item_price_data = {} if item_price_data.to_s.downcase.include? 'incapsula incident'
393
+
350
394
  # assuming we were able to find the item price list
351
395
  if item_price_data&.dig('marketConditionList')
352
396
  item_market_sell_price = item_price_data['marketConditionList']&.last&.dig('pricePerOne').to_i
@@ -381,7 +425,6 @@ class MarketSearcher
381
425
  # taxed profit on selling max amount of this this recipe, with
382
426
  # average procs accounted for
383
427
  # @type [Integer]
384
- #
385
428
  max_taxed_sell_profit_after_procs = (PriceCalculator.calculate_taxed_price(item_market_sell_price * max_potion_count) - total_max_ingredient_cost) * average_procs
386
429
 
387
430
  return nil if max_taxed_sell_profit_after_procs.to_s.downcase == 'nan'
@@ -389,7 +432,8 @@ class MarketSearcher
389
432
 
390
433
  recipe_logger = RecipeLogger.new @cli
391
434
  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] }
435
+
436
+ { information: results[:recipe_info], max_profit: max_taxed_sell_profit_after_procs, gain: results[:gain], out_of_stock: @out_of_stock_items }
393
437
  end
394
438
  end
395
439
 
@@ -403,10 +447,6 @@ class MarketSearcher
403
447
  item_info[:total_in_stock].to_i.zero? ? item_info[:count].to_i : item_info[:total_in_stock].to_i
404
448
  end
405
449
 
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
450
  def do_if_category_matches(options, &procedure)
411
451
  procedure.call if options[:all_subcategories]
412
452
  procedure.call if options[:subcategory] == options[:subcat_to_match]
@@ -16,13 +16,17 @@ module Utils
16
16
  other: 'category 35, subcategory 8',
17
17
  blood: 'searches "\'s blood"',
18
18
  oil: "searches 'oil of'",
19
- 'alchemy stone': "searches 'stone of'",
19
+ 'alchemy stone': "searches 'imperfect alchemy stone of'",
20
20
  reagent: "searches 'reagent'",
21
21
  'black stone': 'category 30, subcategory 1',
22
22
  'misc': 'category 25, subcategory 8',
23
23
  'other tools': 'category 40, subcategory 10',
24
+ 'manos': "searches 'manos'",
25
+ 'purified lightstone': "searches 'purified lightstone of' (requires guru 1 alchemy)",
26
+ 'combined crystals': 'category 50, subcategory 4',
27
+ 'essences of dawn': "searches 'essence of dawn'",
24
28
  # 'magic crystal': "searches 'magic crystal'",
25
- exit: 'stops the search'
29
+ exit: 'stops the search',
26
30
  }.freeze
27
31
 
28
32
  REGION_DOMAINS = {
@@ -39,7 +43,7 @@ module Utils
39
43
  th: 'trade.th.playblackdesert.com',
40
44
  tw: 'trade.tw.playblackdesert.com',
41
45
  sa: 'blackdesert-tradeweb.playredfox.com',
42
- exit: 'stops the search'
46
+ exit: 'stops the search',
43
47
  }.freeze
44
48
 
45
49
  REGION_LANGUAGES = {
@@ -59,13 +63,18 @@ module Utils
59
63
  id: 'basa indonesia',
60
64
  se: 'sea english',
61
65
  gl: 'global lab',
62
- exit: 'stops the search'
66
+ exit: 'stops the search',
63
67
  }.freeze
64
68
 
65
69
  AGGRESSION_LEVELS = {
66
70
  normal: 'evaluate one permutation of each recipe',
67
71
  hyperaggressive: 'evaluate every substitution for every recipe',
68
- exit: 'stops the search'
72
+ exit: 'stops the search',
73
+ }.freeze
74
+
75
+ YES_OR_NO = {
76
+ no: false,
77
+ yes: true,
69
78
  }.freeze
70
79
 
71
80
  def self.set_rvt(rvt)
@@ -99,13 +108,25 @@ module Utils
99
108
  'x-cdn': 'Imperva'
100
109
  },
101
110
  bdo_codex_headers: {
102
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
103
111
  'User-Agent':
104
112
  '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'
113
+ Dnt: '1',
114
+ 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
115
  }
107
116
  }.freeze
108
117
 
118
+ # TODO: wip...
119
+ def self.get_bdo_codex_headers(item_id, region)
120
+ item_url = "https://bdocodex.com/#{region}/item/#{item_id}"
121
+ cookie_string = "bddatabaselang=#{region}"
122
+ if rand > 0.5
123
+ REQUEST_OPTS[:central_market_headers]
124
+ { **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 }
125
+ else
126
+ { **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 }
127
+ end
128
+ end
129
+
109
130
  def self.get_central_market_headers(incap_cookie = '')
110
131
  original_cookie = REQUEST_OPTS[:central_market_headers][:Cookie]
111
132
  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.3"
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.3
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