sc2ai 0.0.4 → 0.0.6

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
 
@@ -16,9 +16,12 @@ module Sc2
16
16
 
17
17
  extend Forwardable
18
18
 
19
- # @!attribute game_loop
20
- # @return [Integer] current game loop
21
- def_delegators :observation, :game_loop
19
+ attr_writer :game_loop
20
+
21
+ # @return [Integer] current game loop
22
+ def game_loop
23
+ @game_loop || 0
24
+ end
22
25
 
23
26
  # @!attribute game_info [rw]
24
27
  # Access useful game information. Used in parsed pathing grid, terrain height, placement grid.
@@ -38,7 +41,7 @@ module Sc2
38
41
  attr_accessor :game_info_loop
39
42
 
40
43
  # Determines if your game_info will be refreshed at this moment
41
- # Has a hard-capped refresh of only ever 2 steps
44
+ # Has a hard-capped refresh of only ever 4 steps
42
45
  # In general game_info is only refreshed Player::Bot reads from pathing_grid or placement_grid
43
46
  # @return [Boolean]
44
47
  def game_info_stale?
@@ -47,7 +50,7 @@ module Sc2
47
50
 
48
51
  # Note: No minimum step count set anymore
49
52
  # We can do something like, only updating every 2+ frames:
50
- game_info_loop + 2 <= game_loop
53
+ game_info_loop + 4 <= game_loop
51
54
  end
52
55
 
53
56
  # @!attribute data
@@ -20,7 +20,7 @@ module Sc2
20
20
  # @return [Integer]
21
21
  def map_width
22
22
  # bot.bot.game_info
23
- bot.game_info.start_raw.map_size.x
23
+ @map_width ||= bot.game_info.start_raw.map_size.x
24
24
  end
25
25
 
26
26
  # Gets the map tile height. Range is 1-255.
@@ -28,7 +28,7 @@ module Sc2
28
28
  # @return [Integer]
29
29
  def map_height
30
30
  # bot.bot.game_info
31
- bot.game_info.start_raw.map_size.y
31
+ @map_height ||= bot.game_info.start_raw.map_size.y
32
32
  end
33
33
 
34
34
  # Returns zero to map_width as range
@@ -83,6 +83,15 @@ module Sc2
83
83
  @parsed_placement_grid
84
84
  end
85
85
 
86
+ # Whether this tile is where an expansion is supposed to be placed.
87
+ # To see if a unit/structure is blocking an expansion, pass their coordinates to this method.
88
+ # @param x [Float, Integer]
89
+ # @param y [Float, Integer]
90
+ # @return [Boolean] true if location has creep on it
91
+ def expo_placement?(x:, y:)
92
+ expo_placement_grid[y.to_i, x.to_i] == 1
93
+ end
94
+
86
95
  # Returns a grid where ony the expo locations are marked
87
96
  # @return [Numo::Bit]
88
97
  def expo_placement_grid
@@ -259,6 +268,13 @@ module Sc2
259
268
  parsed_terrain_height[y.to_i, x.to_i]
260
269
  end
261
270
 
271
+ # Returns the terrain height (z) at position x and y for a point
272
+ # @param position [Sc2::Position]
273
+ # @return [Float] z axis position between -16 and 16
274
+ def terrain_height_for_pos(position)
275
+ terrain_height(x: position.x, y: position.y)
276
+ end
277
+
262
278
  # Returns a parsed terrain_height from bot.game_info.start_raw.
263
279
  # Each value in [row][column] holds a float value which is the z height
264
280
  # @return [Numo::SFloat] Numo array
@@ -334,15 +350,17 @@ module Sc2
334
350
  end
335
351
 
336
352
  # Provides parsed minimap representation of creep spread
353
+ # Caches for 4 frames
337
354
  # @return [Numo::Bit] Numo array
338
355
  def parsed_creep
339
- if @parsed_creep.nil?
356
+ if @parsed_creep.nil? || @parsed_creep[1] + 4 < bot.game_loop
340
357
  image_data = bot.observation.raw_data.map_state.creep
341
358
  # Fix endian for Numo bit parser
342
359
  data = image_data.data.unpack("b*").pack("B*")
343
- @parsed_creep = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
360
+ result = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
361
+ @parsed_creep = [result, bot.game_loop]
344
362
  end
345
- @parsed_creep
363
+ @parsed_creep[0]
346
364
  end
347
365
 
348
366
  # TODO: Removing. Better name or more features for this? Maybe check nearest units.
@@ -537,16 +555,75 @@ module Sc2
537
555
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
538
556
  # @return [Sc2::UnitGroup] UnitGroup of minerals for the base
539
557
  def minerals_for_base(base)
540
- # resources_for_base contains what we need, but slice neutral.minerals,
541
- # so that only active patches remain
542
- bot.neutral.minerals.slice(*resources_for_base(base).minerals.tags)
558
+ base_resources = resources_for_base(base)
559
+ cached_tags = base_resources.minerals.tags
560
+ observed_tags = bot.neutral.minerals.tags
561
+
562
+ # BACK-STORY: Mineral id's are fixed when in vision.
563
+ # Snapshots get random id's every time an object leaves vision.
564
+ # At game launch when we calculate and save minerals, which are mostly snapshot.
565
+
566
+ # Currently, we might have moved vision over minerals, so that their id's have changed.
567
+ # The alive object share a Position with our cached one, so we can get the correct id and update our cache.
568
+
569
+ # PERF: Fix takes 0.70ms, cache takes 0.10ms - we mostly call cached. This is the way.
570
+ # PERF: In contrast, repeated calls to neutral.minerals.units_in_circle? always costs 0.22ms
571
+
572
+ missing_tags = cached_tags - observed_tags
573
+ unless missing_tags.empty?
574
+ other_alive_minerals = bot.neutral.minerals.slice(*(observed_tags - cached_tags))
575
+ # For each missing calculated mineral patch...
576
+ missing_tags.each do |tag|
577
+ missing_resource = base_resources.delete(tag)
578
+
579
+ # Find an alive mineral at that position
580
+ new_resource = other_alive_minerals.find { |live_mineral| live_mineral.pos == missing_resource.pos }
581
+ base_resources.add(new_resource) unless new_resource.nil?
582
+ end
583
+ end
584
+
585
+ base_resources.minerals
543
586
  end
544
587
 
545
588
  # Gets geysers for a base or base position
546
589
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
547
590
  # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
548
591
  def geysers_for_base(base)
549
- resources_for_base(base).geysers
592
+ # @see #minerals_for_base for backstory on these fixes
593
+ base_resources = resources_for_base(base)
594
+ cached_tags = base_resources.geysers.tags
595
+ observed_tags = bot.neutral.geysers.tags
596
+
597
+ missing_tags = cached_tags - observed_tags
598
+ unless missing_tags.empty?
599
+ other_alive_geysers = bot.neutral.geysers.slice(*(observed_tags - cached_tags))
600
+ # For each missing calculated geyser patch...
601
+ missing_tags.each do |tag|
602
+ missing_resource = base_resources.delete(tag)
603
+
604
+ # Find an alive geyser at that position
605
+ new_resource = other_alive_geysers.find { |live_geyser| live_geyser.pos == missing_resource.pos }
606
+ base_resources.add(new_resource) unless new_resource.nil?
607
+ end
608
+ end
609
+
610
+ base_resources.geysers
611
+ end
612
+
613
+ # Gets geysers which have not been taken for a base or base position
614
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
615
+ # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
616
+ def geysers_open_for_base(base)
617
+ geysers = geysers_for_base(base)
618
+
619
+ # Mineral-only base, return nothing
620
+ return UnitGroup.new if geysers.size == 0
621
+
622
+ # Reject all which have a gas structure on-top
623
+ gas_positions = bot.structures.gas.map { |gas| gas.pos }
624
+ geysers.reject do |geyser|
625
+ gas_positions.include?(geyser.pos)
626
+ end
550
627
  end
551
628
 
552
629
  # @private
@@ -554,6 +631,7 @@ module Sc2
554
631
  # @return [Sc2::UnitGroup] UnitGroup of resources (minerals+geysers)
555
632
  private def resources_for_base(base)
556
633
  pos = base.is_a?(Api::Unit) ? base.pos : base
634
+ pos = pos.to_p2d if base.is_a?(Api::Point)
557
635
 
558
636
  # If we have a base setup for this exact position, use it
559
637
  if expansions.has_key?(pos)
@@ -593,12 +671,17 @@ module Sc2
593
671
  def build_coordinates(length:, on_creep: false, in_power: false)
594
672
  length = 1 if length < 1
595
673
  @_build_coordinates ||= {}
596
- cache_key = [length, on_creep].hash
674
+ cache_key = [length, on_creep, in_power].hash
597
675
  return @_build_coordinates[cache_key] if !@_build_coordinates[cache_key].nil? && !bot.game_info_stale?
598
676
 
599
677
  result = []
600
678
  input_grid = parsed_pathing_grid & parsed_placement_grid & ~expo_placement_grid
601
- input_grid = parsed_creep & input_grid if on_creep
679
+ input_grid = if on_creep
680
+ parsed_creep & input_grid
681
+ else
682
+ ~parsed_creep & input_grid
683
+ end
684
+
602
685
  input_grid = parsed_power_grid & input_grid if in_power
603
686
 
604
687
  # Dimensions
@@ -651,16 +734,17 @@ module Sc2
651
734
  # @param length [Integer] length of the building, 2 for depot/pylon, 3 for rax/gate
652
735
  # @param target [Api::Unit, Sc2::Position] near where to find a placement
653
736
  # @param random [Integer] number of nearest points to randomly choose from. 1 for nearest point.
737
+ # @param in_power [Boolean] whether this must be on a power field
654
738
  # @return [Api::Point2D, nil] buildable location, nil if no buildable location found
655
- def build_placement_near(length:, target:, random: 1)
739
+ def build_placement_near(length:, target:, random: 1, in_power: false)
656
740
  target = target.pos if target.is_a? Api::Unit
657
741
  random = 1 if random.to_i.negative?
658
742
  length = 1 if length < 1
659
743
  on_creep = bot.race == Api::Race::Zerg
660
744
 
661
- coordinates = build_coordinates(length:, on_creep:)
745
+ coordinates = build_coordinates(length:, on_creep:, in_power:)
746
+ cache_key = coordinates.hash
662
747
  @_build_coordinate_tree ||= {}
663
- cache_key = [length, on_creep].hash
664
748
  if @_build_coordinate_tree[cache_key].nil?
665
749
  @_build_coordinate_tree[cache_key] = Kdtree.new(
666
750
  coordinates.each_with_index.map { |coords, index| coords + [index] }
@@ -813,14 +897,14 @@ module Sc2
813
897
  # @example
814
898
  # Randomly randomly adjust both x and y by a range of -3.5 or +3.5
815
899
  # geo.point_random_near(point: structures.hq.first, offset: 3.5)
816
- # @param pos [Sc2::Location]
900
+ # @param pos [Sc2::Position]
817
901
  # @param offset [Float]
818
902
  # @return [Api::Point2D]
819
903
  def point_random_near(pos:, offset: 1.0)
820
904
  pos.random_offset(offset)
821
905
  end
822
906
 
823
- # @param pos [Sc2::Location]
907
+ # @param pos [Sc2::Position]
824
908
  # @param radius [Float]
825
909
  # @return [Api::Point2D]
826
910
  def point_random_on_circle(pos:, radius: 1.0)
@@ -24,7 +24,7 @@ module Sc2
24
24
  @status = bot.status
25
25
  @game_info = bot.game_info
26
26
  @observation = bot.observation
27
-
27
+ @game_loop = bot.observation.game_loop
28
28
  @spent_minerals = bot.spent_minerals
29
29
  @spent_vespene = bot.spent_vespene
30
30
  @spent_supply = bot.spent_supply
@@ -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,94 @@ 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
+
80
+ # For this unit type, tells you how many are in progress by checking orders for all it's sources.
81
+ # @return [Integer]
82
+ def units_in_progress(unit_type_id)
83
+ source_unit_types = Api::TechTree.unit_created_from(unit_type_id: unit_type_id)
84
+
85
+ # When building from LARVA, check the intermediate models
86
+ if source_unit_types.include?(Api::UnitTypeId::LARVA)
87
+ source_unit_types << Api::UnitTypeId::EGG
88
+ elsif source_unit_types.include?(Api::UnitTypeId::BANELING)
89
+ # For certain Zerg types, return the count of specific intermediate egg/cocoon
90
+ return units.select_type(Api::UnitTypeId::BANELINGCOCOON).size
91
+ elsif source_unit_types.include?(Api::UnitTypeId::RAVAGER)
92
+ return units.select_type(Api::UnitTypeId::RAVAGERCOCOON).size
93
+ elsif source_unit_types.include?(Api::UnitTypeId::OVERSEER)
94
+ return units.select_type(Api::UnitTypeId::OVERLORDCOCOON).size
95
+ elsif source_unit_types.include?(Api::UnitTypeId::LURKERMP)
96
+ return units.select_type(Api::UnitTypeId::LURKERMPEGG).size
97
+ elsif source_unit_types.include?(Api::UnitTypeId::BROODLORD)
98
+ return units.select_type(Api::UnitTypeId::BROODLORDCOCOON).size
99
+ end
100
+
101
+ unit_create_ability = Api::TechTree.unit_type_creation_ability_id(
102
+ source: source_unit_types.first,
103
+ target: unit_type_id
104
+ )
105
+
106
+ origin = if unit_data(source_unit_types.first).attributes.include?(:Structure)
107
+ structures
108
+ else
109
+ units
110
+ end
111
+ total_in_progress = origin.select_type(source_unit_types).sum do |source|
112
+ source.orders.count do |order|
113
+ true if order.ability_id == unit_create_ability
114
+ end
115
+ end
116
+ total_in_progress *= 2 if unit_type_id == Api::UnitTypeId::ZERGLING
117
+
118
+ total_in_progress
119
+ end
120
+
33
121
  # An array of Protoss power sources, which have a point, radius and unit tag
34
122
  # @!attribute power_sources
35
123
  # @return [Array<Api::PowerSource>] an array of power sources
@@ -107,6 +195,13 @@ module Sc2
107
195
  data.abilities[ability_id]
108
196
  end
109
197
 
198
+ # Returns static [Api::UpgradeData] for an upgrade id
199
+ # @param upgrade_id [Integer] Api::UpgradeId::*
200
+ # @return [Api::UpgradeData]
201
+ def upgrade_data(upgrade_id)
202
+ data.upgrades[upgrade_id]
203
+ end
204
+
110
205
  # Checks unit data for an attribute value
111
206
  # @param unit [Integer,Api::Unit] Api::UnitTypeId or Api::Unit
112
207
  # @param attribute [Symbol] Api::Attribute, i.e. Api::Attribute::Mechanical or :Mechanical
@@ -141,17 +236,13 @@ module Sc2
141
236
  def subtract_cost(unit_type_id)
142
237
  unit_type_data = unit_data(unit_type_id)
143
238
 
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
239
  @spent_minerals += unit_type_data.mineral_cost
150
240
  @spent_vespene += unit_type_data.vespene_cost
151
- @spent_supply += supply_cost
241
+ @spent_supply += unit_type_data.food_required
152
242
  end
153
243
 
154
244
  # Checks whether you have the resources to construct quantity of unit type
245
+ # @return [Boolean]
155
246
  def can_afford?(unit_type_id:, quantity: 1)
156
247
  unit_type_data = unit_data(unit_type_id)
157
248
  return false if unit_type_data.nil?
@@ -178,6 +269,25 @@ module Sc2
178
269
  true
179
270
  end
180
271
 
272
+ # Checks whether you have the resources to
273
+ # @return [Boolean]
274
+ def can_afford_upgrade?(upgrade_id)
275
+ unit_type_data = upgrade_data(upgrade_id)
276
+ return false if unit_type_data.nil?
277
+
278
+ mineral_cost = unit_type_data.mineral_cost
279
+ if common.minerals - spent_minerals < mineral_cost
280
+ return false # not enough minerals
281
+ end
282
+
283
+ vespene_cost = unit_type_data.vespene_cost
284
+ if common.vespene - spent_vespene < vespene_cost
285
+ return false # you require more vespene gas
286
+ end
287
+
288
+ true
289
+ end
290
+
181
291
  private
182
292
 
183
293
  # @private
data/lib/sc2ai/player.rb CHANGED
@@ -226,13 +226,14 @@ module Sc2
226
226
  # Callback for step 0
227
227
  on_step
228
228
 
229
+ puts ""
229
230
  # Step 1 to n
230
231
  loop do
231
232
  r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
232
233
  perform_actions
233
234
  perform_debug_commands # TODO: Detect IS_LADDER? -> unless IS_LADDER?
234
235
  step_forward
235
- puts (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000
236
+ print "\e[2K#{@step_count} Steps Took (ms): #{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000}\n\e[1A\r"
236
237
  return @result unless @result.nil?
237
238
  break if @status != :in_game
238
239
  end
@@ -525,6 +526,7 @@ module Sc2
525
526
  @previous.reset(self)
526
527
  # Reset
527
528
  self.observation = response_observation.observation
529
+ self.game_loop = observation.game_loop
528
530
  self.chats_received = response_observation.chat
529
531
  self.spent_minerals = 0
530
532
  self.spent_vespene = 0
@@ -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 ---
@@ -0,0 +1,22 @@
1
+ module Api
2
+ # This module make sure that a read from method ability_id always returns the proper source id
3
+ module AbilityRemapable
4
+ # Ability Id. The generic id or "remapid".
5
+ # i.e. Api::AbilityId::ATTACK_BATTLECRUISER returns generic Api::AbilityId::ATTACK
6
+ # @return [Integer]
7
+ def ability_id
8
+ @ability_id ||= Api::AbilityId.generic_id(send(:method_missing, :ability_id))
9
+ end
10
+ end
11
+ end
12
+
13
+ # AbilityData should not include, since it holds exact info and contains the remap id as attr
14
+ # Similarly Request* methods only do requests and ew supply correct id's.
15
+ Api::AvailableAbility.include Api::AbilityRemapable
16
+ Api::UnitOrder.include Api::AbilityRemapable
17
+ Api::ActionRawUnitCommand.include Api::AbilityRemapable
18
+ Api::ActionRawToggleAutocast.include Api::AbilityRemapable
19
+ Api::ActionError.include Api::AbilityRemapable
20
+ Api::ActionSpatialUnitCommand.include Api::AbilityRemapable
21
+ Api::BuildItem.include Api::AbilityRemapable
22
+ Api::ActionToggleAutocast.include Api::AbilityRemapable
@@ -3,7 +3,9 @@ module Api
3
3
  module PointExtension
4
4
  # @private
5
5
  def hash
6
- [x, y, z].hash
6
+ # Only one plane is ever used. Ignore hashing on z
7
+ # [x, y, z].hash
8
+ [x, y].hash
7
9
  end
8
10
 
9
11
  def eql?(other)
@@ -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]
@@ -1,12 +1,23 @@
1
1
  module Sc2
2
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.
3
+ # Items which are of type Sc2::Position will have #x and #y property at the least.
4
4
  module Position
5
5
  # Tolerance for floating-point comparisons.
6
6
  TOLERANCE = 1e-9
7
7
 
8
8
  # Basic operations
9
9
 
10
+ # Loose equality matches on floats x and y.
11
+ # We never check z-axis, because the map is single-level.
12
+ # TODO: We should almost certainly introduce TOLERANCE here, but verify it's cost first.
13
+ def ==(other)
14
+ if other.is_a? Position
15
+ x == other.x && y == other.y
16
+ else
17
+ false
18
+ end
19
+ end
20
+
10
21
  # A new point representing the sum of this point and the other point.
11
22
  # @param other [Api::Point2D, Numeric] The other point/number to add.
12
23
  # @return [Api::Point2D]
@@ -50,12 +61,38 @@ module Sc2
50
61
  # @see #divide
51
62
  alias_method :/, :divide
52
63
 
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.
64
+ # Returns x coordinate
65
+ # @return [Float]
66
+ def x
67
+ # Perf: Memoizing attributes which are hit hard, show gain
68
+ @x ||= send(:method_missing, :x)
69
+ end
70
+
71
+ # Sets x coordinate
72
+ # @return [Float]
73
+ def x=(x)
74
+ send(:method_missing, :x=, x)
75
+ @x = x
76
+ end
77
+
78
+ # Returns y coordinate
79
+ # @return [Float]
55
80
  def y
81
+ # Bug: Psych implements method 'y' on Kernel, but protobuf uses method_missing to read AbstractMethod
82
+ # We send method missing ourselves when y to fix this chain.
56
83
  # This is correct, but an unnecessary conditional:
57
84
  # raise NoMethodError unless location == self
58
- send(:method_missing, :y)
85
+
86
+ # Perf: Memoizing attributes which are hit hard, show gain
87
+
88
+ @y ||= send(:method_missing, :y)
89
+ end
90
+
91
+ # Sets y coordinate
92
+ # @return [Float]
93
+ def y=(y)
94
+ send(:method_missing, :y=, y)
95
+ @y = y
59
96
  end
60
97
 
61
98
  # Randomly adjusts both x and y by a range of: -offset..offset
@@ -9,6 +9,22 @@ module Api
9
9
  tag || super
10
10
  end
11
11
 
12
+ # Returns an integer unique identifier
13
+ # If the unit goes out of vision and is snapshot-able, they get a random id
14
+ # - Such a unit gets the same unit tag when it re-enters vision
15
+ # @return [Integer]
16
+ def tag
17
+ # Perf: This speeds up hash and therefore common UnitGroup operations. Sometimes 3x!
18
+ @tag ||= send(:method_missing, :tag)
19
+ end
20
+
21
+ # Sets unit tag
22
+ # @return [Integer]
23
+ def tag=(tag)
24
+ send(:method_missing, :tag=, tag)
25
+ @tag = tag
26
+ end
27
+
12
28
  # Every unit gets access back to the bot to allow api access.
13
29
  # For your own units, this allows API access.
14
30
  # @return [Sc2::Player] player with active connection
@@ -37,9 +53,8 @@ module Api
37
53
  # Checks unit data for an attribute value
38
54
  # @return [Boolean] whether unit has attribute
39
55
  # @example
40
- # has_attribute?(Api::UnitTypeId::SCV, Api::Attribute::Mechanical)
41
- # has_attribute?(units.workers.first, :Mechanical)
42
- # has_attribute?(Api::UnitTypeId::SCV, :Mechanical)
56
+ # unit.has_attribute?(Api::Attribute::Mechanical)
57
+ # unit.has_attribute?(:Mechanical)
43
58
  def has_attribute?(attribute)
44
59
  attributes.include? attribute
45
60
  end
@@ -162,6 +177,15 @@ module Api
162
177
 
163
178
  # @!endgroup Virtual properties
164
179
 
180
+ # Whether unit is effected by buff_id
181
+ # @example
182
+ # unit.has_buff??(Api::BuffId::QUEENSPAWNLARVATIMER)
183
+ # @param [Integer] buff_id
184
+ # @return [Boolean]
185
+ def has_buff?(buff_id)
186
+ buff_ids.include?(buff_id)
187
+ end
188
+
165
189
  # @!group Actions
166
190
 
167
191
  # Performs action on this unit
@@ -233,10 +257,18 @@ module Api
233
257
 
234
258
  # Issues repair command on target
235
259
  # @param target [Api::Unit, Integer] is a unit or unit tag
260
+ # @param queue_command [Boolean] shift+command
236
261
  def repair(target:, queue_command: false)
237
262
  action(ability_id: Api::AbilityId::EFFECT_REPAIR, target:, queue_command:)
238
263
  end
239
264
 
265
+ # Research a specific upgrade
266
+ # @param upgrade_id [Integer] Api::UnitTypeId the unit type which will do the creation
267
+ # @param queue_command [Boolean] shift+command
268
+ def research(upgrade_id:, queue_command: false)
269
+ @bot.research(units: self, upgrade_id:, queue_command:)
270
+ end
271
+
240
272
  # @!endgroup Actions
241
273
  #
242
274
  # Debug ----
@@ -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