Модуль:Автоматическая статистика: различия между версиями

Материал из ЧТМ
Перейти к навигации Перейти к поиску
Восстановлена версия 69713 участника Дача Цанавы (Restorer)
Метка: отмена
Нет описания правки
 
(не показаны 4 промежуточные версии 2 участников)
Строка 1: Строка 1:
-- ==========================================
-- ==========================================
-- Модуль:Автоматическая статистика 6.0
-- Модуль:Автоматическая статистика
-- (Pure UI Render для PlayerStats)
-- (Pure UI Render для PlayerStats JSON)
-- ==========================================
-- ==========================================


Строка 132: Строка 132:
                     td:cssText(style .. Config.styles.header)  
                     td:cssText(style .. Config.styles.header)  
                 else
                 else
                     local m_data = compiled_data.metrics[row.id].years[year]
                     local m_data = compiled_data.metrics[row.id] and compiled_data.metrics[row.id].years[year] or { val=0, num=0, den=0, color="" }
                     if m_data.color ~= "" then style = style .. m_data.color end
                     if m_data.color and m_data.color ~= "" then style = style .. m_data.color end
                     td:cssText(style)
                     td:cssText(style)
                      
                      
Строка 154: Строка 154:
                 td_tot:cssText(style_tot):wikitext("—")  
                 td_tot:cssText(style_tot):wikitext("—")  
             else
             else
                 local m_tot = compiled_data.metrics[row.id]
                 local m_tot = compiled_data.metrics[row.id] or { total_val=0, total_num=0, total_den=0, color="" }
                 if m_tot.color and m_tot.color ~= "" then style_tot = style_tot .. m_tot.color end
                 if m_tot.color and m_tot.color ~= "" then style_tot = style_tot .. m_tot.color end
                 td_tot:cssText(style_tot)
                 td_tot:cssText(style_tot)
Строка 171: Строка 171:
     end
     end


     if show_matches and #compiled_data.player_matches_list > 0 then
     if show_matches and compiled_data.player_matches_list then
         local pml = compiled_data.player_matches_list
        -- 1. СНАЧАЛА копируем Read-Only прокси-массив в чистую Lua-таблицу
        table.sort(pml, function(a, b) return a.num_hist < b.num_hist end)
        -- ipairs работает с JSON-прокси безупречно
         local pml = {}
        for _, m in ipairs(compiled_data.player_matches_list) do
            table.insert(pml, m)  
        end
          
          
         local url_matches = "https://thirdworldcup.ru/index.php?title=%D0%9F%D0%BE%D0%B4%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0:%D0%A0%D0%B0%D0%B7%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D0%BE%D0%B2/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BC%D0%B0%D1%82%D1%87%D0%B5%D0%B9&action=edit"
         -- 2. ТЕПЕРЬ проверяем длину чистого массива pml, а не сломанного JSON-объекта
        local match_gear = " [[Файл:Gear icon.svg|12px|link=" .. url_matches .. "]] "
        if #pml > 0 then
       
            table.sort(pml, function(a, b) return a.num_hist < b.num_hist end)
        local t_list = mw.html.create('table'):addClass('wikitable mw-collapsible mw-collapsed')
           
        t_list:tag('tr'):tag('th'):attr('colspan', '9'):wikitext((compiled_data.played_before_2022 and "Полный список матчей начиная с ЧТМ-2022" or "Полный список матчей") .. match_gear)
            local url_matches = "https://thirdworldcup.ru/index.php?title=%D0%9F%D0%BE%D0%B4%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0:%D0%A0%D0%B0%D0%B7%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D0%BE%D0%B2/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BC%D0%B0%D1%82%D1%87%D0%B5%D0%B9&action=edit"
       
            local match_gear = " [[Файл:Gear icon.svg|12px|link=" .. url_matches .. "]] "
        local h_tr = t_list:tag('tr')
           
        local cols = {"№", "Настоящая<br>дата", "ЧТМ<br>(№ матча)", "Раунд", "Команда", "Соперник", "Счёт", "Статистика", "Дополнительно"}
            local list_classes = 'wikitable mw-collapsible'
        for _, col in ipairs(cols) do h_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header):wikitext(col) end
            if show_main then list_classes = list_classes .. ' mw-collapsed' end
           
            local t_list = mw.html.create('table'):addClass(list_classes)
            t_list:tag('tr'):tag('th'):attr('colspan', '9'):wikitext((compiled_data.played_before_2022 and "Полный список матчей начиная с ЧТМ-2022" or "Полный список матчей") .. match_gear)
           
            local h_tr = t_list:tag('tr')
            local cols = {"№", "Настоящая<br>дата", "ЧТМ<br>(№ матча)", "Раунд", "Команда", "Соперник", "Счёт", "Статистика", "Дополнительно"}
            for _, col in ipairs(cols) do h_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header):wikitext(col) end
 
            local counter = 1
            for _, m in ipairs(pml) do
                local tr = t_list:tag('tr')
                local bg_color = get_match_color(m)
                local style_td = "white-space:nowrap;text-align:center;" .. (bg_color ~= "" and ("background-color:" .. bg_color .. ";") or "")


        local counter = 1
                tr:tag('td'):cssText(style_td):wikitext(m.is_official and tostring(counter) or ""); if m.is_official then counter = counter + 1 end
        for _, m in ipairs(pml) do
                tr:tag('td'):cssText(style_td):wikitext(m.is_official and (m.date ~= "" and (m.date:sub(9,10).."."..m.date:sub(6,7).."."..m.date:sub(1,4)) or "") or "")
            local tr = t_list:tag('tr')
                tr:tag('td'):cssText(style_td):wikitext("[[" .. m.year .. "]] (" .. m.match_num_id .. ")")
            local bg_color = get_match_color(m)
                tr:tag('td'):cssText(style_td):wikitext(format_stage(m.stage, m.letter))
            local style_td = "white-space:nowrap;text-align:center;" .. (bg_color ~= "" and ("background-color:" .. bg_color .. ";") or "")


            tr:tag('td'):cssText(style_td):wikitext(m.is_official and tostring(counter) or ""); if m.is_official then counter = counter + 1 end
                local score_txt = format_score(m); if m.wikilink then score_txt = "'''[[" .. m.wikilink .. "|" .. score_txt .. "]]'''" end
            tr:tag('td'):cssText(style_td):wikitext(m.is_official and (m.date ~= "" and (m.date:sub(9,10).."."..m.date:sub(6,7).."."..m.date:sub(1,4)) or "") or "")
            tr:tag('td'):cssText(style_td):wikitext("[[" .. m.year .. "]] (" .. m.match_num_id .. ")")
            tr:tag('td'):cssText(style_td):wikitext(format_stage(m.stage, m.letter))


            local score_txt = format_score(m); if m.wikilink then score_txt = "'''[[" .. m.wikilink .. "|" .. score_txt .. "]]'''" end
                if m.role_team == 0 then
                    tr:tag('td'):attr('colspan', '3'):cssText(style_td):wikitext("Вратарь ([[" .. Teams.getName(m.team1, 'short') .. "]] - [[" .. Teams.getName(m.team2, 'short') .. "]] - " .. score_txt .. ")")
                else
                    tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team1 or m.team2, 'short') .. "]]")
                    tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team2 or m.team1, 'short') .. "]]")
                    tr:tag('td'):cssText(style_td):wikitext(score_txt)
                end


            if m.role_team == 0 then
                local s_arr = {}
                tr:tag('td'):attr('colspan', '3'):cssText(style_td):wikitext("Вратарь ([[" .. Teams.getName(m.team1, 'short') .. "]] - [[" .. Teams.getName(m.team2, 'short') .. "]] - " .. score_txt .. ")")
                if m.goals > 0 then table.insert(s_arr, "'''" .. (m.goals > 1 and m.goals or "") .. "Г'''") end
            else
                if m.assists > 0 then table.insert(s_arr, "'''" .. (m.assists > 1 and m.assists or "") .. "П'''") end
                 tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team1 or m.team2, 'short') .. "]]")
                if m.is_mvp then table.insert(s_arr, "'''<u>MVP</u>'''") end
                 tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team2 or m.team1, 'short') .. "]]")
                 tr:tag('td'):cssText(style_td):wikitext(table.concat(s_arr, ",&nbsp;"))
                tr:tag('td'):cssText(style_td):wikitext(score_txt)
               
                -- 3. РАСПАКОВЫВАЕМ m.events, чтобы корректно работали оператор длины (#) и table.concat
                local safe_events = {}
                if m.events then
                    for _, ev in ipairs(m.events) do table.insert(safe_events, ev) end
                end
               
                 tr:tag('td'):cssText(style_td):wikitext(#safe_events > 0 and ("<small>" .. table.concat(safe_events, "<br>") .. "</small>") or "<small></small>")
             end
             end
 
             html_list = tostring(t_list)
             local s_arr = {}
            if m.goals > 0 then table.insert(s_arr, "'''" .. (m.goals > 1 and m.goals or "") .. "Г'''") end
            if m.assists > 0 then table.insert(s_arr, "'''" .. (m.assists > 1 and m.assists or "") .. "П'''") end
            if m.is_mvp then table.insert(s_arr, "'''<u>MVP</u>'''") end
            tr:tag('td'):cssText(style_td):wikitext(table.concat(s_arr, ",&nbsp;"))
            tr:tag('td'):cssText(style_td):wikitext(#m.events > 0 and ("<small>" .. table.concat(m.events, "<br>") .. "</small>") or "<small></small>")
         end
         end
        html_list = tostring(t_list)
     end
     end


Строка 220: Строка 239:
          
          
         local show_nav = {
         local show_nav = {
             champs = (N.champ_counts[player_name] or 0) > 0, superchamps = false, zshar = false, zbashmak = false, elnur = false,
             champs = (N.champ_counts and N.champ_counts[player_name] or 0) > 0, superchamps = false, zshar = false, zbashmak = false, elnur = false,
             final_goals = (N.final_goals[player_name] or 0) > 0, final_assists = (N.final_assists[player_name] or 0) > 0,
             final_goals = (N.final_goals and N.final_goals[player_name] or 0) > 0, final_assists = (N.final_assists and N.final_assists[player_name] or 0) > 0,
             club100 = (N.global_goals[player_name] or 0) >= 100, loyalty = false,
             club100 = (N.global_goals and N.global_goals[player_name] or 0) >= 100, loyalty = false,
             org = (player_name == "Диман" or player_name == "Антон" or player_name == "Геныч")
             org = (player_name == "Диман" or player_name == "Антон" or player_name == "Геныч")
         }
         }
Строка 231: Строка 250:
             local icon_td = args.noIcon and '<td style="vertical-align:middle; text-align:left; padding:0; display:none;"></td>'  
             local icon_td = args.noIcon and '<td style="vertical-align:middle; text-align:left; padding:0; display:none;"></td>'  
                 or string.format('<td style="vertical-align:middle; text-align:left; padding:0; width:%s;">%s</td>', args.iconW or '30px', args.icon)
                 or string.format('<td style="vertical-align:middle; text-align:left; padding:0; width:%s;">%s</td>', args.iconW or '30px', args.icon)
           
             local full_edit = mw.uri.encode("Шаблон:" .. args.editName, "PATH")
             local full_edit = mw.uri.encode("Шаблон:" .. args.editName, "PATH")
             local url_edit = "https://thirdworldcup.ru/index.php?title=" .. full_edit .. "&action=edit"
             local url_edit = "https://thirdworldcup.ru/index.php?title=" .. full_edit .. "&action=edit"
             local url_view = "https://thirdworldcup.ru/index.php/" .. full_edit
             local url_view = "https://thirdworldcup.ru/index.php/" .. full_edit
           
             local gear_img = "[[Файл:Gear icon.svg|12px|link=" .. url_view .. "]]"
             local gear_img = "[[Файл:Gear icon.svg|12px|link=" .. url_view .. "]]"
             local edit_link = "[" .. url_edit .. ' <span title="Редактировать">&nbsp;' .. gear_img .. '&nbsp;</span>]'
             local edit_link = "[" .. url_edit .. ' <span title="Редактировать">&nbsp;' .. gear_img .. '&nbsp;</span>]'
Строка 257: Строка 274:
             for y, _ in pairs(N.champs) do table.insert(v_years, y) end
             for y, _ in pairs(N.champs) do table.insert(v_years, y) end
             table.sort(v_years)
             table.sort(v_years)
           
             local running_counts = {}
             local running_counts = {}
             for i, y in ipairs(v_years) do
             for i, y in ipairs(v_years) do
                 local p_list = N.champs[y]; table.sort(p_list)
                -- КОПИРУЕМ СПИСОК ПЕРЕД СОРТИРОВКОЙ
                 local p_list = {}
                for _, p_n in ipairs(N.champs[y]) do table.insert(p_list, p_n) end
                table.sort(p_list)
               
                 local fps = {}
                 local fps = {}
                 for _, p_name in ipairs(p_list) do
                 for _, p_name in ipairs(p_list) do
Строка 279: Строка 299:
         if show_nav.superchamps then add_nav({bg='DarkGray', icon='[[Файл:Суперчемпион.png|30px|]]', editName='Суперчемпионы', title='[[Суперчемпион]]ы', cat='[[Категория:Суперчемпионы]]', content=flat_chron(N.superchamps)}) end
         if show_nav.superchamps then add_nav({bg='DarkGray', icon='[[Файл:Суперчемпион.png|30px|]]', editName='Суперчемпионы', title='[[Суперчемпион]]ы', cat='[[Категория:Суперчемпионы]]', content=flat_chron(N.superchamps)}) end
         if show_nav.zshar then add_nav({bg='PaleGreen', icon='[[Файл:ЗШар.png|30px|]]', editName='Лучшие игроки ЧТМ', title='[[Золотой Шар|Лучшие игроки ЧТМ]]', cat='[[Категория:Обладатели Золотого Шара]]', content=flat_chron(N.zshar)}) end
         if show_nav.zshar then add_nav({bg='PaleGreen', icon='[[Файл:ЗШар.png|30px|]]', editName='Лучшие игроки ЧТМ', title='[[Золотой Шар|Лучшие игроки ЧТМ]]', cat='[[Категория:Обладатели Золотого Шара]]', content=flat_chron(N.zshar)}) end
         if show_nav.zbashmak then
         if show_nav.zbashmak then
             local grp, sy, parts = {}, {}, {}
             local grp, sy, parts = {}, {}, {}
Строка 312: Строка 331:


         if show_nav.loyalty then
         if show_nav.loyalty then
             table.sort(N.loyalty, function(a,b) if a[1] ~= b[1] then return a[1] < b[1] end return a[2] < b[2] end); local pts = {}
            -- КОПИРУЕМ МАССИВ ПЕРЕД СОРТИРОВКОЙ
             for _, i in ipairs(N.loyalty) do table.insert(pts, "[["..i[1].."]] — [["..i[2].."]] (''[["..(Teams.getName(i[3], 'short') or i[3]).."]]'')") end
            local loy = {}
            for _, i in ipairs(N.loyalty) do table.insert(loy, i) end
             table.sort(loy, function(a,b) if a[1] ~= b[1] then return a[1] < b[1] end return a[2] < b[2] end)
           
            local pts = {}
             for _, i in ipairs(loy) do table.insert(pts, "[["..i[1].."]] — [["..i[2].."]] (''[["..(Teams.getName(i[3], 'short') or i[3]).."]]'')") end
             add_nav({bg='MediumAquamarine', noIcon=true, editName='Верность команде', title='Обладатели приза «[[Верность команде]]»', cat='[[Категория:Обладатели приза «Верность команде»]]', content='<span style="font-size:120%;">'..table.concat(pts, "&nbsp;&bull; ")..'</span>'})
             add_nav({bg='MediumAquamarine', noIcon=true, editName='Верность команде', title='Обладатели приза «[[Верность команде]]»', cat='[[Категория:Обладатели приза «Верность команде»]]', content='<span style="font-size:120%;">'..table.concat(pts, "&nbsp;&bull; ")..'</span>'})
         end
         end
Строка 347: Строка 371:


     local out_str = Config.styles.wiki_templates
     local out_str = Config.styles.wiki_templates
     if show_main then out_str = out_str .. html_main .. "\n" end
   
     if show_main then out_str = out_str .. "== Статистика ==\n" .. html_main .. "\n" end
      
      
     if show_matches then
     if show_matches then
        if not show_main then out_str = out_str .. "== Список матчей ==\n" end
         out_str = out_str .. html_list .. "\n"
         out_str = out_str .. html_list .. "\n"
     else
     else

Текущая версия от 03:17, 5 июня 2026

Документация Документация

Модуль для шаблона {{Автоматическая статистика/разработка}}.

Подгружает на страницу игрока таблицу со статистикой, полный список матчей начиная с ЧТМ-2022 и навигационные шаблоны. Вызов:

{{#invoke:Автоматическая статистика|render|Диман|СГ=1|Год=2020|Качество=ИС|sortable=no}}

Для тяжёлых статей рекомендуется выносить список матчей на отдельную подстраницу.

Есть функция медалек, но она слишком тяжёлая, загружается гораздо медленнее. Просьба не использовать на страницах основного пространства, а только в тестовом режиме:

{{#invoke:Автоматическая статистика|render|Диман|СГ=1|Год=2020|Качество=ИС|sortable=no|medals=yes}}

Список передаваемых параметров

{{#invoke:Автоматическая статистика|render<!-- Запуск модуля
-->|Диман<!-- Имя игрока, обязательный параметр
-->|СГ=1<!-- Если статья заняла какое-то место в голосовании за статью года, то оно указывается
-->|Год=2020<!-- Год, когда происходило голосование
-->|Качество=ИС<!-- Избранная (ИС) или хорошая (ХС) статья
-->|sortable=no<!-- Отключает сортировку таблицы статистики, делая её более компактной (по умолчанию — no)
-->|medals=no<!-- Отключает медали в таблице статистики, значительно снижая нагрузку на сервер (по умолчанию — no)
-->|main_table=no<!-- Отключает расчёт и вывод таблицы статистики (по умолчанию — yes)
-->|matches_list=no<!-- Отключает расчёт и вывод полного списка матчей (по умолчанию — yes)
-->|matches_link=<!-- В случае отключения списка матчей необходимо задать ссылку на подстраницу.
-->|navboxes=no<!-- Отключает расчёт и вывод навигационных шаблонов (по умолчанию — yes)
-->}}
Пожалуйста, добавляйте категории на страницу документации.

-- ==========================================
-- Модуль:Автоматическая статистика
-- (Pure UI Render для PlayerStats JSON)
-- ==========================================

local p = {}

local Config = require('Module:Config')
local Teams = require('Module:Data/Teams')

p.row_defs = {
    { id = "matches", title = "[[" .. "Матчи|<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Матчи</abbr>" .. "]]", era = 2022 },
    { id = "field_matches", title = "<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Матчи в поле</abbr>", era = 2022 },
    { id = "goals", title = "[[" .. "Список всех авторов голов на ЧТМ|Голы" .. "]]", era = 2006 },
    { id = "assists", title = "[[" .. "Передачи|<abbr title=\"Подсчитываются начиная с ЧТМ-2026\">Передачи</abbr>" .. "]]", era = 2026 },
    { id = "avg_goals", title = "[[" .. "Средняя результативность|<abbr title=\"Подсчитывается начиная с ЧТМ-2022\">Ср. результативность</abbr>" .. "]]", era = 2022, is_avg = true, num = "goals", den = "field_matches" },
    { id = "avg_assists", title = "<abbr title=\"Подсчитывается начиная с ЧТМ-2026\">Передачи (ср. за матч)</abbr>", era = 2026, is_avg = true, num = "assists", den = "field_matches" },
    { id = "mvp", title = "[[" .. "Игрок матча" .. "]]", era = 2006 },
    { id = "matchday_prizes", title = "[[" .. "Призы игровых дней|<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Призы игровых дней</abbr>" .. "]]", era = 2022 },
    { id = "matchday_places", title = "[[" .. "Призы игровых дней#Призовые места игровых дней|<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Призовые места игр. дней</abbr>" .. "]]", era = 2022 },
    { id = "plus_minus", title = "[[" .. "Показатель полезности|<abbr title=\"Подсчитывается начиная с ЧТМ-2022\">Показатель полезности</abbr>" .. "]]", era = 2022, is_pm = true },
    { id = "mega_tricks", title = "[[" .. "Мега-трики" .. "]]", era = 2006 },
    { id = "assist_mega_tricks", title = "[[" .. "Мега-трики голевых передач|<abbr title=\"Подсчитываются начиная с ЧТМ-2026\">Мега-трики голевых передач</abbr>" .. "]]", era = 2026 },
    { id = "head_goals", title = "[[" .. "Голы головой|<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Голы головой</abbr>" .. "]]", era = 2022 },
    { id = "heel_goals", title = "[[" .. "Голы пяточкой|<abbr title=\"Подсчитываются начиная с ЧТМ-2026\">Голы пяточкой</abbr>" .. "]]", era = 2026 },
    { id = "free_kick_goals", title = "[[" .. "Голы со штрафных|<abbr title=\"Подсчитываются начиная с ЧТМ-2026\">Голы со штрафных</abbr>" .. "]]", era = 2026 },
    { id = "clearances", title = "[[" .. "Выносы из пустых|<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">Выносы из пустых</abbr>" .. "]]", era = 2022 },
    { id = "pens_scored", title = "[[" .. "Пенальти|Забитые пенальти" .. "]]", era = 2006 },
    { id = "pens_missed", title = "Незабитые пенальти", era = 2006 },
    { id = "pens_saved", title = "[[" .. "Список пенальти ЧТМ#Отбитые пенальти|Отбитые пенальти" .. "]]", era = 2006 },
    { id = "caused_pens", title = "[[" .. "Привезённые пенальти|<abbr title=\"Подсчитываются начиная с ЧТМ-2026\">Привезённые пенальти</abbr>" .. "]]", era = 2026 },
    { id = "mvp_goalie", title = "[[" .. "Игрок матча как вратарь" .. "]]", era = 2006 },
    { id = "goalie_goals", title = "<abbr title=\"Подсчитываются начиная с ЧТМ-2022\">[[" .. "Вратарские голы" .. "]]</abbr>", era = 2022 },
    { id = "clean_sheets", title = "[[" .. "Матчи на ноль" .. "]]", era = 2006 },
    { id = "yellow_cards", title = "<abbr title=\"Подсчитываются начиная с ЧТМ-2038\">[[" .. "Карточки|Жёлтые карточки" .. "]]</abbr>", era = 2038 },
    { id = "red_cards", title = "<abbr title=\"Подсчитываются начиная с ЧТМ-2038\">[[" .. "Карточки|Красные карточки" .. "]]</abbr>", era = 2038 },
    { id = "own_goals", title = "Автоголы", era = 2006 }
}

local function format_stage(st, let)
    if st == "Группа" then return "гр. " .. (let or "")
    elseif st == "Полуфинал" then return "1/2"
    elseif st == "За 3-е место" then return "за 3-е"
    elseif st == "Финал" then return "финал"
    end
    return st or ""
end

local function format_score(m)
    local s1, s2 = m.score1 or "0", m.score2 or "0"
    local sh1, sh2 = m.shootout1, m.shootout2
    if m.role_team == 2 then
        s1, s2 = m.score2 or "0", m.score1 or "0"
        sh1, sh2 = m.shootout2, m.shootout1
    end
    local txt = s1 .. ":" .. s2
    if m.aet then txt = txt .. "(ET)" end
    if sh1 and sh2 then txt = txt .. "(пен." .. sh1 .. ":" .. sh2 .. ")" end
    return txt
end

local function get_match_color(m)
    if not m.is_official then return "" end
    if m.role_team == 0 then return "lightgrey" end
    local is_t1 = (m.role_team == 1)
    local s1, s2 = tonumber(m.score1) or 0, tonumber(m.score2) or 0
    local win, loss, draw = false, false, false
    
    if m.shootout1 then
        draw = true
        if m.stage == "Финал" or m.stage == "финал" then
            local p1, p2 = tonumber(m.shootout1) or 0, tonumber(m.shootout2) or 0
            if (is_t1 and p1 > p2) or (not is_t1 and p2 > p1) then return "gold" else return "lightsalmon" end
        end
        return "palegoldenrod"
    else
        if s1 > s2 then win = is_t1; loss = not is_t1
        elseif s2 > s1 then win = not is_t1; loss = is_t1
        else draw = true end
        
        if m.stage == "Финал" or m.stage == "финал" then
            if win then return "gold" end
            if loss then return "lightsalmon" end
            return "palegoldenrod"
        end
        
        if draw then return "palegoldenrod" end
        if win then return "lightgreen" end
        return "lightsalmon"
    end
end

function p.build_ui(player_name, args, compiled_data)
    local sg_place = tonumber(args['СГ'] or args[2])
    local quality = args['Качество'] or args[3]
    local sg_year = args['Год'] or args['год'] or args['СГ_год'] or args[4] or "YYYY"

    local sortable = (args.sortable ~= "no")
    local show_main = (args.main_table ~= "no" and args.main_table ~= "0")
    local show_matches = (args.matches_list ~= "no" and args.matches_list ~= "0")
    local show_navboxes = (args.navboxes ~= "no" and args.navboxes ~= "0")
    local matches_link = args.matches_link

    local html_main = ""
    local html_list = ""
    local navs = {}

    if show_main then
        local classes = Config.styles.classes or "article-table ts-stickytableheader"
        if sortable then classes = classes .. " sortable" else classes = string.gsub(classes, "sortable", "") end
        local t_main = mw.html.create('table'):addClass(classes):attr('border', Config.styles.border or "1"):attr('cellspacing', Config.styles.cellspacing or "1"):attr('cellpadding', Config.styles.cellpadding or "1")

        local header_tr = t_main:tag('tr')
        header_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header)
        for _, year in ipairs(Config.years) do
            local th = header_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header)
            local text = "[[" .. year .. "|'" .. string.sub(tostring(year), 3, 4) .. "]]"
            if not compiled_data.played_years[year] then text = "<s>" .. text .. "</s>" end
            th:wikitext(text)
        end
        header_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header):wikitext("ВСЕГО")

        for _, row in ipairs(p.row_defs) do
            local r_tr = t_main:tag('tr')
            r_tr:tag('td'):cssText("white-space:nowrap;text-align:center;"):wikitext(row.title)

            for _, year in ipairs(Config.years) do
                local td = r_tr:tag('td')
                local style = "white-space:nowrap;text-align:center;"
                
                if year < row.era then 
                    td:cssText(style .. Config.styles.header) 
                else
                    local m_data = compiled_data.metrics[row.id] and compiled_data.metrics[row.id].years[year] or { val=0, num=0, den=0, color="" }
                    if m_data.color and m_data.color ~= "" then style = style .. m_data.color end
                    td:cssText(style)
                    
                    if compiled_data.played_years[year] then
                        local val_str = ""
                        if row.is_avg then 
                            val_str = (m_data.den > 0) and Config.utils.format_avg(m_data.num, m_data.den) or "0,00"
                        else 
                            val_str = tostring(m_data.val)
                            if row.is_pm and m_data.val > 0 then val_str = "+" .. val_str end 
                        end
                        td:wikitext(val_str)
                    end
                end
            end

            local td_tot = r_tr:tag('td')
            local style_tot = "white-space:nowrap;text-align:center;"
            if row.era > compiled_data.last_played_year then 
                td_tot:cssText(style_tot):wikitext("—") 
            else
                local m_tot = compiled_data.metrics[row.id] or { total_val=0, total_num=0, total_den=0, color="" }
                if m_tot.color and m_tot.color ~= "" then style_tot = style_tot .. m_tot.color end
                td_tot:cssText(style_tot)
                
                local tot_str = ""
                if row.is_avg then 
                    tot_str = (m_tot.total_den > 0) and Config.utils.format_avg(m_tot.total_num, m_tot.total_den) or "0,00"
                else 
                    tot_str = tostring(m_tot.total_val)
                    if row.is_pm and m_tot.total_val > 0 then tot_str = "+" .. tot_str end 
                end
                td_tot:wikitext("'''" .. tot_str .. "'''")
            end
        end
        html_main = tostring(t_main)
    end

    if show_matches and compiled_data.player_matches_list then
        -- 1. СНАЧАЛА копируем Read-Only прокси-массив в чистую Lua-таблицу
        -- ipairs работает с JSON-прокси безупречно
        local pml = {}
        for _, m in ipairs(compiled_data.player_matches_list) do 
            table.insert(pml, m) 
        end
        
        -- 2. ТЕПЕРЬ проверяем длину чистого массива pml, а не сломанного JSON-объекта
        if #pml > 0 then
            table.sort(pml, function(a, b) return a.num_hist < b.num_hist end)
            
            local url_matches = "https://thirdworldcup.ru/index.php?title=%D0%9F%D0%BE%D0%B4%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0:%D0%A0%D0%B0%D0%B7%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D0%BE%D0%B2/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BC%D0%B0%D1%82%D1%87%D0%B5%D0%B9&action=edit"
            local match_gear = " [[Файл:Gear icon.svg|12px|link=" .. url_matches .. "]] "
            
            local list_classes = 'wikitable mw-collapsible'
            if show_main then list_classes = list_classes .. ' mw-collapsed' end
            
            local t_list = mw.html.create('table'):addClass(list_classes)
            t_list:tag('tr'):tag('th'):attr('colspan', '9'):wikitext((compiled_data.played_before_2022 and "Полный список матчей начиная с ЧТМ-2022" or "Полный список матчей") .. match_gear)
            
            local h_tr = t_list:tag('tr')
            local cols = {"№", "Настоящая<br>дата", "ЧТМ<br>(№ матча)", "Раунд", "Команда", "Соперник", "Счёт", "Статистика", "Дополнительно"}
            for _, col in ipairs(cols) do h_tr:tag('th'):attr('scope', 'col'):cssText(Config.styles.header):wikitext(col) end

            local counter = 1
            for _, m in ipairs(pml) do
                local tr = t_list:tag('tr')
                local bg_color = get_match_color(m)
                local style_td = "white-space:nowrap;text-align:center;" .. (bg_color ~= "" and ("background-color:" .. bg_color .. ";") or "")

                tr:tag('td'):cssText(style_td):wikitext(m.is_official and tostring(counter) or ""); if m.is_official then counter = counter + 1 end
                tr:tag('td'):cssText(style_td):wikitext(m.is_official and (m.date ~= "" and (m.date:sub(9,10).."."..m.date:sub(6,7).."."..m.date:sub(1,4)) or "") or "")
                tr:tag('td'):cssText(style_td):wikitext("[[" .. m.year .. "]] (" .. m.match_num_id .. ")")
                tr:tag('td'):cssText(style_td):wikitext(format_stage(m.stage, m.letter))

                local score_txt = format_score(m); if m.wikilink then score_txt = "'''[[" .. m.wikilink .. "|" .. score_txt .. "]]'''" end

                if m.role_team == 0 then
                    tr:tag('td'):attr('colspan', '3'):cssText(style_td):wikitext("Вратарь ([[" .. Teams.getName(m.team1, 'short') .. "]] - [[" .. Teams.getName(m.team2, 'short') .. "]] - " .. score_txt .. ")")
                else
                    tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team1 or m.team2, 'short') .. "]]")
                    tr:tag('td'):cssText(style_td):wikitext("[[" .. Teams.getName((m.role_team == 1) and m.team2 or m.team1, 'short') .. "]]")
                    tr:tag('td'):cssText(style_td):wikitext(score_txt)
                end

                local s_arr = {}
                if m.goals > 0 then table.insert(s_arr, "'''" .. (m.goals > 1 and m.goals or "") .. "Г'''") end
                if m.assists > 0 then table.insert(s_arr, "'''" .. (m.assists > 1 and m.assists or "") .. "П'''") end
                if m.is_mvp then table.insert(s_arr, "'''<u>MVP</u>'''") end
                tr:tag('td'):cssText(style_td):wikitext(table.concat(s_arr, ",&nbsp;"))
                
                -- 3. РАСПАКОВЫВАЕМ m.events, чтобы корректно работали оператор длины (#) и table.concat
                local safe_events = {}
                if m.events then
                    for _, ev in ipairs(m.events) do table.insert(safe_events, ev) end
                end
                
                tr:tag('td'):cssText(style_td):wikitext(#safe_events > 0 and ("<small>" .. table.concat(safe_events, "<br>") .. "</small>") or "<small></small>")
            end
            html_list = tostring(t_list)
        end
    end

    if show_navboxes then
        local N = compiled_data.navbox_data
        
        local show_nav = {
            champs = (N.champ_counts and N.champ_counts[player_name] or 0) > 0, superchamps = false, zshar = false, zbashmak = false, elnur = false,
            final_goals = (N.final_goals and N.final_goals[player_name] or 0) > 0, final_assists = (N.final_assists and N.final_assists[player_name] or 0) > 0,
            club100 = (N.global_goals and N.global_goals[player_name] or 0) >= 100, loyalty = false,
            org = (player_name == "Диман" or player_name == "Антон" or player_name == "Геныч")
        }
        local function has_p(lst) for _, i in ipairs(lst) do if i[2] == player_name then return true end end return false end
        show_nav.superchamps = has_p(N.superchamps); show_nav.zshar = has_p(N.zshar); show_nav.zbashmak = has_p(N.zbashmak); show_nav.elnur = has_p(N.elnur); show_nav.loyalty = has_p(N.loyalty)

        local function add_nav(args)
            local icon_td = args.noIcon and '<td style="vertical-align:middle; text-align:left; padding:0; display:none;"></td>' 
                or string.format('<td style="vertical-align:middle; text-align:left; padding:0; width:%s;">%s</td>', args.iconW or '30px', args.icon)
            local full_edit = mw.uri.encode("Шаблон:" .. args.editName, "PATH")
            local url_edit = "https://thirdworldcup.ru/index.php?title=" .. full_edit .. "&action=edit"
            local url_view = "https://thirdworldcup.ru/index.php/" .. full_edit
            local gear_img = "[[Файл:Gear icon.svg|12px|link=" .. url_view .. "]]"
            local edit_link = "[" .. url_edit .. ' <span title="Редактировать">&nbsp;' .. gear_img .. '&nbsp;</span>]'

            local html = string.format([[
<table style="clear:both; font-size:8pt; width:100%%; border-spacing:0; margin: 1px auto 1px auto;  background:%s;" data-collapsetext="скрыть" data-expandtext="показать" class="toccolours mw-collapsible mw-collapsed ">
     <tr style="color: #000000; background: %s; ">
      %s
      <th style="width:100%%; font-weight:bold; vertical-align:middle; text-align:center;"><div style="float:right; font-size:small; font-weight:normal;" class="plainlinks">%s</div><span style="font-size:140%%;">%s</span></th></tr>
     <tr>
      <td colspan="2" style="vertical-align:middle; text-align:center; padding:0;">
       <table style="width:100%%; background-color:transparent ">%s</table>
      </td>
     </tr>
</table>%s]], args.bg, args.bg, icon_td, edit_link, args.title, args.content, args.cat)
            table.insert(navs, html)
        end

        if show_nav.champs then
            local rows, v_years = {}, {}
            for y, _ in pairs(N.champs) do table.insert(v_years, y) end
            table.sort(v_years)
            local running_counts = {}
            for i, y in ipairs(v_years) do
                -- КОПИРУЕМ СПИСОК ПЕРЕД СОРТИРОВКОЙ
                local p_list = {}
                for _, p_n in ipairs(N.champs[y]) do table.insert(p_list, p_n) end
                table.sort(p_list)
                
                local fps = {}
                for _, p_name in ipairs(p_list) do
                    running_counts[p_name] = (running_counts[p_name] or 0) + 1
                    table.insert(fps, "[["..p_name.."]]" .. (running_counts[p_name] > 1 and " ("..running_counts[p_name]..")" or "")) 
                end
                table.insert(rows, string.format('<tr><td style="text-align:right; width:15%%; padding:0 0.5em; font-weight:bold; color: black; background: LightSteelBlue; ">[[Финал ЧТМ-%d|%d]]</td>\n<td style="text-align:left; padding:0 0.5em;   "><span style="font-size:120%%;">%s%s</span></td></tr>', y, y, table.concat(fps, "&nbsp;&bull; "), (i == #v_years and "" or "\n----")))
            end
            add_nav({bg='LightSteelBlue', icon='[[Файл:Star.png|30px|]]', editName='Игроки-чемпионы третьего мира', title='[[Список игроков-чемпионов третьего мира|Игроки-чемпионы третьего мира]]', cat='[[Категория:Игроки-чемпионы]]', content=table.concat(rows)})
        end

        local function flat_chron(list)
            local parts = {}
            for _, i in ipairs(list) do table.insert(parts, "[["..i[2].."]]&nbsp;([["..i[1].."]])") end
            return '<span style="font-size:120%;">' .. table.concat(parts, "&nbsp;&bull; ") .. '</span>'
        end

        if show_nav.superchamps then add_nav({bg='DarkGray', icon='[[Файл:Суперчемпион.png|30px|]]', editName='Суперчемпионы', title='[[Суперчемпион]]ы', cat='[[Категория:Суперчемпионы]]', content=flat_chron(N.superchamps)}) end
        if show_nav.zshar then add_nav({bg='PaleGreen', icon='[[Файл:ЗШар.png|30px|]]', editName='Лучшие игроки ЧТМ', title='[[Золотой Шар|Лучшие игроки ЧТМ]]', cat='[[Категория:Обладатели Золотого Шара]]', content=flat_chron(N.zshar)}) end
        if show_nav.zbashmak then
            local grp, sy, parts = {}, {}, {}
            for _, i in ipairs(N.zbashmak) do if not grp[i[1]] then grp[i[1]]={g=i[3], p={}} end; table.insert(grp[i[1]].p, {i[2], i[4]}) end
            for y in pairs(grp) do table.insert(sy, y) end; table.sort(sy)
            for _, y in ipairs(sy) do
                table.sort(grp[y].p, function(a,b) return a[1] < b[1] end); local ps = {}
                for _, pi in ipairs(grp[y].p) do table.insert(ps, "[["..pi[1].."]] ([["..y.."]], [["..(Teams.getName(pi[2], 'short') or pi[2]).."]])") end
                table.insert(parts, table.concat(ps, "; ") .. "&nbsp;—&nbsp;" .. grp[y].g)
            end
            add_nav({bg='palegoldenrod', icon='[[Файл:Золотой Башмак.png|42px|]]', iconW='42px', editName='Лучшие бомбардиры ЧТМ', title='[[Золотой Башмак|Лучшие бомбардиры ЧТМ]]', cat='[[Категория:Обладатели Золотого Башмака]]', content='<span style="font-size:120%;">' .. table.concat(parts, "&nbsp;&bull; ") .. '</span>'})
        end

        if show_nav.elnur then add_nav({bg='Gainsboro', icon='[[Файл:Приз имени Эльнура.png|16px|]]', iconW='16px', editName='Лучшие вратари ЧТМ', title='[[Приз имени Эльнура|Лучшие вратари ЧТМ]]', cat='[[Категория:Обладатели Приза имени Эльнура]]', content=flat_chron(N.elnur)}) end

        local function flat_actions(map)
            local l, p = {}, {}
            for n, c in pairs(map) do table.insert(l, {n, c}) end; table.sort(l, function(a,b) return a[1] < b[1] end)
            for _, i in ipairs(l) do table.insert(p, "[["..i[1].."]]&nbsp;("..i[2]..")") end
            return '<span style="font-size:120%;">' .. table.concat(p, "&nbsp;&bull; ") .. '</span>'
        end

        if show_nav.final_goals then add_nav({bg='IndianRed', icon='[[Файл:Kipsta ball.png|30px|]]', editName='Авторы голов в финале ЧТМ', title='Авторы голов в финале ЧТМ', cat='[[Категория:Авторы голов в финале]]', content=flat_actions(N.final_goals)}) end
        if show_nav.final_assists then add_nav({bg='tan', icon='[[Файл:Assist.png|30px|]]', editName='Авторы голевых передач в финале ЧТМ', title='Авторы голевых передач в финале ЧТМ', cat='[[Категория:Авторы голевых передач в финале]]', content=flat_actions(N.final_assists)}) end

        if show_nav.club100 then
            local pl = {}
            for p_name, g in pairs(N.global_goals) do if g >= 100 then table.insert(pl, p_name) end end; table.sort(pl)
            for i, p_name in ipairs(pl) do pl[i] = "[["..p_name.."]]" end
            add_nav({bg='plum', icon='[[Файл:Клуб 100.svg|30px|]]', editName='Клуб 100', title='[[Клуб 100]]', cat='[[Категория:Клуб 100]]', content='<span style="font-size:120%;">'..table.concat(pl, "&nbsp;&bull; ")..'</span>'})
        end

        if show_nav.loyalty then
            -- КОПИРУЕМ МАССИВ ПЕРЕД СОРТИРОВКОЙ
            local loy = {}
            for _, i in ipairs(N.loyalty) do table.insert(loy, i) end
            table.sort(loy, function(a,b) if a[1] ~= b[1] then return a[1] < b[1] end return a[2] < b[2] end)
            
            local pts = {}
            for _, i in ipairs(loy) do table.insert(pts, "[["..i[1].."]] — [["..i[2].."]] (''[["..(Teams.getName(i[3], 'short') or i[3]).."]]'')") end
            add_nav({bg='MediumAquamarine', noIcon=true, editName='Верность команде', title='Обладатели приза «[[Верность команде]]»', cat='[[Категория:Обладатели приза «Верность команде»]]', content='<span style="font-size:120%;">'..table.concat(pts, "&nbsp;&bull; ")..'</span>'})
        end

        local gens, g_rows = { {t="Первое поколение<br>(2006—2007)", l={}}, {t="Второе поколение<br>(2011—2013)", l={}}, {t="Третье поколение<br>(2020—<abbr title=\"настоящее время\" class=\"nowrap\">н.&thinsp;в.</abbr>)", l={}} }, {}
        for p_name, y in pairs(N.debuts) do if y <= 2018 then table.insert(gens[1].l, p_name) elseif y <= 2034 then table.insert(gens[2].l, p_name) else table.insert(gens[3].l, p_name) end end
        for i, g in ipairs(gens) do
            if #g.l > 0 then
                table.sort(g.l); local p_strs = {}
                for _, p_name in ipairs(g.l) do table.insert(p_strs, "[["..p_name.."]]"..string.rep("&#9733;", N.champ_counts[p_name] or 0)) end
                table.insert(g_rows, string.format('<tr><td style="text-align:right; width:15%%; padding:0 0.5em; font-weight:bold; color: black; background: Thistle; ">%s</td>\n<td style="text-align:left; padding:0 0.5em;   "><span style="font-size:120%%;">%s%s</span></td></tr>', g.t, table.concat(p_strs, "&nbsp;&bull; "), (i == #gens and "" or "\n----")))
            end
        end
        add_nav({bg='Thistle', icon='[[Файл:Logo.png|32px|]]', iconW='32px', editName='Игроки ЧТМ', title='Игроки ЧТМ', cat='[[Категория:Игроки]]', content=table.concat(g_rows)})

        if show_nav.org then table.insert(navs, '{{Организация ЧТМ}}') end
    end

    local function render_amboxes()
        local out = {}
        if sg_place == 1 then
            table.insert(out, '<indicator name="1">[[Файл:Лучшая статья года.png|36px|link=ЧТМ:Статьи года|Эта статья была признана лучшей статьёй ЧТМ Вики '..sg_year..' года]]</indicator>\n{{Ambox\n|id = 1-message\n|name  = Статья года\n|type  = good\n|image = [[Файл:Лучшая статья года.png|24px|link=ЧТМ:Статьи года|alt=✰]]\n|text  = Эта статья была признана \'\'\'[[ЧТМ Вики:Статьи года|лучшей статьёй]]\'\'\' ЧТМ Вики '..sg_year..' года.\n}}[[Категория:Лучшие статьи года]][[Категория:Статьи года]]')
        elseif sg_place and sg_place >= 2 and sg_place <= 10 then
            table.insert(out, '<indicator name="2">[[Файл:Статья года.png|32px|link=ЧТМ:Статьи года|Эта статья заняла '..sg_place..' место в голосовании за звание лучшей статьи '..sg_year..' года]]</indicator>\n{{Ambox\n|id = 2-message\n|name  = Статья года\n|type  = good\n|image = [[Файл:Статья года.png|24px|link=ЧТМ:Статьи года|alt=✰]]\n|text  = Эта статья заняла '..sg_place..' место в голосовании за звание \'\'\'[[ЧТМ Вики:Статьи года|лучшей статьи]]\'\'\' '..sg_year..' года.\n}}[[Категория:Статьи года]]')
        end
        if quality == 'ИС' then
            table.insert(out, '<indicator name="3">[[Файл:Skew star.png|28px|link=ЧТМ:Избранные статьи|Эта статья входит в число избранных статей ЧТМ Вики]]</indicator>\n{{Ambox\n|id = 3-message\n|name  = Избранная статья\n|type  = good\n|image = [[Файл:Skew star.png|24px|link=ЧТМ:Избранные статьи|alt=✰]]\n|text  = Эта статья входит в число [[ЧТМ:Избранные статьи|избранных статей]] ЧТМ Вики.\n}}[[Категория:Избранные статьи]]')
        elseif quality == 'ХС' then
            table.insert(out, '<indicator name="4">[[Файл:Blue star.png|28px|link=ЧТМ:Хорошие статьи|Эта статья входит в число хороших статей ЧТМ Вики]]</indicator>\n{{Ambox\n|id = 4-message\n|name  = Хорошая статья\n|type  = good\n|image = [[Файл:Blue star.png|24px|link=ЧТМ:Хорошие статьи|alt=✰]]\n|text  = Эта статья входит в число [[ЧТМ:Хорошие статьи|хороших статей]] ЧТМ Вики.\n}}[[Категория:Хорошие статьи]]')
        end
        return table.concat(out, "\n")
    end

    local out_str = Config.styles.wiki_templates
    
    if show_main then out_str = out_str .. "== Статистика ==\n" .. html_main .. "\n" end
    
    if show_matches then
        if not show_main then out_str = out_str .. "== Список матчей ==\n" end
        out_str = out_str .. html_list .. "\n"
    else
        local link_target = matches_link or "/Список матчей"
        out_str = out_str .. "'''[[" .. link_target .. "|Полный список матчей]]'''.\n"
    end
    
    out_str = out_str .. render_amboxes() .. "\n"
    if show_navboxes then out_str = out_str .. table.concat(navs, "\n") end
    
    return out_str
end

return p