Module:Getdata-Attacks

From Helldivers Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Getdata-Attacks/doc

local getArgs = require("Module:Arguments").getArgs
local getWeightMetric = require("Module:UnitConverter").getWeightMetricLua
local p = {} --p stands for package
local data = mw.loadData("Module:Decodedata-Attacks")

function cbShowAP(value, opts)
  return opts.frame:expandTemplate({
    title = "Armor",
    args = { value, "AP" },
  })
end

function cbShowOuterRadiusAP(value, opts)
  value = value - 1
  if value < 2 then
    value = 2
  end
  return cbShowAP(value, opts)
end

function cbShowStatus(value, opts)
  return "[[Status Effects#" .. value .. "|" .. value .. "]]"
end

function cbShowDecline(value)
  return value - 1 .. " - 0"
end

function cbShowWeight(value)
  return getWeightMetric(value)
end

function cbShowPercent(value)
  return value * 100 .. "%"
end

function cbHalf(value)
  return math.max(1, math.floor(value * 0.5))
end

function filterNonZero(value)
  return value > 0 or value < 0
end

function filterGt(n)
  function filterGtN(value)
    return value > n
  end
  return filterGtN
end

-- Positional vars:
-- 1: Displayed key in the table
-- 2: Name of user-facing variable
-- 3: Name of corresponding data object in DecodeData-Attacks/data.json
-- If the data name is missing, the two keys are assumed identical
local weapon_setup = {
  { "colspan=2", 1, header = true },
  { "Main Health", "health" },
  { "Main Armor", "armor",
    cb = function(value, opts)
      return opts.frame:expandTemplate({
        title = "Armor", 
        args = { value },
      })
    end },
  { "Shield Capacity", "shield" },
  { "Shield Regen", "shieldregen", suffix = "shield/s" },
  { "Shield Regen Delay", "shielddelay", suffix = "s" },
  { "Shield Broken Delay", "shieldbreakdelay", suffix = "s" },
  { "Shield Radius", "shieldradius", suffix = "m" },
  { "Fire Rate", "fire_rate", "rpm", suffix = "rpm" },
  { "Recoil", "recoil" },
  { "Total Uses", "uses" },
  { "Eagle Stock", "eagle_stock", "eaglestock" },
  { "Cooldown", "cooldown", suffix = "sec" },
  { "Call-in Time", "callin_time", "calltime", suffix = "s"},
  { "Salvos", "salvos" },
  { "Capacity", "capacity", "cap" },
  { "Fire Limit", "fire_limit", "limit", suffix = "s" },
  { "Reload Time", "reload", suffix = " s" },
  { "Tactical Reload", "reload_early", "reloadearly" , suffix = "s" },
  { "Spare Magazines", "magazines_spare", "mags", },
  { "Starting Magazines", "magazines_starting", "magstart", },
  { "Mags from Supply", "magazines_from_supply", "supply" },
  { "Mags from Ammo Box", "magazines_from_box", "supply", cb = cbHalf },
  { "Reloading 1 Round", "reload_one", "reloadone", suffix = "s" },
  { "Reloading n Rounds", "reload_n", "reloadx", suffix = "s",
    cb = function(value, opts)
      local n = opts.args["reload_n_count"] or opts.medium["reloadxnum"] or "n"
      return value, "Reloading " .. n .. " Rounds"
    end },
  { "Spare Rounds", "rounds_spare", "rounds", },
  { "Starting Rounds", "rounds_starting", "roundstart", },
  { "Rounds from Supply", "rounds_from_supply", "roundsupply" },
  { "Rounds from Ammo Box", "rounds_from_box", "roundsupply", cb = cbHalf },
  { "Clip Capacity", "clips_size", "clipsize", },
  { "Spare Clips", "clips_spare", "clips", },
  { "Starting Clips", "clips_starting", "clipstart", },
  { "Clips from Supply", "clips_from_supply", "clipsupply" },
  { "Clips from Ammo Box", "clips_from_box", "clipssupply", cb = cbHalf },
}

local damage_setup = {
  { "colspan=2", "damage_name", header = true }, -- Just there for manual input, really.
  { "colspan=2", literal = "Damage", header = true },
  { "Element", "element", "element_name" },
  { "Standard", "standard_damage", "dmg" },
  { "vs. Durable", "durable_damage", "dmg2" },
  { "colspan=2", literal = "Penetration", header = true },
  { "Direct", "penetration_direct", "ap1", cb = cbShowAP },
  { "Slight Angle", "penetration_angle_slight", "ap2", cb = cbShowAP },
  { "Large Angle", "penetration_angle_large", "ap3", cb = cbShowAP },
  { "Extreme Angle", "penetration_angle_extreme", "ap4", cb = cbShowAP },
  { "colspan=2", literal = "Special Effects", header = true },
  { "Status", "status", "status_name", cb = cbShowStatus },
  { "Status Strength", "status_strength", "param1", filter = filterNonZero},
  { "Second Status", "status_2", "status_name2", cb = cbShowStatus },
  { "Status Strength", "status_strength_2", "param2" , filter = filterNonZero},
  { "Third Status", "status_3", "status_name3", cb = cbShowStatus },
  { "Status Strength", "status_strength_3", "param3", filter = filterNonZero},
  { "Fourth Status", "status_4", "status_name4", cb = cbShowStatus },
  { "Status Strength", "status_strength_4", "param4", filter = filterNonZero},
  { "Demolition Force", "demolition_force", "demo"},
  { "Stagger Force", "stagger_force", "stun"},
  { "Push Force", "push_force", "push"},
}

local projectile_setup = {
  { "colspan=2", "projectile_name", "name", header = true },
  { "colspan=2", literal = "Projectile", header = true },
  { "Pellets", "pellets", filter = filterGt(1), prefix = "x" },
  { "Caliber", "caliber", suffix = "mm" },
  { "Mass", "mass", cb = cbShowWeight },
  { "Initial Velocity", "velocity", suffix = "m/s" },
  { "Drag Factor", "drag", cb = cbShowPercent },
  { "Gravity Factor", "gravity", cb = cbShowPercent },
  { "Lifetime", "lifetime", filter = filterNonZero, suffix = "sec" },
  { "Penetration Slowdown", "penetration_slowdown", "penslow", cb = cbShowPercent },
}

local explosion_setup = {
  { "colspan=2", "aoe_name", "name", header = true },
  { "colspan=2", literal = "Area of Effect", header = true },
  { "Inner Radius", "inner_radius", "r1", suffix = "m" },
  { "Outer Radius", "outer_radius", "r2", suffix = "m" },
  { "Shockwave Radius", "shockwave_radius", "r3", suffix = "m" },
}

local explosion_damage_setup = {
  { "colspan=2", literal = "Damage", header = true },
  { "Damage Element", "element", "element_name" },
  { "Inner Radius", "aoe_damage", "dmg" },
  { "Outer Radius", "aoe_falloff", "dmg", filter = filterGt(1), cb = cbShowDecline },
  { "colspan=2", literal = "Penetration", header = true },
  { "Inner Radius", "penetration_direct", "ap1", cb = cbShowAP },
  { "Outer Radius", "aoe_penetration", "ap1", cb = cbShowOuterRadiusAP },
  { "Shockwave", "aoe_penetration", "ap1", cb = cbShowAP },
  { "colspan=2", literal = "AoE Effect", header = true },
  { "Status", "status", "status_name", cb = cbShowStatus },
  { "Status Strength", "status_strength", "param1", filter = filterNonZero},
  { "Second Status", "status_2", "status_name2", cb = cbShowStatus },
  { "Status Strength", "status_strength_2", "param2" , filter = filterNonZero},
  { "Third Status", "status_3", "status_name3", cb = cbShowStatus },
  { "Status Strength", "status_strength_3", "param3", filter = filterNonZero},
  { "Fourth Status", "status_4", "status_name4", cb = cbShowStatus },
  { "Status Strength", "status_strength_4", "param4", filter = filterNonZero},
  { "Demolition Force", "demolition_force", "demo"},
  { "Stagger Force", "stagger_force", "stun"},
  { "Push Force", "push_force", "push"},
}

local beam_setup = {
  { "colspan=2", "beam_name", "name", header = true },
  { "colspan=2", literal = "Beam", header = true },
  { "Beam Range", "range", suffix = "m"},
}

local arc_setup = {
  { "colspan=2", "arc_name", "name", header = true },
  { "colspan=2", literal = "Arc", header = true },
  { "Arc Range", "arc_range", "range", suffix = "m"},
  { "Arc Velocity", "arc_velocity", "velocity", suffix = "m/s"},
  { "Arc Aim Angle", "arc_aim_angle", "aimangle", suffix = "°"},
}

local attack_type = {
  weapon = "Weapon",
  projectile = "Projectile",
  explosion = "Explosion",
  damage = "Damage",
  spray = "Spray",
  beam = "Beam",
  arc = "Arc",
}

local status_damages = {
  [2] = "DPS_Avatar_Bleed",
  [6] = "DPS_Fire",
  [10] = "DPS_Acid_Splash",
  [11] = "DPS_Acid_Stream",
  [12] = "DPS_Thermite",
  [13] = "DPS_Cyborg_Fire",
  [18] = "DPS_Thornbush",
  [22] = "DPS_Pure_Damage",
  [39] = "DPS_Gas",
}

local table_setups = {
  weapon = { weapon_setup, damage_setup },
  damage = { damage_setup },
  projectile = { projectile_setup, damage_setup },
  explosion = { explosion_setup, explosion_damage_setup },
  beam = { beam_setup, damage_setup },
  arc = { arc_setup, damage_setup },
}

function addRow(row, opts)
  local label = row[1]
  local user_key = row[2]
  local data_key = row[3] or user_key
  local value = row.literal
  local user_value = nil

  if not value then
    user_value = opts.args[user_key]
    value = opts.args[user_key] or opts.medium[data_key]
  end
  if not value then -- If value doesn"t exist we omit the table row entirely
    return ""
  end
  if not user_value then
    if row.filter and not row.filter(value, opts) then
      return ""
    end
    if row.cb then
      local newValue, newLabel = row.cb(value, opts)
      value = newValue
      if newLabel then label = newLabel end
    end
    if row.prefix then
      value = row.prefix .. " " .. value
    end
    if row.suffix then
      value = value .. " " .. row.suffix
    end
  end

  if row.header then
    -- label is header info, typically colspan=2
    -- value is text to put in header
    return "!" .. label .. "|" .. value .. row_end()
  else
    return "|" .. label .. "||" .. value .. row_end()
  end
end

function table_start(category)
  return "{| class=\"wikitable tableleftjustified Secondcolumnright table-weapon-stats attack-data-table-"
    .. category .. "\"\n"
end

function table_end()
  return "|}\n"
end

function row_end()
  return "\n|-\n"
end

function unpackTableSetup(opts)
  local out = ""

  if opts.statuses_seen and opts.medium.func1 then
    for i = 1, 4 do -- Track all statuses seen for unrolling later
      local status = opts.medium["func" .. i]
      if status > 0 then
        opts.statuses_seen[status] = true
      end
    end
  end

  for i, row in ipairs(opts.setup) do
    out = out .. addRow(row, opts)
  end
  return out
end

function getAttackTable(opts) 
  local out  = ""
  local table_setup = opts.table_setup
  local damage_table_setup = opts.damage_table_setup or damage_setup
  local attack = opts.attack
  local medium = opts.medium or data[attack.type][attack.name]
  local frame = opts.frame
  local args = opts.args

  out = out .. unpackTableSetup({
    setup = table_setup,
    attack = attack,
    medium = medium,
    frame = frame,
    args = args,
    statuses_seen = opts.statuses_seen,
  })
  if medium.damageid then
    local damage = data.damage[medium.damageid]
    out = out .. unpackTableSetup({
      setup = damage_table_setup,
      attack = attack,
      medium = damage,
      frame = frame,
      args = args,
      statuses_seen = opts.statuses_seen,
    })
  end
  return out
end

function p.attackDataTemplate(frame)
    local args = getArgs(frame, {
      removeBlanks = false,
    })

    local weapon_name = args[1]
    local weapon =  data.weapon[weapon_name]
    local statuses_seen = {}
    if not weapon then
      return "Weapon not found: \"" .. weapon_name .. "\""
    end
    local out = ""

    local weapon_table = getAttackTable({
      table_setup = weapon_setup,
      medium = weapon,
      frame = frame,
      args = args,
      statuses_seen = statuses_seen,
    })

    local attack_rows = ""
    
    local attacks = {}
    local j = 0
    local attack_index = 0 -- For placing the header "Attacks"
    for _, attack in ipairs(weapon.attacks or {}) do
      j = j + 1
      attacks[j] = attack
      if attack.type == "weapon" then
        local subweapon = data[attack.type][attack.name]
        for _, subattack in ipairs(subweapon.attacks or {}) do
          j = j + 1
          attacks[j] = subattack
        end
        attack_index = j
      end
    end

    if attacks[1] and attacks[1].type == "weapon" then
      attack_rows = attack_rows .. "!colspan=2|Weapons" .. row_end()
    end

    for i, attack in ipairs(attacks) do
      if i == attack_index + 1 then
        attack_rows = attack_rows .. "!colspan=2|Attacks" .. row_end()
      end

      local medium = data[attack.type][attack.name]
      local name = medium.fullname or medium.name or ""
      local override_table = args["override_table_" .. i]
      local override_attack = args["override_attack_" .. i]
      local attack_type_name = attack_type[attack.type]

      local attack_row = "|" .. name .. "||" .. attack_type_name
      if override_attack then
        attack_row = "|" .. override_attack
      elseif attack.count then
        attack_row = attack_row .. " x " .. attack.count
      end

      if override_table then
        out = out .. override_table .. "\n"
        attack_rows = attack_rows .. attack_row .. row_end()
      else
        local setup_config = table_setups[attack.type]
        if attack.type == "weapon" then
          args[1] = attack.name
        end
        local attack_table = getAttackTable({
          table_setup = setup_config[1],
          damage_table_setup = setup_config[2],
          attack = attack,
          medium = medium,
          frame = frame,
          args = args,
          statuses_seen = statuses_seen,
        })

        if attack.type == "damage" then
          attack_table = "!colspan=2|"
            .. medium.name
            .. row_end()
            .. attack_table
        end

        if attack.type == "weapon" then
          attack_rows = attack_rows .. attack_table
        else
          attack_rows = attack_rows .. attack_row .. row_end()
          out = out
            .. table_start(attack.type)
            .. attack_table
            .. table_end()
        end
      end
    end

    for status, _ in pairs(statuses_seen) do
      local dps_name = status_damages[status]
      if dps_name then
        j = j + 1
        local override_table = args["override_table_" .. j]
        if override_table then
          out = out .. override_table
        else
          local dps = data.damage[dps_name]
          args["damage_name"] = dps.name
          local attack_table = getAttackTable({
            table_setup = damage_setup,
            medium = dps,
            frame = frame,
            args = args,
          })
          out = out
            .. table_start("damage")
            .. attack_table
            .. table_end()
        end
      end
    end

    out = table_start("weapon")
      .. weapon_table
      .. attack_rows
      .. table_end()
      .. out

    return "<div class=\"flextablediv\">\n" .. out .. "\n</div>"
end

function p.subAttackTemplate(frame)
  local args = getArgs(frame, {
    removeBlanks = false,
    wrappers = "Template:Attack_Data/attack",
  })
  local scope = args[1]
  local name = args[2]
  if not scope then
    return "No type supplied"
  end
  if not name then
    return "No name supplied"
  end

  local setup_config = table_setups[scope]
  if not setup_config then
    return "Type not supported: \"" .. args .. "\""
  end

  local medium = data[scope][name]
  if not medium then
    medium = {}
    args[1] = name
    if scope == "damage" then
      args["damage_name"] = name
    end
  end

  if scope == "damage" and not args["damage_name"] then
    args["damage_name"] = medium.name
  end

  local attack_table = getAttackTable({
    table_setup = setup_config[1],
    damage_table_setup = setup_config[2],
    medium = medium,
    frame = frame,
    args = args,
  })

  return table_start(scope) .. attack_table .. table_end()
end

local weapon_category_pages = {
  ["Assault Rifle"] = "Assault Rifles",
  ["Shotgun"] = "Shotguns",
  ["Marksman Rifle"] = "Marksman Rifles",
  ["Submachine Gun"] = "Submachine Guns",
  ["Explosive"] = "Explosive",
  ["Energy-Based"] = "Energy-Based",
  ["Pistol"] = "Pistols",
  ["Support Weapon"] = "Support Weapons",
}

function p.fullWeaponTable(frame)
  local args = getArgs(frame, {
    wrappers = 'Template:Damage_Comparison_Table',
  })
  local out = ""
  local ordered = {}
  local names = data.weapon_order
  local rows = {}

  for i, name in ipairs(names) do
    local override = args["override_" .. string.gsub(name, " ", "_")]
    if override then
      rows[i] = "|" .. override
    else
      local weapon = data.weapon[name]
      local max_ap = 0
      local dmg = 0
      local dmg2 = 0
      local projectile_type = "Projectile"
      local special

      local modes = {}
      for _, mode in pairs(weapon.modes or {}) do
        modes[mode] = "X"
      end
      for _, mode in pairs(weapon.tags or {}) do
        modes[mode] = "X"
      end

      for _, attack in ipairs(weapon.attacks or {}) do
        local medium = data[attack.type][attack.name]

        if attack.type == "beam" then
          projectile_type = "Beam"
        elseif attack.type == "arc" then
          projectile_type = "Arc"
        elseif medium.pellets and medium.pellets > 1 then
          projectile_type = "Scatter x " .. medium.pellets
        end

        local damage = medium
        local count = (attack.count or 1) * (medium.pellets or 1)
        if medium.damageid then
          damage = data.damage[medium.damageid]
        end

        if damage.ap1 and damage.ap1 > max_ap then
          max_ap = damage.ap1
        end

        if damage.status_name and attack.type ~= "beam" then
          special = damage.status_name
        end

        if not special and attack.type == "explosion" then
          special =  "AoE (" .. medium.r1 .. "-" .. medium.r2 .. "m)"
        end

        if name == "RL-77 Airburst Rocket Launcher" then
          special = "Clusters"
        end

        if modes["lock"] then
          special = "Lock-On"
        end

        if modes["overcharge"] then
          special = "Overcharge"
        elseif modes["charge"] then
          special = "Charge-up"
        end

        dmg = dmg + count * (damage.dmg or 0) dmg2 = dmg2 + count * (damage.dmg2 or 0)
      end

      local category = weapon.category
      local category_page = weapon_category_pages[category]

      local dps
      if dmg and weapon.rpm then
        dps = math.floor(dmg * weapon.rpm / 60)
      end

      local cap = weapon.cap
      if weapon.limit then
        cap = weapon.limit .. " Seconds"
      end
      if not cap then
        cap = "Infinite"
      end

      local mags = weapon.mags
      if weapon.clips then
        mags = weapon.clips .. " Clips"
      end
      if weapon.rounds then
        mags = weapon.rounds .. " Rounds"
      end
      if not mags then
        mags = "Infinite"
      end

      local scopes = weapon.scopes or {}
      local zooms = {}
      for i, scope in ipairs(weapon.scopes or {}) do
        zooms[i] = scope .. "m"
      end

      rows[i] = "|" .. table.concat({
        "[[" .. name .. "]]",
        "[[:Category:" .. category_page .. "|" .. category .. "]]",
        dmg,
        dmg2,
        weapon.rpm or "",
        dps or "",
        weapon.reload or "",
        cap,
        mags,
        weapon.recoil or "",
        special or "",
        projectile_type,
        frame:expandTemplate({ title = "Armor", args = { max_ap, "AP" } }),
        modes.semi or modes.charge or "",
        modes.burst2 or modes.burst3 or "",
        modes.auto or "",
        modes["1h"] or "",
        zooms[1] or "",
        zooms[2] or "",
        zooms[3] or "",
      }, "||")
    end
  end
  return table.concat(rows, "\n|-\n")
end

function getTotalDamage(weapon)
  local total = 0
  for _, attack in ipairs(weapon.attacks or {}) do
    local obj = data[attack.type][attack.name]
    local damage = data.damage[obj.damage_name]
    if attack.type == "damage" then
      damage = obj
    end
    if damage then
      total = total + damage.dmg * (obj.pellets or 1) * (attack.count or 1)
    end
  end
  return total
end

local getters = {
  total_damage = getTotalDamage,
  max_ap = function(weapon)
    local max = 0
    for _, attack in ipairs(weapon.attacks or {}) do
      local obj = data[attack.type][attack.name]
      local damage = data.damage[obj.damage_name]
      if attack.type == "damage" then
        damage = obj
      end
      if damage and damage.ap1 > max then
        max = damage.ap1
      end
    end
    return max
  end,
  capacity = function(weapon)
    return weapon.cap
  end,
  recoil = function(weapon)
    return weapon.recoil
  end,
  dps = function(weapon)
    if not weapon.rpm then
      return ""
    end
    if not weapon.cap or weapon.cap < 2 then
      return ""
    end
    return math.floor(getTotalDamage(weapon) * weapon.rpm / 60)
  end,
  fire_rate = function(weapon)
    if weapon.chargeearly then
      return math.floor(60 / weapon.chargeearly)
    end
    if weapon.charge then
      return math.floor(60 / weapon.charge)
    end
    return weapon.rpm
  end,
  spare_ammo = function(weapon)
    if weapon.rounds then
      return weapon.rounds .. " Rounds"
    end
    if weapon.clips then
      return weapon.clips .. " Clips"
    end
    if weapon.mags then
      return weapon.mags .. " Magazines"
    end
  end,
}

function p.get(frame)
  local PAGENAME = mw.title.getCurrentTitle().prefixedText
  local args = getArgs(frame, { removeBlanks = false })
  local property = args[1]
  local weapon = data.weapon[PAGENAME]
  if not weapon then
    return "" -- Omit error message. It looks bad if exposed to users.
  end
  local getter = getters[property]
  if not getter then
    return "" -- Omit error message. It looks bad if exposed to users.
  end
  return getter(weapon)
end

return p