sc2ai 0.0.0.pre → 0.0.2

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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/data/data.json +1 -0
  3. data/data/data_readable.json +22842 -0
  4. data/data/sc2ai/protocol/common.proto +59 -0
  5. data/data/sc2ai/protocol/data.proto +120 -0
  6. data/data/sc2ai/protocol/debug.proto +127 -0
  7. data/data/sc2ai/protocol/error.proto +221 -0
  8. data/data/sc2ai/protocol/query.proto +55 -0
  9. data/data/sc2ai/protocol/raw.proto +202 -0
  10. data/data/sc2ai/protocol/sc2api.proto +718 -0
  11. data/data/sc2ai/protocol/score.proto +108 -0
  12. data/data/sc2ai/protocol/spatial.proto +115 -0
  13. data/data/sc2ai/protocol/ui.proto +145 -0
  14. data/data/setup/setup.SC2Map +0 -0
  15. data/data/setup/setup.SC2Replay +0 -0
  16. data/data/stableid.json +35730 -0
  17. data/data/versions.json +554 -0
  18. data/exe/sc2ai +35 -0
  19. data/lib/docker_build/Dockerfile.ruby +74 -0
  20. data/lib/docker_build/docker-compose-base-image.yml +10 -0
  21. data/lib/docker_build/docker-compose-ladderzip.yml +9 -0
  22. data/lib/sc2ai/api/ability_id.rb +1644 -0
  23. data/lib/sc2ai/api/buff_id.rb +306 -0
  24. data/lib/sc2ai/api/data.rb +101 -0
  25. data/lib/sc2ai/api/effect_id.rb +20 -0
  26. data/lib/sc2ai/api/tech_tree.rb +83 -0
  27. data/lib/sc2ai/api/tech_tree_data.rb +2338 -0
  28. data/lib/sc2ai/api/unit_type_id.rb +2022 -0
  29. data/lib/sc2ai/api/upgrade_id.rb +310 -0
  30. data/lib/sc2ai/cli/cli.rb +175 -0
  31. data/lib/sc2ai/cli/ladderzip.rb +154 -0
  32. data/lib/sc2ai/cli/new.rb +88 -0
  33. data/lib/sc2ai/configuration.rb +145 -0
  34. data/lib/sc2ai/connection/connection_listener.rb +30 -0
  35. data/lib/sc2ai/connection/requests.rb +417 -0
  36. data/lib/sc2ai/connection/status_listener.rb +15 -0
  37. data/lib/sc2ai/connection.rb +146 -0
  38. data/lib/sc2ai/local_play/client/configurable_options.rb +115 -0
  39. data/lib/sc2ai/local_play/client.rb +159 -0
  40. data/lib/sc2ai/local_play/client_manager.rb +70 -0
  41. data/lib/sc2ai/local_play/map_file.rb +48 -0
  42. data/lib/sc2ai/local_play/match.rb +184 -0
  43. data/lib/sc2ai/overrides/array.rb +14 -0
  44. data/lib/sc2ai/overrides/async/process/child.rb +31 -0
  45. data/lib/sc2ai/overrides/kernel.rb +33 -0
  46. data/lib/sc2ai/paths.rb +294 -0
  47. data/lib/sc2ai/player/actions.rb +386 -0
  48. data/lib/sc2ai/player/debug.rb +224 -0
  49. data/lib/sc2ai/player/game_state.rb +131 -0
  50. data/lib/sc2ai/player/geometry.rb +766 -0
  51. data/lib/sc2ai/player/previous_state.rb +49 -0
  52. data/lib/sc2ai/player/units.rb +337 -0
  53. data/lib/sc2ai/player.rb +661 -0
  54. data/lib/sc2ai/ports.rb +152 -0
  55. data/lib/sc2ai/protocol/_meta_documentation.rb +39 -0
  56. data/lib/sc2ai/protocol/common_pb.rb +43 -0
  57. data/lib/sc2ai/protocol/data_pb.rb +47 -0
  58. data/lib/sc2ai/protocol/debug_pb.rb +56 -0
  59. data/lib/sc2ai/protocol/error_pb.rb +36 -0
  60. data/lib/sc2ai/protocol/extensions/color.rb +20 -0
  61. data/lib/sc2ai/protocol/extensions/point.rb +23 -0
  62. data/lib/sc2ai/protocol/extensions/point_2_d.rb +26 -0
  63. data/lib/sc2ai/protocol/extensions/position.rb +202 -0
  64. data/lib/sc2ai/protocol/extensions/power_source.rb +19 -0
  65. data/lib/sc2ai/protocol/extensions/unit.rb +489 -0
  66. data/lib/sc2ai/protocol/query_pb.rb +47 -0
  67. data/lib/sc2ai/protocol/raw_pb.rb +57 -0
  68. data/lib/sc2ai/protocol/sc2api_pb.rb +130 -0
  69. data/lib/sc2ai/protocol/score_pb.rb +40 -0
  70. data/lib/sc2ai/protocol/spatial_pb.rb +48 -0
  71. data/lib/sc2ai/protocol/ui_pb.rb +56 -0
  72. data/lib/sc2ai/unit_group/action_ext.rb +74 -0
  73. data/lib/sc2ai/unit_group/filter_ext.rb +379 -0
  74. data/lib/sc2ai/unit_group.rb +277 -0
  75. data/lib/sc2ai/version.rb +2 -1
  76. data/lib/sc2ai.rb +93 -2
  77. data/lib/templates/ladderzip/bin/ladder.tt +23 -0
  78. data/lib/templates/new/.ladderignore +20 -0
  79. data/lib/templates/new/Gemfile.tt +7 -0
  80. data/lib/templates/new/api/common.proto +59 -0
  81. data/lib/templates/new/api/data.proto +120 -0
  82. data/lib/templates/new/api/debug.proto +127 -0
  83. data/lib/templates/new/api/error.proto +221 -0
  84. data/lib/templates/new/api/query.proto +55 -0
  85. data/lib/templates/new/api/raw.proto +202 -0
  86. data/lib/templates/new/api/sc2api.proto +718 -0
  87. data/lib/templates/new/api/score.proto +108 -0
  88. data/lib/templates/new/api/spatial.proto +115 -0
  89. data/lib/templates/new/api/ui.proto +145 -0
  90. data/lib/templates/new/boot.rb.tt +6 -0
  91. data/lib/templates/new/my_bot.rb.tt +23 -0
  92. data/lib/templates/new/run_example_match.rb.tt +14 -0
  93. data/sc2ai.gemspec +80 -0
  94. metadata +344 -13
@@ -0,0 +1,202 @@
1
+ module Sc2
2
+ # A unified construct that tames Api::* messages which contain location data
3
+ # Items which are of type Sc2::Location will have #x and #y property at the least.
4
+ module Position
5
+ # Tolerance for floating-point comparisons.
6
+ TOLERANCE = 1e-9
7
+
8
+ # Basic operations
9
+
10
+ # A new point representing the sum of this point and the other point.
11
+ # @param other [Api::Point2D, Numeric] The other point/number to add.
12
+ # @return [Api::Point2D]
13
+ def add(other)
14
+ if other.is_a? Numeric
15
+ Api::Point2D[x + other, y + other]
16
+ else
17
+ Api::Point2D[x + other.x, y + other.y]
18
+ end
19
+ end
20
+ alias_method :+, :add
21
+
22
+ # Returns a new point representing the difference between this point and the other point/number.
23
+ # @param other [Api::Point2D, Numeric] The other to subtract.
24
+ # @return [Api::Point2D]
25
+ def subtract(other)
26
+ if other.is_a? Numeric
27
+ Api::Point2D[x - other, y - other]
28
+ else
29
+ Api::Point2D[x - other.x, y - other.y]
30
+ end
31
+ end
32
+ alias_method :-, :subtract
33
+
34
+ # Returns this point multiplied by the scalar
35
+ # @param scalar [Float] The scalar to multiply by.
36
+ # @return [Api::Point2D]
37
+ def multiply(scalar)
38
+ Api::Point2D[x * scalar, y * scalar]
39
+ end
40
+ # @see #divide
41
+ alias_method :*, :multiply
42
+
43
+ # @param scalar [Float] The scalar to divide by.
44
+ # @return [Api::Point2D] A new point representing this point divided by the scalar.
45
+ # @raise [ZeroDivisionError] if the scalar is zero.
46
+ def divide(scalar)
47
+ raise ZeroDivisionError if scalar.zero?
48
+ Api::Point2D[x / scalar, y / scalar]
49
+ end
50
+ # @see #divide
51
+ alias_method :/, :divide
52
+
53
+ # Bug: Psych implements method 'y' on Kernel, but protobuf uses method_missing to read AbstractMethod
54
+ # We send method missing ourselves when y to fix this chain.
55
+ def y
56
+ # This is correct, but an unnecessary conditional:
57
+ # raise NoMethodError unless location == self
58
+ send(:method_missing, :y)
59
+ end
60
+
61
+ # Randomly adjusts both x and y by a range of: -offset..offset
62
+ # @param offset [Float]
63
+ # @return [Api::Point2D]
64
+ def random_offset(offset)
65
+ Api::Point2D.new[x, y].random_offset!(offset)
66
+ end
67
+
68
+ # Changes this point's x and y by the supplied offset
69
+ # @return [Sc2::Position] self
70
+ def random_offset!(offset)
71
+ offset = offset.to_f
72
+ range = rand(-offset..offset)
73
+ offset!(rand(range), rand(range))
74
+ self
75
+ end
76
+
77
+ # Creates a new point with x and y which is offset
78
+ # @return [Api::Point2D] self
79
+ def offset(x, y)
80
+ Api::Point2D.new[x, y].offset!(x, y)
81
+ self
82
+ end
83
+
84
+ # Changes this point's x and y by the supplied offset
85
+ # @return [Sc2::Position] self
86
+ def offset!(x, y)
87
+ self.x -= x
88
+ self.y -= y
89
+ self
90
+ end
91
+
92
+ # Vector operations ---
93
+
94
+ # For vector returns the magnitude, synonymous with Math.hypot
95
+ # @return [Float]
96
+ def magnitude
97
+ Math.hypot(x, y)
98
+ end
99
+
100
+ # The dot product of this vector and the other vector.
101
+ # @param other [Api::Point2D] The other vector to calculate the dot product with.
102
+ # @return [Float]
103
+ def dot(other)
104
+ x * other.x + y * other.y
105
+ end
106
+
107
+ # The cross product of this vector and the other vector.
108
+ # @param other [Api::Point2D] The other vector to calculate the cross product with.
109
+ # @return [Float]
110
+ def cross_product(other)
111
+ x * other.y - y * other.x
112
+ end
113
+
114
+ # The angle between this vector and the other vector, in radians.
115
+ # @param other [Api::Point2D] The other vector to calculate the angle to.
116
+ # @return [Float]
117
+ def angle_to(other)
118
+ Math.acos(dot(other) / (magnitude * other.magnitude))
119
+ end
120
+
121
+ # A new point representing the normalized version of this vector (unit length).
122
+ # @return [Api::Point2D]
123
+ def normalize
124
+ divide(magnitude)
125
+ end
126
+
127
+ # Other methods ---
128
+
129
+ # Linear interpolation between this point and another for scale
130
+ # Finds a point on a line between two points at % along the way. 0.0 returns self, 1.0 returns other, 0.5 is halfway.
131
+ # @param scale [Float] a value between 0.0..1.0
132
+ # @return [Api::Point2D]
133
+ def lerp(other, scale)
134
+ Api::Point2D[x + (other.x - x) * scale, y + (other.y - y) * scale]
135
+ end
136
+
137
+ # Distance calculations ---
138
+
139
+ # Calculates the distance between self and other
140
+ # @param other [Sc2::Position]
141
+ # @return [Float]
142
+ def distance_to(other)
143
+ if other.nil? || other == self
144
+ return 0.0
145
+ end
146
+ Math.hypot(self.x - other.x, self.y - other.y)
147
+ end
148
+
149
+ # The squared distance between this point and the other point.
150
+ # @param other [Point2D] The other point to calculate the squared distance to.
151
+ # @return [Float]
152
+ def distance_squared_to(other)
153
+ if other.nil? || other == self
154
+ return 0.0
155
+ end
156
+ (x - other.x) * (y - other.y)
157
+ end
158
+
159
+ # Distance between this point and coordinate of x and y
160
+ # @return [Float]
161
+ def distance_to_coordinate(x:, y:)
162
+ Math.hypot(self.x - x, self.y - y)
163
+ end
164
+
165
+ # The distance from this point to the circle.
166
+ # @param center [Point2D] The center of the circle.
167
+ # @param radius [Float] The radius of the circle.
168
+ # @return [Float]
169
+ def distance_to_circle(center, radius)
170
+ distance_to_center = distance_to(center)
171
+ if distance_to_center <= radius
172
+ 0.0 # Point is inside the circle
173
+ else
174
+ distance_to_center - radius
175
+ end
176
+ end
177
+
178
+ # Movement ---
179
+
180
+ # Moves in direction towards other point by distance
181
+ # @param other [Api::Point2D] The target point to move to.
182
+ # @param distance [Float] The distance to move.
183
+ # @return [Api::Point2D]
184
+ def towards(other, distance)
185
+ direction = other.subtract(self).normalize
186
+ add(direction.multiply(distance))
187
+ end
188
+
189
+ # Moves in direction away from the other point by distance
190
+ # @param other [Api::Point2D] The target point to move away from
191
+ # @param distance [Float] The distance to move.
192
+ # @return [Api::Point2D]
193
+ def away_from(other, distance)
194
+ towards(other, -distance)
195
+ end
196
+ end
197
+ end
198
+
199
+ Api::Point.include Sc2::Position
200
+ Api::Point2D.include Sc2::Position
201
+ Api::PointI.include Sc2::Position
202
+ Api::Size2DI.include Sc2::Position
@@ -0,0 +1,19 @@
1
+ module Api
2
+ # Adds additional functionality to message object Api::PowerSource
3
+ module PowerSourceExtension
4
+ include Sc2::Position
5
+
6
+ # Adds additional functionality to message class Api::PowerSource
7
+ module ClassMethods
8
+ # Shorthand for creating an instance for [x, y, z]
9
+ # @example
10
+ # Api::Point[1,2,3] # Where x is 1.0, y is 2.0 and z is 3.0
11
+ # @return [Api::Point]
12
+ def [](x, y, z)
13
+ Api::Point.new(x: x, y: y, z: z)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ Api::PowerSource.include Api::PowerSourceExtension
19
+ Api::PowerSource.extend Api::PowerSourceExtension::ClassMethods
@@ -0,0 +1,489 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ # Adds additional functionality to message object Api::Unit
5
+ # Mostly adds convenience methods by adding direct access to the Sc2::Bot data and api
6
+ module UnitExtension
7
+ # @private
8
+ def hash
9
+ tag || super
10
+ end
11
+
12
+ # Every unit gets access back to the bot to allow api access.
13
+ # For your own units, this allows API access.
14
+ # @return [Sc2::Player] player with active connection
15
+ attr_accessor :bot
16
+
17
+ # Returns static [Api::UnitTypeData] for a unit
18
+ # @return [Api::UnitTypeData]
19
+ def unit_data
20
+ @bot.data.units[unit_type]
21
+ end
22
+
23
+ # Get the unit as from the previous frame. Good for comparison.
24
+ # @return [Api::Unit, nil] this unit from the previous frame or nil if it wasn't present
25
+ def previous
26
+ @bot.previous.all_units[tag]
27
+ end
28
+
29
+ # Attributes ---
30
+
31
+ # Returns static [Api::UnitTypeData] for a unit
32
+ # @return [Array<Api::Attribute>]
33
+ def attributes
34
+ unit_data.attributes
35
+ end
36
+
37
+ # Checks unit data for an attribute value
38
+ # @return [Boolean] whether unit has attribute
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)
43
+ def has_attribute?(attribute)
44
+ attributes.include? attribute
45
+ end
46
+
47
+ # Checks if unit is light
48
+ # @return [Boolean] whether unit has attribute :Light
49
+ def is_light?
50
+ has_attribute?(:Light)
51
+ end
52
+
53
+ # Checks if unit is armored
54
+ # @return [Boolean] whether unit has attribute :Armored
55
+ def is_armored?
56
+ has_attribute?(:Armored)
57
+ end
58
+
59
+ # Checks if unit is biological
60
+ # @return [Boolean] whether unit has attribute :Biological
61
+ def is_biological?
62
+ has_attribute?(:Biological)
63
+ end
64
+
65
+ # Checks if unit is mechanical
66
+ # @return [Boolean] whether unit has attribute :Mechanical
67
+ def is_mechanical?
68
+ has_attribute?(:Mechanical)
69
+ end
70
+
71
+ # Checks if unit is robotic
72
+ # @return [Boolean] whether unit has attribute :Robotic
73
+ def is_robotic?
74
+ has_attribute?(:Robotic)
75
+ end
76
+
77
+ # Checks if unit is psionic
78
+ # @return [Boolean] whether unit has attribute :Psionic
79
+ def is_psionic?
80
+ has_attribute?(:Psionic)
81
+ end
82
+
83
+ # Checks if unit is massive
84
+ # @return [Boolean] whether unit has attribute :Massive
85
+ def is_massive?
86
+ has_attribute?(:Massive)
87
+ end
88
+
89
+ # Checks if unit is structure
90
+ # @return [Boolean] whether unit has attribute :Structure
91
+ def is_structure?
92
+ has_attribute?(:Structure)
93
+ end
94
+
95
+ # Checks if unit is hover
96
+ # @return [Boolean] whether unit has attribute :Hover
97
+ def is_hover?
98
+ has_attribute?(:Hover)
99
+ end
100
+
101
+ # Checks if unit is heroic
102
+ # @return [Boolean] whether unit has attribute :Heroic
103
+ def is_heroic?
104
+ has_attribute?(:Heroic)
105
+ end
106
+
107
+ # Checks if unit is summoned
108
+ # @return [Boolean] whether unit has attribute :Summoned
109
+ def is_summoned?
110
+ has_attribute?(:Summoned)
111
+ end
112
+
113
+ # @!group Virtual properties
114
+
115
+ # Helpers for unit properties
116
+
117
+ def width = radius * 2
118
+ # @!parse
119
+ # # @!attribute width
120
+ # # width = radius * 2
121
+ # # @return [Float]
122
+ # attr_reader :width
123
+
124
+ # Some overrides to allow question mark references to boolean properties
125
+
126
+ # @!attribute [r] is_flying?
127
+ # @return [Boolean] Unit is currently flying.
128
+ def is_flying? = is_flying
129
+
130
+ # @!attribute [r] is_burrowed?
131
+ # @return [Boolean] Zerg burrowed ability active on unit.
132
+ def is_burrowed? = is_burrowed
133
+
134
+ # @!attribute [r] is_hallucination?
135
+ # @return [Boolean] Unit is your own or detected as a hallucination.
136
+ def is_hallucination? = is_hallucination
137
+
138
+ # @!attribute [r] is_selected?
139
+ # @return [Boolean] Whether unit is selected visually or on Feature layer.
140
+ def is_selected? = is_selected
141
+
142
+ # @!attribute [r] is_on_screen?
143
+ # @return [Boolean] Visible and within the camera frustrum.
144
+ def is_on_screen? = is_on_screen
145
+
146
+ # @!attribute [r] is_blip?
147
+ # @return [Boolean] Detected by sensor tower
148
+ def is_blip? = is_blip
149
+
150
+ # @!attribute [r] is_powered?
151
+ # @return [Boolean] Protoss building is powered by a source.
152
+ def is_powered? = is_powered
153
+
154
+ # @!attribute [r] is_active?
155
+ # @return [Boolean] Building is training/researching (i.e. animated).
156
+ def is_active? = is_active
157
+
158
+ # @!attribute [r] is_ground?
159
+ # Returns whether the unit is grounded (not flying).
160
+ # @return [Boolean]
161
+ def is_ground? = !is_flying?
162
+
163
+ # @!endgroup Virtual properties
164
+
165
+ # @!group Actions
166
+
167
+ # Performs action on this unit
168
+ # @param ability_id [Integer]
169
+ # @param target [Api::Unit, Integer, Api::Point2D] is a unit, unit tag or a Api::Point2D
170
+ # @param queue_command [Boolean] shift+command
171
+ def action(ability_id:, target: nil, queue_command: false)
172
+ @bot.action(units: self, ability_id:, target:, queue_command:)
173
+ end
174
+
175
+ # Shorthand for performing action SMART (right-click)
176
+ # @param target [Api::Unit, Integer, Api::Point2D] is a unit, unit tag or a Api::Point2D
177
+ # @param queue_command [Boolean] shift+command
178
+ def smart(target: nil, queue_command: false)
179
+ action(ability_id: Api::AbilityId::SMART, target:, queue_command:)
180
+ end
181
+
182
+ # Shorthand for performing action ATTACK
183
+ # @param target [Api::Unit, Integer, Api::Point2D] is a unit, unit tag or a Api::Point2D
184
+ # @param queue_command [Boolean] shift+command
185
+ def attack(target:, queue_command: false)
186
+ action(ability_id: Api::AbilityId::ATTACK, target:, queue_command:)
187
+ end
188
+
189
+ # Inverse of #attack, where you target self using another unit (source_unit)
190
+ # @param units [Api::Unit, Sc2::UnitGroup] a unit or unit group
191
+ # @param queue_command [Boolean] shift+command
192
+ # @return [void]
193
+ def attack_with(units:, queue_command: false)
194
+ return unless units.is_a?(Api::Unit) || units.is_a?(Sc2::UnitGroup)
195
+
196
+ units.attack(target: self, queue_command:)
197
+ end
198
+
199
+ # Builds target unit type, i.e. issuing a build command to worker.build(...Api::UnitTypeId::BARRACKS)
200
+ # @param unit_type_id [Integer] Api::UnitTypeId the unit type which will do the creation
201
+ # @param target [Api::Point2D, Integer, nil] is a unit tag or a Api::Point2D. Nil for addons/orbital
202
+ # @param queue_command [Boolean] shift+command
203
+ def build(unit_type_id:, target: nil, queue_command: false)
204
+ @bot.build(units: self, unit_type_id:, target:, queue_command:)
205
+ end
206
+
207
+ # This structure creates a unit, i.e. this Barracks trains a Marine
208
+ # @see #build
209
+ alias_method :train, :build
210
+ # def train(unit_type_id:, target: nil, queue_command: false)
211
+ # @bot.build(units: self, unit_type_id:, target:, queue_command:)
212
+ # end
213
+
214
+ # Issues repair command on target
215
+ # @param target [Api::Unit, Integer] is a unit or unit tag
216
+ def repair(target:, queue_command: false)
217
+ action(ability_id: Api::AbilityId::EFFECT_REPAIR, target:, queue_command:)
218
+ end
219
+
220
+ # @!endgroup Actions
221
+ #
222
+ # Debug ----
223
+
224
+ # Draws a placement outline
225
+ # @param color [Api::Color] optional api color, default white
226
+ # @return [void]
227
+ def debug_draw_placement(color = nil)
228
+ # Slightly elevate the Z position so that the line doesn't clip into the terrain at same Z level
229
+ z_elevated = pos.z + 0.01
230
+ offset = footprint_radius
231
+ # Box corners
232
+ p0 = Api::Point.new(x: pos.x - offset, y: pos.y - offset, z: z_elevated)
233
+ p1 = Api::Point.new(x: pos.x - offset, y: pos.y + offset, z: z_elevated)
234
+ p2 = Api::Point.new(x: pos.x + offset, y: pos.y + offset, z: z_elevated)
235
+ p3 = Api::Point.new(x: pos.x + offset, y: pos.y - offset, z: z_elevated)
236
+ @bot.queue_debug_command Api::DebugCommand.new(
237
+ draw: Api::DebugDraw.new(
238
+ lines: [
239
+ Api::DebugLine.new(
240
+ color:,
241
+ line: Api::Line.new(p0:, p1:)
242
+ ),
243
+ Api::DebugLine.new(
244
+ color:,
245
+ line: Api::Line.new(p0: p2, p1: p3)
246
+ ),
247
+ Api::DebugLine.new(
248
+ color:,
249
+ line: Api::Line.new(p0:, p1: p3)
250
+ ),
251
+ Api::DebugLine.new(
252
+ color:,
253
+ line: Api::Line.new(p0: p1, p1: p2)
254
+ )
255
+ ]
256
+ )
257
+ )
258
+ end
259
+
260
+ # Draws a sphere around the unit's attack range
261
+ # @param weapon_index [Api::Color] default first weapon, see UnitTypeData.weapons
262
+ # @param color [Api::Color] optional api color, default red
263
+ def debug_fire_range(weapon_index = 0, color = nil)
264
+ color = Api::Color.new(r: 255, b: 0, g: 0) if color.nil?
265
+ attack_range = unit_data.weapons[weapon_index].range
266
+ raised_position = pos.dup
267
+ raised_position.z += 0.01
268
+ @bot.debug_draw_sphere(point: raised_position, radius: attack_range, color:)
269
+ end
270
+
271
+ # Geometric/Map/Micro functions ---
272
+
273
+ # Calculates the distance between self and other
274
+ # @param other [Sc2::Position, Api::Unit, Api::PowerSource, Api::RadarRing, Api::Effect]
275
+ def distance_to(other)
276
+ return 0.0 if other.nil? || other == self
277
+
278
+ other = other.pos unless other.is_a? Sc2::Position
279
+ pos.distance_to(other)
280
+ end
281
+
282
+ # Gets the nearest amount of unit(s) from a group, relative to this unit
283
+ # Omitting amount returns a single Unit.
284
+ # @param units [Sc2::UnitGroup]
285
+ # @param amount [Integer]
286
+ # @return [Sc2::UnitGroup, Api::Unit, nil] return group or a Unit if amount is not passed
287
+ def nearest(units:, amount: nil)
288
+ amount = 1 if !amount.nil? && amount.to_i <= 0
289
+
290
+ # Performs suboptimal if sending an array. Don't.
291
+ if units.is_a? Array
292
+ units = Sc2::UnitGroup.new(units)
293
+ units.use_kdtree = false # we will not re-use it's distance cache
294
+ end
295
+
296
+ units.nearest_to(pos:, amount:)
297
+ end
298
+
299
+ # Detects whether a unit is within a given circle
300
+ # @param point [Api::Point2D, Api::Point]
301
+ def in_circle?(point:, radius:)
302
+ distance_to(point) <= radius
303
+ end
304
+
305
+ # Returns whether unit is currently engaged with another
306
+ # @param target [Api::Unit, Integer] optionally check if unit is engaged with specific target
307
+ def is_attacking?(target: nil)
308
+ is_performing_ability_on_target?(
309
+ [Api::AbilityId::ATTACK_ATTACK],
310
+ target:
311
+ )
312
+ end
313
+
314
+ # Returns whether the unit's current order is to repair and optionally check it's target
315
+ # @param target [Api::Unit, Integer] optionally check if unit is engaged with specific target
316
+ # @return [Boolean]
317
+ def is_repairing?(target: nil)
318
+ is_performing_ability_on_target?(
319
+ [Api::AbilityId::EFFECT_REPAIR, Api::AbilityId::EFFECT_REPAIR_SCV, Api::AbilityId::EFFECT_REPAIR_MULE],
320
+ target:
321
+ )
322
+ end
323
+
324
+ # Checks whether the unit has
325
+ # @param ability_ids [Integer, Array<Integer>] accepts one or an array of Api::AbilityId
326
+ def is_performing_ability?(ability_ids)
327
+ return false if orders.empty?
328
+
329
+ if ability_ids.is_a? Array
330
+ ability_ids.include?(orders.first&.ability_id)
331
+ else
332
+ ability_ids == orders.first&.ability_id
333
+ end
334
+ end
335
+
336
+ # Returns whether engaged_target_tag or the current order matches supplied unit
337
+ # @param unit [Api::Unit, Integer] optionally check if unit is engaged with specific target
338
+ # @return [Boolean]
339
+ def is_engaged_with?(unit)
340
+ # First match on unit#engaged_target_tag, since it's solid for attacks
341
+ unit = unit.tag if unit.is_a? Api::Unit
342
+ return true if engaged_target_tag == unit
343
+
344
+ # Alternatively, check your order to see if your command ties you to the unit
345
+ # It may not be in distance or actively performing, just yet.
346
+ return orders.first.target_unit_tag == unit unless orders.empty?
347
+
348
+ false
349
+ end
350
+
351
+ # Checks whether enemy is within range of weapon or ability and can target ground/air.
352
+ # Defaults to basic weapon. Pass in ability to override
353
+ # @param unit [Api::Unit] enemy
354
+ # @param weapon_index [Integer] defaults to 0 which is it's basic weapon for it's current form
355
+ # @param ability_id [Integer] passing this will override weapon Api::AbilityId::*
356
+ # @return [Boolean]
357
+ # @example
358
+ # ghost.can_attack?(enemy, weapon_index: 0, ability_id: Api::AbilityId::SNIPE)
359
+ def can_attack?(unit:, weapon_index: 0, ability_id: nil)
360
+ if ability_id.nil?
361
+ # weapon
362
+ source_weapon = weapon(weapon_index)
363
+ can_weapon_target_unit?(unit:, weapon: source_weapon)
364
+ else
365
+ # ability
366
+ ability = @bot.ability_data(ability_id)
367
+ can_ability_target_unit?(unit:, ability:)
368
+ end
369
+ end
370
+
371
+ # Checks whether a weapon can target a unit
372
+ # @param unit [Api::unit]
373
+ # @param weapon [Api::Weapon]
374
+ # @return [Boolean]
375
+ def can_weapon_target_unit?(unit:, weapon:)
376
+ # false if enemy is air and we can only shoot ground
377
+ return false if unit.is_flying && weapon.type == :Ground # Api::Weapon::TargetType::Ground
378
+
379
+ # false if enemy is ground and we can only shoot air
380
+ return false if unit.is_ground? && weapon.type == :Air # A pi::Weapon::TargetType::Air
381
+
382
+ # Check if weapon and unit models are in range
383
+ in_attack_range?(unit:, range: weapon.range)
384
+ end
385
+
386
+ def can_ability_target_unit?(unit:, ability:)
387
+ # false if enemy is air and we can only shoot ground
388
+ return false if ability.target == Api::AbilityData::Target::None
389
+
390
+ # Check if weapon and unit models are in range
391
+ in_attack_range?(unit:, range: ability.cast_range)
392
+ end
393
+
394
+ # Checks whether opposing unit is in the attack range.
395
+ # @param unit [Api::Unit]
396
+ # @param range [Float, nil] nil. will use default weapon range if nothing provided
397
+ # @return [Boolean]
398
+ def in_attack_range?(unit:, range: nil)
399
+ range = weapon.range if range.nil?
400
+ distance_to(unit) <= radius + unit.radius + range
401
+ end
402
+
403
+ # Gets a weapon for this unit at index (default weapon is index 0)
404
+ # @param index [Integer] default 0
405
+ # @return [Api::Weapon]
406
+ def weapon(index = 0)
407
+ unit_data.weapons[index]
408
+ end
409
+
410
+ # Macro functions ---
411
+
412
+ # For saturation counters on bases or gas, get the amount of missing harvesters required to saturate.
413
+ # For a unit to which this effect doesn't apply, the amount is zero.
414
+ # @return [Integer] number of harvesters required to saturate this structure
415
+ def missing_harvesters
416
+ return 0 if ideal_harvesters.zero?
417
+
418
+ missing = ideal_harvesters - assigned_harvesters
419
+ missing.positive? ? missing : 0
420
+ end
421
+
422
+ # The placement size, by looking up unit's creation ability, then game ability data
423
+ # This value should be correct for building placement math (unit.radius is not good for this)
424
+ # @return [Float] placement radius
425
+ def footprint_radius
426
+ @bot.data.abilities[unit_data.ability_id].footprint_radius
427
+ end
428
+
429
+ # Returns true if build progress is 100%
430
+ # @return [Boolean]
431
+ def is_completed?
432
+ build_progress == 1.0 # standard:disable Lint/FloatComparison
433
+ end
434
+
435
+ # Convenience functions ---
436
+
437
+ # TERRAN Convenience functions ---
438
+
439
+ # Returns the Api::Unit add-on (Reactor/Tech Lab), if present for this structure
440
+ # @return [Api::Unit, nil] the unit if an addon is present or nil if not present
441
+ def add_on
442
+ @add_on ||= @bot.structures[add_on_tag]
443
+ end
444
+
445
+ # Returns whether the structure has a reactor add-on
446
+ # @return [Boolean] if the unit has a reactor attached
447
+ def has_reactor
448
+ Sc2::UnitGroup::TYPE_REACTOR.include?(add_on&.unit_type)
449
+ end
450
+
451
+ # Returns whether the structure has a tech lab add-on
452
+ # @example
453
+ # # Find the first Starport with a techlab
454
+ # sp = structures.select_type(Api::UnitTypeId::STARPORT).find(&:has_tech_lab)
455
+ # # Get the actual tech-lab with #add_on
456
+ # sp.add_on.research ...
457
+ # @return [Boolean] if the unit has a tech lab attached
458
+ def has_tech_lab
459
+ Sc2::UnitGroup::TYPE_TECHLAB.include?(add_on&.unit_type)
460
+ end
461
+
462
+ # For Terran builds a tech lab add-on on the current structure
463
+ # @return [void]
464
+ def build_reactor
465
+ build(unit_type_id: Api::UnitTypeId::REACTOR)
466
+ end
467
+
468
+ # For Terran builds a tech lab add-on on the current structure
469
+ # @return [void]
470
+ def build_tech_lab
471
+ build(unit_type_id: Api::UnitTypeId::TECHLAB)
472
+ end
473
+
474
+ private
475
+
476
+ # @private
477
+ # Reduces repitition in the is_*action*?(target:) methods
478
+ def is_performing_ability_on_target?(abilities, target: nil)
479
+ # Exit if not actioning the ability
480
+ return false unless is_performing_ability?(abilities)
481
+
482
+ # If a target was given and we're targeting it, us that value
483
+ return is_engaged_with?(target) unless target.nil?
484
+
485
+ true
486
+ end
487
+ end
488
+ end
489
+ Api::Unit.include Api::UnitExtension