DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Open Source Adventures: Episode 46: BATTLETECH Damage Per Ton Weapon Ranking

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.

#!/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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
jeremyf profile image
Jeremy Friesen

Oh my heavens…I haven't played much BATTLETECH but the amount of time and energy I've spent writing Ruby code for RPGs is staggering.