ballistics-ng 0.1.0.1

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.
@@ -0,0 +1,123 @@
1
+ require 'ballistics/yaml'
2
+
3
+ class Ballistics::Gun
4
+ class ChamberNotFound < KeyError; end
5
+
6
+ MANDATORY = {
7
+ "name" => :string,
8
+ "chamber" => :string,
9
+ "barrel_length" => :float,
10
+ "sight_height" => :float,
11
+ }
12
+ OPTIONAL = {
13
+ "zero_range" => :float,
14
+ }
15
+
16
+ # map a chamber name to a built-in cartridge YAML filename
17
+ CHAMBER_CARTRIDGE = {
18
+ '5.56' => '5_56',
19
+ '300 BLK' => '300_blk',
20
+ # TODO: create these cartridge yamls
21
+ # '.45 ACP' => '45_acp',
22
+ # '12ga' => '12ga',
23
+ }
24
+
25
+ # Load a built-in YAML file and instantiate gun objects
26
+ # Return a hash of gun objects keyed by gun id (per the YAML)
27
+ #
28
+ def self.find(short_name)
29
+ objects = {}
30
+ Ballistics::YAML.load_built_in('guns', short_name).each { |id, hsh|
31
+ obj = self.new(hsh)
32
+ if block_given?
33
+ objects[id] = obj if yield obj
34
+ else
35
+ objects[id] = obj
36
+ end
37
+ }
38
+ objects
39
+ end
40
+
41
+ def self.find_id(id)
42
+ Ballistics::YAML::BUILT_IN.fetch('guns').each { |short_name|
43
+ object = self.find(short_name)[id]
44
+ return object if object
45
+ }
46
+ nil
47
+ end
48
+
49
+ def self.fetch_id(id)
50
+ self.find_id(id) or raise("id #{id} not found")
51
+ end
52
+
53
+ attr_reader(*MANDATORY.keys)
54
+ attr_reader(*OPTIONAL.keys)
55
+ attr_reader(:yaml_data, :extra)
56
+
57
+ def initialize(hsh)
58
+ @yaml_data = hsh
59
+ MANDATORY.each { |field, type|
60
+ val = hsh.fetch(field)
61
+ val = val.to_s if field == "chamber"
62
+ Ballistics::YAML.check_type!(val, type)
63
+ self.instance_variable_set("@#{field}", val)
64
+ }
65
+
66
+ OPTIONAL.each { |field, type|
67
+ if hsh.key?(field)
68
+ val = hsh[field]
69
+ Ballistics::YAML.check_type!(val, type)
70
+ self.instance_variable_set("@#{field}", val)
71
+ end
72
+ }
73
+
74
+ # Keep track of fields that we don't expect
75
+ @extra = {}
76
+ (hsh.keys - MANDATORY.keys - OPTIONAL.keys).each { |k|
77
+ @extra[k] = hsh[k]
78
+ }
79
+ @cartridges = []
80
+ end
81
+
82
+ def chamber=(new_chamber)
83
+ @cartridges = []
84
+ @chamber = new_chamber
85
+ end
86
+
87
+ def cartridge_file
88
+ CHAMBER_CARTRIDGE[@chamber] or raise(ChamberNotFound, @chamber)
89
+ end
90
+
91
+ # this will pull in cartridges and projectiles based on the gun chamber
92
+ def cartridges
93
+ if @cartridges.empty?
94
+ require 'ballistics/cartridge'
95
+
96
+ cartridge_file = CHAMBER_CARTRIDGE[@chamber] or
97
+ raise(ChamberNotFound, @chamber)
98
+ @cartridges =
99
+ Ballistics::Cartridge.find_with_projectiles(cartridge_file)
100
+ end
101
+ @cartridges
102
+ end
103
+
104
+ def params
105
+ params = { sight_height: @sight_height }
106
+ params[:zero_range] = @zero_range if @zero_range
107
+ params
108
+ end
109
+
110
+ def multiline
111
+ lines = ["GUN: #{name}", "==="]
112
+ fields = {
113
+ "Chamber" => @chamber,
114
+ "Barrel length" => @barrel_length,
115
+ "Sight height" => @sight_height,
116
+ }
117
+ fields["Zero Range"] = @zero_range if @zero_range
118
+ fields.each { |name, val|
119
+ lines << [name.rjust(13, ' ' ), val].join(': ')
120
+ }
121
+ lines.join("\n")
122
+ end
123
+ end
@@ -0,0 +1,6 @@
1
+ "1911":
2
+ name: M1911A1
3
+ sight_height: 0.8
4
+ barrel_length: 5
5
+ zero_range: 25
6
+ chamber: .45 ACP
@@ -0,0 +1,34 @@
1
+ ar15_carbine:
2
+ name: Standard AR-15 carbine with 16 inch barrel (5.56)
3
+ sight_height: 2.6
4
+ barrel_length: 16
5
+ zero_range: 50
6
+ chamber: 5.56
7
+
8
+ m4:
9
+ name: Standard M4 carbine with 14.5 inch barrel (5.56)
10
+ sight_height: 2.6
11
+ barrel_length: 14.5
12
+ zero_range: 50
13
+ chamber: 5.56
14
+
15
+ m16:
16
+ name: Standard M16 rifle with 20 inch barrel (5.56)
17
+ sight_height: 2.6
18
+ barrel_length: 20
19
+ zero_range: 100
20
+ chamber: 5.56
21
+
22
+ ar15_sbr:
23
+ name: AR-15 SBR with 10.5 inch barrel (5.56)
24
+ sight_height: 2.6
25
+ barrel_length: 10.5
26
+ zero_range: 50
27
+ chamber: 5.56
28
+
29
+ ar15_300_blk:
30
+ name: AR-15 with 10.5 inch barrel (300 BLK)
31
+ sight_height: 2.6
32
+ barrel_length: 10.5
33
+ zero_range: 50
34
+ chamber: 300 BLK
@@ -0,0 +1,6 @@
1
+ mossberg_500:
2
+ name: "Mossberg 500 12ga 18.5in"
3
+ sight_height: 1.2
4
+ barrel_length: 18.5
5
+ zero_range: 25
6
+ chamber: 12ga
@@ -0,0 +1,102 @@
1
+ require 'ballistics'
2
+ require 'ballistics/projectile'
3
+ require 'ballistics/cartridge'
4
+ require 'ballistics/gun'
5
+ require 'ballistics/atmosphere'
6
+
7
+ class Ballistics::Problem
8
+ DEFAULTS = {
9
+ shooting_angle: 0, # degrees; downhill 0 to -90, uphill 0 to +90
10
+ wind_speed: 0, # mph
11
+ wind_angle: 90, # degrees; 0-360 clockwise; 90 right, 270 left
12
+ interval: 50, # yards
13
+ max_range: 500, # yards
14
+ y_intercept: 0, # inches; -2 means POI 2 inches below POA @ zero range
15
+ }
16
+
17
+ def self.simple(gun_id:, cart_id:, gun_family: nil)
18
+ if gun_family
19
+ gun = Ballistics::Gun.find(gun_family).fetch(gun_id)
20
+ else
21
+ gun = Ballistics::Gun.fetch_id(gun_id)
22
+ end
23
+ cart = gun.cartridges.fetch(cart_id)
24
+ self.new(projectile: cart.projectile,
25
+ cartridge: cart,
26
+ gun: gun)
27
+ end
28
+
29
+ attr_accessor :projectile, :cartridge, :gun, :atmosphere
30
+
31
+ def initialize(projectile: nil,
32
+ cartridge: nil,
33
+ gun: nil,
34
+ atmosphere: nil)
35
+ @projectile = projectile
36
+ @cartridge = cartridge
37
+ @gun = gun
38
+ @atmosphere = atmosphere
39
+ end
40
+
41
+ # Given a hash of specified options / params
42
+ # Return a hash of params enriched by DEFAULTS as well as any inferred
43
+ # parameters from @projectile, @cartridge, @gun, and @atmosphere
44
+ #
45
+ def enrich(opts = {})
46
+ mine = {}
47
+ mine.merge!(@projectile.params) if @projectile
48
+ mine.merge!(@gun.params) if @gun
49
+ mine[:velocity] = @cartridge.mv(@gun.barrel_length) if @cartridge and @gun
50
+
51
+ # Set up the return hash
52
+ # opts overrides mine overrides DEFAULT
53
+ ret = DEFAULTS.merge(mine.merge(opts))
54
+
55
+ # validate drag function and replace with the numeral
56
+ if ret[:drag_function] and !ret[:drag_number]
57
+ ret[:drag_number] =
58
+ Ballistics::Projectile.drag_number(ret[:drag_function])
59
+ end
60
+
61
+ # apply atmospheric correction to ballistic coefficient
62
+ if ret[:ballistic_coefficient] and @atmosphere
63
+ ret[:ballistic_coefficient] =
64
+ @atmosphere.translate(ret[:ballistic_coefficient])
65
+ end
66
+
67
+ ret
68
+ end
69
+
70
+ # Return a multiline string showing each component of the problem
71
+ #
72
+ def report
73
+ lines = []
74
+ lines << @gun.multiline if @gun
75
+ lines << @cartridge.multiline if @cartridge
76
+ lines << @projectile.multiline if @projectile
77
+ lines << @atmosphere.multiline if @atmosphere
78
+ lines.join("\n\n")
79
+ end
80
+
81
+ # Given a zero range and basic ballistics parameters
82
+ # Return the angle between sight axis and bore axis necessary to achieve zero
83
+ #
84
+ def zero_angle(opts = {})
85
+ Ballistics.zero_angle self.enrich opts
86
+ end
87
+
88
+ # Return a data structure with trajectory data at every interval for the
89
+ # specified range
90
+ #
91
+ def trajectory(opts = {})
92
+ Ballistics.trajectory self.enrich opts
93
+ end
94
+
95
+ # Return a multiline string based on trajectory data
96
+ #
97
+ def table(trajectory: nil, fields: nil, opts: {})
98
+ Ballistics.table(trajectory: trajectory,
99
+ fields: fields,
100
+ opts: self.enrich(opts))
101
+ end
102
+ end
@@ -0,0 +1,165 @@
1
+ require 'ballistics/yaml'
2
+
3
+ class Ballistics::Projectile
4
+ MANDATORY = {
5
+ "name" => :string,
6
+ "cal" => :float,
7
+ "grains" => :count,
8
+ }
9
+ # one of these fields is mandatory
10
+ BALLISTIC_COEFFICIENT = {
11
+ "g1" => :float,
12
+ "g7" => :float,
13
+ }
14
+ OPTIONAL = {
15
+ "sd" => :float,
16
+ "intended" => :string,
17
+ "base" => :string,
18
+ "desc" => :string,
19
+ }
20
+ DRAG_FUNCTION = {
21
+ "flat" => "g1",
22
+ "boat" => "g7",
23
+ }
24
+ DRAG_NUMBER = {
25
+ "g1" => 1,
26
+ "g7" => 7,
27
+ }
28
+
29
+ # Load a built-in YAML file and instantiate projectile objects
30
+ # Return a hash of projectile objects keyed by projectile id (per the YAML)
31
+ #
32
+ def self.find(short_name)
33
+ objects = {}
34
+ Ballistics::YAML.load_built_in('projectiles', short_name).each { |id, hsh|
35
+ obj = self.new(hsh)
36
+ if block_given?
37
+ objects[id] = obj if yield obj
38
+ else
39
+ objects[id] = obj
40
+ end
41
+ }
42
+ objects
43
+ end
44
+
45
+ # Normalize common flat-base and boat-tail terms to flat or boat
46
+ #
47
+ def self.base(candidate)
48
+ c = candidate.to_s.downcase.gsub(/[\-\_ ]/, '')
49
+ case c
50
+ when "boat", "boattail", "bt"
51
+ "boat"
52
+ when "flat", "flatbase", "fb"
53
+ "flat"
54
+ else
55
+ raise "unknown base: #{candidate}"
56
+ end
57
+ end
58
+
59
+ # Convert e.g. G1 to 1
60
+ #
61
+ def self.drag_number(drag_function)
62
+ DRAG_NUMBER.fetch(drag_function.to_s.downcase)
63
+ end
64
+
65
+ attr_reader(*MANDATORY.keys)
66
+ attr_reader(*BALLISTIC_COEFFICIENT.keys)
67
+ attr_reader(*OPTIONAL.keys)
68
+ attr_reader :ballistic_coefficient, :yaml_data, :extra
69
+
70
+ def initialize(hsh)
71
+ @yaml_data = hsh
72
+ MANDATORY.each { |field, type|
73
+ val = hsh.fetch(field)
74
+ Ballistics::YAML.check_type!(val, type)
75
+ self.instance_variable_set("@#{field}", val)
76
+ }
77
+
78
+ # Extract ballistic coefficients per drag model (e.g. G1)
79
+ # We need at least one
80
+ #
81
+ @ballistic_coefficient = {}
82
+ BALLISTIC_COEFFICIENT.each { |field, type|
83
+ if hsh.key?(field)
84
+ val = hsh[field]
85
+ Ballistics::YAML.check_type!(val, type)
86
+ self.instance_variable_set("@#{field}", val)
87
+ @ballistic_coefficient[field] = val
88
+ end
89
+ }
90
+ raise "no valid BC" if @ballistic_coefficient.empty?
91
+
92
+ OPTIONAL.each { |field, type|
93
+ if hsh.key?(field)
94
+ val = hsh[field]
95
+ Ballistics::YAML.check_type!(val, type)
96
+ if field == "base"
97
+ @base = self.class.base(val)
98
+ else
99
+ self.instance_variable_set("@#{field}", val)
100
+ end
101
+ end
102
+ }
103
+
104
+ # Keep track of fields that we don't expect
105
+ @extra = {}
106
+ (hsh.keys -
107
+ MANDATORY.keys -
108
+ BALLISTIC_COEFFICIENT.keys -
109
+ OPTIONAL.keys).each { |k|
110
+ @extra[k] = hsh[k]
111
+ }
112
+
113
+ # Make sure @base and @drag_function are initialized so that
114
+ # self.drag_function works without warnings
115
+ #
116
+ @base ||= nil
117
+ @drag_function = nil
118
+ end
119
+
120
+ # Return the preferred drag function if there is a BC available
121
+ #
122
+ def drag_function
123
+ return @drag_function if @drag_function
124
+ @drag_function = @ballistic_coefficient.keys.first
125
+ if @base
126
+ preferred = DRAG_FUNCTION.fetch(@base)
127
+ if @ballistic_coefficient.key?(preferred)
128
+ @drag_function = preferred
129
+ end
130
+ end
131
+ @drag_function
132
+ end
133
+
134
+ # Return the BC for the preferred drag function
135
+ #
136
+ def bc
137
+ @ballistic_coefficient.fetch(self.drag_function)
138
+ end
139
+
140
+ # Return params that can be used by Ballistics::Problem
141
+ #
142
+ def params
143
+ { drag_function: self.drag_function,
144
+ drag_number: self.class.drag_number(self.drag_function),
145
+ ballistic_coefficient: self.bc }
146
+ end
147
+
148
+ # Return lines of text separated by newlines
149
+ #
150
+ def multiline
151
+ lines = ["PROJECTILE: #{@name}", "=========="]
152
+ fields = {
153
+ "Caliber" => @cal,
154
+ "Grains" => @grains,
155
+ }
156
+ @ballistic_coefficient.each { |df, bc|
157
+ fields["BC (#{df.upcase})"] = bc
158
+ }
159
+ fields["Desc"] = @desc if @desc
160
+ fields.each { |name, val|
161
+ lines << [name.rjust(7, ' '), val].join(': ')
162
+ }
163
+ lines.join("\n")
164
+ end
165
+ end
@@ -0,0 +1,321 @@
1
+ # MANDATORY
2
+ # $id:
3
+ # name:
4
+ # cal:
5
+ # grains:
6
+ # AT LEAST ONE OF
7
+ # g1:
8
+ # g7:
9
+ # OPTIONAL
10
+ # desc:
11
+ # intended: # e.g. 300 BLK
12
+ # base: flat|boat
13
+ # sd:
14
+
15
+ # FLAT BASE INTENDED FOR 300 BLK
16
+
17
+ barnes_90_otfb:
18
+ name: Barnes 90gr Zinc OTFB
19
+ cal: .308
20
+ grains: 90
21
+ g1: .246
22
+ intended: 300 BLK
23
+ base: flat
24
+ desc: Open tip, copper jacket, zinc core
25
+
26
+ barnes_110_tac_tx_300_blk:
27
+ name: Barnes 110gr TAC-TX 300 BLK
28
+ cal: .308
29
+ grains: 110
30
+ g1: .289
31
+ sd: .166
32
+ intended: 300 BLK
33
+ base: flat
34
+ desc: Black polymer tip, copper bullet
35
+
36
+ hornady_110_v_max_black:
37
+ name: Hornady 110gr V-MAX BLACK
38
+ cal: .308
39
+ grains: 110
40
+ g1: .290
41
+ sd: .166
42
+ intended: 300 BLK
43
+ base: flat
44
+ desc: Red polymer tip, copper jacket, lead core
45
+
46
+ remington_120_umc_otfb:
47
+ name: Remington 120gr UMC OTFB
48
+ cal: .308
49
+ grains: 120
50
+ g1: .297
51
+ intended: 300 BLK
52
+ base: flat
53
+ desc: Copper jacket, lead core
54
+
55
+ hornady_125_hp_match:
56
+ name: Hornady 125gr HP Match
57
+ cal: .308
58
+ grains: 125
59
+ g1: .320
60
+ sd: .128
61
+ intended: 300 BLK
62
+ base: flat
63
+ desc: Open tip, copper jacket, lead core
64
+
65
+ sierra_125_mk:
66
+ name: Sierra 125gr MatchKing OTFB
67
+ cal: .308
68
+ grains: 125
69
+ g1: .330
70
+ sd: .188
71
+ intended: 300 BLK
72
+ base: flat
73
+ desc: Open tip, copper jacket, lead core
74
+
75
+ sierra_125_tmk:
76
+ name: Sierra 125gr Tipped MatchKing
77
+ cal: .308
78
+ grains: 125
79
+ g1: .332
80
+ sd: .188
81
+ intended: 300 BLK
82
+ base: flat
83
+ desc: Green polymer tip, copper jacket, lead core
84
+
85
+ hornady_135_ftx:
86
+ name: Hornady 135gr FTX
87
+ cal: .308
88
+ grains: 135
89
+ g1: .274
90
+ sd: .203
91
+ intended: 300 BLK
92
+ base: flat
93
+ desc: Red polymer tip, copper jacket, lead core with InterLock
94
+
95
+ hornady_208_a_max_black:
96
+ name: Hornady 208gr A-MAX BLACK
97
+ cal: .308
98
+ grains: 208
99
+ g1: .648
100
+ sd: .313
101
+ intended: 300 BLK
102
+ base: flat
103
+ desc: Red polymer tip, copper jacket, lead core
104
+
105
+ remington_220_umc_otfb:
106
+ name: Remington UMC 220gr OTFB
107
+ cal: .308
108
+ grains: 220
109
+ g1: 0.680 # best guess, based on Rem 220gr OTFB
110
+ sd: 0.331 # best guess, based on 220gr SMK
111
+ intended: 300 BLK
112
+ base: flat
113
+ desc: Open tip, copper jacket, lead core
114
+
115
+ remington_220_otfb:
116
+ name: Remington 220gr Open Tip Flat Base
117
+ cal: .308
118
+ grains: 220
119
+ g1: .680
120
+ sd: 0.331 # best guess, based on 220gr SMK
121
+ intended: 300 BLK
122
+ base: flat
123
+ desc: Open tip, copper jacket, lead core
124
+
125
+ # OTHER FLAT BASE
126
+
127
+ # Note, Barnes TAC-TX is essentially identical to TTSX -- just a rebranding
128
+ # with restricted choices appropriate to Mil/LE rifles
129
+ barnes_110_tac_tx:
130
+ name: Barnes 110gr TAC-TX 30 CAL
131
+ cal: .308
132
+ grains: 110
133
+ g1: .295
134
+ sd: .166
135
+ base: flat
136
+ desc: Blue polymer tip, copper bullet, general 30 cal
137
+
138
+ # Note, Barnes TAC-X is essentially identical to TSX -- just a rebranding
139
+ # with restricted choices appropriate to Mil/LE rifles
140
+ barnes_110_tac_x:
141
+ name: Barnes 110gr TAC X
142
+ cal: .308
143
+ grains: 110
144
+ g1: .264
145
+ sd: .166
146
+ base: flat
147
+ desc: All copper
148
+
149
+ barnes_110_tsx:
150
+ name: Barnes 110gr TSX
151
+ cal: .308
152
+ grains: 110
153
+ g1: .264
154
+ sd: .166
155
+ base: flat
156
+ desc: All copper
157
+
158
+ barnes_110_ttsx:
159
+ name: Barnes 110gr TTSX
160
+ cal: .308
161
+ grains: 110
162
+ g1: .295
163
+ sd: .166
164
+ base: flat
165
+ desc: Blue polymer tip, copper bullet
166
+
167
+ sierra_110_fmj:
168
+ name: Sierra 110gr FMJ
169
+ cal: .308
170
+ grains: 110
171
+ g1: .170
172
+ sd: .166
173
+ intended: M1 Carbine
174
+ base: flat
175
+ desc: Copper jacket, lead core
176
+
177
+ sierra_110_hp:
178
+ name: Sierra 110gr HP
179
+ cal: .308
180
+ grains: 110
181
+ g1: .204
182
+ sd: .166
183
+ base: flat
184
+ desc: Hollow point, copper jacket, lead core, high accuracy
185
+
186
+ sierra_110_rn:
187
+ name: Sierra 110gr RN
188
+ cal: .308
189
+ grains: 110
190
+ g1: .170
191
+ sd: .166
192
+ intended: M1 Carbine
193
+ base: flat
194
+ desc: Round lead nose, copper jacket, lead core, expands at low velocity
195
+
196
+ berger_115_fb:
197
+ name: Berger 115gr FB Target
198
+ cal: .308
199
+ grains: 115
200
+ g1: .289
201
+ base: flat
202
+ desc: Copper jacket, lead core, 1:19 twist
203
+
204
+ sierra_125_spt:
205
+ name: Sierra 125gr SPT
206
+ cal: .308
207
+ grains: 125
208
+ g1: .264
209
+ sd: .188
210
+ base: flat
211
+ desc: Copper jacket, lead core, spitzer, intended for hunting
212
+
213
+ sierra_135_hp:
214
+ name: Sierra 135gr HP Varminter
215
+ cal: .308
216
+ grains: 135
217
+ g1: .275
218
+ sd: .203
219
+ base: flat
220
+ desc: Hollow point, copper jacket, lead core, 300 BLK compatible
221
+
222
+ barnes_150_tac_rrlp:
223
+ name: Barnes 150gr TAC RRLP
224
+ cal: .308
225
+ grains: 150
226
+ g1: .357
227
+ sd: .226
228
+ base: flat
229
+ desc: Frangible; Reduced Ricochet Limited Penetration
230
+
231
+ sierra_150_spt:
232
+ name: Sierra 150gr SPT
233
+ cal: .308
234
+ grains: 150
235
+ g1: .360
236
+ sd: .226
237
+ base: flat
238
+ desc: Copper jacket, lead core, spitzer, intended for hunting
239
+
240
+ sierra_150_rn:
241
+ name: Sierra 150gr RN
242
+ cal: .308
243
+ grains: 150
244
+ g1: .270
245
+ sd: .226
246
+ base: flat
247
+ desc: Round lead nose, copper jacket, lead core, accurate short range hunting
248
+
249
+ #
250
+ # BOAT TAILS
251
+ #
252
+
253
+ hornady_110_gmx:
254
+ name: Hornady 110gr GMX
255
+ intended: 300 BLK
256
+ cal: .308
257
+ grains: 110
258
+ g1: .305
259
+ sd: .166
260
+ base: boat
261
+ desc: Red polymer tip, copper alloy bullet, very short boat tail
262
+ # g7: TODO
263
+
264
+ barnes_120_tac_tx:
265
+ name: Barnes 120gr TAC-TX
266
+ cal: .308
267
+ grains: 120
268
+ g1: .358
269
+ sd: .181
270
+ base: boat
271
+ desc: Black polymer tip, copper bullet
272
+ # g7: TODO
273
+
274
+ barnes_130_tsx:
275
+ name: Barnes 130gr TSX
276
+ cal: .308
277
+ grains: 130
278
+ g1: .340
279
+ sd: .196
280
+ base: boat
281
+ desc: All copper
282
+ # g7: TODO
283
+
284
+ barnes_130_ttsx:
285
+ name: Barnes 130gr TTSX
286
+ cal: .308
287
+ grains: 130
288
+ g1: .350
289
+ sd: .196
290
+ base: boat
291
+ desc: Blue polymer tip, copper bullet
292
+ # g7: TODO
293
+
294
+ barnes_150_tsx:
295
+ name: Barnes 150gr TSX
296
+ cal: .308
297
+ grains: 150
298
+ g1: .369
299
+ sd: .226
300
+ base: boat
301
+ desc: All copper
302
+
303
+ barnes_150_ttsx:
304
+ name: Barnes 150gr TTSX
305
+ cal: .308
306
+ grains: 150
307
+ g1: .420
308
+ sd: .226
309
+ base: boat
310
+ desc: Blue polymer tip, copper bullet
311
+ # g7: TODO
312
+
313
+ sierra_220_hpbt_mk:
314
+ name: Sierra 220gr
315
+ cal: .308
316
+ grains: 220
317
+ g1: .608
318
+ sd: .331
319
+ base: boat
320
+ desc: Open tip, copper jacket, lead core
321
+ # g7: TODO