sc2ai 0.0.3 → 0.0.5

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.
@@ -14,6 +14,7 @@ module Sc2
14
14
  # @param debug_command [Api::DebugCommand]
15
15
  # @return [void]
16
16
  def queue_debug_command(debug_command)
17
+ @debug_command_queue ||= []
17
18
  @debug_command_queue << debug_command
18
19
  end
19
20
 
@@ -37,6 +38,52 @@ module Sc2
37
38
  )
38
39
  end
39
40
 
41
+ # Prints text on screen from top and left
42
+ # @param text [String] will respect newlines
43
+ # @param left_percent [Numeric] range 0..100. percent from left of screen
44
+ # @param top_percent [Numeric] range 0..100. percent from top of screen
45
+ # @param color [Api::Color] default white
46
+ # @param size [Size] of font, default 14px
47
+ # @return [void]
48
+ def debug_text_screen(text, left_percent: 1.0, top_percent: 1.0, color: nil, size: 14)
49
+ queue_debug_command Api::DebugCommand.new(
50
+ draw: Api::DebugDraw.new(
51
+ text: [
52
+ Api::DebugText.new(
53
+ text:,
54
+ virtual_pos: Api::Point.new(
55
+ x: left_percent.to_f / 100,
56
+ y: top_percent.to_f / 100
57
+ ),
58
+ color:,
59
+ size:
60
+ )
61
+ ]
62
+ )
63
+ )
64
+ end
65
+
66
+ # Prints text on screen at 3d world position
67
+ # @param text [String] will respect newlines
68
+ # @param point [Api::Point] point in the world, i.e. unit.pos
69
+ # @param color [Api::Color] default white
70
+ # @param size [Size] of font, default 14px
71
+ # @return [void]
72
+ def debug_text_world(text, point:, color: nil, size: 14)
73
+ queue_debug_command Api::DebugCommand.new(
74
+ draw: Api::DebugDraw.new(
75
+ text: [
76
+ Api::DebugText.new(
77
+ text:,
78
+ world_pos: point,
79
+ color:,
80
+ size:
81
+ )
82
+ ]
83
+ )
84
+ )
85
+ end
86
+
40
87
  # Draws a line between two Api::Point's for color
41
88
  # @param p0 [Api::Point] the first point
42
89
  # @param p1 [Api::Point] the second point
@@ -63,8 +110,8 @@ module Sc2
63
110
  # # Draws a box on structure placement grid
64
111
  # debug_draw_box(point: unit.pos, radius: unit.footprint_radius)
65
112
  #
66
- # Note: Api::Color RGB is broken for this command. Will use min(r,b)
67
- # Note: Z index is elevated 0.01 so the line is visible and doesn't clip through terrain
113
+ # @note Api::Color RGB is broken for this command. Will use min(r,b)
114
+ # @note Z index is elevated 0.02 so the line is visible and doesn't clip through terrain
68
115
  # @param point [Api::Point]
69
116
  # @param radius [Float] default one tile wide, 1.0
70
117
  # @param color [Api::Color] default white. min(r,b) is used for both r&b
@@ -74,8 +121,8 @@ module Sc2
74
121
  draw: Api::DebugDraw.new(
75
122
  boxes: [
76
123
  Api::DebugBox.new(
77
- min: Api::Point.new(x: point.x - radius, y: point.y - radius, z: point.z + 0.01),
78
- max: Api::Point.new(x: point.x + radius, y: point.y + radius, z: point.z + (radius * 2) + 0.01),
124
+ min: Api::Point.new(x: point.x - radius, y: point.y - radius, z: point.z + 0.02),
125
+ max: Api::Point.new(x: point.x + radius, y: point.y + radius, z: point.z + (radius * 2) + 0.02),
79
126
  color:
80
127
  )
81
128
  ]
@@ -7,7 +7,7 @@ module Sc2
7
7
  class Player
8
8
  # Holds map and geography helper functions
9
9
  class Geometry
10
- # @!attribute Holds the parent bot object
10
+ # @!attribute bot
11
11
  # @return [Sc2::Player] player with active connection
12
12
  attr_accessor :bot
13
13
 
@@ -72,6 +72,7 @@ module Sc2
72
72
  # Each value in [row][column] holds a boolean value represented as an integer
73
73
  # It does not say whether a position is occupied by another building.
74
74
  # One pixel covers one whole block. Rounds fractionated positions down.
75
+ # @return [Numo::Bit]
75
76
  def parsed_placement_grid
76
77
  if @parsed_placement_grid.nil?
77
78
  image_data = bot.game_info.start_raw.placement_grid
@@ -83,6 +84,7 @@ module Sc2
83
84
  end
84
85
 
85
86
  # Returns a grid where ony the expo locations are marked
87
+ # @return [Numo::Bit]
86
88
  def expo_placement_grid
87
89
  if @expo_placement_grid.nil?
88
90
  @expo_placement_grid = Numo::Bit.zeros(map_height, map_width)
@@ -97,6 +99,7 @@ module Sc2
97
99
  end
98
100
 
99
101
  # Returns a grid where powered locations are marked true
102
+ # @return [Numo::Bit]
100
103
  def parsed_power_grid
101
104
  # Cache for based on power unit tags
102
105
  cache_key = bot.power_sources.map(&:tag).sort.hash
@@ -256,6 +259,12 @@ module Sc2
256
259
  parsed_terrain_height[y.to_i, x.to_i]
257
260
  end
258
261
 
262
+ # Returns the terrain height (z) at position x and y for a point
263
+ # @return [Float] z axis position between -16 and 16
264
+ def terrain_height_for_pos(position)
265
+ terrain_height(x: position.x, y: position.y)
266
+ end
267
+
259
268
  # Returns a parsed terrain_height from bot.game_info.start_raw.
260
269
  # Each value in [row][column] holds a float value which is the z height
261
270
  # @return [Numo::SFloat] Numo array
@@ -399,12 +408,25 @@ module Sc2
399
408
  output_grid
400
409
  end
401
410
 
411
+ # Returns own 2d start position as set by initial camera
412
+ # This differs from position of first base structure
413
+ # @return [Api::Point2D]
414
+ def start_position
415
+ @start_position ||= bot.observation.raw_data.player.camera
416
+ end
417
+
418
+ # Returns the enemy 2d start position
419
+ # @return [Api::Point2D]
420
+ def enemy_start_position
421
+ bot.game_info.start_raw.start_locations.first
422
+ end
423
+
402
424
  # Gets expos and surrounding minerals
403
425
  # The index is a build location for an expo and the value is a UnitGroup, which has minerals and geysers
404
426
  # @example
405
- # random_expo = expansions.keys.sample #=> Point2D
427
+ # random_expo = geo.expansions.keys.sample #=> Point2D
406
428
  # expo_resources = geo.expansions[random_expo] #=> UnitGroup
407
- # alive_minerals = expo_resources.minerals - neutral.minerals
429
+ # alive_minerals = expo_resources.minerals & neutral.minerals
408
430
  # geysers = expo_resources.geysers
409
431
  # @return [Hash<Api::Point2D, UnitGroup>] Location => UnitGroup of resources (minerals+geysers)
410
432
  def expansions
@@ -503,24 +525,77 @@ module Sc2
503
525
  end
504
526
 
505
527
  # Returns a slice of #expansions where a base hasn't been built yet
528
+ # The has index is a build position and the value is a UnitGroup of resources for the base
506
529
  # @example
507
530
  # # Lets find the nearest unoccupied expo
508
531
  # expo_pos = expansions_unoccupied.keys.min { |p2d| p2d.distance_to(structures.hq.first) }
509
532
  # # What minerals/geysers does it have?
510
533
  # puts expansions_unoccupied[expo_pos].minerals # or expansions[expo_pos]... => UnitGroup
511
534
  # puts expansions_unoccupied[expo_pos].geysers # or expansions[expo_pos]... => UnitGroup
512
- # @return [Hash<Api::Point2D], UnitGroup] Location => UnitGroup of resources (minerals+geysers)
535
+ # @return [Hash<Api::Point2D, UnitGroup>] Location => UnitGroup of resources (minerals+geysers)
513
536
  def expansions_unoccupied
514
537
  taken_bases = bot.structures.hq.map { |hq| hq.pos.to_p2d } + bot.enemy.structures.hq.map { |hq| hq.pos.to_p2d }
515
538
  remaining_points = expansion_points - taken_bases
516
539
  expansions.slice(*remaining_points)
517
540
  end
518
541
 
542
+ # Gets minerals for a base or base position
543
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
544
+ # @return [Sc2::UnitGroup] UnitGroup of minerals for the base
545
+ def minerals_for_base(base)
546
+ # resources_for_base contains what we need, but slice neutral.minerals,
547
+ # so that only active patches remain
548
+ bot.neutral.minerals.slice(*resources_for_base(base).minerals.tags)
549
+ end
550
+
551
+ # Gets geysers for a base or base position
552
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
553
+ # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
554
+ def geysers_for_base(base)
555
+ resources_for_base(base).geysers
556
+ end
557
+
558
+ # @private
559
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
560
+ # @return [Sc2::UnitGroup] UnitGroup of resources (minerals+geysers)
561
+ private def resources_for_base(base)
562
+ pos = base.is_a?(Api::Unit) ? base.pos : base
563
+
564
+ # If we have a base setup for this exact position, use it
565
+ if expansions.has_key?(pos)
566
+ return expansions[pos]
567
+ end
568
+
569
+ # Tolerance for misplaced base: Find the nearest base to this position
570
+ pos = expansion_points.min_by { |p| p.distance_to(pos) }
571
+
572
+ expansions[pos]
573
+ end
574
+
575
+ # Gets gasses for a base or base position
576
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
577
+ # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
578
+ def gas_for_base(base)
579
+ # No gas structures at all yet, return nothing
580
+ return UnitGroup.new if bot.structures.gas.size.zero?
581
+
582
+ geysers = geysers_for_base(base)
583
+
584
+ # Mineral-only base, return nothing
585
+ return UnitGroup.new if geysers.size == 0
586
+
587
+ # Loop and collect gasses places exactly on-top of geysers
588
+ bot.structures.gas.select do |gas|
589
+ geysers.any? { |geyser| geyser.pos.to_p2d.eql?(gas.pos.to_p2d) }
590
+ end
591
+ end
592
+
519
593
  # Gets buildable point grid for squares of size, i.e. 3 = 3x3 placements
520
594
  # Uses pathing grid internally, to ignore taken positions
521
595
  # Does not query the api and is generally fast.
522
596
  # @param length [Integer] length of the building, 2 for depot/pylon, 3 for rax/gate
523
- # @param on_creep [Boolean] whether this build locatin should be on creep
597
+ # @param on_creep [Boolean] whether this build location should be on creep
598
+ # @return [Array<Array<(Float, Float)>>] Array of [x,y] tuples
524
599
  def build_coordinates(length:, on_creep: false, in_power: false)
525
600
  length = 1 if length < 1
526
601
  @_build_coordinates ||= {}
@@ -36,13 +36,13 @@ module Sc2
36
36
  # Override to modify the previous frame before being set to current
37
37
  # @param bot [Sc2::Player::Bot]
38
38
  def before_reset(bot)
39
- # pp "### before_reset"
39
+ # no op
40
40
  end
41
41
 
42
42
  # Override to modify previous frame after reset is complete
43
43
  # @param bot [Sc2::Player::Bot]
44
44
  def after_reset(bot)
45
- # pp "### after_reset"
45
+ # no op
46
46
  end
47
47
  end
48
48
  end
@@ -30,6 +30,53 @@ module Sc2
30
30
  # @return [Sc2::UnitGroup] a group of neutral units
31
31
  attr_accessor :effects # not a unit
32
32
 
33
+ # Returns the upgrade ids you have acquired such as weapon upgrade and armor upgrade ids.
34
+ # Shorthand for observation.raw_data.player.upgrade_ids
35
+ # @!attribute [r] upgrades_completed
36
+ # @return [Array<Integer>] a group of neutral units
37
+ def upgrades_completed = observation&.raw_data&.player&.upgrade_ids.to_a || [] # not a unit
38
+
39
+ # Returns the upgrade ids which are researching or queued
40
+ # Not set for enemy.
41
+ # @return [Array<Integer>]
42
+ def upgrades_in_progress
43
+ # We need to scan every structure which performs upgrades for any order with an upgrade ability
44
+
45
+ result = []
46
+ # Loop every upgrade structure
47
+ structures
48
+ .select_type(Api::TechTree.upgrade_structure_unit_type_ids)
49
+ .each do |structure|
50
+ next unless structure.is_active? # Skip idle
51
+
52
+ # Check if any order at a structure contains an upgrade ability
53
+ structure.orders.each do |order|
54
+ Api::TechTree.upgrade_ability_data(structure.unit_type).each do |upgrade_id, update_info|
55
+ if update_info[:ability] == order.ability_id
56
+ # Save the upgrade_id
57
+ result << upgrade_id
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # If the API told use it's complete, but an order still lingers, trust the API
64
+ result - upgrades_completed
65
+ end
66
+
67
+ # Returns the upgrade ids which are researching or queued
68
+ # @return [Boolean]
69
+ def upgrade_in_progress?(upgrade_id)
70
+ structure_unit_type_id = Api::TechTree.upgrade_researched_from(upgrade_id: upgrade_id)
71
+ research_ability_id = Api::TechTree.upgrade_research_ability_id(upgrade_id: upgrade_id)
72
+ structures.select_type(structure_unit_type_id).each do |structure|
73
+ structure.orders.each do |order|
74
+ return true if order.ability_id == research_ability_id
75
+ end
76
+ end
77
+ false
78
+ end
79
+
33
80
  # An array of Protoss power sources, which have a point, radius and unit tag
34
81
  # @!attribute power_sources
35
82
  # @return [Array<Api::PowerSource>] an array of power sources
@@ -47,10 +94,10 @@ module Sc2
47
94
 
48
95
  # Event-driven unit groups ---
49
96
 
97
+ # @!attribute event_units_created
50
98
  # Units created since last frame (visible only, units not structures)
51
99
  # Read this on_step. Alternative to callback on_unit_created
52
- # Note: Morphed units should watch #event_units_type_changed
53
- # @!attribute event_units_created
100
+ # @note Morphed units should watch #event_units_type_changed
54
101
  # @return [Sc2::UnitGroup] group of created units
55
102
  attr_accessor :event_units_created
56
103
 
@@ -107,6 +154,13 @@ module Sc2
107
154
  data.abilities[ability_id]
108
155
  end
109
156
 
157
+ # Returns static [Api::UpgradeData] for an upgrade id
158
+ # @param upgrade_id [Integer] Api::UpgradeId::*
159
+ # @return [Api::UpgradeData]
160
+ def upgrade_data(upgrade_id)
161
+ data.upgrades[upgrade_id]
162
+ end
163
+
110
164
  # Checks unit data for an attribute value
111
165
  # @param unit [Integer,Api::Unit] Api::UnitTypeId or Api::Unit
112
166
  # @param attribute [Symbol] Api::Attribute, i.e. Api::Attribute::Mechanical or :Mechanical
@@ -141,17 +195,13 @@ module Sc2
141
195
  def subtract_cost(unit_type_id)
142
196
  unit_type_data = unit_data(unit_type_id)
143
197
 
144
- # food_required is a float. ensure half units are counted as full
145
- # TODO: Extend UnitTypeData message. def food_required = unit_id == Api::UnitTypeId::ZERGLING ? 1 : send("method_missing", :food_required)
146
- supply_cost = unit_type_data.food_required
147
- supply_cost = 1 if unit_type_id == Api::UnitTypeId::ZERGLING
148
-
149
198
  @spent_minerals += unit_type_data.mineral_cost
150
199
  @spent_vespene += unit_type_data.vespene_cost
151
- @spent_supply += supply_cost
200
+ @spent_supply += unit_type_data.food_required
152
201
  end
153
202
 
154
203
  # Checks whether you have the resources to construct quantity of unit type
204
+ # @return [Boolean]
155
205
  def can_afford?(unit_type_id:, quantity: 1)
156
206
  unit_type_data = unit_data(unit_type_id)
157
207
  return false if unit_type_data.nil?
@@ -178,6 +228,25 @@ module Sc2
178
228
  true
179
229
  end
180
230
 
231
+ # Checks whether you have the resources to
232
+ # @return [Boolean]
233
+ def can_afford_upgrade?(upgrade_id)
234
+ unit_type_data = upgrade_data(upgrade_id)
235
+ return false if unit_type_data.nil?
236
+
237
+ mineral_cost = unit_type_data.mineral_cost
238
+ if common.minerals - spent_minerals < mineral_cost
239
+ return false # not enough minerals
240
+ end
241
+
242
+ vespene_cost = unit_type_data.vespene_cost
243
+ if common.vespene - spent_vespene < vespene_cost
244
+ return false # you require more vespene gas
245
+ end
246
+
247
+ true
248
+ end
249
+
181
250
  private
182
251
 
183
252
  # @private
data/lib/sc2ai/player.rb CHANGED
@@ -97,7 +97,7 @@ module Sc2
97
97
  @difficulty = difficulty
98
98
  @ai_build = ai_build
99
99
  @realtime = false
100
- @step_count = 1
100
+ @step_count = 2
101
101
 
102
102
  @enable_feature_layer = false
103
103
  @interface_options = {}
@@ -201,7 +201,7 @@ module Sc2
201
201
 
202
202
  # Override to customize initialization
203
203
  # Alias of before_join
204
- # You can enable_feature_layer=true, set step_size, define
204
+ # You can enable_feature_layer=true, set step_count, define
205
205
  # @example
206
206
  # def configure
207
207
  # step_count = 4 # Update less frequently
@@ -490,6 +490,8 @@ module Sc2
490
490
  def started
491
491
  # Calculate expansions
492
492
  geo.expansions
493
+ # Set our start position base on camera
494
+ geo.start_position
493
495
  end
494
496
 
495
497
  # Moves emulation ahead and calls back #on_step
@@ -498,9 +500,7 @@ module Sc2
498
500
  # Sc2.logger.debug "#{self.class} step_forward"
499
501
 
500
502
  unless @realtime
501
- # ##TODO: Numsteps as config
502
- num_steps = 1
503
- @api.step(num_steps)
503
+ @api.step(@step_count)
504
504
  end
505
505
 
506
506
  refresh_state
@@ -610,9 +610,9 @@ module Sc2
610
610
  # ##TODO: perfect loop implementation
611
611
  # observation has an optional param game_loop and will only return once that step is reached (blocking).
612
612
  # without it, it returns things as they are.
613
- # broadly, i think this is what it should be doing, with step_size being minimum of 1, so no zero-steps occur.
613
+ # broadly, i think this is what it should be doing, with step_count being minimum of 1, so no zero-steps occur.
614
614
  # @example
615
- # desired_game_loop = current_game_loop + step_size
615
+ # desired_game_loop = current_game_loop + step_count
616
616
  # response = client.observation(game_loop: desired_game_loop)
617
617
  #
618
618
  # if response.game_loop > desired_game_loop {
@@ -21,6 +21,8 @@
21
21
  # class Point < Google::Protobuf::AbstractMessage; end;
22
22
  # # Protobuf virtual class.
23
23
  # class Unit < Google::Protobuf::AbstractMessage; end;
24
+ # # Protobuf virtual class.
25
+ # class UnitTypeData < Google::Protobuf::AbstractMessage; end;
24
26
  # end
25
27
 
26
28
  # Protobuf enums ---
@@ -1,6 +1,15 @@
1
1
  module Api
2
2
  # Adds additional functionality to message object Api::Point
3
3
  module PointExtension
4
+ # @private
5
+ def hash
6
+ [x, y, z].hash
7
+ end
8
+
9
+ def eql?(other)
10
+ self.class == other.class && hash == other.hash
11
+ end
12
+
4
13
  # Creates a Point2D using x and y
5
14
  # @return [Api::Point2D]
6
15
  def to_p2d
@@ -13,6 +22,9 @@ module Api
13
22
  # @example
14
23
  # Api::Point[1,2,3] # Where x is 1.0, y is 2.0 and z is 3.0
15
24
  # @return [Api::Point]
25
+ # @param [Float] x
26
+ # @param [Float] y
27
+ # @param [Float] z
16
28
  def [](x, y, z)
17
29
  Api::Point.new(x: x, y: y, z: z)
18
30
  end
@@ -20,4 +32,4 @@ module Api
20
32
  end
21
33
  end
22
34
  Api::Point.include Api::PointExtension
23
- Api::Point.include Api::PointExtension::ClassMethods
35
+ Api::Point.extend Api::PointExtension::ClassMethods
@@ -10,6 +10,12 @@ module Api
10
10
  self.class == other.class && hash == other.hash
11
11
  end
12
12
 
13
+ # Create a new 3d Point, by adding a y axis.
14
+ # @return [Api::Point]
15
+ def to_3d(z:)
16
+ Api::Point[x, y, z]
17
+ end
18
+
13
19
  # Adds additional functionality to message class Api::Point2D
14
20
  module ClassMethods
15
21
  # Shorthand for creating an instance for [x, y]
@@ -37,9 +37,8 @@ module Api
37
37
  # Checks unit data for an attribute value
38
38
  # @return [Boolean] whether unit has attribute
39
39
  # @example
40
- # has_attribute?(Api::UnitTypeId::SCV, Api::Attribute::Mechanical)
41
- # has_attribute?(units.workers.first, :Mechanical)
42
- # has_attribute?(Api::UnitTypeId::SCV, :Mechanical)
40
+ # unit.has_attribute?(Api::Attribute::Mechanical)
41
+ # unit.has_attribute?(:Mechanical)
43
42
  def has_attribute?(attribute)
44
43
  attributes.include? attribute
45
44
  end
@@ -179,6 +178,26 @@ module Api
179
178
  action(ability_id: Api::AbilityId::SMART, target:, queue_command:)
180
179
  end
181
180
 
181
+ # Shorthand for performing action MOVE
182
+ # @param target [Api::Unit, Integer, Api::Point2D] is a unit, unit tag or a Api::Point2D
183
+ # @param queue_command [Boolean] shift+command
184
+ def move(target:, queue_command: false)
185
+ action(ability_id: Api::AbilityId::MOVE, target:, queue_command:)
186
+ end
187
+
188
+ # Shorthand for performing action STOP
189
+ # @param queue_command [Boolean] shift+command
190
+ def stop(queue_command: false)
191
+ action(ability_id: Api::AbilityId::STOP, queue_command:)
192
+ end
193
+
194
+ # Shorthand for performing action HOLDPOSITION
195
+ # @param queue_command [Boolean] shift+command
196
+ def hold(queue_command: false)
197
+ action(ability_id: Api::AbilityId::HOLDPOSITION, queue_command:)
198
+ end
199
+ alias_method :hold_position, :hold
200
+
182
201
  # Shorthand for performing action ATTACK
183
202
  # @param target [Api::Unit, Integer, Api::Point2D] is a unit, unit tag or a Api::Point2D
184
203
  # @param queue_command [Boolean] shift+command
@@ -213,10 +232,18 @@ module Api
213
232
 
214
233
  # Issues repair command on target
215
234
  # @param target [Api::Unit, Integer] is a unit or unit tag
235
+ # @param queue_command [Boolean] shift+command
216
236
  def repair(target:, queue_command: false)
217
237
  action(ability_id: Api::AbilityId::EFFECT_REPAIR, target:, queue_command:)
218
238
  end
219
239
 
240
+ # Research a specific upgrade
241
+ # @param upgrade_id [Integer] Api::UnitTypeId the unit type which will do the creation
242
+ # @param queue_command [Boolean] shift+command
243
+ def research(upgrade_id:, queue_command: false)
244
+ @bot.research(units: self, upgrade_id:, queue_command:)
245
+ end
246
+
220
247
  # @!endgroup Actions
221
248
  #
222
249
  # Debug ----
@@ -224,6 +251,7 @@ module Api
224
251
  # Draws a placement outline
225
252
  # @param color [Api::Color] optional api color, default white
226
253
  # @return [void]
254
+ # noinspection RubyArgCount
227
255
  def debug_draw_placement(color = nil)
228
256
  # Slightly elevate the Z position so that the line doesn't clip into the terrain at same Z level
229
257
  z_elevated = pos.z + 0.01
@@ -432,6 +460,12 @@ module Api
432
460
  build_progress == 1.0 # standard:disable Lint/FloatComparison
433
461
  end
434
462
 
463
+ # Returns true if build progress is < 100%
464
+ # @return [Boolean]
465
+ def in_progress?
466
+ !is_completed?
467
+ end
468
+
435
469
  # Convenience functions ---
436
470
 
437
471
  # TERRAN Convenience functions ---
@@ -444,7 +478,7 @@ module Api
444
478
 
445
479
  # Returns whether the structure has a reactor add-on
446
480
  # @return [Boolean] if the unit has a reactor attached
447
- def has_reactor
481
+ def has_reactor?
448
482
  Sc2::UnitGroup::TYPE_REACTOR.include?(add_on&.unit_type)
449
483
  end
450
484
 
@@ -455,26 +489,46 @@ module Api
455
489
  # # Get the actual tech-lab with #add_on
456
490
  # sp.add_on.research ...
457
491
  # @return [Boolean] if the unit has a tech lab attached
458
- def has_tech_lab
492
+ def has_tech_lab?
459
493
  Sc2::UnitGroup::TYPE_TECHLAB.include?(add_on&.unit_type)
460
494
  end
461
495
 
462
496
  # For Terran builds a tech lab add-on on the current structure
463
497
  # @return [void]
464
- def build_reactor
465
- build(unit_type_id: Api::UnitTypeId::REACTOR)
498
+ def build_reactor(queue_command: false)
499
+ unit_type_id = case unit_type
500
+ when Api::UnitTypeId::BARRACKS, Api::UnitTypeId::BARRACKSFLYING
501
+ Api::UnitTypeId::BARRACKSREACTOR
502
+ when Api::UnitTypeId::FACTORY, Api::UnitTypeId::FACTORYFLYING
503
+ Api::UnitTypeId::FACTORYREACTOR
504
+ when Api::UnitTypeId::STARPORT, Api::UnitTypeId::STARPORTFLYING
505
+ Api::UnitTypeId::STARPORTREACTOR
506
+ end
507
+ build(unit_type_id: unit_type_id, queue_command:)
466
508
  end
467
509
 
468
510
  # For Terran builds a tech lab add-on on the current structure
469
511
  # @return [void]
470
- def build_tech_lab
471
- build(unit_type_id: Api::UnitTypeId::TECHLAB)
512
+ def build_tech_lab(queue_command: false)
513
+ unit_type_id = case unit_type
514
+ when Api::UnitTypeId::BARRACKS, Api::UnitTypeId::BARRACKSFLYING
515
+ Api::UnitTypeId::BARRACKSTECHLAB
516
+ when Api::UnitTypeId::FACTORY, Api::UnitTypeId::FACTORYFLYING
517
+ Api::UnitTypeId::FACTORYTECHLAB
518
+ when Api::UnitTypeId::STARPORT, Api::UnitTypeId::STARPORTFLYING
519
+ Api::UnitTypeId::STARPORTTECHLAB
520
+ end
521
+ build(unit_type_id: unit_type_id, queue_command:)
472
522
  end
473
523
 
524
+ # GENERAL Convenience functions ---
525
+
526
+ # ...
527
+
474
528
  private
475
529
 
476
530
  # @private
477
- # Reduces repitition in the is_*action*?(target:) methods
531
+ # Reduces repetition in the is_*action*?(target:) methods
478
532
  def is_performing_ability_on_target?(abilities, target: nil)
479
533
  # Exit if not actioning the ability
480
534
  return false unless is_performing_ability?(abilities)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ # Adds additional functionality to message object Api::UnitTypeData
5
+ module UnitTypeDataExtension
6
+ # @!attribute mineral_cost_sum
7
+ # Sum of all morphs mineral cost
8
+ # i.e. 550M Orbital command = 400M CC + 150M Upgrade
9
+ # i.e. 350M Hatchery = 50M Drone + 300M Build
10
+ # @return [Integer] sum of mineral costs
11
+ attr_accessor :mineral_cost_sum
12
+
13
+ # @!attribute vespene_cost_sum
14
+ # Sum of all morphs vespene gas cost
15
+ # i.e. 250G Broodlord = 100G Corruptor + 150G Morph
16
+ # @return [Integer] sum of vespene gas costs
17
+ attr_accessor :vespene_cost_sum
18
+ end
19
+ end
20
+ Api::UnitTypeData.include Api::UnitTypeDataExtension