Difference between revisions of "Module:Mech"

From BTAWiki
Jump to navigation Jump to search
Line 3: Line 3:
 
-- p for "package", the interface of this module. Returned at the end
 
-- p for "package", the interface of this module. Returned at the end
 
local p = {}
 
local p = {}
 +
 +
local core = {}
 +
p.core = core
  
 
-- imports
 
-- imports
 
local cargo = mw.ext.cargo
 
local cargo = mw.ext.cargo
 
local getArgs = require('Module:Arguments').getArgs
 
local getArgs = require('Module:Arguments').getArgs
 +
local gear = require('Module:Gear').core
  
 
p.cargo = {}
 
p.cargo = {}
Line 95: Line 99:
 
     rightArmActuatorLimit = {
 
     rightArmActuatorLimit = {
 
       field = 'RightArmActuatorLimit',
 
       field = 'RightArmActuatorLimit',
 +
      type = 'String'
 +
    },
 +
    startingTonnage = {
 +
      field = 'StartingTonnage',
 +
      type = 'Float',
 
     },
 
     },
 
   }
 
   }
Line 369: Line 378:
 
   end
 
   end
 
   return frame:callParserFunction('#cargo_store:', cargo_data)
 
   return frame:callParserFunction('#cargo_store:', cargo_data)
 +
end
 +
 +
--
 +
-- Classes
 +
 +
-- core.mech defines the lua-centric mech object
 +
core.mech = {}
 +
 +
function core.mech:new(variantName)
 +
  local mech = {
 +
    variantName = variantName
 +
  }
 +
 +
  setmetatable(mech, self)
 +
  self.__index = self
 +
 +
  local chassisfields = {
 +
    'Id',
 +
    'Name',
 +
    'UIName',
 +
    'Details',
 +
    'StockRole',
 +
    'weightClass',
 +
    'Tonnage',
 +
    -- 'StartingTonnage',
 +
    'LeftArmActuatorLimit',
 +
    -- 'RightArmActuatorLimit',
 +
  }
 +
 +
  where = string.format(
 +
    '%s.%s="%s"', p.cargo.chassis.table, 'VariantName', variantName
 +
  )
 +
 +
  local chassisRow = cargo.query(
 +
    p.cargo.chassis.table, table.concat(chassisfields, ','), { where = where }
 +
  )
 +
  chassisRow = chassisRow[1]
 +
 +
  mech.chassisID = chassisRow['Id']
 +
  mech.uiName = chassisRow['UIName']
 +
  mech.name = chassisRow['Name']
 +
  mech.details = chassisRow['Details']
 +
  mech.weightClass = chassisRow['weightClass']
 +
  mech.stockRole = chassisRow['StockRole']
 +
  mech.leftArmActuatorLimit = chassisRow['LeftArmActuatorLimit']
 +
  -- mech.rightArmActuatorLimit = chassisRow['RightArmActuatorLimit']
 +
  mech.tonnage = tonumber(chassisRow['Tonnage'], 10)
 +
  -- mech.startingTonnage = tonumber(chassisRow['StartingTonnage'], 10)
 +
 +
  local mechfields = {
 +
    'MechTags',
 +
    'Id',
 +
  }
 +
  local mechwhere = string.format(
 +
    '%s.%s="%s"', p.cargo.mech.table, 'ChassisID', mech.chassisID
 +
  )
 +
 +
  local mechRow = cargo.query(
 +
    p.cargo.mech.table, table.concat(mechfields, ','), { where = mechwhere }
 +
  )
 +
  mechRow = mechRow[1]
 +
  mech.mechID = mechRow['Id']
 +
  mech.tags = mw.text.split(mechRow['MechTags'], ',')
 +
 +
  return mech
 +
end
 +
 +
function core.mech:getLocations()
 +
  if self.locations ~= nil then
 +
    return self.locations
 +
  end
 +
 +
  where = string.format(
 +
    '%s.%s="%s"',
 +
    p.cargo.chassis_locations.table, 'ChassisID', self.chassisID
 +
  )
 +
  fields = {
 +
    'Location',
 +
    'InventorySlots',
 +
    'MaxArmor',
 +
    'MaxRearArmor',
 +
    'InternalStructure',
 +
    'Hardpoints',
 +
    'OmniHardpoints',
 +
  }
 +
 +
  local chassisLocations = cargo.query(
 +
    p.cargo.chassis_locations.table,
 +
    table.concat(fields, ','),
 +
    { where = where }
 +
  )
 +
 +
  self.locations = {}
 +
  for _, row in ipairs(chassisLocations) do
 +
    local location = {
 +
      tonnage = tonumber(row['Tonnage'], 10),
 +
      location = row['Location'],
 +
      maxArmor = tonumber(row['MaxArmor'], 10),
 +
      maxRearArmor = tonumber(row['MaxRearArmor'], 10),
 +
      internalStructure = tonumber(row['InternalStructure'], 10),
 +
      hardpoints = mw.text.split(row['Hardpoints'], ','),
 +
      omniHardpoints = mw.text.split(row['OmniHardpoints'], ',')
 +
    }
 +
 +
    self.locations[row['Location']] = location
 +
  end
 +
 +
  local mechwhere = string.format(
 +
    '%s.%s="%s"',
 +
    p.cargo.mech_locations.table, 'MechID', self.mechID
 +
  )
 +
  local mechfields = {
 +
    'Location',
 +
    'AssignedArmor',
 +
    'AssignedRearArmor',
 +
  }
 +
 +
 +
  local mechlocations = cargo.query(
 +
    p.cargo.mech_locations.table,
 +
    table.concat(mechfields, ','),
 +
    { where = mechwhere }
 +
  )
 +
 +
  for _, row in ipairs(mechlocations) do
 +
    self.locations[row['Location']].assignedArmor = tonumber(row['AssignedArmor'], 10)
 +
    self.locations[row['Location']].assignedRearArmor = tonumber(row['AssignedRearArmor'], 10)
 +
  end
 +
 +
  return self.locations
 +
end
 +
 +
function core.mech:getEngine()
 +
  if self.engine ~= nil then
 +
    return self.engine
 +
  end
 +
 +
  self.engine = gear.get_engine(self.chassisID, self.mechID)
 +
 +
  return self.engine
 
end
 
end
  
Line 375: Line 524:
 
--
 
--
  
-- rawTable generates a raw table consisting of the data
+
-- raw_table generates a raw table consisting of the data
function rawTable(schema, tpl_args)
+
function raw_table(schema, tpl_args)
 
   local rawTable = mw.html.create('table')
 
   local rawTable = mw.html.create('table')
 
   rawTable:addClass('wikitable')
 
   rawTable:addClass('wikitable')
Line 398: Line 547:
 
   -- p.mechChassis is a template for raw data, and so we display the raw data
 
   -- 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
 
   -- instead of anything fancy. To really display a mech, we need way more than
   -- the chassis, anyway.
+
   -- the chassis, anywayr.
 
   local chassisTable = rawTable(p.cargo.chassis, tpl_args)
 
   local chassisTable = rawTable(p.cargo.chassis, tpl_args)
  
Line 444: Line 593:
 
   })
 
   })
  
   cargo_store(frame, p.cargo.mech_inventory, tpl_args)
+
   cargo_store(frame, p.cargo.mech_locations, tpl_args)
  
 
   return tostring(rawTable(p.cargo.mech_inventory, tpl_args))
 
   return tostring(rawTable(p.cargo.mech_inventory, tpl_args))
Line 456: Line 605:
  
 
   -- this function takes one argument, the VariantName.
 
   -- this function takes one argument, the VariantName.
 
+
   mech = core.mech:new(tpl_args['VariantName'])
   -- we only need a subset of the chassis fields.
 
  local chassisFields = 'VariantName,weightClass,Tonnage,StockRole,Id'
 
  local chassisArgs = {
 
    -- this uses string formatting to produce a string something like:
 
    --  'Chassis.VariantName="MAD-5A"'
 
    -- the where tells the database to only take entries with this VariantName.
 
    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)
 
 
 
  -- we should only ever get 1 entry for each VariantName, but cargo.query
 
  -- always returns a list. Lists in Lua are 1-indexed (as opposed to the
 
  -- typical 0-indexing in most languages), so this just mean's we're taking
 
  -- the first entry.
 
  chassisData = chassisData[1]
 
  local chassisID = chassisData['Id']
 
 
 
  -- setting up another database query, this time for the mech locations. We
 
  -- will use this data to get the sum of the internal structure, and to count
 
  -- up the number of available hardpoints.
 
  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 structure = 0
 +
  local armor = 0
 
   -- table keys in lua are also strings. these table keys are the same strings
 
   -- table keys in lua are also strings. these table keys are the same strings
 
   -- we expect the hardpoints to be. this is a shortcut to make counting
 
   -- we expect the hardpoints to be. this is a shortcut to make counting
Line 511: Line 622:
 
   local mechtype = 'Standard BattleMech'
 
   local mechtype = 'Standard BattleMech'
  
   for _, location in ipairs(chassisLocations) do
+
   for _, location in pairs(mech:getLocations()) do
 
     -- add the value of each location's InternalStructure together. all of the
 
     -- add the value of each location's InternalStructure together. all of the
 
     -- fields returned from cargo are strings, so we have to first convert them
 
     -- fields returned from cargo are strings, so we have to first convert them
 
     -- to numbers. I think. I might be wrong about this, actually.
 
     -- to numbers. I think. I might be wrong about this, actually.
     structure = structure + tonumber(location['InternalStructure'], 10)
+
     structure = structure + location.internalStructure
 +
    armor = armor + location.assignedArmor
 +
    if location.assignedRearArmor ~= -1 then
 +
      armor = armor + location.assignedRearArmor
 +
    end
  
 
     -- hardpoints is a list of strings. we split that list along the commas to
 
     -- hardpoints is a list of strings. we split that list along the commas to
 
     -- get an actual list we can iterate.
 
     -- get an actual list we can iterate.
     for _, hardpoint in ipairs(mw.text.split(location['Hardpoints'], ",")) do
+
     for _, hardpoint in ipairs(location.hardpoints) do
 
       -- then, we check to see if the hardpoint is one of the keys of the
 
       -- then, we check to see if the hardpoint is one of the keys of the
 
       -- hardpoint_counts table
 
       -- hardpoint_counts table
Line 526: Line 641:
 
         hardpoint_counts[hardpoint] = hardpoint_counts[hardpoint] + 1
 
         hardpoint_counts[hardpoint] = hardpoint_counts[hardpoint] + 1
 
       end
 
       end
    end
 
    if #location['OmniHardpoints'] > 0 then
 
      mechtype = 'OmniMech'
 
 
     end
 
     end
 
   end
 
   end
  
  -- in this iteration, the only thing we need is the mechdef's ID. currently,
+
   local engine = mech:getEngine()
  -- we only have the chassisdef's ID.
 
   local mechdefFields = 'Id'
 
  local mechdefArgs = {
 
    -- this is where we select only the row with the same chassis ID.
 
    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
 
  )
 
 
 
  -- remember, Lua is 1-indexed.
 
  mechID = mechdef[1]['Id']
 
 
 
  -- now, we need to get the total armor. This is part of the MechLocations,
 
  -- not the ChassisLocations, so we do another query.
 
  local mechLocationFields = 'AssignedArmor,AssignedRearArmor'
 
  local mechLocationArgs = {
 
    where = string.format(
 
      '%s.%s="%s"',
 
      p.cargo.mech_locations.table,
 
      p.cargo.mech_locations.fields.mechID.field,
 
      mechID
 
    )
 
  }
 
 
 
  local mechLocations = cargo.query(
 
    p.cargo.mech_locations.table, mechLocationFields, mechLocationArgs
 
  )
 
 
 
  local armor = 0
 
  for _, location in ipairs(mechLocations) do
 
    -- same thing as with structure.
 
    armor = armor + tonumber(location['AssignedArmor'], 10)
 
    -- for locations, like arms, legs, and the head, that do not have rear
 
    -- armor, the rear armor value is '-1'. We should only add the rear armor
 
    -- value to the total if it is not -1, because otherwise we'd come up 5
 
    -- points short from adding in the -1s
 
    if location['AssignedRearArmor'] ~= '-1' then
 
      armor = armor + tonumber(location['AssignedRearArmor'], 10)
 
    end
 
  end
 
 
 
  -- this isn't actually used yet, because we need to fully populate the item
 
  -- database.  However, when the item database is filled,  we can get the full
 
  -- details of all of the items on the 'Mech, and use that to calculate things
 
  -- like alpha strike damage.
 
  local inventoryFields = 'ComponentDefID,ComponentDefType'
 
  local 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
 
    )
 
  }
 
  
 
   -- finally, to avoid re-inventing the wheel for this demo, we call the
 
   -- finally, to avoid re-inventing the wheel for this demo, we call the
Line 602: Line 650:
 
   -- mech's database entries.
 
   -- mech's database entries.
 
   local template_args = {
 
   local template_args = {
     variantname = chassisData['VariantName'],
+
     variantname = mech.variantName,
     mechtype = mechtype,
+
     mechtype = 'Standard BattleMech',
     class = chassisData['weightClass'],
+
     class = mech.weightClass,
 
     ballistic = hardpoint_counts.Ballistic,
 
     ballistic = hardpoint_counts.Ballistic,
 
     energy = hardpoint_counts.Energy,
 
     energy = hardpoint_counts.Energy,
Line 610: Line 658:
 
     support = hardpoint_counts.AntiPersonnel,
 
     support = hardpoint_counts.AntiPersonnel,
 
     armor = armor,
 
     armor = armor,
     structure = structure
+
     structure = structure,
 +
    enginetype = engine.shield.name,
 +
    coresize = engine.rating,
 +
    ecooling = string.format('+%d', engine.ecooling)
 
   }
 
   }
   template_args['stock role'] = chassisData['StockRole']
+
   template_args['stock role'] = mech.stockRole
  
 
   return frame:expandTemplate{
 
   return frame:expandTemplate{

Revision as of 20:03, 17 January 2021

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 = {}

local core = {}
p.core = core

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

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',
      type = 'String'
    },
    startingTonnage = {
      field = 'StartingTonnage',
      type = 'Float',
    },
  }
}

-- 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',
    },
    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

--
-- Classes

-- core.mech defines the lua-centric mech object
core.mech = {}

function core.mech:new(variantName)
  local mech = {
    variantName = variantName
  }

  setmetatable(mech, self)
  self.__index = self

  local chassisfields = {
    'Id',
    'Name',
    'UIName',
    'Details',
    'StockRole',
    'weightClass',
    'Tonnage',
    -- 'StartingTonnage',
    'LeftArmActuatorLimit',
    -- 'RightArmActuatorLimit',
  }

  where = string.format(
    '%s.%s="%s"', p.cargo.chassis.table, 'VariantName', variantName
  )

  local chassisRow = cargo.query(
    p.cargo.chassis.table, table.concat(chassisfields, ','), { where = where }
  )
  chassisRow = chassisRow[1]

  mech.chassisID = chassisRow['Id']
  mech.uiName = chassisRow['UIName']
  mech.name = chassisRow['Name']
  mech.details = chassisRow['Details']
  mech.weightClass = chassisRow['weightClass']
  mech.stockRole = chassisRow['StockRole']
  mech.leftArmActuatorLimit = chassisRow['LeftArmActuatorLimit']
  -- mech.rightArmActuatorLimit = chassisRow['RightArmActuatorLimit']
  mech.tonnage = tonumber(chassisRow['Tonnage'], 10)
  -- mech.startingTonnage = tonumber(chassisRow['StartingTonnage'], 10)

  local mechfields = {
    'MechTags',
    'Id',
  }
  local mechwhere = string.format(
    '%s.%s="%s"', p.cargo.mech.table, 'ChassisID', mech.chassisID
  )

  local mechRow = cargo.query(
    p.cargo.mech.table, table.concat(mechfields, ','), { where = mechwhere }
  )
  mechRow = mechRow[1]
  mech.mechID = mechRow['Id']
  mech.tags = mw.text.split(mechRow['MechTags'], ',')

  return mech
end

function core.mech:getLocations()
  if self.locations ~= nil then
    return self.locations
  end

  where = string.format(
    '%s.%s="%s"', 
    p.cargo.chassis_locations.table, 'ChassisID', self.chassisID
  )
  fields = {
    'Location',
    'InventorySlots',
    'MaxArmor',
    'MaxRearArmor',
    'InternalStructure',
    'Hardpoints',
    'OmniHardpoints',
  }

  local chassisLocations = cargo.query(
    p.cargo.chassis_locations.table, 
    table.concat(fields, ','),
    { where = where }
  )

  self.locations = {}
  for _, row in ipairs(chassisLocations) do
    local location = {
      tonnage = tonumber(row['Tonnage'], 10),
      location = row['Location'],
      maxArmor = tonumber(row['MaxArmor'], 10),
      maxRearArmor = tonumber(row['MaxRearArmor'], 10),
      internalStructure = tonumber(row['InternalStructure'], 10),
      hardpoints = mw.text.split(row['Hardpoints'], ','),
      omniHardpoints = mw.text.split(row['OmniHardpoints'], ',')
    }

    self.locations[row['Location']] = location
  end

  local mechwhere = string.format(
    '%s.%s="%s"', 
    p.cargo.mech_locations.table, 'MechID', self.mechID
  )
  local mechfields = {
    'Location',
    'AssignedArmor',
    'AssignedRearArmor',
  }


  local mechlocations = cargo.query(
    p.cargo.mech_locations.table,
    table.concat(mechfields, ','),
    { where = mechwhere }
  )

  for _, row in ipairs(mechlocations) do
    self.locations[row['Location']].assignedArmor = tonumber(row['AssignedArmor'], 10)
    self.locations[row['Location']].assignedRearArmor = tonumber(row['AssignedRearArmor'], 10)
  end

  return self.locations
end

function core.mech:getEngine()
  if self.engine ~= nil then
    return self.engine
  end

  self.engine = gear.get_engine(self.chassisID, self.mechID)

  return self.engine
end

--
-- Templates
--

-- raw_table generates a raw table consisting of the data
function raw_table(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, anywayr.
  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_locations, 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
  })

  -- this function takes one argument, the VariantName.
  mech = core.mech:new(tpl_args['VariantName'])

  local structure = 0
  local armor = 0
  -- table keys in lua are also strings. these table keys are the same strings
  -- we expect the hardpoints to be. this is a shortcut to make counting
  -- hardpoints easier.
  local hardpoint_counts = {
    Energy = 0,
    Ballistic = 0,
    Missile = 0,
    AntiPersonnel = 0,
  }
  -- mechtype is 'Standard BattleMech', unless we find a non-zero number of
  -- Omni hardpoints. then, we'll change it to be 'OmniMech'.
  local mechtype = 'Standard BattleMech'

  for _, location in pairs(mech:getLocations()) do
    -- add the value of each location's InternalStructure together. all of the
    -- fields returned from cargo are strings, so we have to first convert them
    -- to numbers. I think. I might be wrong about this, actually.
    structure = structure + location.internalStructure
    armor = armor + location.assignedArmor
    if location.assignedRearArmor ~= -1 then
      armor = armor + location.assignedRearArmor
    end

    -- hardpoints is a list of strings. we split that list along the commas to
    -- get an actual list we can iterate.
    for _, hardpoint in ipairs(location.hardpoints) do
      -- then, we check to see if the hardpoint is one of the keys of the
      -- hardpoint_counts table
      if hardpoint_counts[hardpoint] ~= nil then
        -- if it is, then we increment that entry's counter.
        hardpoint_counts[hardpoint] = hardpoint_counts[hardpoint] + 1
      end
    end
  end

  local engine = mech:getEngine()

  -- finally, to avoid re-inventing the wheel for this demo, we call the
  -- existing InfoboxVariant template with the data we've computed from the
  -- mech's database entries.
  local template_args = {
    variantname = mech.variantName,
    mechtype = 'Standard BattleMech',
    class = mech.weightClass,
    ballistic = hardpoint_counts.Ballistic,
    energy = hardpoint_counts.Energy,
    missile = hardpoint_counts.Missile,
    support = hardpoint_counts.AntiPersonnel,
    armor = armor,
    structure = structure,
    enginetype = engine.shield.name,
    coresize = engine.rating,
    ecooling = string.format('+%d', engine.ecooling)
  }
  template_args['stock role'] = mech.stockRole

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

return p