sc2ai 0.5.0 → 0.6.1

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: 5b421602e2cdff4d59e4b3b4ab168f7d894a43f4474244b229ec72c78d6bf6a9
4
- data.tar.gz: cbc072cb8a55cacabd0ac751ff5286f582c0145241a98343834f0c0def7c31c1
3
+ metadata.gz: ddd8bd1e37b50ba6d4e89c24fb330d646589d869cc53cf96a69624eaa0e6133c
4
+ data.tar.gz: d88b2674f094621c11aa7de9a0c59ae82a6668aa6c882783a54236776ceb9f37
5
5
  SHA512:
6
- metadata.gz: df8e5f7a8557d0f3dce348cbd9dcdc8ef8f03f88b5f00376d7972b36c223e042ed61123612642189e2722b0a62eb22f7f02a16ca5650864bdd8f3a49ddc86680
7
- data.tar.gz: d5094e865be75f4b7c31b25a41ff8f021edbb4380567f304a39c1ade99d3155fa408d821c980f2751daf85eddee3f8f9c84a81557255e923db524d9570fad817
6
+ metadata.gz: 10c922d086149c4f2e4cac906607d7f5fb2c31fde1ace225186b0d83342efaea6720c48b799eb01e4d29fec5b295a4890d44b9cf803bb0c47e75e5830489c466
7
+ data.tar.gz: 7430bdcd6935e457b88b562838c0dc112f28ecdfbacd072293ddfdf358d065287dcdbc300bb7cc76b4efe0e671a82006f9d53766b79bd2d006496982061e3fc7
@@ -89,7 +89,7 @@ message UnitTypeData {
89
89
  optional float sight_range = 25; // Range unit reveals vision.
90
90
 
91
91
  repeated uint32 tech_alias = 21 [packed=false]; // Other units that satisfy the same tech requirement.
92
- optional uint32 unit_alias = 22 [packed=false]; // The morphed variant of this unit.
92
+ optional uint32 unit_alias = 22; // The morphed variant of this unit.
93
93
 
94
94
  optional uint32 tech_requirement = 23; // Structure required to build this unit. (Or any with the same tech_alias)
95
95
  optional bool require_attached = 24; // Whether tech_requirement is an add-on.
@@ -191,7 +191,7 @@ message ActionRawUnitCommand {
191
191
  uint64 target_unit_tag = 3;
192
192
  }
193
193
  repeated uint64 unit_tags = 4 [packed=false];
194
- optional bool queue_command = 5 [packed=false];
194
+ optional bool queue_command = 5;
195
195
  }
196
196
 
197
197
  message ActionRawCameraMove {
@@ -4,12 +4,12 @@ LABEL service="bot-ruby-local"
4
4
  USER root
5
5
  WORKDIR /root/ruby-builder
6
6
 
7
- ARG RUBY_VERSION=3.4.1
7
+ ARG RUBY_VERSION=3.4.2
8
8
  ARG DEBIAN_DISABLE_RUBYGEMS_INTEGRATION=true
9
9
 
10
10
  # Deps - Ruby build
11
11
  RUN apt-get update
12
- RUN apt install --assume-yes rustc curl build-essential libssl-dev zlib1g-dev libgmp-dev libffi-dev uuid-dev
12
+ RUN apt install --assume-yes rustc curl build-essential libssl-dev zlib1g-dev libgmp-dev libffi-dev uuid-dev libopenblas0-serial
13
13
 
14
14
  # Deps - libyaml from source
15
15
  RUN curl https://pyyaml.org/download/libyaml/yaml-0.2.5.tar.gz -o yaml-0.2.5.tar.gz
@@ -33,20 +33,20 @@ RUN rm yaml-0.2.5.tar.gz
33
33
 
34
34
  # Package config
35
35
  # numo-linalg needs openblas, copy to ruby-prefix/lib/ dir.
36
- RUN apt download libopenblas0-serial
37
- RUN mkdir openblas
38
- RUN dpkg-deb -x ./libopenblas*.deb openblas
39
- RUN cp -d openblas/usr/lib/x86_64-linux-gnu/openblas-serial/* /root/ruby-builder/.ruby/lib/
40
- RUN rm -rf ./openblas
41
- RUN rm ./libopenblas0-serial*.deb
42
-
43
- RUN apt download libgfortran5
44
- RUN mkdir libgfortran5
45
- RUN dpkg-deb -x ./libgfortran*.deb libgfortran5
46
- RUN find libgfortran5
47
- RUN cp libgfortran5/usr/lib/x86_64-linux-gnu/libgfortran.so.5 /root/ruby-builder/.ruby/lib/
48
- RUN rm -rf ./libgfortran5
49
- RUN rm ./libgfortran5*.deb
36
+ #RUN apt download libopenblas0-serial
37
+ #RUN mkdir openblas
38
+ #RUN dpkg-deb -x ./libopenblas*.deb openblas
39
+ #RUN cp -d openblas/usr/lib/x86_64-linux-gnu/openblas-serial/* /root/ruby-builder/.ruby/lib/
40
+ #RUN rm -rf ./openblas
41
+ #RUN rm ./libopenblas0-serial*.deb
42
+
43
+ #RUN apt download libgfortran5
44
+ #RUN mkdir libgfortran5
45
+ #RUN dpkg-deb -x ./libgfortran*.deb libgfortran5
46
+ #RUN find libgfortran5
47
+ #RUN cp libgfortran5/usr/lib/x86_64-linux-gnu/libgfortran.so.5 /root/ruby-builder/.ruby/lib/
48
+ #RUN rm -rf ./libgfortran5
49
+ #RUN rm ./libgfortran5*.deb
50
50
 
51
51
  RUN /root/ruby-builder/.ruby/bin/ruby --yjit -v
52
52
 
@@ -108,6 +108,8 @@ module Sc2
108
108
  correct_unit_type_costs
109
109
  correct_unit_type_sum
110
110
  decorate_unit_type_placement_length
111
+ decorate_missing_values
112
+ decorate_weapon_helpers
111
113
  end
112
114
 
113
115
  # @private
@@ -256,9 +258,11 @@ module Sc2
256
258
  end
257
259
  end
258
260
 
261
+ private
262
+
259
263
  # @private
260
264
  # Adds placement_length to units if applicable
261
- private def decorate_unit_type_placement_length
265
+ def decorate_unit_type_placement_length
262
266
  @units.each_value do |unit_type_data|
263
267
  unit_type_data.placement_length = 0
264
268
  next unless unit_type_data.ability_id
@@ -269,5 +273,59 @@ module Sc2
269
273
  end
270
274
  end
271
275
  end
276
+
277
+ # @private
278
+ # Adds values missing from the API
279
+ def decorate_missing_values
280
+ # Battlecruiser has no weapons. Force these in by hand.
281
+ @units[Api::UnitTypeId::BATTLECRUISER].weapons = [
282
+ Api::Weapon.new(
283
+ type: Api::Weapon::TargetType::GROUND,
284
+ damage: 8.0,
285
+ damage_bonus: [],
286
+ attacks: 1,
287
+ range: 6.0,
288
+ speed: 0.224
289
+ ),
290
+ Api::Weapon.new(
291
+ type: Api::Weapon::TargetType::AIR,
292
+ damage: 5.0,
293
+ damage_bonus: [],
294
+ attacks: 1,
295
+ range: 6.0,
296
+ speed: 0.224
297
+ )
298
+ ]
299
+ end
300
+
301
+ # @private
302
+ # Adds ground_damage, air_damage, ground_range, air_range, ground_dps and air_dps
303
+ def decorate_weapon_helpers
304
+ @units.each do |unit_type_id, unit_type_data|
305
+ ground_weapon = unit_type_data.weapons.find do |weapon|
306
+ weapon.type == :GROUND || weapon.type == :ANY
307
+ end
308
+
309
+ air_weapon = unit_type_data.weapons.find do |weapon|
310
+ weapon.type == :AIR || weapon.type == :ANY
311
+ end
312
+
313
+ unit_type_data.ground_range = ground_weapon&.range || 0.0
314
+ unit_type_data.air_range = air_weapon&.range || 0.0
315
+
316
+ ground_attacks = ground_weapon&.attacks || 0
317
+ air_attacks = air_weapon&.attacks || 0
318
+ base_ground_damage = ground_weapon&.damage || 0.0
319
+ base_air_damage = air_weapon&.damage || 0.0
320
+ ground_attack_speed = ground_weapon&.speed || 1.0
321
+ air_attack_speed = air_weapon&.speed || 1.0
322
+
323
+ unit_type_data.ground_damage = base_ground_damage * ground_attacks
324
+ unit_type_data.air_damage = base_air_damage * air_attacks
325
+
326
+ unit_type_data.ground_dps = unit_type_data.ground_damage / ground_attack_speed
327
+ unit_type_data.air_dps = unit_type_data.air_damage / air_attack_speed
328
+ end
329
+ end
272
330
  end
273
331
  end
data/lib/sc2ai/cli/cli.rb CHANGED
@@ -21,31 +21,16 @@ module Sc2
21
21
  desc "setup410", "Downloads and install SC2 v4.10"
22
22
  # downloads and install SC2 v4.10
23
23
  def setup410
24
- say " "
25
- say "This script sets up SC2 at version 4.10, which we use competitively."
26
- say "Press any key to continue..."
27
- ask ""
28
-
29
- say "You must accept the Blizzard® StarCraft® II AI and Machine Learning License at"
30
- say "https://blzdistsc2-a.akamaihd.net/AI_AND_MACHINE_LEARNING_LICENSE.html"
31
- say "It is PERMISSIVE and grants you freedoms over the standard EULA."
32
- say "We do not record this action, but depend on software goverend by that license to continue."
33
- puts 'If you accept, type "iagreetotheeula" (without quotes) and press ENTER to continue:'
34
-
35
- while $stdin.gets.chomp != "iagreetotheeula"
36
- say ""
37
- puts 'Type "iagreetotheeula" (without quotes) and press ENTER to continue:'
38
- end
39
- say ""
40
- say ""
41
- say "Great decision."
42
-
43
24
  require "sc2ai"
44
25
  Async do
45
26
  Sc2.logger.level = :fatal
27
+ say " "
28
+ say "This script sets up SC2 at version 4.10, which we use competitively."
46
29
  say "SC2 will launch a blank window, be unresponsive, but download about 100mb in the background."
30
+ say ""
31
+ say "It will appear to hang as it updates. This is normal."
47
32
  say "Let it finish and close itself."
48
- say "Press ENTER if you understand."
33
+ say "Press any key to continue..."
49
34
  ask ""
50
35
 
51
36
  say ""
@@ -110,8 +110,9 @@ module Sc2
110
110
  # game_info = observer.api.game_info
111
111
  # end
112
112
  # ensure
113
+ # observer.disconnect
113
114
  # Sc2::ClientManager.stop(0)
114
- # end
115
+ # end.wait
115
116
  # @param replay_path [String] path to replay
116
117
  # @param replay_data [String] alternative to file, binary string of replay_file.read
117
118
  # @param map_data [String] optional binary string of SC2 map if not present in paths
@@ -195,6 +196,7 @@ module Sc2
195
196
 
196
197
  # Snapshot of the current game state. Primary source for raw information
197
198
  # @param game_loop [Integer] you wish to wait for (realtime only)
199
+ # @return [Api::ResponseObservation]
198
200
  def observation(game_loop: nil)
199
201
  # Sc2.logger.debug { "#{self.class}.#{__method__} game_loop: #{game_loop}" }
200
202
  if game_loop.nil?
@@ -275,6 +277,7 @@ module Sc2
275
277
  # Advances the game simulation by step_count. Not used in realtime mode.
276
278
  # Only constant step size supported - subsequent requests use cache.
277
279
  def step(step_count = 1)
280
+ step_count = step_count.to_i
278
281
  @_cached_request_step ||= {}
279
282
  @_cached_request_step[step_count] ||= Api::Request.new(
280
283
  step: Api::RequestStep.new(count: step_count)
@@ -68,7 +68,7 @@ module Sc2
68
68
  ensure
69
69
  Sc2.logger.debug { "Game over, disconnect players." }
70
70
  # Suppress interrupt errors #$stderr.reopen File.new(File::NULL, "w")
71
- player.quit if Gem.win_platform?
71
+ player.quit if [Paths::PF_WINDOWS, Paths::PF_WSL1, Paths::PF_WSL2].include?(Paths.platform)
72
72
  player.disconnect
73
73
  ClientManager.stop(player_index) # unless keep_clients_alive
74
74
  end
@@ -95,7 +95,7 @@ module Sc2
95
95
  response = player.api.save_replay
96
96
  path = Pathname(Paths.bot_data_replay_dir).join("autosave-#{safe_player_name}.SC2Replay")
97
97
  f = File.new(path, "wb:ASCII-8BIT")
98
- f.write(response.data)
98
+ f.write_nonblock(response.data)
99
99
  f.close
100
100
  end
101
101
 
@@ -36,16 +36,9 @@ module Sc2
36
36
  end
37
37
 
38
38
  def game_info=(new_info)
39
- @game_info_loop = game_loop
40
39
  @game_info = new_info
41
40
  end
42
41
 
43
- # @!attribute game_info_loop
44
- # This is the last loop at which game_info was set.
45
- # Used to determine staleness.
46
- # @return [Integer]
47
- attr_accessor :game_info_loop
48
-
49
42
  # @!attribute data
50
43
  # @return [Api::ResponseData]
51
44
  attr_accessor :data
@@ -35,6 +35,11 @@ module Sc2
35
35
  # @return [Sc2::UnitGroup] a group of neutral units
36
36
  attr_accessor :effects # not a unit
37
37
 
38
+ # An array of Units which are (red) blips on radar
39
+ # @!attribute blips
40
+ # @return [Sc2::UnitGroup] a group of blip units
41
+ attr_accessor :blips
42
+
38
43
  # Returns the upgrade ids you have acquired such as weapon upgrade and armor upgrade ids.
39
44
  # Shorthand for observation.raw_data.player.upgrade_ids
40
45
  # @!attribute [r] upgrades_completed
@@ -254,7 +259,7 @@ module Sc2
254
259
 
255
260
  # Checks whether you have the resources to construct quantity of unit type
256
261
  # @return [Boolean]
257
- def can_afford?(unit_type_id:, quantity: 1)
262
+ def can_afford?(unit_type_id, quantity: 1)
258
263
  unit_type_data = unit_data(unit_type_id)
259
264
  return false if unit_type_data.nil?
260
265
 
@@ -343,10 +348,7 @@ module Sc2
343
348
  enemy_alliance = self.enemy_alliance
344
349
 
345
350
  # To prevent several loops over all units per frame, use this single loop for all checks
346
- all_unit_size = observation.raw_data.units.size
347
- i = 0
348
- while i < all_unit_size
349
- unit = observation.raw_data.units[i]
351
+ observation.raw_data.units.each do |unit|
350
352
  tag = unit.tag
351
353
  tag = unit.tag = unit.hash if tag.zero?
352
354
  # Reluctantly assigning player to unit
@@ -377,7 +379,7 @@ module Sc2
377
379
  @neutral[tag] = unit
378
380
  end
379
381
 
380
- # Dont parse callbacks on first loop or for neutral units
382
+ # Don't parse callbacks on first loop or for neutral units
381
383
  if !@previous.all_units.nil? &&
382
384
  unit.alliance != :NEUTRAL &&
383
385
  unit.display_type != :PLACEHOLDER &&
@@ -392,11 +394,13 @@ module Sc2
392
394
  issue_existing_unit_callbacks(unit, previous_unit)
393
395
  end
394
396
  end
397
+ end
395
398
 
399
+ # Due to race condition with callbacks and Unit#bot not being set,
400
+ # rather do a second iteration just for the callbacks
401
+ observation.raw_data.units.each do |unit|
396
402
  # Allow user to fiddle with unit
397
403
  on_parse_observation_unit(unit)
398
-
399
- i += 1
400
404
  end
401
405
  end
402
406
 
data/lib/sc2ai/player.rb CHANGED
@@ -213,6 +213,7 @@ module Sc2
213
213
  # enable_feature_layer = true
214
214
  #
215
215
  # end
216
+ # @return [void]
216
217
  def configure
217
218
  end
218
219
 
@@ -276,12 +277,14 @@ module Sc2
276
277
 
277
278
  # Override to perform steps before first on_step gets called.
278
279
  # Current game_loop is 0 and @api is available
280
+ # @return [void]
279
281
  def on_start
280
282
  # Sc2.logger.debug { "#{self.class} on_start" }
281
283
  end
282
284
 
283
285
  # Override to implement your own game logic.
284
286
  # Gets called whenever the game moves forward.
287
+ # @return [void]
285
288
  def on_step
286
289
  return unless is_a? Bot
287
290
 
@@ -305,6 +308,7 @@ module Sc2
305
308
  # puts "Lets try again!"
306
309
  # end
307
310
  # end
311
+ # @return [void]
308
312
  def on_finish(result)
309
313
  # Sc2.logger.debug { "#{self.class} on_finish" }
310
314
  end
@@ -312,12 +316,14 @@ module Sc2
312
316
  # Called when Random race is first detected.
313
317
  # Override to handle race identification of random enemy.
314
318
  # @param race [Integer] see {Api::Race}
319
+ # @return [void]
315
320
  def on_random_race_detected(race)
316
321
  end
317
322
 
318
323
  # Called on step if errors are present. Equivalent of UI red text errors.
319
324
  # Override to read action errors.
320
325
  # @param errors [Array<Api::ActionError>]
326
+ # @return [void]
321
327
  def on_action_errors(errors)
322
328
  # Sc2.logger.debug errors
323
329
  end
@@ -325,6 +331,7 @@ module Sc2
325
331
  # Actions this player performed since the last Observation.
326
332
  # Override to read actions successfully performed
327
333
  # @param actions [Array<Api::Action>] a list of actions which were performed
334
+ # @return [void]
328
335
  def on_actions_performed(actions)
329
336
  # Sc2.logger.debug actions
330
337
  end
@@ -342,11 +349,13 @@ module Sc2
342
349
  # end
343
350
  # end
344
351
  # @param alerts [Array<Integer>] array of Api::Alert::*
352
+ # @return [void]
345
353
  def on_alerts(alerts)
346
354
  end
347
355
 
348
356
  # Callback when upgrades are completed, multiple might finish on the same observation.
349
357
  # @param upgrade_ids [Array<Integer>] Api::UpgradeId::*
358
+ # @return [void]
350
359
  def on_upgrades_completed(upgrade_ids)
351
360
  end
352
361
 
@@ -359,6 +368,7 @@ module Sc2
359
368
  # Callback, on observation parse when iterating over every unit
360
369
  # Can be useful for decorating additional properties on a unit before on_step
361
370
  # A Sc2::Player should override this to decorate additional properties
371
+ # @return [void]
362
372
  def on_parse_observation_unit(unit)
363
373
  end
364
374
 
@@ -367,12 +377,14 @@ module Sc2
367
377
  # Override to use in your bot class or use Player.
368
378
  # @param unit [Api::Unit]
369
379
  # @see Sc2::Player::Units#units_destroyed
380
+ # @return [void]
370
381
  def on_unit_destroyed(unit)
371
382
  end
372
383
 
373
384
  # Callback for unit created.
374
385
  # Override to use in your bot class.
375
386
  # @param unit [Api::Unit]
387
+ # @return [void]
376
388
  def on_unit_created(unit)
377
389
  end
378
390
 
@@ -381,18 +393,21 @@ module Sc2
381
393
  # Override to use in your bot class or use Player.
382
394
  # @param unit [Api::Unit]
383
395
  # @param previous_unit_type_id [Integer] Api::UnitTypeId::*
396
+ # @return [void]
384
397
  def on_unit_type_changed(unit, previous_unit_type_id)
385
398
  end
386
399
 
387
400
  # Callback for structure building began
388
401
  # Override to use in your bot class.
389
402
  # @param unit [Api::Unit]
403
+ # @return [void]
390
404
  def on_structure_started(unit)
391
405
  end
392
406
 
393
407
  # Callback for structure building is completed
394
408
  # Override to use in your bot class or use Player.
395
409
  # @param unit [Api::Unit]
410
+ # @return [void]
396
411
  def on_structure_completed(unit)
397
412
  end
398
413
 
@@ -400,6 +415,7 @@ module Sc2
400
415
  # Override to use in your bot class or use Player.
401
416
  # @param unit [Api::Unit]
402
417
  # @param amount [Integer] of damage
418
+ # @return [void]
403
419
  def on_unit_damaged(unit, amount)
404
420
  end
405
421
 
@@ -530,18 +546,27 @@ module Sc2
530
546
  on_structure_completed
531
547
  on_unit_damaged]
532
548
 
549
+ # @private
550
+ # @return [Array<Symbol>] callbacks implemented on player class
551
+ attr_accessor :callbacks_defined
552
+
533
553
  # @private
534
554
  # Checks if callback method is defined on our bot
535
555
  # Used to skip processing on unused callbacks
536
556
  # @param callback [Symbol]
537
557
  def callback_defined?(callback)
538
- CALLBACK_METHODS.include?(callback)
558
+ if @callbacks_defined.nil?
559
+ # Cache the intersection check, assuming nobody defines a callback method while actually running
560
+ @callbacks_defined = CALLBACK_METHODS.intersection(self.class.instance_methods(false))
561
+ end
562
+ @callbacks_defined.include?(callback)
539
563
  end
540
564
 
541
565
  # Initialize data on step 0 before stepping and before on_start is called
542
566
  def prepare_start
543
567
  @data = Sc2::Data.new(@api.data)
544
568
  clear_action_queue
569
+ clear_action_errors
545
570
  clear_debug_command_queue
546
571
  end
547
572
 
@@ -0,0 +1,25 @@
1
+ module Api
2
+ # Adds additional functionality to message object Api::Point2D
3
+ module PlayerCommonExt
4
+ # @!attribute supply_cap
5
+ # Maximum supply. alias of #food_cap
6
+ # @return [Integer]
7
+ def supply_cap = food_cap
8
+
9
+ # @!attribute supply_used
10
+ # Supply used. alias of #food_used
11
+ # @return [Integer]
12
+ def supply_used = food_used
13
+
14
+ # @!attribute supply_army
15
+ # Supply used for army. alias of #food_army
16
+ # @return [Integer]
17
+ def supply_army = food_army
18
+
19
+ # @!attribute supply_workers
20
+ # Supply used in workers. alias of #food_workers
21
+ # @return [Integer]
22
+ def supply_workers = food_workers
23
+ end
24
+ end
25
+ Api::PlayerCommon.prepend Api::PlayerCommonExt
@@ -207,7 +207,7 @@ module Sc2
207
207
  # Conversion ---
208
208
 
209
209
  # Returns [x,y] array tuple
210
- # @return [Array[Float,Float]]
210
+ # @return [Array<Float, Float>]
211
211
  def to_axy
212
212
  [x, y]
213
213
  end
@@ -23,7 +23,7 @@ module Api
23
23
  # Get the unit as from the previous frame. Good for comparison.
24
24
  # @return [Api::Unit, nil] this unit from the previous frame or nil if it wasn't present
25
25
  def previous
26
- @bot.previous.all_units[tag]
26
+ @bot.previous.all_units&.[](tag)
27
27
  end
28
28
 
29
29
  # Returns whether a unit is alive or not
@@ -48,7 +48,7 @@ module Api
48
48
 
49
49
  # Attributes ---
50
50
 
51
- # Returns static [Api::UnitTypeData] for a unit
51
+ # Returns static an array of attributes for a unit
52
52
  # @return [Array<Api::Attribute>]
53
53
  def attributes
54
54
  unit_data.attributes
@@ -111,7 +111,7 @@ module Api
111
111
  has_attribute?(:STRUCTURE)
112
112
  end
113
113
 
114
- # Checks if unit is hover
114
+ # Checks if unit is hovering
115
115
  # @return [Boolean] whether unit has attribute :Hover
116
116
  def is_hover?
117
117
  has_attribute?(:HOVER)
@@ -133,13 +133,31 @@ module Api
133
133
 
134
134
  # Helpers for unit properties
135
135
 
136
- def width = radius * 2
136
+ def width = radius * 2.0
137
137
  # @!parse
138
138
  # # @!attribute width
139
139
  # # width = radius * 2
140
140
  # # @return [Float]
141
141
  # attr_reader :width
142
142
 
143
+ # @!attribute [r] full_health?
144
+ # @return [Boolean] health is at maximum
145
+ def full_health?
146
+ health == health_max
147
+ end
148
+
149
+ # @!attribute [r] full_shields?
150
+ # @return [Boolean] shields are at maximum
151
+ def full_shields?
152
+ shield == shield_max
153
+ end
154
+
155
+ # @!attribute [r] full_energy?
156
+ # @return [Boolean] energy is at maximum
157
+ def full_energy?
158
+ energy == energy_max
159
+ end
160
+
143
161
  # Some overrides to allow question mark references to boolean properties
144
162
 
145
163
  # @!attribute [r] is_flying?
@@ -179,6 +197,14 @@ module Api
179
197
  # @return [Boolean]
180
198
  def is_ground? = !is_flying?
181
199
 
200
+ # @!attribute [r] is_cloaked??
201
+ # Returns whether the unit is cloaked. Revealed cloak units also return true.
202
+ # For further distinction, use Unit#cloaked, which uses enum CloakedState
203
+ # @return [Boolean]
204
+ def is_cloaked?
205
+ cloak != :NOT_CLOAKED && cloak != :CLOAKED_UNKNOWN
206
+ end
207
+
182
208
  # @!endgroup Virtual properties
183
209
 
184
210
  # Whether unit is effected by buff_id
@@ -315,14 +341,18 @@ module Api
315
341
  )
316
342
  end
317
343
 
318
- # Draws a sphere around the unit's attack range
319
- # @param weapon_index [Api::Color] default first weapon, see UnitTypeData.weapons
344
+ # Draws a sphere around the unit's attack range (weapon range + radius)
345
+ # @param weapon_index [Integer] default first weapon, see UnitTypeData#weapons
320
346
  # @param color [Api::Color] optional api color, default red
321
347
  def debug_fire_range(weapon_index = 0, color = nil)
348
+ attack_range = unit_data.weapons[weapon_index]&.range
349
+ return if attack_range.nil?
350
+ attack_range += radius
351
+
322
352
  color = Api::Color.new(r: 255, b: 0, g: 0) if color.nil?
323
- attack_range = unit_data.weapons[weapon_index].range
324
353
  raised_position = pos.dup
325
354
  raised_position.z += 0.01
355
+
326
356
  @bot.debug_draw_sphere(point: raised_position, radius: attack_range, color:)
327
357
  end
328
358
 
@@ -413,22 +443,30 @@ module Api
413
443
  end
414
444
 
415
445
  # Checks whether enemy is within range of weapon or ability and can target ground/air.
416
- # Defaults to basic weapon. Pass in ability to override
446
+ # By default, checks all weapons.
447
+ # Pass weapon_index or ability_id to target a specific source of damage.
417
448
  # @param unit [Api::Unit] enemy
418
- # @param weapon_index [Integer] defaults to 0 which is it's basic weapon for it's current form
449
+ # @param weapon_index [Integer] passing this will select a specific Weapon
419
450
  # @param ability_id [Integer] passing this will override weapon Api::AbilityId::*
420
451
  # @return [Boolean]
421
452
  # @example
453
+ # queen.can_attack?(enemy, weapon_index: 0) # Air attack
454
+ # queen.can_attack?(enemy, weapon_index: 1) # Ground attack
455
+ # queen.can_attack?(enemy) # Auto detect (scans all weapons)
422
456
  # ghost.can_attack?(enemy, weapon_index: 0, ability_id: Api::AbilityId::SNIPE)
423
- def can_attack?(unit:, weapon_index: 0, ability_id: nil)
424
- if ability_id.nil?
457
+ def can_attack?(unit:, weapon_index: nil, ability_id: nil)
458
+ return false if unit.nil?
459
+ if ability_id
460
+ # ability
461
+ ability = @bot.ability_data(ability_id)
462
+ can_ability_target_unit?(unit:, ability:)
463
+ elsif weapon_index
425
464
  # weapon
426
465
  source_weapon = weapon(weapon_index)
427
466
  can_weapon_target_unit?(unit:, weapon: source_weapon)
428
467
  else
429
- # ability
430
- ability = @bot.ability_data(ability_id)
431
- can_ability_target_unit?(unit:, ability:)
468
+ # auto-detect weapon
469
+ unit_data.weapons.any? { |weapon| can_weapon_target_unit?(unit:, weapon:) }
432
470
  end
433
471
  end
434
472
 
@@ -445,6 +483,7 @@ module Api
445
483
  # @param weapon [Api::Weapon]
446
484
  # @return [Boolean]
447
485
  def can_weapon_target_unit?(unit:, weapon:)
486
+ return false if unit.nil? || weapon.nil?
448
487
  # false if enemy is air and we can only shoot ground
449
488
  return false if unit.is_flying && weapon.type == :GROUND # Api::Weapon::TargetType::GROUND
450
489
 
@@ -455,7 +494,12 @@ module Api
455
494
  in_attack_range?(unit:, range: weapon.range)
456
495
  end
457
496
 
497
+ # Checks whether an ability can target a unit
498
+ # @param unit [Api::Unit]
499
+ # @param ability [Api::AbilityData]
500
+ # @return [Boolean]
458
501
  def can_ability_target_unit?(unit:, ability:)
502
+ return false if unit.nil? || ability.nil?
459
503
  # false if enemy is air and we can only shoot ground
460
504
  return false if ability.target == Api::AbilityData::Target::NONE
461
505
 
@@ -495,6 +539,7 @@ module Api
495
539
  # This value should be correct for building placement math (unit.radius is not good for this)
496
540
  # @return [Float] placement radius
497
541
  def footprint_radius
542
+ return 0.0 if unit_data.ability_id == 0
498
543
  @bot.data.abilities[unit_data.ability_id].footprint_radius
499
544
  end
500
545