sc2ai 0.0.0.pre → 0.0.2

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