Module:Mech

From BTAWiki
Revision as of 04:17, 15 January 2021 by Rust dev (talk | contribs)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Mech/doc

-- Module for handling information related to a BattleMech

-- p for "package", the interface of this module. Returned at the end
local p = {}

-- imports
local cargo = mw.ext.cargo
local getArgs = require('Module:Arguments').getArgs

p.cargo = {}

-- p.cargo.chassis represents the schema of the information contained in a
-- chassisdef file.
p.cargo.chassis = {
  -- table is the name of the cargo table
  table = 'Chassis',
  -- fields is the schema of all of the fields of the Mech
  fields = {
    -- id is the primary key for this table. We expect it to be unique. It
    -- will be used as the foreign key in other tables to refer to this
    -- particular mech variant.
    id = {
      field = 'Id',
      type = 'String',
    },
    cost = {
      -- field defines the name of the field in the cargo table. for the
      -- field name, we use the name of the property in the ChassisDef
      -- json.
      field = 'Cost',
      -- type defines the type of the field
      type = 'Integer',
    },
    rarity = {
      field = 'Rarity',
      type = 'Integer',
    },
    purchasable = {
      field = 'Purchasable',
      type = 'Boolean',
    },
    manufacturer = {
      field = 'Manufacturer',
      type = 'String',
    },
    model = {
      field = 'Model',
      type = 'String',
    },
    uiName = {
      field = 'UIName',
      type = 'String',
    },
    name = {
      field = 'Name',
      type = 'String',
    },
    details = {
      field = 'Details',
      -- Details use the Text type, which is unindexed and intended for
      -- longer-form data.
      type = 'Text',
    },
    icon = {
      field = 'Icon',
      type = 'String',
    },
    tonnage = {
      field = 'Tonnage',
      type = 'Integer',
    },
    weightClass = {
      -- TODO(rust dev): this is probably an enum type. I bet we can
      -- do something with that.
      field = 'weightClass',
      type = 'String'
    },
    variantName = {
      field = 'VariantName',
      type = 'String',
    },
    stockRole = {
      field = 'StockRole',
      type = 'Text',
    },
    yangsThoughts = {
      field = 'YangsThoughts',
      type = 'Text',
    },
    leftArmActuatorLimit = {
      -- TODO(rust dev): another enum
      field = 'LeftArmActuatorLimit',
      type = 'String',
    },
    rightArmActuatorLimit = {
      field = 'RightArmActuatorLimit',
    },
  }
}

-- p.cargo.chassis_locations describes the information about a location on a mech
-- chassis
p.cargo.chassis_locations = {
  table = 'ChassisLocations',
  fields = {
    -- chassisID is the id of the chassis this location belongs to
    chassisID = {
      field = 'ChassisID',
      type = 'String',
    },
    location = {
      -- TODO(rust dev): this is probably an enum, and we can do something with
      -- that.
      field = 'Location',
      type = 'String'
    },
    tonnage = {
      field = 'Tonnage',
      type = 'Integer',
    },
    inventorySlots = {
      field = 'InventorySlots',
      type = 'Integer',
    },
    maxArmor = {
      field = 'MaxArmor',
      type = 'Integer',
    },
    maxRearArmor = {
      field = 'MaxRearArmor',
      type = 'Integer',
    },
    internalStructure = {
      field = 'InternalStructure',
      type = 'Integer',
    },
    hardpoints = {
      -- Hardpoints are done as a list of string. The value of the string is the
      -- type of hardpoint so, it might have the form in the template like
      --
      --   |Hardpoints = Energy,Energy,Missile
      --
      field = 'Hardpoints',
      type = 'List (,) of String',
    },
    omniHardpoints = {
      -- OmniHardpoints are the same, but for hardpoints where Omni == true
      field = 'OmniHardpoints',
      type = 'List (,) of String',
    }
  }
}

-- p.cargo.mech represents the information contained in a mechdef file.
p.cargo.mech = {
  table = 'Mech',
  fields = {
    -- id is the id of this mechdef.
    id = {
      field = 'ID',
      type = 'String',
    },
    -- chassisID is the foreign key pointed to the ChassisDef that corresponds
    -- to this mech.
    chassisID = {
      field = 'ChassisID',
      type = 'String',
    },
    -- mechTags is a list of strings, the tags for the mech. corresponds to
    -- MechTags.items in the json file.
    mechTags = {
      field = 'MechTags',
      type = 'List (,) of String',
    },
    cost = {
      field = 'Cost',
      type = 'Integer',
    },
    rarity = {
      field = 'Rarity',
      type = 'Integer',
    },
    purchasable = {
      field = 'Purchasable',
      type = 'Boolean',
    },
    manufacturer = {
      field = 'Manufacturer',
      type = 'String',
    },
    model = {
      field = 'Model',
      type = 'String',
    },
    uiName = {
      field = 'UIName',
      type = 'String',
    },
    name = {
      field = 'Name',
      type = 'String',
    },
    details = {
      field = 'Details',
      -- Details use the Text type, which is unindexed and intended for
      -- longer-form data.
      type = 'Text',
    },
    icon = {
      field = 'Icon',
      type = 'String',
    },
    simGameMechPartCost = {
      field = 'simGameMechPartCost',
      type = 'Integer',
    },
    version = {
      field = 'Version',
      type = 'Integer',
    },
  },
}

p.cargo.mech_locations = {
  table = 'MechLocations',
  -- a lot of the data in a mechdef seems redundant and not interesting for
  -- our purposes. however, include it here in the interest of correctness.
  fields = {
    -- mechID is the ID of the mech that this location belongs to
    mechID = {
      field = 'MechID',
      type = 'String',
    },
    -- location is the location being described here
    location = {
      field = 'Location',
      type = 'String',
    },
    damageLevel = {
      field = 'DamageLevel',
      -- another enum, probably.
      type = 'String',
    },
    currentArmor = {
      field = 'CurrentArmor',
      type = 'Integer',
    },
    currentRearArmor = {
      field = 'CurrentRearArmor',
      type = 'Integer',
    },
    currentInternalStructure = {
      field = 'CurrentInternalStructure',
      type = 'Integer',
    },
    assignedArmor = {
      field = 'AssignedArmor',
      type = 'Integer',
    },
    assignedRearArmor = {
      field = 'AssignedRearArmor',
      type = 'Integer',
    },
  },
}

-- p.cargo.mech_inventory describes an item in the inventory of a mech
--
-- excludes some fields which don't appear to be relevant:
--   * SimGameUID
--   * GUID
--   * DamageLevel
--   * prefabName
--   * hasPrefabName
p.cargo.mech_inventory = {
  table = 'MechInventory',
  fields = {
    -- mechID is the ID of the mechdef that this inventory item is from
    mechID = {
      field = 'MechID',
      type = 'String',
    },
    mountedLocation = {
      field = 'MountedLocation',
      type = 'String',
    },
    -- componentDefID is the ID of the component that that is mounted in this
    -- location
    componentDefID = {
      field = 'ComponentDefID',
      type = 'String',
    },
    componentDefType = {
      field = 'ComponentDefType',
      -- probably yet another enum.
      type = 'String'
    },
    hardpointSlot = {
      field = 'HardpointSlot',
      type = 'Integer',
    },
    -- fixedEquipment is a bit different. a mech can have equipment defined in
    -- the chassisdef as well as the mechdef. Equipment defined in the
    -- chassisdef is "FixedEquipment". Instead of having a separate table for
    -- this, we will instead set this flag true, and the mechID will instead
    -- refer to a chassis ID.
    fixedEquipment = {
      field = 'FixedEquipment',
      type = 'Boolean',
    },
  },
}

-- cargo_declare is a function which returns another function. The return value
-- is evaluated to declare the cargo table.
--
-- this is taken in large part from the Path of Exile wiki's Item2 module.
--
-- TODO(rust dev): define this in a separate module for reuse
function cargo_declare (data)
  return function (frame)
    if frame == nil then
      frame = mw.getCurrentFrame()
    end

    -- dcl_args are the arguments we will be passing to #cargo_declare
    local dcl_args = {}

    -- set the table name
    dcl_args._table = data.table

    -- for every field, we create an argument mapping the field name to its
    -- type.
    for k, field_data in pairs(data.fields) do
      if field_data then
        dcl_args[field_data.field] = field_data.type
      end
    end

    -- call #cargo_declare to declare the table before ending the function.
    frame:callParserFunction('#cargo_declare:', dcl_args)
  end
end

-- invoke these functions to declare the tables.
p.table_chassis = cargo_declare(p.cargo.chassis)
p.table_chassis_locations = cargo_declare(p.cargo.chassis_locations)
p.table_mech = cargo_declare(p.cargo.mech)
p.table_mech_locations = cargo_declare(p.cargo.mech_locations)
p.table_inventory = cargo_declare(p.cargo.mech_inventory)

-- stores the data in cargo. We don't just pass in all the template arguments,
-- as the template may have parameters that are not part of the chassis
-- definition. here instead, we iterate through the known fields of the table
-- and look for matching argument names. If we find one, we add it to the data
-- we use in the cargo store call.
function cargo_store(frame, schema, tpl_args)
  local cargo_data = {_table = schema.table}

  for _, field in pairs(schema.fields) do
    local arg = tpl_args[field.field]
    if arg ~= nil then
      -- consistently cast boolean values to the same type as 1 or 0.
      if type(arg) == 'boolean' then
        if arg == true then
          arg = '1'
        else
          arg ='0'
        end
      end

      cargo_data[field.field] = arg
    end
  end
  return frame:callParserFunction('#cargo_store:', cargo_data)
end

--
-- Templates
--

-- rawTable generates a raw table consisting of the data
function rawTable(schema, tpl_args)
  local rawTable = mw.html.create('table')
  rawTable:addClass('wikitable')

  for _, field in pairs(schema.fields) do
    rawTable:tag('tr')
      :tag('th'):wikitext(field.field):done()
      :tag('td'):wikitext(tpl_args[field.field])
  end
  return rawTable
end

-- p.chassis is a template which defines a Mech chassis.
function p.chassis (frame)
  tpl_args = getArgs(frame, {
    -- parentFirst tells us to prefer args from the parent page (the template
    -- that calls #invoke).
    parentFirst = true
  })

  -- p.mechChassis is a template for raw data, and so we display the raw data
  -- instead of anything fancy. To really display a mech, we need way more than
  -- the chassis, anyway.
  local chassisTable = rawTable(p.cargo.chassis, tpl_args)

  cargo_store(frame, p.cargo.chassis, tpl_args)

  return tostring(chassisTable)
end

-- p.chassisLocation is a template which defines a Mech chassis location
function p.chassisLocation (frame)
  tpl_args = getArgs(frame, {
    parentFirst = true,
  })

  local locationTable = rawTable(p.cargo.chassis_locations, tpl_args)

  cargo_store(frame, p.cargo.chassis_locations, tpl_args)

  return tostring(locationTable)
end

function p.mech (frame)
  tpl_args = getArgs(frame, {
    parentFirst = true
  })

  cargo_store(frame, p.cargo.mech, tpl_args)

  return tostring(rawTable(p.cargo.mech, tpl_args))
end

function p.mechLocation (frame)
  tpl_args = getArgs(frame, {
    parentFirst = true
  })

  cargo_store(frame, p.cargo.mech_locations, tpl_args)

  return tostring(rawTable(p.cargo.mech_locations, tpl_args))
end

function p.mechInventory (frame)
  tpl_args = getArgs(frame, {
    parentFirst = true
  })

  cargo_store(frame, p.cargo.mech_inventory, tpl_args)

  return tostring(rawTable(p.cargo.mech_inventory, tpl_args))
end

-- mechInfoBox creates a mech infobox.
function p.mechInfoBox (frame)
  tpl_args = getArgs(frame, {
    parentFirst = true
  })

  local chassisFields = 'VariantName,weightClass,Tonnage,StockRole,Id'
  local chassisArgs = {
    where = string.format(
      '%s.%s="%s"', 
      p.cargo.chassis.table, 
      p.cargo.chassis.fields.variantName.field, 
      tpl_args[p.cargo.chassis.fields.variantName.field]
    )
  }

  local chassisData = cargo.query(p.cargo.chassis.table, chassisFields, chassisArgs)
  chassisData = chassisData[1]
  local chassisID = chassisData['Id']

  local locationFields = 'InternalStructure,Hardpoints,OmniHardpoints'
  local locationArgs = {
    where = string.format(
      '%s.%s="%s"',
      p.cargo.chassis_locations.table,
      p.cargo.chassis_locations.fields.chassisID.field,
      chassisID
    )
  }

  local chassisLocations = cargo.query(
    p.cargo.chassis_locations.table, locationFields, locationArgs
  )

  local structure = 0
  local hardpoint_counts = {
    Energy = 0,
    Ballistic = 0,
    Missile = 0,
    AntiPersonnel = 0,
  }
  local mechtype = 'Standard BattleMech'

  for _, location in ipairs(chassisLocations) do
    structure = structure + location['InternalStructure']
    for _, hardpoint in ipairs(mw.text.split(location['Hardpoints'], ",")) do
      if hardpoint_counts[hardpoint] ~= nil then
        hardpoint_counts[hardpoint] = hardpoint_counts[hardpoint] + 1
      end
    end
    if #location['OmniHardpoints'] > 0 then
      mechtype = 'OmniMech'
    end
  end

  local mechdefFields = 'ID'
  local mechdefArgs = {
    where = string.format(
      '%s.%s="%s"',
      p.cargo.mech.table,
      p.cargo.mech.fields.chassisID.field,
      chassisID
    )
  }

  local mechdef = cargo.query(
    p.cargo.mech.table, mechdefFields, mechdefArgs
  )

  mechID = mechdef[1]['ID']

  inventoryFields = 'ComponentDefID,ComponentDefType'
  inventoryArgs = {
    where = string.format(
      '%s.%s="%s" OR %s.%s="%s"',
      p.cargo.mech_inventory.table,
      p.cargo.mech_inventory.fields.mechID.field,
      mechID,
      p.cargo.mech_inventory.table,
      p.cargo.mech_inventory.fields.mechID.field,
      chassisID
    )
  }

  template_args = {
    variantname = chassisData['VariantName'],
    mechtype = mechtype,
    class = chassisData['weightClass'],
    ballistic = hardpoint_counts.Ballistic,
    energy = hardpoint_counts.Energy,
    missile = hardpoint_counts.Missile,
    support = hardpoint_counts.AntiPersonnel,
  }
  template_args['stock role'] = chassisData['StockRole']

  return frame:expandTemplate{
    title = 'Infobox',
    args = template_args
  }
end

return p