Open Source Adventures: Episode 46: BATTLETECH Damage Per Ton Weapon Ranking
Tags: gamedev, ruby
Published: 2022-04-28 23:52:45

Let's start writing some BATTLETECH analysis code. By the way the game is capitalized like that, but in the code I'll write it as `BattleTech` as it looks a bit dumb.

### Analysis Script

The script is very simple. You point it at game root directory. Then it reads some JSONs, and prints a report.

```ruby
#!/usr/bin/env ruby

require "json"
require "memoist"
require "pathname"
require "pry"

class Weapon
  extend Memoist
  attr_reader :data

  def initialize(path)
    @data = JSON.parse(path.read)
  end

  memoize def name
    bonuses = [bonus_a, bonus_b].compact
    if bonuses.empty?
      base_name
    else
      "#{base_name} (#{bonuses.join(", ")})"
    end
  end

  memoize def base_name
    [
      data["Description"]["Name"],
      data["Description"]["UIName"],
    ].compact.last.gsub(" +", "+")
  end

  memoize def bonus_a
    data["BonusValueA"] == "" ? nil : data["BonusValueA"].gsub(/[a-z]\K\./, "")
  end

  memoize def bonus_b
    data["BonusValueB"] == "" ? nil : data["BonusValueB"].gsub(/[a-z]\K\./, "")
  end

  def category
    @data["Category"]
  end

  def tonnage
    @data["Tonnage"]
  end

  def damage
    shots * base_damage
  end

  def base_damage
    @data["Damage"]
  end

  def shots
    @data["ShotsWhenFired"]
  end

  def heat
    @data["HeatGenerated"]
  end

  def ammo_category
    @data["ammoCategoryID"]
  end

  def purchasable?
    @data["Description"]["Purchasable"]
  end
end

class BattleTechGame
  extend Memoist

  def initialize(game_root)
    @game_root = Pathname(game_root)
  end

  memoize def data_root
    @game_root + "BattleTech_Data/StreamingAssets/data"
  end

  memoize def weapon_files
    data_root
      .glob("weapon/*.json")
      .select{|n| n.basename.to_s != "WeaponTemplate.json"}
  end

  memoize def weapons
    weapon_files.map{|path| Weapon.new(path)}
  end

  def print_weapons_report
    weapons
      .select{|w| w.category != "Melee" and w.name != "AI Laser"}
      .sort_by{|w|
        [w.damage.to_f / w.tonnage, w.damage, w.name]
      }
      .reverse
      .each{|w|
        puts "* #{w.name}: #{w.damage} / #{w.tonnage}"
      }
  end
end

game = BattleTechGame.new(ARGV[0])

game.print_weapons_report
```

Some points:

* `memoize` used aggressively, mostly because why not
* we want to exclude some weapons (Melee, DFA, and AI Laser) from analysis as they're not real weapons you can buy and use
* we need some stylistic changes for nicer names, so `AC/10 + + + (+ 50% Crit., + 20 Stb.Dmg.)` becomes `AC/10+++ (+ 50% Crit, + 20 StbDmg)`

### Report

Here's the most naive report. This ranking assumes ammo is free, and heat is free, ignores weapon range, any special effects, and so on:

* MG++ (+ 5 Shots, - 0.5 Ton): 30 / 0.0
* MG++ (+ 50% Crit, - 0.5 Ton): 15 / 0.0
* ER S Laser++ (+ 10 Dmg): 40 / 0.5
* ER S Laser+ (+ 5 Dmg): 35 / 0.5
* S Laser+++ (+ 10 Dmg, + 50% Crit): 30 / 0.5
* S Laser++ (+ 10 Dmg): 30 / 0.5
* MG+ (+ 5 Shots): 30 / 0.5
* ER S Laser: 30 / 0.5
* S Laser++ (+ 5 Dmg, + 25% Crit): 25 / 0.5
* S Laser+ (+ 5 Dmg): 25 / 0.5
* S Pulse++ (+ 10 Dmg, - 2 Heat): 45 / 1
* ER M Laser++ (+ 10 Dmg): 45 / 1
* S Pulse+ (+ 5 Dmg, - 1 Heat): 40 / 1
* ER M Laser+ (+ 5 Dmg): 40 / 1
* S Laser++ (+ 50% Crit): 20 / 0.5
* S Laser++ (+ 3 Acc): 20 / 0.5
* S Laser+ (+ 25% Crit): 20 / 0.5
* S Laser+ (+ 1 Acc): 20 / 0.5
* S Laser: 20 / 0.5
* S Pulse: 35 / 1
* M Laser++ (+ 10 Dmg): 35 / 1
* ER M Laser: 35 / 1
* M Laser+ (+ 5 Dmg): 30 / 1
* MG+ (+ 25% Crit): 15 / 0.5
* MG: 15 / 0.5
* M Pulse++ (- 4 Heat, + 1 Acc): 50 / 2
* M Laser+++ (+ 3 Acc, + 50% Crit): 25 / 1
* M Laser++ (+3 Acc): 25 / 1
* M Laser++ (+ 50% Crit): 25 / 1
* M Laser++ (+ 1 Acc, + 25% Crit): 25 / 1
* M Laser+ (+1 Acc): 25 / 1
* M Laser+ (+ 25% Crit): 25 / 1
* M Laser: 25 / 1
* SRM6+++ (+ 4 Dmg, + 2 StbDmg): 72 / 3
* SRM6++ (+ 4 Dmg): 72 / 3
* SRM4+++ (+ 4 Dmg, + 2 StbDmg): 48 / 2
* SRM4++ (+ 4 Dmg): 48 / 2
* SRM2+++ (+ 4 Dmg, + 2 StbDmg): 24 / 1
* SRM2++ (+ 4 Dmg): 24 / 1
* M Pulse+ (- 2 Heat): 45 / 2
* SRM6++ (+ 2 Dmg, + 1 StbDmg): 60 / 3
* SRM6+ (+ 2 Dmg): 60 / 3
* SRM4++ (+ 2 Dmg, + 1 StbDmg): 40 / 2
* SRM4+ (+ 2 Dmg): 40 / 2
* M Pulse: 40 / 2
* SRM2++ (+ 2 Dmg, + 1 StbDmg): 20 / 1
* SRM2+ (+ 2 Dmg): 20 / 1
* SRM6++ (+ 50% Crit): 48 / 3
* SRM6+ (+ 25% Crit): 48 / 3
* SRM6: 48 / 3
* SRM4++ (+ 50% Crit): 32 / 2
* SRM4+ (+ 25% Crit): 32 / 2
* SRM4: 32 / 2
* SRM2++ (+ 50% Crit): 16 / 1
* SRM2+ (+ 25% Crit): 16 / 1
* SRM2: 16 / 1
* LRM5++ (+ 2 Dmg): 30 / 2
* LRM15++ (+ 2 Dmg): 90 / 7
* ER L Laser++ (+ 5 Dmg, - 1 Ton): 50 / 4
* LRM5+ (+ 1 Dmg): 25 / 2
* LRM20++ (+ 2 Dmg): 120 / 10
* LRM10++ (+ 2 Dmg): 60 / 5
* ER PPC++ (+ 10 Dmg, - 1 Ton): 70 / 6
* L Pulse++ (+ 10 Dmg, - 1 Ton): 65 / 6
* ER PPC+ (+ 5 Dmg, - 1 Ton): 65 / 6
* LRM15+ (+ 1 Dmg): 75 / 7
* LRM20+ (+ 1 Dmg): 100 / 10
* L Pulse+ (+ 5 Dmg, - 1 Ton): 60 / 6
* LRM10+ (+ 1 Dmg): 50 / 5
* L Laser+++ (+ 10 Dmg, + 3 Acc): 50 / 5
* L Laser++ (+ 10 Dmg): 50 / 5
* ER L Laser+ (+ 5 Dmg): 50 / 5
* LRM5+++ (+ 50% Crit, + 2 StbDmg): 20 / 2
* LRM5++ (+ 50% Crit): 20 / 2
* LRM5++ (+ 25% Crit, + 1 StbDmg): 20 / 2
* LRM5++ (+ 2 StbDmg): 20 / 2
* LRM5+ (+ 25% Crit): 20 / 2
* LRM5+ (+ 1 StbDmg): 20 / 2
* LRM5: 20 / 2
* Flamer+ (* 4 Ammo, + 5 Dmg): 10 / 1
* L Laser++ (+ 5 Dmg, + 1 Acc): 45 / 5
* L Laser+ (+ 5 Dmg): 45 / 5
* ER L Laser: 45 / 5
* AC/20+++ (+ 20 Dmg, + 20 StbDmg): 120 / 14
* AC/20++ (+ 20 Dmg): 120 / 14
* PPC++ (+ 10 Dmg): 60 / 7
* LRM15+++ (+ 50% Crit, + 2 StbDmg): 60 / 7
* LRM15++ (+ 50% Crit): 60 / 7
* LRM15++ (+ 25% Crit, + 1 StbDmg): 60 / 7
* LRM15++ (+ 2 StbDmg): 60 / 7
* LRM15+ (+ 25% Crit): 60 / 7
* LRM15+ (+ 1 StbDmg): 60 / 7
* LRM15: 60 / 7
* ER PPC: 60 / 7
* LRM20+++ (+ 50% Crit, + 2 StbDmg): 80 / 10
* LRM20++ (+ 50% Crit): 80 / 10
* LRM20++ (+ 25% Crit, + 1 StbDmg): 80 / 10
* LRM20++ (+ 2 StbDmg): 80 / 10
* LRM20+ (+ 25% Crit): 80 / 10
* LRM20+ (+ 1 StbDmg): 80 / 10
* LRM20: 80 / 10
* LRM10+++ (+ 50% Crit, + 2 StbDmg): 40 / 5
* LRM10++ (+ 50% Crit): 40 / 5
* LRM10++ (+ 25% Crit, + 1 StbDmg): 40 / 5
* LRM10++ (+ 2 StbDmg): 40 / 5
* LRM10+ (+ 25% Crit): 40 / 5
* LRM10+ (+ 1 StbDmg): 40 / 5
* LRM10: 40 / 5
* L Laser++ (+ 50% Crit): 40 / 5
* L Laser++ (+ 3 Acc): 40 / 5
* L Laser+ (+ 25% Crit): 40 / 5
* L Laser+ (+ 1 Acc): 40 / 5
* L Laser: 40 / 5
* AC/20++ (+ 10 Dmg, + 10 StbDmg): 110 / 14
* AC/20+ (+ 10 Dmg): 110 / 14
* PPC+ (+ 5 Dmg): 55 / 7
* L Pulse: 55 / 7
* AC/20+++ : 100 / 14
* AC/20++ (+ 50% Crit): 100 / 14
* AC/20++ (+ 4 Acc): 100 / 14
* AC/20++ (+ 20 StbDmg): 100 / 14
* AC/20+ (+ 25% Crit): 100 / 14
* AC/20+ (+ 2 Acc): 100 / 14
* AC/20+ (+ 10 StbDmg): 100 / 14
* AC/20: 100 / 14
* PPC++ (+ 4 Acc): 50 / 7
* PPC++ (+ 30 StbDmg): 50 / 7
* PPC+ (+ 2 Acc): 50 / 7
* PPC+ (+ 15 StbDmg): 50 / 7
* PPC: 50 / 7
* AC/5++ (+ 10 Dmg): 55 / 8
* AC/5+ (+ 5 Dmg): 50 / 8
* AC/10++ (+ 10 Dmg): 70 / 12
* AC/2++ (+ 10 Dmg): 35 / 6
* Gauss Rifle++ (- 2 Tons, - 1 Slot): 75 / 13
* Gauss Rifle+ (- 2 Tons): 75 / 13
* AC/5+++ (+ 4 Acc, + 20 StbDmg): 45 / 8
* AC/5++ (+ 50% Crit): 45 / 8
* AC/5++ (+ 4 Acc): 45 / 8
* AC/5++ (+ 20 StbDmg): 45 / 8
* AC/5++ (+ 2 Acc, + 10 StbDmg): 45 / 8
* AC/5+ (+ 25% Crit): 45 / 8
* AC/5+ (+ 2 Acc): 45 / 8
* AC/5+ (+ 10 StbDmg): 45 / 8
* AC/5: 45 / 8
* AC/10+ (+ 5 Dmg): 65 / 12
* Gauss Rifle: 75 / 15
* AC/10+++ (+ 50% Crit, + 20 StbDmg): 60 / 12
* AC/10++ (+ 50% Crit): 60 / 12
* AC/10++ (+ 4 Acc): 60 / 12
* AC/10++ (+ 25% Crit, + 10 StbDmg): 60 / 12
* AC/10++ (+ 20 StbDmg): 60 / 12
* AC/10+ (+ 25% Crit): 60 / 12
* AC/10+ (+ 2 Acc): 60 / 12
* AC/10+ (+ 10 StbDmg): 60 / 12
* AC/10: 60 / 12
* AC/2+ (+ 5 Dmg): 30 / 6
* Flamer+++ (* 40 Ammo): 5 / 1
* Flamer++ (* 4 Ammo, + 5 Dmg (H)): 5 / 1
* Flamer (* 4 Ammo): 5 / 1
* AC/2+++ (+ 4 Acc, + 50% Crit): 25 / 6
* AC/2++ (+ 50% Crit): 25 / 6
* AC/2++ (+ 4 Acc): 25 / 6
* AC/2++ (+ 20 StbDmg): 25 / 6
* AC/2++ (+ 2 Acc, + 25% Crit): 25 / 6
* AC/2+ (+ 25% Crit): 25 / 6
* AC/2+ (+ 2 Acc): 25 / 6
* AC/2+ (+ 10 StbDmg): 25 / 6
* AC/2: 25 / 6

The winners are some weapons that have weight of 0. But that's only true if we ignore their ammo. Ignoring that we have enormous range from 80:1 (ER S Laser++) to 4.16:1 (for AC/2).

Unfortunately we can't really use these amazing weapons that consume no ammo and generate no heat. In the next episode we'll try to include heat and ammo in the analysis to get a more useful ranking.

### Story so far

[All the code is on GitHub](https://github.com/taw/open-source-adventures).