sc2ai 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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