Star Citizen Wiki Modul:CharArmor
Wir laden dich herzlich auf den Star Citizen Wiki Discord Server ein! Du kannst uns auch auf unserem neuen YouTube-Chanel finden!

Modul:CharArmor

From Star Citizen Wiki

Modulabhängigkeiten

Modulinfo

Dieses Modul setzt die Vorlage:Rüstung um. Anweisungen zur Verwendung findest du auf der Vorlagenseite.


local CharArmor = {}

local metatable = {}
local methodtable = {}

metatable.__index = methodtable

local objectData = mw.loadData( 'Module:CharArmor/Data' )
local TNT = require( 'Module:TNT' )

-- Extensions
local common = require( 'Module:Common' )

--- Sorts all attachments into pre-defined groups
--- Outputs a table containing the min and max size for the attachment port
--- As well as the overall count of attachments for a group
--- @param attachments table
--- @return table
local function makeArmorAttachmentTable( attachments )
    local out = {}
    local sortedOut = {}

    for _, attachment in pairs( attachments ) do
        local attachmentName = CharArmor.translateArmorAttachment( attachment[ 'Halterungsname' ] )

        local minNum = tonumber( attachment[ 'Minimalgröße' ] )
        if minNum == nil then
            minNum = 0
        end

        local maxNum = tonumber( attachment[ 'Maximalgröße' ] )
        if maxNum == nil then
            maxNum = 0
        end

        if attachmentName ~= nil then
            if out[ attachmentName ] == nil then
                out[ attachmentName ] = {
                    name = attachmentName,
                    min = minNum,
                    max = maxNum,
                    count = 1
                }
            else
                out[ attachmentName ].count = out[ attachmentName ].count + 1

                if minNum < out[ attachmentName ].min then
                    out[ attachmentName ].min = minNum
                end

                if maxNum > out[ attachmentName ].max then
                    out[ attachmentName ].max = maxNum
                end
            end
        end
    end

    for _, class in ipairs( objectData.attachmentOrder ) do
        if out[ class ] ~= nil then
            table.insert(sortedOut, out[ class ])
        end
    end

    --[[ TODO remove comment to show all attachments
    for name, attachment in pairs( out ) do
        if order[ name ] == nil then
            table.insert( sortedOut, attachment )
        end
    end
    ]]--

    return sortedOut
end

--- Add manual smw data
function methodtable.addManual( t )
    if t.frameArgs == nil then
        return
    end

    local setObj = {
        [ 'Name' ]                                = t.frameArgs.Name                                    or nil,
        [ 'Beschreibung' ]                        = t.frameArgs.Beschreibung                            or nil,
        [ 'Größe' ]                               = t.frameArgs[ 'Größe' ]                              or nil,
        [ 'Hersteller' ]                          = t.frameArgs[ 'Hersteller' ]                         or nil,
        [ 'Rüstungstyp' ]                         = t.frameArgs[ 'Rüstungstyp' ]                        or nil,
        [ 'Rüstungsklasse' ]                      = t.frameArgs[ 'Rüstungsklasse' ]                     or nil,
        [ 'Ist Basisversion' ]                    = t.frameArgs[ 'Ist Basisversion' ]                   or nil,
        [ 'Basisversion UUID' ]                   = t.frameArgs[ 'Basisversion UUID' ]                  or nil,
        [ 'SP' ]                                  = t.frameArgs[ 'SP' ]                                 or nil,
        [ 'Temperaturresistenz Minmal' ]          = t.frameArgs[ 'Temperaturresistenz Minmal' ]         or nil,
        [ 'Temperaturresistenz Maximal' ]         = t.frameArgs[ 'Temperaturresistenz Maximal' ]        or nil,
        [ 'Resistenzmultiplikator Physisch' ]     = t.frameArgs[ 'Resistenzmultiplikator Physisch' ]    or nil,
        [ 'Resistenzmultiplikator Energie' ]      = t.frameArgs[ 'Resistenzmultiplikator Energie' ]     or nil,
        [ 'Resistenzmultiplikator Distortion' ]   = t.frameArgs[ 'Resistenzmultiplikator Distortion' ]  or nil,
        [ 'Resistenzmultiplikator Thermisch' ]    = t.frameArgs[ 'Resistenzmultiplikator Thermisch' ]   or nil,
        [ 'Resistenzmultiplikator Biochemisch' ]  = t.frameArgs[ 'Resistenzmultiplikator Biochemisch' ] or nil,
        [ 'Resistenzmultiplikator Betäubung' ]    = t.frameArgs[ 'Resistenzmultiplikator Betäubung' ]   or nil,
        [ 'Spielversion' ]                        = t.frameArgs[ 'Spielversion' ]                       or nil,
    }

    mw.smw.set( setObj )
end

--- Request Api Data
--- Using current subpage name
--- @return table
function methodtable.getApiDataForCurrentPage( t )
    local name = t.frameArgs[ 'uuid' ] or t.frameArgs[ 'name' ] or mw.title.getCurrentTitle().text

    local json = mw.text.jsonDecode( mw.ext.Apiunto.get_char_armor( name, {
        locale = '',
        include = 'attachments,shops.items',
    } ) )

    common.checkApiResponse( json, true, false )

    t.apiData = json[ 'data' ]

    return t.apiData
end

--- Request armor attachments from SMW
--- @param name string
--- @return table
function methodtable.getArmorAttachments( t, name )
    local query = {
        '[[-Has subobject::' .. name .. ']][[Typ::Halterung]]',
        '?Halterungsname#-',
        '?Minimalgröße#-',
        '?Maximalgröße#-',
        'mainlabel=-'
    }

    return mw.smw.ask( query )
end

--- Request the first armor price from smw
--- @param name string
--- @return table
function methodtable.getArmorPrice( t, name )
    local query = {
        '[[Name::' .. name .. ']][[Kaufbar::1]]',
        '?Preis#-p0=price',
        'mainlabel=-',
        'limit=1'
    }

    return mw.smw.ask( query )
end


--- Queries the SMW Store
--- @return table
function methodtable.getSmwData( t )
    -- Cache multiple calls
    if t.smwData ~= nil then
        return t.smwData
    end

    -- name from args or current page
    local queryName = t.frameArgs[ 'name' ] or require( 'Module:Localized' ).getMainTitle()

    local data = mw.smw.ask( {
        '[[' .. queryName .. ']][[Hersteller::+]]',
        '?#-=page',
        '?Name#-',
        '?Größe#-',
        '?Hersteller#-',
        '?Beschreibung', '+lang=' .. common.getLocaleForPage(),
        '?Schadensreduktion#-',
        '?SP',
        '?Rüstungstyp=type', '+lang=' .. common.getLocaleForPage(),
        '?Rüstungstyp=type_de', '+lang=de',
        '?Rüstungsklasse=class', '+lang=' .. common.getLocaleForPage(),
        '?Rüstungsklasse=class_de', '+lang=de',
        '?Temperaturresistenz#-n',
        '?Resistenzmultiplikator Physisch#-n',
        '?Resistenzmultiplikator Energie#-n',
        '?Resistenzmultiplikator Distortion#-n',
        '?Resistenzmultiplikator Thermisch#-n',
        '?Resistenzmultiplikator Biochemisch#-n',
        '?Resistenzmultiplikator Betäubung#-n',
        '?Länge',
        '?Breite',
        '?Höhe',
        '?Volumen',
        '?Spielversion#-=version',
        'mainlabel=-'
    } )

    if data == nil or data[ 1 ] == nil then
        -- error( 'Seite "' .. queryName .. '" besitzt keine semantischen Daten.', 0 )
        return TNT.format( 'I18n/Module:CharArmor', 'msg_smw_loading' )
    end

    t.smwData = data[ 1 ]

    t.smwData.attachments = t:getArmorAttachments( queryName )

    t.smwData.price = t:getArmorPrice( queryName )
    if t.smwData.price ~= nil then
        t.smwData.price = t.smwData.price[ 1 ].price
    end

    return t.smwData
end

--- Base Properties that are shared across all Vehicles
--- @return table SMW Result
function methodtable.setSemanticProperties( t )
    -- Api Error, don't set anything
    if t.apiData == nil then
        return
    end

    local manufacturerName = t.apiData.manufacturer or nil
    if manufacturerName ~= nil then
        manufacturerName = mw.ustring.gsub( manufacturerName, '%[PH%]', '' )
    end

    if manufacturerName == '@LOC_PLACEHOLDER' or manufacturerName == '@LOC PLACEHOLDER' or manufacturerName == 'Unknown Manufacturer' then
        manufacturerName = 'Unbekannter Hersteller'
    end

	if manufacturerName == 'Virgil' then
		manufacturerName = 'Virgil Ltd'
	end

    local setObj = {
        [ 'UUID' ]              = t.apiData.uuid        or nil,
        [ 'Name' ]              = t.apiData.name        or nil,
        [ 'Beschreibung' ]      = common.mapTranslation( t.apiData.description or nil ),
        [ 'Größe' ]             = t.apiData.size        or nil,
        [ 'Hersteller' ]        = manufacturerName,
        [ 'Set' ]               = CharArmor.getArmorSet( t.apiData.name ),
        [ 'Schadensreduktion' ] = t.apiData.damage_reduction or nil,
        [ 'SP' ]                = CharArmor.formatCarryingCapacity( t.apiData.carrying_capacity ),
        [ 'Rüstungstyp' ]       = common.mapTranslation( CharArmor.translateArmorType( t.apiData.armor_type or nil ) ),
        [ 'Rüstungsklasse' ]    = common.mapTranslation( CharArmor.translateClass( t.apiData.type or nil ) ),
        [ 'Temperaturresistenz' ] = {
            CharArmor.formatTemperature( t.apiData.resistances.temperature.min ),
            CharArmor.formatTemperature( t.apiData.resistances.temperature.max ),
        },
        [ 'Länge' ]        = common.formatNum( t.apiData.volume.length or nil, nil ),
        [ 'Breite' ]       = common.formatNum( t.apiData.volume.width or nil, nil ),
        [ 'Höhe' ]         = common.formatNum( t.apiData.volume.height or nil, nil ),
        [ 'Volumen' ]      = common.formatNum( t.apiData.volume.volume or nil, nil ),
        [ 'Spielversion' ] = t.apiData.version or nil,
    }

    if type( t.apiData.base_model ) == 'table' then
        setObj[ 'Ist Basisversion' ] = 0
        setObj[ 'Basisversion UUID' ] = t.apiData.base_model.uuid
        setObj[ 'Basisversion' ] = t.apiData.base_model.name
    else
    	setObj['Ist Basisversion'] = 1
    end

    if type( t.apiData.resistances.physical ) == 'table' then
        setObj['Resistenzmultiplikator Physisch'] = common.formatNum( t.apiData.resistances.physical.multiplier )
    end
    if type( t.apiData.resistances.energy ) == 'table' then
        setObj['Resistenzmultiplikator Energie'] = common.formatNum( t.apiData.resistances.energy.multiplier )
    end
    if type( t.apiData.resistances.distortion ) == 'table' then
        setObj['Resistenzmultiplikator Distortion'] = common.formatNum( t.apiData.resistances.distortion.multiplier )
    end
    if type( t.apiData.resistances.thermal ) == 'table' then
        setObj['Resistenzmultiplikator Thermisch'] = common.formatNum( t.apiData.resistances.thermal.multiplier )
    end
    if type( t.apiData.resistances.biochemical ) == 'table' then
        setObj['Resistenzmultiplikator Biochemisch'] = common.formatNum( t.apiData.resistances.biochemical.multiplier )
    end
    if type( t.apiData.resistances.stun ) == 'table' then
        setObj['Resistenzmultiplikator Betäubung'] = common.formatNum( t.apiData.resistances.stun.multiplier )
    end

    local result = mw.smw.set( setObj )

    if t.apiData.attachments ~= nil and type( t.apiData.attachments.data ) == 'table' then
        for _, attachment in pairs( t.apiData.attachments.data ) do
            if attachment.name ~= nil then
                mw.smw.subobject( {
                    [ 'Typ' ] = {
                        '[email protected]',
                        '[email protected]',
                    },
                    [ 'Halterungsname' ] = attachment.name,
                    [ 'Minimalgröße' ] = attachment.min_size or 0,
                    [ 'Maximalgröße' ] = attachment.max_size or 0,
                } )
            end
        end
    end

    local commodity = require( 'Module:Commodity' ):new()
    commodity:addShopData( t.apiData )

    return result
end

--- Entrypoint for {{#seo:}}
function methodtable.setSeoData( t )
    if t.currentFrame == nil then
        error( 'No frame set. Call "setFrame" first.', 0 )
    end

    local data = t:getSmwData()

    if nil == data.Hersteller then
        -- Faulty SMW data, don't call #seo
        return
    end

    require( 'Module:SEO' ).set(
        TNT.format( 'I18n/Module:CharArmor', 'seo_section' ),
        data.page,
        table.concat({
            data.Name,
            data.Hersteller,
            t.currentFrame:preprocess( '{{SITENAME}}' )
        }, ' - '),
        'replace',
        {
            data.Name,
            data.type or '',
            data.class or '',
            data.Hersteller,
            require( 'Module:Hersteller' ).getCodeFromName( data.Hersteller ),
            TNT.format( 'I18n/Module:CharArmor', 'seo_section' ),
        },
        nil,
        t.frameArgs[ 'image' ],
        data.page
    )
end

--- Creates the infobox
function methodtable.getInfoBox( t )
    local data = t:getSmwData()

	if type( data ) == 'string' then
		return string.format( '<p class="hatnote">%s</p>', data )
	end

    if nil == data.Hersteller then
        data.Hersteller = 'Unbekannter Hersteller'
    end

    table.insert( t.categories, '[[Category:' .. data.Hersteller .. '|' .. data.Name .. ' ]]' )

    if data.class ~= nil then
        table.insert( t.categories, string.format('[[Kategorie:%s|%s]]', data.class_de, data.Name ) )
    end

    if data.type ~= nil and data.type ~= 'Unbekannter Typ' then
        table.insert( t.categories, string.format('[[Kategorie:%s|%s]]', data.type_de, data.Name ) )
    end

    -- Set Title
    common.setDisplayTitle( t.currentFrame, data.Name )

    local box = require( 'Module:Infobox' ).create( {
        bodyClass = 'floatright',
        allowReplace = false,
        removeEmpty = true,
        emptyString = '-',
        placeholderImage = 'Platzhalter Rüstung.webp',
    } )

	local nameNormalized, _ = mw.ustring.gsub( data.Name, "[^%w-]", ' ' )
	nameNormalized, _ = mw.ustring.gsub( nameNormalized, "%s+", ' ' )

    box:addImage( common.getImage( {
        t.frameArgs[ 'image' ],
        nameNormalized .. '.jpg',
    } ), {
        [ 'alternativtext' ] = data.Name,
        'rahmenlos',
        '600px'
    } )

    local source = t.currentFrame:extensionTag{
        name = 'ref',
        content = TNT.format( 'I18n/Module:CharArmor', 'msg_version_info', data.version, '[https://github.com/StarCitizenWiki/scunpacked-data GitHub]' )
    }

    box:addTitle( data.Name )

    box:addRow(
        TNT.format( 'I18n/Module:CharArmor', 'lbl_manufacturer' ),
        '[[' .. data.Hersteller .. ']]' .. source,
        nil,
        'col2'
    )

    local tempMinRes, tempMaxRes = CharArmor.temperatureResistanceToText( data[ 'Temperaturresistenz' ] )

    local armorClass = data.class
    if armorClass ~= nil then
        armorClass = string.format('[[:Kategorie:%s|%s]]', data.class_de, armorClass)
    end

    local armorType = data.type
    if armorType ~= nil then
        armorType = string.format('[[:Kategorie:%s|%s]]', data.type_de, armorType)
    end

    local set = CharArmor.getArmorSet( data.Name )
    if set ~= nil then
        table.insert( t.categories, string.format('[[Kategorie:%s|%s]]', set, data.Name ) )
        set = mw.ustring.format( '[[:Kategorie:%s|%s]]', set, set )
    end

    box:addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_type' ), armorType or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_price' ), data.price or TNT.format( 'I18n/Module:CharArmor', 'txt_cant_buy' ), nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_carrying_capacity' ), data[ 'SP' ] or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_weight' ), data[ 'Volumen' ] or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_damage_reduction' ), data[ 'Schadensreduktion' ] or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_class' ), armorClass or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_set' ), set or '-', nil, 'col2' )


       :addHeader( TNT.format( 'I18n/Module:CharArmor', 'lbl_temperature_resistance' ) )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_temperature_resistance_min' ), tempMinRes or '-', nil, 'col2' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_temperature_resistance_max' ), tempMaxRes or '-', nil, 'col2' )


       :addHeader( TNT.format( 'I18n/Module:CharArmor', 'lbl_resistances' ) )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_physical' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Physisch' ] ), nil, 'col3' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_energy' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Physisch' ] ), nil, 'col3' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_distortion' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Distortion' ] ), nil, 'col3' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_thermal' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Thermisch' ] ), nil, 'col3' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_biochemical' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Biochemisch' ] ), nil, 'col3' )
       :addRow( TNT.format( 'I18n/Module:CharArmor', 'lbl_stun' ), CharArmor.resistanceToString( data[ 'Resistenzmultiplikator Betäubung' ] ), nil, 'col3' )

    if data.attachments ~= nil and type( data.attachments ) == 'table' and #data.attachments > 0 then
        local attachTable = makeArmorAttachmentTable( data.attachments )

        if #attachTable > 0 then
            box:addHeader( TNT.format( 'I18n/Module:CharArmor', 'lbl_attachments' ) )

            for _, attachment in ipairs( attachTable ) do
                local size = ''
                if attachment.min ~= 0 and attachment.max ~= 0 then
                    if attachment.min ~= attachment.max then
                        size = string.format( '(S%s - S%s)', attachment.min, attachment.max )
                    end
                elseif attachment.min ~= nil and attachment.min ~= 1 then
                    size = string.format( '(S%s)', attachment.min )
                end

                if attachment.name == 'Abwerfbar' then
                    attachment.name = 'Abwerfbar ' ..  mw.smw.info( 'Granaten, Knicklichter, etc.' )
                end

                box:addRow( attachment.name, string.format( '%dx %s', attachment.count, size ), nil, 'col3' )
            end
        end
    end

    if t.frameArgs ~= nil then
        box:addRowsFromArgs( t.frameArgs, '!' )
    end

    return tostring( box )
end

--- Set the frame and load args
--- @param frame table
function methodtable.setFrame( t, frame )
    t.currentFrame = frame
    t.frameArgs = require( 'Module:Arguments' ).getArgs( frame )
end

--- Get categories
function methodtable.getCategories( t )
    return tostring( table.concat( t.categories ) )
end

--- Save Api Data to SMW store
function methodtable.saveApiData( t )
    if t.currentFrame == nil then
        error( 'No frame set. Call "setFrame" first.', 0 )
    end

    local data = t:getApiDataForCurrentPage()

    t:setSemanticProperties()

    return data
end


--[[

Char Armor Methods

]]--

--- @param name string
--- @return string
function CharArmor.getArmorSet( name )
    for _, setName in ipairs( objectData.armorSets ) do
        if mw.ustring.find( name, setName, 1, true ) then
            return setName
        end
    end

    return nil
end

--- @param class string
--- @return table
function CharArmor.translateClass( class )
    if objectData.itemClassTranslations[ class ] == nil then
        return {
            de_DE = class,
            en_EN = class
        }
    end

    return objectData.itemClassTranslations[ class ]
end

--- @param type string
--- @return table
function CharArmor.translateArmorType( type )
    return {
        de_DE = objectData.armorTypeTranslations[ type ] or type,
        en_EN = type
    }
end

--- Calculates the resistance
--- @return number
function CharArmor.calculateResistance( resistance )
    if resistance == nil then
        return nil
    end

    return 100 * ( 1 - resistance )
end

--- Resistance to string
--- @return string
function CharArmor.resistanceToString( resistance )
    local res = CharArmor.calculateResistance( resistance )
    local span = mw.html.create( 'span' )

    if res == nil then
        return '-'
    end

    if res > 100 then
        -- Armor has negative resistance
        span:addClass( 'resistance-negative' )
    else
        span:addClass( 'resistance-positive' )
    end

    span:wikitext( res .. '%' )

    return tostring( span:allDone() )
end

--- Returns temperature resistances as strings containing °C
--- @param resTable table containing an arbitrary amount of temperatures
--- @return string, string
function CharArmor.temperatureResistanceToText( resTable )
    local tempMinRes = math.huge
    local tempMaxRes = -math.huge

    if resTable ~= nil and type( resTable ) == 'table' then
        for _, res in pairs( resTable ) do
            res = common.toNumber( res, 0 )

            if res < tempMinRes then
                tempMinRes = res
            end

            if res > tempMaxRes then
                tempMaxRes = res
            end
        end
    end

    if tempMinRes == math.huge then
        tempMinRes = '-'
    else
        tempMinRes = tempMinRes .. '°C'
    end

    if tempMaxRes == -math.huge then
        tempMaxRes = '-'
    else
        tempMaxRes = tempMaxRes .. '°C'
    end

    return tempMinRes, tempMaxRes
end

--- Translates an internal attachment name to text
--- @param name string The internal name
--- @return string
function CharArmor.translateArmorAttachment( name )
    if name == nil then
        return name
    end

    local matchAttachName = mw.ustring.match(name, "(%w+)_attach_%d")
    local matchStockedName = mw.ustring.match(name, "(%w+)_stocked_%d")

    if matchAttachName ~= nil then
        name = matchAttachName
    elseif matchStockedName ~= nil then
        name = matchStockedName
    end

    if objectData.armorAttachmentTranslations[ name ] ~= nil then
        if common.getLocaleForPage() == 'de' then
            return objectData.armorAttachmentTranslations[ name ].de_DE
        end

        return objectData.armorAttachmentTranslations[ name ].en_EN
    end

    return name
end

--- Format the carrying capacity
--- @param capacity string|number|nil
--- @return string
function CharArmor.formatCarryingCapacity( capacity )
    if capacity == nil then
        return nil
    end

    if type( capacity ) == 'string' then
        capacity = mw.ustring.gsub( capacity, 'K SP', '' )
        capacity = mw.ustring.gsub( capacity, 'K µSCU', '' )

        capacity = common.toNumber( capacity )
        if capacity ~= nil then
        	capacity = capacity * 1000
    	end
    end

    capacity = common.toNumber( capacity )
    if capacity == nil then
        return nil
    end

    return common.formatNum( capacity ) .. ' µSCU'
end

--- Format the emperature resistance
--- @param temperature string|number|nil
--- @return string
function CharArmor.formatTemperature( temperature )
    if temperature == nil then
        return nil
    end

    temperature = common.toNumber(temperature, false )
    if temperature == nil then
        return nil
    end

    return common.formatNum( temperature ) .. ' °C'
end

--- Template entry
function CharArmor.main( frame )
    local instance = CharArmor:new()
    instance:setFrame( frame )

	if not mw.title.getCurrentTitle().isSubpage then
	    if instance.frameArgs[ 'Manuell' ] ~= nil then
	        instance:addManual()
	    else
	        instance:saveApiData()
	    end
    end

    instance:setSeoData()

    return tostring( instance:getInfoBox() ) .. instance:getCategories()
end

--- New Instance
function CharArmor.new( self, name )
    local instance = {
        categories = {},
        frameArgs = {
            [ 'name' ] = name
        }
    }

    setmetatable( instance, metatable )

    return instance
end

return CharArmor
Cookies help us deliver our services. By using our services, you agree to our use of cookies.