Для документации этого модуля может быть создана страница Модуль:Stats/Penalties/doc

local p = {}
local Config = require('Module:Config')

function p.render(frame)
    local json_data = mw.text.jsonDecode(mw.title.new('Module:Data/GrandStats.json'):getContent())
    local data = json_data.StatsPages.Penalties
    local global_played = json_data.Players

    local function calc_pok(g, u) return g - (u - g) * 2 end
    local function bld_pok(pok) return {text = (pok > 0 and "+" .. pok or tostring(pok)), style = Config.styles.center .. " font-weight:bold;"} end
    local function format_pct(g, u) return u == 0 and "0" or string.gsub(string.format("%.2f", math.floor((g / u * 100) * 100 + 0.5) / 100), "%.", ",") end

    local function get_medal(p_name, metric, y_num)
        if not metric then return "" end
        local gp = global_played[p_name]
        if not gp or not gp.AS_compiled or not gp.AS_compiled.metrics[metric] then return "" end
        if y_num then
            local y_data = gp.AS_compiled.metrics[metric].years[y_num] or gp.AS_compiled.metrics[metric].years[tostring(y_num)]
            return y_data and y_data.color or ""
        else
            return gp.AS_compiled.metrics[metric].color or ""
        end
    end

    local function build_chrono(data_type)
        local list = {}
        for name, p in pairs(data.players) do if p[data_type].u > 0 then p.name = name; table.insert(list, {name = name, d = p[data_type]}) end end
        table.sort(list, function(a, b)
            local pokA, pokB = calc_pok(a.d.g, a.d.u), calc_pok(b.d.g, b.d.u)
            if pokA ~= pokB then return pokA > pokB end
            if a.d.g ~= b.d.g then return a.d.g > b.d.g end
            local sA, sB = a.d.g, b.d.g
            for i = #Config.years, 1, -1 do
                local y = Config.years[i]
                local a_y = a.d.years[y] or a.d.years[tostring(y)]
                local b_y = b.d.years[y] or b.d.years[tostring(y)]
                sA = sA - (a_y and a_y.g or 0); sB = sB - (b_y and b_y.g or 0)
                if sA ~= sB then return sA > sB end
            end
            return a.name < b.name
        end)

        local cols = Config.builder.merge({'Место', 'Игрок'}, Config.builder.years(Config.years), {'ВСЕГО', '%', '[[Лучший пенальтист#Система определения победителя|Показатель]]'})
        local tbl = Config.builder.start(cols)
        local col_u, col_g, tot_u, tot_g = {}, {}, 0, 0
        for _, y in ipairs(Config.years) do col_u[y]=0; col_g[y]=0 end

        local metric = (data_type == "all") and "pens_scored" or nil
        for rank, item in ipairs(list) do
            local r_data = { rank, {text = "[[" .. string.gsub(item.name, "_", " ") .. "]]", style = Config.styles.center_nowrap} }
            for _, y in ipairs(Config.years) do
                local y_node = item.d.years[y] or item.d.years[tostring(y)]
                local u = y_node and y_node.u or 0
                local g = y_node and y_node.g or 0
                col_u[y] = col_u[y] + u; col_g[y] = col_g[y] + g
                
                local y_medal = get_medal(item.name, metric, y)
                local cell_style = Config.styles.center_nowrap
                if y_medal ~= "" then cell_style = cell_style .. " " .. y_medal end

                if u > 0 then
                    table.insert(r_data, {text = u .. '/' .. g, style = cell_style})
                else
                    local gp = global_played[item.name]
                    local played = gp and gp.AS_compiled.played_years and (gp.AS_compiled.played_years[y] or gp.AS_compiled.played_years[tostring(y)])
                    if played then
                        table.insert(r_data, {text = "0/0", style = cell_style})
                    else
                        table.insert(r_data, "")
                    end
                end
            end
            tot_u = tot_u + item.d.u; tot_g = tot_g + item.d.g
            
            local tot_medal = get_medal(item.name, metric, nil)
            local tot_style = Config.styles.center .. " font-weight:bold;"
            if tot_medal ~= "" then tot_style = tot_style .. " " .. tot_medal end
            
            table.insert(r_data, {text = item.d.u .. '/' .. item.d.g, style = tot_style})
            table.insert(r_data, format_pct(item.d.g, item.d.u) .. '%')
            table.insert(r_data, bld_pok(calc_pok(item.d.g, item.d.u)))
            Config.builder.row(tbl, r_data)
        end

        local f_data = { "", {text="'''ВСЕГО'''", style = Config.styles.center} }
        for _, y in ipairs(Config.years) do table.insert(f_data, {text = col_u[y] > 0 and (col_u[y] .. '/' .. col_g[y]) or '0', style = Config.styles.center .. " font-weight:bold;"}) end
        table.insert(f_data, {text = tot_u .. '/' .. tot_g, style = Config.styles.center .. " font-weight:bold;"})
        table.insert(f_data, {text = format_pct(tot_g, tot_u) .. '%', style = Config.styles.center .. " font-weight:bold;"})
        table.insert(f_data, bld_pok(calc_pok(tot_g, tot_u)))
        Config.builder.row(tbl, f_data)
        return tostring(tbl)
    end

    local function build_results(data_type, spec_year)
        local list, t_u, t_g, t_k, t_o, t_w, t_p, t_c = {}, 0, 0, 0, 0, 0, 0, 0
        for name, p in pairs(data.players) do
            local d = spec_year and (p[data_type].years[spec_year] or p[data_type].years[tostring(spec_year)]) or p[data_type]
            if d and d.u > 0 then table.insert(list, {name = name, d = d, stvor = d.g + d.k, frame = d.g + d.k + d.p + d.c}) end
        end
        table.sort(list, function(a, b)
            local pA, pB = calc_pok(a.d.g, a.d.u), calc_pok(b.d.g, b.d.u)
            if pA ~= pB then return pA > pB end
            if a.d.g ~= b.d.g then return a.d.g > b.d.g end
            if not spec_year then
                local sA, sB = a.d.g, b.d.g
                for i = #Config.years, 1, -1 do
                    local y = Config.years[i]
                    local a_y = data.players[a.name][data_type].years[y] or data.players[a.name][data_type].years[tostring(y)]
                    local b_y = data.players[b.name][data_type].years[y] or data.players[b.name][data_type].years[tostring(y)]
                    sA = sA - (a_y and a_y.g or 0); sB = sB - (b_y and b_y.g or 0)
                    if sA ~= sB then return sA > sB end
                end
            end
            if a.stvor ~= b.stvor then return a.stvor > b.stvor end
            if a.frame ~= b.frame then return a.frame > b.frame end
            return a.name < b.name
        end)

        local tbl = Config.builder.start({'Место', 'Игрок', '%', '[[Лучший пенальтист#Система определения победителя|Показатель]]', 'Удары', 'Голы', 'вр.', 'в.', 'м.', 'шт.', 'п.'})
        for rank, item in ipairs(list) do
            local d = item.d
            t_u=t_u+d.u; t_g=t_g+d.g; t_k=t_k+d.k; t_o=t_o+d.o; t_w=t_w+d.w; t_p=t_p+d.p; t_c=t_c+d.c
            Config.builder.row(tbl, { rank, {text = "[[" .. string.gsub(item.name, "_", " ") .. "]]", style = Config.styles.center_nowrap}, format_pct(d.g, d.u), bld_pok(calc_pok(d.g, d.u)), {text=d.u, style=Config.styles.center.." font-weight:bold;"}, {text=d.g, style=Config.styles.center.." font-weight:bold;"}, d.k, d.o, d.w, d.p, d.c })
        end
        if t_u > 0 then
            Config.builder.row(tbl, { {text="'''ВСЕГО'''", colspan=2, style=Config.styles.center}, {text=format_pct(t_g, t_u), style=Config.styles.center.." font-weight:bold;"}, bld_pok(calc_pok(t_g, t_u)), {text=t_u, style=Config.styles.center.." font-weight:bold;"}, {text=t_g, style=Config.styles.center.." font-weight:bold;"}, {text=t_k, style=Config.styles.center.." font-weight:bold;"}, {text=t_o, style=Config.styles.center.." font-weight:bold;"}, {text=t_w, style=Config.styles.center.." font-weight:bold;"}, {text=t_p, style=Config.styles.center.." font-weight:bold;"}, {text=t_c, style=Config.styles.center.." font-weight:bold;"} })
        end
        return tostring(tbl)
    end

    local function build_tournaments(data_type)
        local tbl = Config.builder.start({'№', 'ЧТМ', '%', '[[Лучший пенальтист#Система определения победителя|Показатель]]', 'Удары', 'Голы', 'вр.', 'в.', 'м.', 'шт.', 'п.'})
        local tot_u, tot_g, tot_k, tot_o, tot_w, tot_p, tot_c, rank = 0,0,0,0,0,0,0,1
        for _, year in ipairs(Config.years) do
            local d = data.tournaments[year] or data.tournaments[tostring(year)]
            if d and d[data_type].u > 0 then
                local td = d[data_type]
                Config.builder.row(tbl, { rank, "[[" .. year .. "]]", format_pct(td.g, td.u), bld_pok(calc_pok(td.g, td.u)), {text=td.u, style=Config.styles.center.." font-weight:bold;"}, {text=td.g, style=Config.styles.center.." font-weight:bold;"}, td.k, td.o, td.w, td.p, td.c })
                rank = rank + 1; tot_u=tot_u+td.u; tot_g=tot_g+td.g; tot_k=tot_k+td.k; tot_o=tot_o+td.o; tot_w=tot_w+td.w; tot_p=tot_p+td.p; tot_c=tot_c+td.c
            end
        end
        Config.builder.row(tbl, { {text="'''ВСЕГО'''", colspan=2, style=Config.styles.center}, {text=format_pct(tot_g, tot_u), style=Config.styles.center.." font-weight:bold;"}, bld_pok(calc_pok(tot_g, tot_u)), {text=tot_u, style=Config.styles.center.." font-weight:bold;"}, {text=tot_g, style=Config.styles.center.." font-weight:bold;"}, {text=tot_k, style=Config.styles.center.." font-weight:bold;"}, {text=tot_o, style=Config.styles.center.." font-weight:bold;"}, {text=tot_w, style=Config.styles.center.." font-weight:bold;"}, {text=tot_p, style=Config.styles.center.." font-weight:bold;"}, {text=tot_c, style=Config.styles.center.." font-weight:bold;"} })
        return tostring(tbl)
    end

    local function build_simple(data_type, metric_key)
        local start_year = Config.years[1]
        if metric_key and Config.metrics[metric_key] and Config.metrics[metric_key].start then start_year = Config.metrics[metric_key].start end
        local valid_years = {}
        for _, y in ipairs(Config.years) do if y >= start_year then table.insert(valid_years, y) end end

        local list = {}
        for name, p in pairs(data.players) do if p[data_type].total > 0 then table.insert(list, {name = name, d = p[data_type]}) end end
        table.sort(list, function(a, b)
            if a.d.total ~= b.d.total then return a.d.total > b.d.total end
            local sA, sB = a.d.total, b.d.total
            for i = #valid_years, 1, -1 do
                local y = valid_years[i]
                sA = sA - (a.d.years[y] or a.d.years[tostring(y)] or 0)
                sB = sB - (b.d.years[y] or b.d.years[tostring(y)] or 0)
                if sA ~= sB then return sA > sB end
            end
            return a.name < b.name
        end)

        local cols = Config.builder.merge({'Место', 'Игрок'}, Config.builder.years(valid_years), {'ВСЕГО'})
        local tbl = Config.builder.start(cols)
        local col_totals = {}; for _, y in ipairs(valid_years) do col_totals[y] = 0 end
        local grand_total = 0
        
        for rank, item in ipairs(list) do
            local r_data = { rank, {text="[[" .. string.gsub(item.name, "_", " ") .. "]]", style=Config.styles.center_nowrap} }
            for _, y in ipairs(valid_years) do 
                local val = item.d.years[y] or item.d.years[tostring(y)] or 0
                
                local y_medal = get_medal(item.name, metric_key, y)
                local cell_style = Config.styles.center_nowrap
                if y_medal ~= "" then cell_style = cell_style .. " " .. y_medal end

                if val > 0 then 
                    table.insert(r_data, {text = tostring(val), style = cell_style})
                    col_totals[y] = col_totals[y] + val
                else
                    local gp = global_played[item.name]
                    local played = gp and gp.AS_compiled.played_years and (gp.AS_compiled.played_years[y] or gp.AS_compiled.played_years[tostring(y)])
                    if played then
                        table.insert(r_data, {text = "0", style = cell_style})
                    else
                        table.insert(r_data, "")
                    end
                end
            end
            
            local tot_medal = get_medal(item.name, metric_key, nil)
            local tot_style = Config.styles.center .. " font-weight:bold;"
            if tot_medal ~= "" then tot_style = tot_style .. " " .. tot_medal end
            
            table.insert(r_data, {text=tostring(item.d.total), style=tot_style}); grand_total = grand_total + item.d.total
            Config.builder.row(tbl, r_data)
        end

        if metric_key and Config.metrics[metric_key] and Config.metrics[metric_key].adjustments then
            for k, v in pairs(Config.metrics[metric_key].adjustments) do
                if k == "total" then grand_total = grand_total + v 
                else
                    local k_num = tonumber(k)
                    if k_num and col_totals[k_num] then col_totals[k_num] = col_totals[k_num] + v; grand_total = grand_total + v end
                end
            end
        end

        local footer_data = { "", {text="'''ВСЕГО'''", style=Config.styles.center} }
        for _, year in ipairs(valid_years) do table.insert(footer_data, {text="'''" .. tostring(col_totals[year]) .. "'''", style=Config.styles.center}) end
        table.insert(footer_data, {text="'''" .. tostring(grand_total) .. "'''", style=Config.styles.center})
        Config.builder.row(tbl, footer_data)
        return tostring(tbl)
    end

    local output = { Config.styles.wiki_templates }
    table.insert(output, "== Пробитые пенальти =="); table.insert(output, "=== Всего ===\n''(Всего ударов/Забитых ударов)''"); table.insert(output, build_chrono("all"))
    table.insert(output, "=== Только в игровое время ===\n''(Без учёта серий пенальти)''"); table.insert(output, build_chrono("ingame"))
    table.insert(output, "== Результаты ударов =="); table.insert(output, "=== Все пенальти ==="); table.insert(output, build_results("all"))
    for _, y in ipairs(Config.years) do 
        local t_data = data.tournaments[y] or data.tournaments[tostring(y)]
        if t_data and t_data.all.u > 0 then table.insert(output, "==== [[" .. y .. "]] ===="); table.insert(output, build_results("all", y)) end 
    end
    table.insert(output, "=== Только в игровое время ==="); table.insert(output, build_results("ingame"))
    for _, y in ipairs(Config.years) do 
        local t_data = data.tournaments[y] or data.tournaments[tostring(y)]
        if t_data and t_data.ingame.u > 0 then table.insert(output, "==== [[" .. y .. "]] ===="); table.insert(output, build_results("ingame", y)) end 
    end
    table.insert(output, "== Статистика по чемпионатам =="); table.insert(output, "=== Все пенальти ==="); table.insert(output, build_tournaments("all"))
    table.insert(output, "=== Только в игровое время ==="); table.insert(output, build_tournaments("ingame"))
    table.insert(output, "== Отбитые пенальти ==\n''Учитываются только сэйвы. Удары мимо и выше ворот, а также в каркас в данную статистику не входят.''"); table.insert(output, build_simple("saves", "pens_saved"))
    table.insert(output, "== Привезённые пенальти ==\n''Официально подсчитываются начиная с [[ЧТМ-2026]]. Данный термин означает фол в собственной штрафной, после которого был назначен пенальти.''"); table.insert(output, build_simple("fouls", "caused_pens"))

    return frame:preprocess(table.concat(output, "\n\n"))
end

return p