Модуль:PlayerAchievements

Материал из ЧТМ
Версия от 00:07, 4 июня 2026; Дача Цанавы (обсуждение | вклад) (исправление бага с бесконечной десятичной дробью в очках мегарейтинга)
Перейти к навигации Перейти к поиску
Документация Документация

debug


Пожалуйста, добавляйте категории на страницу документации.

-- ==========================================
-- Модуль:PlayerAchievements
-- Автоматический генератор достижений и рекордов игроков ЧТМ
-- ==========================================

local PlayerAchievements = {}

local Config = require('Module:Config')
local CoreEngine = require('Module:StatEngine/Pure')
local ModuleDataTeams = require('Module:Data/Teams')
local TournamentAwards = require('Module:StatEngine/TournamentAwards')
local ModuleMegarating = require('Module:Megarating')

-- ==========================================
-- ГЛОБАЛЬНЫЙ КЭШ (Выполняется ровно 1 раз при загрузке страницы!)
-- ==========================================
local is_initialized = false
local GlobalStreaks = {} 
local GrandStats
local CustomStats = {}
local Leaders = {}
local TeamLeaders = {}
local StadiumLeaders = {}
local ShoeTeams = {}
local AssistTeams = {}

-- Блок машины рекордов
local Players = {}
local GlobalRecords = { AllTime = {}, PerTournament = {} }

local word_map = {
    [1] = "Первый", [2] = "Второй", [3] = "Третий", [4] = "Четвёртый",
    [5] = "Пятый", [6] = "Шестой", [7] = "Седьмой", [8] = "Восьмой",
    [9] = "Девятый", [10] = "Десятый", [11] = "Одиннадцатый", [12] = "Двенадцатый"
}

-- Утилиты плюрализации и дат
local function plural(n, w1, w2, w5)
    local n10 = n % 10; local n100 = n % 100
    if n100 >= 11 and n100 <= 19 then return w5 end
    if n10 == 1 then return w1 end
    if n10 >= 2 and n10 <= 4 then return w2 end
    return w5
end

local function date_diff(date1, date2)
    local y1, m1, d1 = date1:match("(%d+)%-(%d+)%-(%d+)")
    local y2, m2, d2 = date2:match("(%d+)%-(%d+)%-(%d+)")
    if not y1 or not y2 then return 0, "" end
    y1, m1, d1 = tonumber(y1), tonumber(m1), tonumber(d1)
    y2, m2, d2 = tonumber(y2), tonumber(m2), tonumber(d2)

    local dy = y2 - y1; local dm = m2 - m1; local dd = d2 - d1
    if dd < 0 then dm = dm - 1; dd = dd + 30 end
    if dm < 0 then dy = dy - 1; dm = dm + 12 end

    local total_days = dy * 365 + dm * 30 + dd
    local parts = {}
    if dy > 0 then table.insert(parts, dy .. " " .. plural(dy, "год", "года", "лет")) end
    if dm > 0 then table.insert(parts, dm .. " " .. plural(dm, "месяц", "месяца", "месяцев")) end
    if dd > 0 then table.insert(parts, dd .. " " .. plural(dd, "день", "дня", "дней")) end

    return total_days, table.concat(parts, " ")
end

local function format_years_ranges(years_table)
    if not years_table or #years_table == 0 then return "" end
    local hash = {}
    local sorted = {}
    for _, y in ipairs(years_table) do
        if not hash[y] then hash[y] = true; table.insert(sorted, y) end
    end
    table.sort(sorted)

    local year_idx = {}
    for i, y in ipairs(Config.years) do year_idx[y] = i end

    local ranges = {}
    local start_y = sorted[1]
    local prev_y = sorted[1]

    for i = 2, #sorted do
        local cur_y = sorted[i]
        if year_idx[cur_y] == (year_idx[prev_y] or -99) + 1 then
            prev_y = cur_y
        else
            if start_y == prev_y then table.insert(ranges, start_y)
            else table.insert(ranges, start_y .. "—" .. prev_y) end
            start_y = cur_y
            prev_y = cur_y
        end
    end
    
    if start_y == prev_y then table.insert(ranges, start_y)
    else table.insert(ranges, start_y .. "—" .. prev_y) end

    return table.concat(ranges, ", ")
end

local function get_streak_years(len, end_year)
    if len == 0 or not end_year then return {} end
    local res = {}
    local y_idx = 0
    for i, y in ipairs(Config.years) do
        if y == end_year then y_idx = i; break end
    end
    if y_idx > 0 then
        for i = math.max(1, y_idx - len + 1), y_idx do
            table.insert(res, Config.years[i])
        end
    end
    return res
end

local function has_player(aw_data, player)
    if type(aw_data) == "table" then
        for _, p in ipairs(aw_data) do if p == player then return true end end
    elseif type(aw_data) == "string" then
        return aw_data == player
    end
    return false
end

-- ==========================================
-- ФАБРИКИ ЯЧЕЕК РЕКОРДОВ
-- ==========================================
local function get_p(name)
    if not Players[name] then
        Players[name] = {
            name = name,
            champs = 0, champs_years = {}, 
            champ_captain = 0, champ_captain_years = {},
            champ_streak = 0, cur_champ_streak = 0, last_champ_year = nil, champ_streak_end_year = nil,
            champ_team_streak = 0, cur_champ_team_streak = 0, last_champ_team = nil, champ_team_streak_code = "", champ_team_streak_end_year = nil,
            superchamps = 0, superchamps_years = {}, 
            golden_goals = 0, golden_goals_years = {}, 
            final_goals_real = 0, final_assists_real = 0, final_ga_real = 0,
            max_final_goals = 0, max_final_goals_occurences = {}, 
            finals_played = 0, mvp_final = 0, mvp_final_years = {},
            goals = 0, max_goals_team = 0, max_goals_team_code = "",
            max_goals_match = 0, max_goals_match_occurences = {},
            mega_tricks = 0, max_mega_tricks_team = 0, max_mega_tricks_team_code = "",
            classic_hat_tricks = 0, pokers = 0, pentas = 0, hexas = 0,
            pens_scored = 0, pens_in_game_scored = 0,
            max_assists_match = 0, max_assists_match_occurences = {}, 
            pm = 0, md_prizes = 0, md_places = 0,
            assists = 0, heel_goals = 0,
            golden_spheres = 0, golden_spheres_years = {}, total_spheres = 0, 
            golden_shoes = 0, golden_shoes_years = {}, total_shoes = 0,
            mvp = 0, max_team_mvp = 0, max_team_mvp_code = "",
            mvp_goalie = 0, mr_points_pct = 0, pens_saved = 0,
            matches = 0, field_matches = 0, goalie_matches = 0, goalie_goals = 0, head_goals = 0, clearances = 0,
            max_streaks = {}, cur_streaks = {}, last_played_hist = nil, cur_team = nil,
            max_team_matches_code = "", max_goalie_clean_code = "",
            max_gap_days = 0, max_gap_str = "", last_date = nil,
            wins = { 
                goals = 0, goals_years = {}, 
                mega_tricks = 0, mega_tricks_years = {}, 
                pm = 0, pm_years = {}, 
                mvp = 0, mvp_years = {}, 
                mvp_goalie = 0, mvp_goalie_years = {}, 
                head_goals = 0, head_goals_years = {}, 
                clearances = 0, clearances_years = {}, 
                gaa = 0, gaa_years = {} 
            },
            tournaments = {}
        }
    end
    return Players[name]
end

local function get_t(p, year)
    if not p.tournaments[year] then
        p.tournaments[year] = {
            year = year,
            goals = 0, max_goals_team = 0, max_goals_team_code = "",
            mega_tricks = 0, pm = 0, matches = 0, pts_pct = 0, md_prizes = 0, md_places = 0,
            heel_goals = 0, mvp = 0, max_team_mvp = 0, max_team_mvp_code = "",
            playoff_wins = 0, head_goals = 0, clearances = 0, gaa = 0,
            playoff_goals = 0, winning_goals = 0, avg_goals = 0,
            max_team_assists = 0, max_team_assists_code = "",
            mvp_goalie = 0, mr_points_pct = 0, mr_points = 0, assists = 0, avg_assists = 0,
            ga = 0, max_team_ga = 0, max_team_ga_code = "",
            goalie_matches = 0, field_matches = 0,
            max_mega_tricks_team = 0, max_mega_tricks_team_code = ""
        }
    end
    return p.tournaments[year]
end

local function initialize_data()
    if is_initialized then return end
    
    local loaded_databases = {}
    for _, year in ipairs(Config.years) do
        local success, year_db = pcall(require, 'Module:Data/' .. year)
        if success then loaded_databases[year] = year_db end
    end

    -- === 1. ЕДИНСТВЕННЫЙ ПОЛНЫЙ ПРОХОД КОМБАЙНОМ! ===
    GrandStats = CoreEngine.Harvester.run_all_time(loaded_databases, {
        need_players = true, need_teams = true, need_combos = true, keep_years = true
    })
    
    -- ПРИМЕНЕНИЕ ИСКЛЮЧЕНИЙ ДЛЯ НАГРАД (award_adjustments) ИЗ КОНФИГА
    if Config.award_adjustments then
        for year, year_adj in pairs(Config.award_adjustments) do
            for award_type, combos in pairs(year_adj) do
                for combo_key, adj in pairs(combos) do
                    local p_name, t_code = string.match(combo_key, "^(.*)_(.*)$")
                    if p_name and t_code and adj.total then
                        local add = adj.total
                        -- Модифицируем общие данные
                        if GrandStats.Players[p_name] then
                            if award_type == "goals" then GrandStats.Players[p_name].goals.total = GrandStats.Players[p_name].goals.total + add
                            elseif award_type == "assists" then GrandStats.Players[p_name].assists.total = GrandStats.Players[p_name].assists.total + add end
                        end
                        -- Модифицируем данные года
                        if GrandStats.Years[year] and GrandStats.Years[year].Players[p_name] then
                            if award_type == "goals" then GrandStats.Years[year].Players[p_name].goals.total = GrandStats.Years[year].Players[p_name].goals.total + add
                            elseif award_type == "assists" then GrandStats.Years[year].Players[p_name].assists.total = GrandStats.Years[year].Players[p_name].assists.total + add end
                        end
                        -- Модифицируем комбо Игрок-Команда
                        if GrandStats.PlayerTeam[combo_key] then
                            if award_type == "goals" then GrandStats.PlayerTeam[combo_key].goals.total = GrandStats.PlayerTeam[combo_key].goals.total + add
                            elseif award_type == "assists" then GrandStats.PlayerTeam[combo_key].assists.total = GrandStats.PlayerTeam[combo_key].assists.total + add end
                        end
                        if GrandStats.Years[year] and GrandStats.Years[year].PlayerTeam[combo_key] then
                            if award_type == "goals" then GrandStats.Years[year].PlayerTeam[combo_key].goals.total = GrandStats.Years[year].PlayerTeam[combo_key].goals.total + add
                            elseif award_type == "assists" then GrandStats.Years[year].PlayerTeam[combo_key].assists.total = GrandStats.Years[year].PlayerTeam[combo_key].assists.total + add end
                        end
                    end
                end
            end
        end
    end

    local flat_matches = {}
    CustomStats = { club100 = {}, stadiums = {}, finals = {} }
    local global_goals = {}

    for _, year in ipairs(Config.years) do
        local year_db = loaded_databases[year]
        if year_db then
            CustomStats.finals[year] = {}
            for id, m in pairs(year_db) do
                table.insert(flat_matches, { year = year, id = id, m = m, num = m.num_hist or 0 })
            end
        end
    end
    table.sort(flat_matches, function(a, b)
        if a.year ~= b.year then return a.year < b.year end
        return tostring(a.id) < tostring(b.id)
    end)

    local StreaksEngine = require('Module:StatEngine/StreaksCore').new()
    local current_scorer = nil
    local consecutive_goals = 0

    -- === 2. СВЕРХБЫСТРЫЙ ЛОКАЛЬНЫЙ ЦИКЛ ===
    for _, item in ipairs(flat_matches) do
        local year = item.year
        local match = item.m
        local is_playoff = Config.playoff_stages[match.stage] == true
        local is_final = (match.stage == "Финал")

        StreaksEngine:process_match(year, match, match.num_hist)

        -- Паузы (даты)
        if match.date then
            local p_teams = CoreEngine.Harvester.get_all_teams(match)
            for p_name, _ in pairs(p_teams) do
                local p = get_p(p_name)
                if p.last_date then
                    local days, str = date_diff(p.last_date, match.date)
                    if days > p.max_gap_days then
                        p.max_gap_days = days
                        p.max_gap_str = str
                    end
                end
                p.last_date = match.date
            end
        end

        local match_g = {}
        local match_a = {}
        
        if match.goals then
            for _, g in ipairs(match.goals) do
                if g.scorer and not g.own_scorer then
                    if g.scorer == current_scorer then
                        consecutive_goals = consecutive_goals + 1
                        if consecutive_goals == 3 then
                            get_p(g.scorer).classic_hat_tricks = get_p(g.scorer).classic_hat_tricks + 1
                        end
                    else
                        current_scorer = g.scorer
                        consecutive_goals = 1
                    end
                else
                    current_scorer = nil
                    consecutive_goals = 0
                end

                if g.scorer then
                    local p = get_p(g.scorer)
                    local t = get_t(p, year)
                    
                    global_goals[g.scorer] = (global_goals[g.scorer] or 0) + 1
                    if global_goals[g.scorer] == 100 then table.insert(CustomStats.club100, {name = g.scorer, year = year}) end
                    
                    if match.stadium then
                        CustomStats.stadiums[match.stadium] = CustomStats.stadiums[match.stadium] or {}
                        CustomStats.stadiums[match.stadium][g.scorer] = (CustomStats.stadiums[match.stadium][g.scorer] or 0) + 1
                    end

                    match_g[g.scorer] = (match_g[g.scorer] or 0) + 1
                    local mg = match_g[g.scorer]
                    if mg > p.max_goals_match then
                        p.max_goals_match = mg
                        p.max_goals_match_occurences = {year}
                    elseif mg == p.max_goals_match and mg > 0 then
                        table.insert(p.max_goals_match_occurences, year)
                    end
                    
                    if is_final then
                        if mg > p.max_final_goals then
                            p.max_final_goals = mg
                            p.max_final_goals_occurences = {year}
                        elseif mg == p.max_final_goals and mg > 0 then
                            table.insert(p.max_final_goals_occurences, year)
                        end
                    end
                    if is_playoff then t.playoff_goals = t.playoff_goals + 1 end
                end
                
                if g.assist then
                    local p = get_p(g.assist)
                    match_a[g.assist] = (match_a[g.assist] or 0) + 1
                    local ma = match_a[g.assist]
                    if ma > p.max_assists_match then
                        p.max_assists_match = ma
                        p.max_assists_match_occurences = {year}
                    elseif ma == p.max_assists_match and ma > 0 then
                        table.insert(p.max_assists_match_occurences, year)
                    end
                end
            end
        end

        -- Специфическая логика Финалов
        if is_final then
            local f = CustomStats.finals[year]
            f.goals_count = match_g; f.assists_count = match_a

            if match.goals then
                for _, g in ipairs(match.goals) do
                    if g.scorer then get_p(g.scorer).final_goals_real = get_p(g.scorer).final_goals_real + 1 end
                    if g.assist then get_p(g.assist).final_assists_real = get_p(g.assist).final_assists_real + 1 end
                end
            end

            local function parse_aw(aw_data)
                if type(aw_data) == "table" then return aw_data end
                if type(aw_data) == "string" then return {aw_data} end
                return {}
            end
            
            f.superchampions = parse_aw(match.superchampions)
            f.golden_sphere = parse_aw(match.golden_sphere); f.silver_sphere = parse_aw(match.silver_sphere)
            f.bronze_sphere = parse_aw(match.bronze_sphere); f.wooden_sphere = parse_aw(match.wooden_sphere)
            f.golden_shoe = parse_aw(match.golden_shoe); f.silver_shoe = parse_aw(match.silver_shoe)
            f.bronze_shoe = parse_aw(match.bronze_shoe); f.wooden_shoe = parse_aw(match.wooden_shoe)
            f.golden_assistant = parse_aw(match.golden_assistant); f.silver_assistant = parse_aw(match.silver_assistant)
            f.bronze_assistant = parse_aw(match.bronze_assistant); f.wooden_assistant = parse_aw(match.wooden_assistant)
            f.elnur_award = parse_aw(match.elnur_award); f.best_goal = parse_aw(match.best_goal)
            f.mvp = match.mvp and match.mvp.player or nil
            
            f.has_shootout = (match.shootout_score1 ~= nil); f.shootout_winner = nil
            if match.shootout and f.has_shootout then
                local w_team = match.shootout_score1 > match.shootout_score2 and 1 or 2
                local w_score = 0; local max_score = math.max(match.shootout_score1, match.shootout_score2)
                for _, shot in ipairs(match.shootout) do
                    if shot.team == w_team and shot.result == "гол" then
                        w_score = w_score + 1; if w_score == max_score then f.shootout_winner = shot.taker; break end
                    end
                end
            end

            for _, p_name in ipairs(f.superchampions or {}) do get_p(p_name).superchamps = get_p(p_name).superchamps + 1; table.insert(get_p(p_name).superchamps_years, year) end
            for _, p_name in ipairs(f.golden_sphere or {}) do get_p(p_name).golden_spheres = get_p(p_name).golden_spheres + 1; table.insert(get_p(p_name).golden_spheres_years, year); get_p(p_name).total_spheres = get_p(p_name).total_spheres + 1 end
            for _, p_name in ipairs(f.silver_sphere or {}) do get_p(p_name).total_spheres = get_p(p_name).total_spheres + 1 end
            for _, p_name in ipairs(f.bronze_sphere or {}) do get_p(p_name).total_spheres = get_p(p_name).total_spheres + 1 end
            for _, p_name in ipairs(f.wooden_sphere or {}) do get_p(p_name).total_spheres = get_p(p_name).total_spheres + 1 end
            for _, p_name in ipairs(f.golden_shoe or {}) do get_p(p_name).golden_shoes = get_p(p_name).golden_shoes + 1; table.insert(get_p(p_name).golden_shoes_years, year); get_p(p_name).total_shoes = get_p(p_name).total_shoes + 1 end
            for _, p_name in ipairs(f.silver_shoe or {}) do get_p(p_name).total_shoes = get_p(p_name).total_shoes + 1 end
            for _, p_name in ipairs(f.bronze_shoe or {}) do get_p(p_name).total_shoes = get_p(p_name).total_shoes + 1 end
            for _, p_name in ipairs(f.wooden_shoe or {}) do get_p(p_name).total_shoes = get_p(p_name).total_shoes + 1 end

            if f.mvp then get_p(f.mvp).mvp_final = get_p(f.mvp).mvp_final + 1; table.insert(get_p(f.mvp).mvp_final_years, year) end
            if f.shootout_winner then get_p(f.shootout_winner).golden_goals = get_p(f.shootout_winner).golden_goals + 1; table.insert(get_p(f.shootout_winner).golden_goals_years, year) end

            local m_res = CoreEngine.Harvester.evaluate_match(match)
            local win_team = (m_res and m_res[1].pts > m_res[2].pts) and 1 or ((m_res and m_res[2].pts > m_res[1].pts) and 2 or 0)
            local p_teams = CoreEngine.Harvester.get_all_teams(match)
            local cap1 = match.squad1 and match.squad1.captain; local cap2 = match.squad2 and match.squad2.captain
            
            for p_name, t_id in pairs(p_teams) do
                local p = get_p(p_name)
                p.finals_played = p.finals_played + 1
                
                if t_id == win_team then
                    p.champs = p.champs + 1
                    table.insert(p.champs_years, year)
                    if (t_id == 1 and cap1 == p_name) or (t_id == 2 and cap2 == p_name) then
                        p.champ_captain = p.champ_captain + 1
                        table.insert(p.champ_captain_years, year)
                    end

                    local prev_idx = 0
                    for i, y in ipairs(Config.years) do if y == year then prev_idx = i - 1 break end end
                    local prev_year = Config.years[prev_idx]
                    local cur_t_code = (t_id == 1) and match.team1 or match.team2

                    if p.last_champ_year == prev_year then
                        p.cur_champ_streak = p.cur_champ_streak + 1
                        if p.last_champ_team == cur_t_code then p.cur_champ_team_streak = p.cur_champ_team_streak + 1
                        else p.cur_champ_team_streak = 1 end
                    else
                        p.cur_champ_streak = 1; p.cur_champ_team_streak = 1
                    end
                    p.last_champ_year = year; p.last_champ_team = cur_t_code
                    if p.cur_champ_streak > p.champ_streak then
                        p.champ_streak = p.cur_champ_streak
                        p.champ_streak_end_year = year
                    end
                    if p.cur_champ_team_streak > p.champ_team_streak then
                        p.champ_team_streak = p.cur_champ_team_streak
                        p.champ_team_streak_code = cur_t_code
                        p.champ_team_streak_end_year = year
                    end
                end
            end
        end
    end

    -- === 3. ИЗВЛЕКАЕМ ДАННЫЕ ИЗ МАШИНЫ СЕРИЙ ===
    StreaksEngine:finish()
    GlobalStreaks = StreaksEngine:get_results()
    for name, ep in pairs(GlobalStreaks.players) do
        local p = get_p(name)
        p.max_streaks.team_scored = ep.team_scored or 0
        p.max_streaks.matches_played = ep.seq or 0
        p.max_streaks.goals = ep.goals or 0
        p.max_streaks.double = ep.double or 0
        p.max_streaks.hat_trick = ep.hat_trick or 0
        p.max_streaks.poker = ep.poker or 0
        p.max_streaks.both_ga = ep.both_ga or 0
        p.max_streaks.ga = ep.ga or 0
        p.max_streaks.missed_matches = ep.missed_matches or 0
        p.max_streaks.unbeaten = ep.unbeaten or 0
        p.max_streaks.win = ep.win or 0
    end
    for name, et in pairs(GlobalStreaks.team_streaks) do
        local p = get_p(name)
        p.max_streaks.team_matches = et.len or 0; p.max_team_matches_code = et.team or ""
    end
    for name, egk in pairs(GlobalStreaks.goalie_clean) do
        local p = get_p(name)
        p.max_streaks.goalie_clean = egk.len or 0; p.max_goalie_clean_code = egk.team or ""
    end

        -- === 4. ПЕРЕНОС ДАННЫХ ИЗ GRANDSTATS ===
    for p_name, gs_p in pairs(GrandStats.Players) do
        local p = get_p(p_name)
        p.goals = gs_p.goals.total; p.assists = gs_p.assists.total
        p.matches = gs_p.matches_total; p.field_matches = gs_p.matches_field; p.goalie_matches = gs_p.matches_goalie
        p.pm = gs_p.plus_minus; p.clearances = gs_p.clearances
        p.head_goals = gs_p.goals.head; p.heel_goals = gs_p.goals.heel; p.goalie_goals = gs_p.goals.goalie
        p.pens_scored = gs_p.penalties.in_game.g + gs_p.penalties.shootout.g
        p.pens_in_game_scored = gs_p.penalties.in_game.g; p.pens_saved = gs_p.penalties.saved_as_goalie
        p.mvp = gs_p.mvp.is_mvp; p.mvp_goalie = gs_p.mvp.is_goalie_mvp
        
        p.mega_tricks = gs_p.goals.hat_trick + gs_p.goals.poker + gs_p.goals.penta + gs_p.goals.hexa
        p.pokers = gs_p.goals.poker + gs_p.goals.penta + gs_p.goals.hexa
        p.pentas = gs_p.goals.penta + gs_p.goals.hexa
        p.hexas = gs_p.goals.hexa
        
        p.final_ga_real = p.final_goals_real + p.final_assists_real
        p.golden_goals = p.golden_goals + gs_p.megarating.final_gold_goals
        
        p.avg_goals = (gs_p.avg.goals_den >= 20) and (gs_p.avg.goals_num / gs_p.avg.goals_den) or 0
        p.avg_assists = (gs_p.avg.assists_den >= 20) and (gs_p.avg.assists_num / gs_p.avg.assists_den) or 0
        p.pts_pct = (p.matches >= 20 and gs_p.megarating.mr_matches > 0) and (gs_p.megarating.mr_points / (gs_p.megarating.mr_matches * 3) * 100) or 0
        p.gaa = (p.goalie_matches >= 20) and (gs_p.weighted_ga / p.goalie_matches) or 9999
    end

    for year, year_stats in pairs(GrandStats.Years) do
        for p_name, gs_t in pairs(year_stats.Players) do
            local p = get_p(p_name); local t = get_t(p, year)
            t.goals = gs_t.goals.total; t.assists = gs_t.assists.total; t.ga = t.goals + t.assists
            t.matches = gs_t.matches_total; t.field_matches = gs_t.matches_field; t.goalie_matches = gs_t.matches_goalie
            t.pm = gs_t.plus_minus; t.clearances = gs_t.clearances
            t.head_goals = gs_t.goals.head; t.heel_goals = gs_t.goals.heel
            t.mvp = gs_t.mvp.is_mvp; t.mvp_goalie = gs_t.mvp.is_goalie_mvp
            t.mega_tricks = gs_t.goals.hat_trick + gs_t.goals.poker + gs_t.goals.penta + gs_t.goals.hexa
            t.winning_goals = gs_t.advanced.winning_goals
            t.playoff_wins = gs_t.megarating.playoff_wins.r16 + gs_t.megarating.playoff_wins.qf + gs_t.megarating.playoff_wins.sf + (gs_t.megarating.is_champion and 1 or 0)
            
            if gs_t.megarating and gs_t.megarating.final_gold_goals and gs_t.megarating.final_gold_goals > 0 then
                for _ = 1, gs_t.megarating.final_gold_goals do
                    table.insert(p.golden_goals_years, year)
                end
            end
            
            t.avg_goals = (gs_t.avg.goals_den >= 20) and (gs_t.avg.goals_num / gs_t.avg.goals_den) or 0
            t.avg_assists = (gs_t.avg.assists_den >= 20) and (gs_t.avg.assists_num / gs_t.avg.assists_den) or 0
            t.pts_pct = (t.matches >= 20 and gs_t.megarating.mr_matches > 0) and (gs_t.megarating.mr_points / (gs_t.megarating.mr_matches * 3) * 100) or 0
            t.gaa = (t.goalie_matches >= 5) and (gs_t.weighted_ga / t.goalie_matches) or 0
        end
    end

    -- === 5. ПЕРЕНОС РЕКОРДОВ ПО СБОРНЫМ ===
    for combo, c_data in pairs(GrandStats.PlayerTeam) do
        local p = get_p(c_data.name); local t_code = c_data.team
        
        if t_code ~= "Нейтрал" then
            if c_data.goals.total > p.max_goals_team then p.max_goals_team = c_data.goals.total; p.max_goals_team_code = t_code end
            local mt = c_data.goals.hat_trick + c_data.goals.poker + c_data.goals.penta + c_data.goals.hexa
            if mt > p.max_mega_tricks_team then p.max_mega_tricks_team = mt; p.max_mega_tricks_team_code = t_code end
            if c_data.mvp.is_mvp > p.max_team_mvp then p.max_team_mvp = c_data.mvp.is_mvp; p.max_team_mvp_code = t_code end

            if c_data.years then
                for year, yc in pairs(c_data.years) do
                    local t = get_t(p, year)
                    if yc.goals.total > t.max_goals_team then t.max_goals_team = yc.goals.total; t.max_goals_team_code = t_code end
                    local y_mt = yc.goals.hat_trick + yc.goals.poker + yc.goals.penta + yc.goals.hexa
                    if y_mt > t.max_mega_tricks_team then t.max_mega_tricks_team = y_mt; t.max_mega_tricks_team_code = t_code end
                    if yc.mvp.is_mvp > t.max_team_mvp then t.max_team_mvp = yc.mvp.is_mvp; t.max_team_mvp_code = t_code end
                    if yc.assists.total > t.max_team_assists then t.max_team_assists = yc.assists.total; t.max_team_assists_code = t_code end
                    local y_ga = yc.goals.total + yc.assists.total
                    if y_ga > t.max_team_ga then t.max_team_ga = y_ga; t.max_team_ga_code = t_code end
                end
            end
        end
    end

    -- === 6. БАШМАКИ, ИГРОВЫЕ ДНИ И МЕГАРЕЙТИНГ ===
    local mr_hist = ModuleMegarating.get_public_history()
    for p_name, hp in pairs(mr_hist.players) do
        local p = get_p(p_name)
        p.mr_points_pct = hp.total_pct
        for year, ydata in pairs(hp.years) do
            local t = get_t(p, year)
            t.mr_points_pct = ydata.pct
            t.mr_points = ydata.pts
        end
    end

    for year, year_db in pairs(loaded_databases) do
        local shoes = TournamentAwards.getTournamentAwards(year, year_db, "goals")
        ShoeTeams[year] = {}; for _, p in ipairs(shoes) do if p.rank <= 4 and not ShoeTeams[year][p.player] then ShoeTeams[year][p.player] = p.team_code end end
        local assists = TournamentAwards.getTournamentAwards(year, year_db, "assists")
        AssistTeams[year] = {}; for _, p in ipairs(assists) do if p.rank <= 4 and not AssistTeams[year][p.player] then AssistTeams[year][p.player] = p.team_code end end

        if year >= 2022 then
            local mds = {}
            for _, m in pairs(year_db) do if m.matchday then mds[m.matchday] = true end end
            for md, _ in pairs(mds) do
                local prizes = TournamentAwards.evaluateMatchdayPrizes(year_db, md)
                for _, cat in ipairs({"mvp", "scorer", "assistant", "productive", "effective", "useful", "goalie_good", "goalie_bad"}) do
                    if prizes[cat] then
                        for _, p in ipairs(prizes[cat]) do
                            local pl = get_p(p.name); local t = get_t(pl, year)
                            if p.prize then pl.md_prizes = pl.md_prizes + 1; t.md_prizes = t.md_prizes + 1 end
                            if p.prize_place then pl.md_places = pl.md_places + 1; t.md_places = t.md_places + 1 end
                        end
                    end
                end
            end
        end
    end

    -- === 7. ФИНАЛИЗАЦИЯ И РАСЧЁТ ЛИДЕРОВ ===
    local function evaluate_leader(year, metric_func)
        local max_val = -9999; local leaders = {}
        for p, p_stats in pairs(GrandStats.Years[year].Players) do
            local val = metric_func(p_stats)
            if val and val > 0 then
                if val > max_val then max_val = val; leaders = {p}
                elseif val == max_val then table.insert(leaders, p) end
            end
        end
        return leaders
    end

    local function evaluate_min_leader(year, metric_func, min_req)
        local min_val = 9999; local leaders = {}
        for p, p_stats in pairs(GrandStats.Years[year].Players) do
            if min_req(p_stats) then
                local val = metric_func(p_stats)
                if val then
                    if val < min_val then min_val = val; leaders = {p}
                    elseif val == min_val then table.insert(leaders, p) end
                end
            end
        end
        return leaders
    end

    for _, year in ipairs(Config.years) do
        if GrandStats.Years[year] then
            Leaders.goals = Leaders.goals or {}; Leaders.goals[year] = evaluate_leader(year, function(s) return s.goals.total end)
            Leaders.assists = Leaders.assists or {}; Leaders.assists[year] = evaluate_leader(year, function(s) return s.assists.total end)
            Leaders.matches = Leaders.matches or {}; Leaders.matches[year] = evaluate_leader(year, function(s) return s.matches_total end)
            Leaders.mvp = Leaders.mvp or {}; Leaders.mvp[year] = evaluate_leader(year, function(s) return s.mvp.is_mvp end)
            Leaders.mvp_goalie = Leaders.mvp_goalie or {}; Leaders.mvp_goalie[year] = evaluate_leader(year, function(s) return s.mvp.is_goalie_mvp end)
            Leaders.mega_tricks = Leaders.mega_tricks or {}; Leaders.mega_tricks[year] = evaluate_leader(year, function(s) return (s.goals.hat_trick + s.goals.poker + s.goals.penta + s.goals.hexa) end)
            Leaders.head_goals = Leaders.head_goals or {}; Leaders.head_goals[year] = evaluate_leader(year, function(s) return s.goals.head end)
            Leaders.pens_saved = Leaders.pens_saved or {}; Leaders.pens_saved[year] = evaluate_leader(year, function(s) return s.penalties.saved_as_goalie end)
            Leaders.clearances = Leaders.clearances or {}; Leaders.clearances[year] = evaluate_leader(year, function(s) return s.clearances end)
            Leaders.playoff_wins = Leaders.playoff_wins or {}; Leaders.playoff_wins[year] = evaluate_leader(year, function(s) return s.megarating.playoff_wins.r16 + s.megarating.playoff_wins.qf + s.megarating.playoff_wins.sf + (s.megarating.is_champion and 1 or 0) end)
            Leaders.plus_minus = Leaders.plus_minus or {}; Leaders.plus_minus[year] = evaluate_leader(year, function(s) return s.plus_minus end)
            Leaders.pens_scored = Leaders.pens_scored or {}; Leaders.pens_scored[year] = evaluate_leader(year, function(s) return s.penalties.in_game.g + s.penalties.shootout.g end)
            Leaders.avg_goals = Leaders.avg_goals or {}; Leaders.avg_goals[year] = evaluate_leader(year, function(s) return s.avg.goals_den >= 20 and s.avg.goals_num / s.avg.goals_den or 0 end)
            Leaders.gaa = Leaders.gaa or {}; Leaders.gaa[year] = evaluate_min_leader(year, function(s) return s.weighted_ga / s.matches_goalie end, function(s) return s.matches_goalie and s.matches_goalie >= 5 end)
        end
    end

    for combo, c_data in pairs(GrandStats.PlayerTeam) do
        local p = c_data.name; local t = c_data.team; local goals = c_data.goals.total
        if t ~= "Нейтрал" and goals > 0 then
            TeamLeaders[t] = TeamLeaders[t] or {players = {}, goals = 0}
            if goals > TeamLeaders[t].goals then TeamLeaders[t].goals = goals; TeamLeaders[t].players = {p}
            elseif goals == TeamLeaders[t].goals then table.insert(TeamLeaders[t].players, p) end
        end
    end

    for st, p_list in pairs(CustomStats.stadiums) do
        local max = 0; local best = {}
        for p, goals in pairs(p_list) do
            if goals > max then max = goals; best = {p}
            elseif goals == max then table.insert(best, p) end
        end
        if max > 0 then StadiumLeaders[st] = {players = best, goals = max} end
    end

    for _, p in pairs(Players) do
        for year, list in pairs(Leaders.goals or {}) do if Config.utils.has_value(list, p.name) then p.wins.goals = p.wins.goals + 1; table.insert(p.wins.goals_years, year) end end
        for year, list in pairs(Leaders.mega_tricks or {}) do if Config.utils.has_value(list, p.name) then p.wins.mega_tricks = p.wins.mega_tricks + 1; table.insert(p.wins.mega_tricks_years, year) end end
        for year, list in pairs(Leaders.plus_minus or {}) do if Config.utils.has_value(list, p.name) then p.wins.pm = p.wins.pm + 1; table.insert(p.wins.pm_years, year) end end
        for year, list in pairs(Leaders.mvp or {}) do if Config.utils.has_value(list, p.name) then p.wins.mvp = p.wins.mvp + 1; table.insert(p.wins.mvp_years, year) end end
        for year, list in pairs(Leaders.mvp_goalie or {}) do if Config.utils.has_value(list, p.name) then p.wins.mvp_goalie = p.wins.mvp_goalie + 1; table.insert(p.wins.mvp_goalie_years, year) end end
        for year, list in pairs(Leaders.head_goals or {}) do if Config.utils.has_value(list, p.name) then p.wins.head_goals = p.wins.head_goals + 1; table.insert(p.wins.head_goals_years, year) end end
        for year, list in pairs(Leaders.clearances or {}) do if Config.utils.has_value(list, p.name) then p.wins.clearances = p.wins.clearances + 1; table.insert(p.wins.clearances_years, year) end end
        for year, list in pairs(Leaders.gaa or {}) do if Config.utils.has_value(list, p.name) then p.wins.gaa = p.wins.gaa + 1; table.insert(p.wins.gaa_years, year) end end
    end

    local metrics_all = {
        {"champs", false}, {"champ_captain", false}, {"champ_streak", false}, {"champ_team_streak", false},
        {"superchamps", false}, {"golden_goals", false}, {"final_goals_real", false}, {"final_assists_real", false},
        {"final_ga_real", false}, {"max_final_goals", false}, {"finals_played", false}, {"goals", false},
        {"max_goals_team", false}, {"max_goals_match", false}, {"mega_tricks", false}, {"max_mega_tricks_team", false},
        {"classic_hat_tricks", false}, {"pokers", false}, {"pentas", false}, {"hexas", false},
        {"pens_scored", false}, {"pens_in_game_scored", false}, {"max_assists_match", false}, {"pm", false},
        {"pts_pct", false}, {"md_prizes", false}, {"md_places", false}, {"assists", false}, {"avg_assists", false},
        {"heel_goals", false}, {"golden_spheres", false}, {"total_spheres", false}, {"golden_shoes", false},
        {"total_shoes", false}, {"avg_goals", false}, {"gaa", true}, {"mvp", false}, {"max_team_mvp", false},
        {"mvp_goalie", false}, {"mr_points_pct", false}, {"pens_saved", false}, {"matches", false},
        {"field_matches", false}, {"goalie_matches", false}, {"head_goals", false}, {"clearances", false},
        {"max_gap_days", false}
    }
    
    local function get_val(p, key)
        if string.sub(key, 1, 12) == "max_streaks_" then return p.max_streaks[string.sub(key, 13)] or 0 end
        if string.sub(key, 1, 5) == "wins_" then return p.wins[string.sub(key, 6)] or 0 end
        return p[key]
    end

    for _, row in ipairs(metrics_all) do
        local key = row[1]; local is_min = row[2]; local best = is_min and 9999 or -9999
        for _, p in pairs(Players) do
            local v = get_val(p, key)
            if v and v ~= 0 and (not is_min or v < 9999) then
                if (is_min and v < best) or (not is_min and v > best) then best = v end
            end
        end
        GlobalRecords.AllTime[key] = best
    end
    
    local streak_wins_keys = {"max_streaks_team_scored", "max_streaks_matches_played", "max_streaks_goals", "max_streaks_double", "max_streaks_hat_trick", "max_streaks_poker", "max_streaks_both_ga", "max_streaks_ga", "max_streaks_missed_matches", "max_streaks_goalie_clean", "max_streaks_team_matches", "max_streaks_unbeaten", "max_streaks_win", "wins_goals", "wins_mega_tricks", "wins_pm", "wins_mvp", "wins_mvp_goalie", "wins_head_goals", "wins_clearances", "wins_gaa"}
    for _, key in ipairs(streak_wins_keys) do
        local best = -9999
        for _, p in pairs(Players) do
            local v = get_val(p, key)
            if v and v ~= 0 and v > best then best = v end
        end
        GlobalRecords.AllTime[key] = best
    end

    local metrics_tourney = {
        {"goals", false}, {"max_goals_team", false}, {"mega_tricks", false}, {"pm", false}, {"matches", false},
        {"pts_pct", false}, {"md_prizes", false}, {"md_places", false}, {"heel_goals", false}, {"mvp", false},
        {"max_team_mvp", false}, {"playoff_wins", false}, {"head_goals", false}, {"clearances", false},
        {"gaa", true}, {"playoff_goals", false}, {"winning_goals", false}, {"avg_goals", false},
        {"max_team_assists", false}, {"mvp_goalie", false}, {"mr_points", false}, {"assists", false},
        {"avg_assists", false}, {"ga", false}, {"max_team_ga", false}
    }
    
    for _, row in ipairs(metrics_tourney) do
        local key = row[1]; local is_min = row[2]; local best = is_min and 9999 or -9999
        for _, p in pairs(Players) do
            for _, t in pairs(p.tournaments) do
                local v = t[key]
                if v and v ~= 0 and (not is_min or v < 9999) then
                    if key == "gaa" and t.goalie_matches < 5 then v = nil end
                    if (key == "avg_goals" or key == "avg_assists") and t.field_matches < 20 then v = nil end
                    if key == "pts_pct" and t.matches < 20 then v = nil end
                    if v and ((is_min and v < best) or (not is_min and v > best)) then best = v end
                end
            end
        end
        GlobalRecords.PerTournament[key] = best
    end

    is_initialized = true
end

-- ==========================================
-- УТИЛИТЫ ФОРМАТИРОВАНИЯ СТРОК
-- ==========================================
local function format_achievement(prefix_singular, prefix_plural, items, sum_counts)
    if #items == 0 then return "" end
    table.sort(items, function(a, b) return a.year < b.year end)
    local texts = {}; local total = 0
    for _, item in ipairs(items) do
        local t = tostring(item.year)
        if item.addon and item.addon ~= "" then t = t .. " " .. item.addon end
        table.insert(texts, t); total = total + (item.count or 1)
    end
    local display_prefix = (#items > 1 or (sum_counts and total > 1)) and prefix_plural or prefix_singular
    local count_str = ""
    if sum_counts and total > 1 then count_str = " (" .. total .. ")"
    elseif not sum_counts and #items > 1 then count_str = " (" .. #items .. ")" end
    return "* " .. display_prefix .. count_str .. ": " .. table.concat(texts, ", ") .. "."
end

local function format_list_with_and(list)
    if #list == 1 then return list[1] end
    if #list == 2 then return list[1] .. " и " .. list[2] end
    local last = table.remove(list)
    return table.concat(list, ", ") .. " и " .. last
end

local function get_hardcoded_historical(player)
    if player == "Алишер" then return "Единственный игрок, забивший гол своим первым касанием мяча в рамках ЧТМ." end
    if player == "Андрей О." then return "Самая продолжительная серия матчей, в которых игроку удавалось как забить гол, так и отдать голевую передачу — 5." end
    if player == "Бирюк" then return "Автор единственного в истории ЧТМ гола, забитого прямым ударом с углового." end
    if player == "Дима Кес." then return "Наибольшее число вратарских голевых передач за один матч — 2." end
    if player == "Диман" then return "Наибольшее число вратарских голевых передач за один матч — 2.\n* Самая продолжительная сухая серия за одну команду — 57 минут ([[Индия]])." end
    if player == "Макс" then return "Наибольшее число [[Вратарские голы|вратарских голов]] — 6." end
    return nil
end

-- ==========================================
-- СБОРКА ТЕКСТА ДОСТИЖЕНИЙ
-- ==========================================
local function build_achievements_text(player)
    if not GrandStats.Players[player] then return "" end

    local main = {}; local add = {}
    local latest_year = Config.years[#Config.years]
    local is_latest_finished = Config.is_latest_finished
    local p = get_p(player)

    local function check_and_add(list, singular, plural, condition_func, sum_counts)
        local items = {}
        for _, year in ipairs(Config.years) do
            local res = condition_func(year)
            if res then table.insert(items, res) end
        end
        local str = format_achievement(singular, plural, items, sum_counts)
        if str ~= "" then table.insert(list, str) end
    end

    local function check_mr(y, target_rank)
        local is_finished = (y ~= latest_year) or is_latest_finished
        local mr_hist = ModuleMegarating.get_public_history()
        if is_finished and mr_hist.players[player] and mr_hist.players[player].years[y] and mr_hist.players[player].years[y].rank == target_rank then return {year = y} end
    end

    check_and_add(main, "[[Список игроков-чемпионов третьего мира|Чемпион третьего мира]]", "[[Список игроков-чемпионов третьего мира|Чемпион третьего мира]]", function(y)
        if Config.champions_players[y] and Config.utils.has_value(Config.champions_players[y], player) then return {year = y} end
    end)
    check_and_add(main, "[[Суперчемпион]]", "[[Суперчемпион]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].superchampions, player) then return {year = y} end
    end)
    check_and_add(main, "Обладатель [[Золотой Шар|Золотого Шара]]", "Обладатель [[Золотой Шар|Золотого Шара]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].golden_sphere, player) then return {year = y} end
    end)
    check_and_add(main, "Обладатель [[Золотой Башмак|Золотого Башмака]]", "Обладатель [[Золотой Башмак|Золотого Башмака]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].golden_shoe, player) then
            local t = ShoeTeams[y] and ShoeTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(main, "[[Лучший ассистент]]", "[[Лучший ассистент]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].golden_assistant, player) then
            local t = AssistTeams[y] and AssistTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(main, "[[Приз имени Эльнура|Лучший вратарь турнира]]", "[[Приз имени Эльнура|Лучший вратарь турнира]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].elnur_award, player) then return {year = y} end
    end)

    local club_index, club_year = 0, 0
    for i, p_info in ipairs(CustomStats.club100) do
        if p_info.name == player then club_index = i; club_year = p_info.year; break end
    end
    if club_index > 0 then
        local w = word_map[club_index] or tostring(club_index) .. "-й"
        local suffix = (club_index > 4) and " по счёту участник" or " участник"
        table.insert(main, "* " .. w .. suffix .. " [[Клуб 100|Клуба 100]]: " .. club_year .. ".")
    end

    check_and_add(add, "Лидер [[мегарейтинг]]а", "Лидер [[мегарейтинг]]а", function(y) return check_mr(y, 1) end)
    check_and_add(add, "Второе место в [[мегарейтинг]]е", "Второе место в [[мегарейтинг]]е", function(y) return check_mr(y, 2) end)
    check_and_add(add, "Третье место в [[мегарейтинг]]е", "Третье место в [[мегарейтинг]]е", function(y) return check_mr(y, 3) end)
    check_and_add(add, "Четвёртое место в [[мегарейтинг]]е", "Четвёртое место в [[мегарейтинг]]е", function(y) return check_mr(y, 4) end)

    check_and_add(add, "Обладатель [[Золотой Шар|Серебряного Шара]]", "Обладатель [[Золотой Шар|Серебряного Шара]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].silver_sphere, player) then return {year = y} end
    end)
    check_and_add(add, "Обладатель [[Золотой Шар|Бронзового Шара]]", "Обладатель [[Золотой Шар|Бронзового Шара]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].bronze_sphere, player) then return {year = y} end
    end)
    check_and_add(add, "Обладатель [[Золотой Шар|Деревянного Шара]]", "Обладатель [[Золотой Шар|Деревянного Шара]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].wooden_sphere, player) then return {year = y} end
    end)

    check_and_add(add, "Обладатель [[Золотой Башмак|Серебряного Башмака]]", "Обладатель [[Золотой Башмак|Серебряного Башмака]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].silver_shoe, player) then
            local t = ShoeTeams[y] and ShoeTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(add, "Обладатель [[Золотой Башмак|Бронзового Башмака]]", "Обладатель [[Золотой Башмак|Бронзового Башмака]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].bronze_shoe, player) then
            local t = ShoeTeams[y] and ShoeTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(add, "Обладатель [[Золотой Башмак|Деревянного Башмака]]", "Обладатель [[Золотой Башмак|Деревянного Башмака]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].wooden_shoe, player) then
            local t = ShoeTeams[y] and ShoeTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)

    check_and_add(add, "Второе место в списке [[Лучший ассистент|лучших ассистентов]]", "Второе место в списке [[Лучший ассистент|лучших ассистентов]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].silver_assistant, player) then
            local t = AssistTeams[y] and AssistTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(add, "Третье место в списке [[Лучший ассистент|лучших ассистентов]]", "Третье место в списке [[Лучший ассистент|лучших ассистентов]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].bronze_assistant, player) then
            local t = AssistTeams[y] and AssistTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)
    check_and_add(add, "Четвёртое место в списке [[Лучший ассистент|лучших ассистентов]]", "Четвёртое место в списке [[Лучший ассистент|лучших ассистентов]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].wooden_assistant, player) then
            local t = AssistTeams[y] and AssistTeams[y][player] or "Неизвестно"
            return {year = y, addon = "([[" .. ModuleDataTeams.getName(t) .. "]])"}
        end
    end)

    check_and_add(add, "Автор золотого гола ЧТМ", "Автор золотых голов ЧТМ", function(y)
        local ys = GrandStats.Years[y] and GrandStats.Years[y].Players[player]
        local is_gold, is_shootout = false, false
        if ys and ys.megarating and ys.megarating.final_gold_goals > 0 then is_gold = true end
        if CustomStats.finals[y] and CustomStats.finals[y].shootout_winner == player then is_gold = true; is_shootout = true end
        if is_gold then return {year = y, addon = is_shootout and "(в серии пенальти)" or "", count = 1} end
    end, false)
    
    check_and_add(add, "Автор гола в финале ЧТМ", "Автор голов в финалах ЧТМ", function(y)
        local f = CustomStats.finals[y]
        if f and f.goals_count and f.goals_count[player] then
            local c = f.goals_count[player]
            return {year = y, addon = (c > 1) and ("(" .. c .. ")") or "", count = c}
        end
    end, true)

    check_and_add(add, "Автор голевой передачи в финале ЧТМ", "Автор голевых передач в финалах ЧТМ", function(y)
        local f = CustomStats.finals[y]
        if f and f.assists_count and f.assists_count[player] then
            local c = f.assists_count[player]
            return {year = y, addon = (c > 1) and ("(" .. c .. ")") or "", count = c}
        end
    end, true)

    check_and_add(add, "Лучший игрок финала ЧТМ", "Лучший игрок финалов ЧТМ", function(y)
        if CustomStats.finals[y] and CustomStats.finals[y].mvp == player then return {year = y} end
    end)
    
    check_and_add(add, "Автор [[Лучший гол турнира|лучшего гола турнира]]", "Автор [[Лучший гол турнира|лучшего гола турнира]]", function(y)
        if CustomStats.finals[y] and has_player(CustomStats.finals[y].best_goal, player) then return {year = y} end
    end)

    local function check_leader(list_arr, title_sing, title_plur, cat)
        check_and_add(list_arr, title_sing, title_plur, function(y)
            if Leaders[cat] and Leaders[cat][y] and Config.utils.has_value(Leaders[cat][y], player) then return {year = y} end
        end)
    end

    check_leader(add, "Лидер по общему числу [[Голы|голов]]", "Лидер по общему числу [[Голы|голов]]", "goals")
    check_leader(add, "Лидер по общему числу [[Голевые передачи|голевых передач]]", "Лидер по общему числу [[Голевые передачи|голевых передач]]", "assists")
    check_leader(add, "Лидер по количеству признаний [[Игрок матча|игроком матча]]", "Лидер по количеству признаний [[Игрок матча|игроком матча]]", "mvp")
    check_leader(add, "Лидер по количеству признаний [[Игрок матча как вратарь|игроком матча в качестве вратаря]]", "Лидер по количеству признаний [[Игрок матча как вратарь|игроком матча в качестве вратаря]]", "mvp_goalie")
    check_leader(add, "[[Самый полезный игрок]]", "[[Самый полезный игрок]]", "plus_minus")
    check_leader(add, "Лидер по числу [[Победы в плей-офф|побед в плей-офф]]", "Лидер по числу [[Победы в плей-офф|побед в плей-офф]]", "playoff_wins")
    check_leader(add, "Лидер по [[Средняя результативность|средней результативности]]", "Лидер по [[Средняя результативность|средней результативности]]", "avg_goals")
    check_leader(add, "Лидер по общему числу [[Матчи|сыгранных матчей]]", "Лидер по общему числу [[Матчи|сыгранных матчей]]", "matches")
    check_leader(add, "[[Лучший пенальтист]]", "[[Лучший пенальтист]]", "pens_scored")
    check_leader(add, "Лидер по общему числу [[Мега-трики|мега-триков]]", "Лидер по общему числу [[Мега-трики|мега-триков]]", "mega_tricks")
    check_leader(add, "Лидер по [[Голы головой|голам головой]]", "Лидер по [[Голы головой|голам головой]]", "head_goals")
    check_leader(add, "Лидер по [[Выносы из пустых|выносам из пустых]]", "Лидер по [[Выносы из пустых|выносам из пустых]]", "clearances")
    check_leader(add, "Лидер по числу [[Отбитые пенальти|отбитых пенальти]]", "Лидер по числу [[Отбитые пенальти|отбитых пенальти]]", "pens_saved")
    check_leader(add, "Лидер по [[Коэффициент пропущенных голов|коэффициенту пропущенных голов]]", "Лидер по [[Коэффициент пропущенных голов|коэффициенту пропущенных голов]]", "gaa")

    local player_teams_best = {}
    for t, info in pairs(TeamLeaders) do
        if t ~= "Нейтрал" and Config.utils.has_value(info.players, player) then table.insert(player_teams_best, {team = t, goals = info.goals}) end
    end
    if #player_teams_best > 0 then
        table.sort(player_teams_best, function(a, b) return a.goals > b.goals end)
        if #player_teams_best == 1 then
            local t_name = ModuleDataTeams.getName(player_teams_best[1].team, 'gen')
            local g_word = (player_teams_best[1].goals % 10 == 1 and player_teams_best[1].goals % 100 ~= 11) and "гол" or (player_teams_best[1].goals % 10 >= 2 and player_teams_best[1].goals % 10 <= 4 and (player_teams_best[1].goals % 100 < 10 or player_teams_best[1].goals % 100 >= 20)) and "гола" or "голов"
            table.insert(add, "* [[Список голов ЧТМ по сборным|Лучший бомбардир]] в истории сборной [[" .. ModuleDataTeams.getName(player_teams_best[1].team) .. "|" .. t_name .. "]] (" .. player_teams_best[1].goals .. " " .. g_word .. ").")
        else
            local t_strs = {}
            for _, info in ipairs(player_teams_best) do table.insert(t_strs, "[[" .. ModuleDataTeams.getName(info.team) .. "]] (" .. info.goals .. ")") end
            local w_map = { [2]="двух", [3]="трёх", [4]="четырёх", [5]="пяти", [6]="шести", [7]="семи", [8]="восьми", [9]="девяти", [10]="десяти", [11]="одиннадцати" }
            local word = w_map[#player_teams_best] or tostring(#player_teams_best)
            table.insert(add, "* [[Список голов ЧТМ по сборным|Лучший бомбардир]] в истории " .. word .. " сборных: " .. table.concat(t_strs, ", ") .. ".")
        end
    end

    local player_stadiums_best = {}
    for st, info in pairs(StadiumLeaders) do
        if Config.utils.has_value(info.players, player) then table.insert(player_stadiums_best, {stadium = st, goals = info.goals}) end
    end
    if #player_stadiums_best > 0 then
        table.sort(player_stadiums_best, function(a, b) return a.goals > b.goals end)
        local s_strs = {}
        for _, info in ipairs(player_stadiums_best) do
            local g_word = (info.goals % 10 == 1 and info.goals % 100 ~= 11) and "гол" or (info.goals % 10 >= 2 and info.goals % 10 <= 4 and (info.goals % 100 < 10 or info.goals % 100 >= 20)) and "гола" or "голов"
            table.insert(s_strs, "«[[" .. info.stadium .. "]]» (" .. info.goals .. " " .. g_word .. ")")
        end
        if #player_stadiums_best == 1 then table.insert(add, "* Лучший бомбардир в истории стадиона " .. s_strs[1] .. ".")
        else table.insert(add, "* Лучший бомбардир в истории стадионов " .. format_list_with_and(s_strs) .. ".") end
    end

    local output = {}
    if #main > 0 then
        table.insert(output, "; Основные:")
        for _, item in ipairs(main) do table.insert(output, item) end
    end
    
    if #add > 0 then
        if #main > 0 then table.insert(output, "") end
        table.insert(output, "; Дополнительные:")
        for _, item in ipairs(add) do table.insert(output, item) end
    end

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

-- ==========================================
-- СБОРКА ТЕКСТА РЕКОРДОВ (для страницы ИГРОКА)
-- ==========================================
local function build_records_text(player)
    local p = Players[player]
    if not p then return "" end

    local function get_val(pp, key)
        if string.sub(key, 1, 12) == "max_streaks_" then return pp.max_streaks[string.sub(key, 13)] or 0 end
        if string.sub(key, 1, 5) == "wins_" then return pp.wins[string.sub(key, 6)] or 0 end
        return pp[key]
    end

    local metrics_all = {
        {"champs", "Наибольшее число [[Список игроков-чемпионов третьего мира|чемпионских титулов]]", false},
        {"champ_captain", "Наибольшее число чемпионских титулов в качестве капитана", false},
        {"champ_streak", "Наибольшее число чемпионских титулов, выигранных подряд", false},
        {"champ_team_streak", "Наибольшее число чемпионских титулов, выигранных подряд в составе одной команды", false},
        {"superchamps", "Наибольшее число [[Суперчемпион|суперчемпионств]]", false},
        {"golden_goals", "Наибольшее число золотых голов", false},
        {"final_goals_real", "Наибольшее число голов в финалах", false},
        {"final_assists_real", "Наибольшее число голевых передач в финалах", false},
        {"final_ga_real", "Наибольшее число очков по системе «гол+пас» в финалах", false},
        {"max_final_goals", "Наибольшее число голов в одном финале", false},
        {"finals_played", "Наибольшее число сыгранных финалов", false},
        {"goals", "Наибольшее число забитых [[Голы|голов]]", false},
        {"max_goals_team", "Наибольшее число голов, забитых за одну сборную", true},
        {"max_goals_match", "Наибольшее число голов, забитых за один матч", false},
        {"mega_tricks", "Наибольшее число [[Мега-трики ЧТМ|мега-триков]]", false},
        {"max_mega_tricks_team", "Наибольшее число мега-триков за одну сборную", true},
        {"classic_hat_tricks", "Наибольшее число классических мега-триков", false},
        {"pokers", "Наибольшее число покеров", false},
        {"pentas", "Наибольшее число пента-триков", false},
        {"hexas", "Наибольшее число гекса-триков", false},
        {"pens_scored", "Наибольшее число забитых пенальти", false},
        {"pens_in_game_scored", "Наибольшее число забитых пенальти в игровое время", false},
        {"max_assists_match", "Наибольшее число голевых передач за один матч", false},
        {"pm", "Наивысший [[показатель полезности]]", false, true},
        {"pts_pct", "Наивысший [[процент набранных очков]]", false, false, true},
        {"md_prizes", "Наибольшее число [[Призы игровых дней|призов игровых дней]]", false},
        {"md_places", "Наибольшее число [[Призы игровых дней|призовых мест игровых дней]]", false},
        {"assists", "Наибольшее число [[Голевые передачи|голевых передач]]", false},
        {"avg_assists", "Наивысший показатель голевых передач в среднем за один матч", false, false, true},
        {"heel_goals", "Наибольшее число [[Голы пяточкой|голов пяточкой]]", false},
        {"golden_spheres", "Наибольшее количество [[Золотой Шар|Золотых Шаров]]", false},
        {"total_spheres", "Наибольшее количество Шаров", false},
        {"golden_shoes", "Наибольшее количество [[Золотой Башмак|Золотых Башмаков]]", false},
        {"total_shoes", "Наибольшее количество Башмаков", false},
        {"wins_goals", "Наибольшее количество побед в зачёте забитых голов", false},
        {"wins_mega_tricks", "Наибольшее количество побед в зачёте мега-триков", false},
        {"wins_pm", "Наибольшее количество побед в зачёте показателя полезности", false},
        {"wins_mvp", "Наибольшее количество побед в зачёте признаний игроком матча", false},
        {"wins_mvp_goalie", "Наибольшее количество побед в зачёте признаний игроком матча в качестве вратаря", false},
        {"wins_head_goals", "Наибольшее количество побед в зачёте голов головой", false},
        {"wins_clearances", "Наибольшее количество побед в зачёте выносов из пустых", false},
        {"wins_gaa", "Наибольшее количество побед в зачёте коэффициента пропущенных голов", false},
        {"max_streaks_team_scored", "Самая продолжительная серия матчей, в каждом из которых команда игрока забивала минимум один гол", false},
        {"max_streaks_matches_played", "Самая продолжительная серия матчей, сыгранных подряд", false},
        {"max_streaks_goals", "Самая продолжительная серия матчей с забитыми голами", false},
        {"max_streaks_double", "Самая продолжительная серия матчей, в каждом из которых был оформлен минимум дубль", false},
        {"max_streaks_hat_trick", "Самая продолжительная серия матчей, в каждом из которых был оформлен минимум хет-трик", false},
        {"max_streaks_poker", "Самая продолжительная серия матчей, в каждом из которых был оформлен минимум покер", false},
        {"max_streaks_ga", "Самая продолжительная серия матчей, в которых игроку удавалось набрать хотя бы одно очко по системе «гол+пас»", false},
        {"max_streaks_missed_matches", "Наибольшее число [[Возвращения|матчей, пропущенных подряд]]", false},
        {"max_gap_days", "Самая длительная [[Возвращения|пауза между матчами]]", false},
        {"avg_goals", "Наивысшая [[средняя результативность]]", false, false, true},
        {"gaa", "Лучший [[коэффициент пропущенных голов]]", false, false, true, true},
        {"mvp", "Наибольшее число признаний [[Игрок матча|игроком матча]]", false},
        {"max_team_mvp", "Наибольшее число признаний игроком матча за одну сборную", true},
        {"mvp_goalie", "Наибольшее число признаний [[Игрок матча как вратарь|игроком матча в качестве вратаря]]", false},
        {"mr_points_pct", "Наивысший процент очков [[мегарейтинг]]а", false, false, true},
        {"pens_saved", "Наибольшее число отбитых пенальти", false},
        {"matches", "Наибольшее число [[Матчи|сыгранных матчей]]", false},
        {"field_matches", "Наибольшее число матчей в поле", false},
        {"goalie_matches", "Наибольшее число матчей в воротах", false},
        {"head_goals", "Наибольшее число [[Голы головой|голов головой]]", false},
        {"clearances", "Наибольшее число [[Выносы из пустых|выносов из пустых]]", false},
        {"max_streaks_goalie_clean", "Наибольшее число матчей подряд за одну команду в качестве вратаря без пропущенных голов", true},
        {"max_streaks_team_matches", "Самая продолжительная серия матчей за одну команду", true},
        {"max_streaks_unbeaten", "Самая продолжительная беспроигрышная серия", false},
        {"max_streaks_win", "Самая продолжительная победная серия", false}
    }

    local hist = {}
    for _, row in ipairs(metrics_all) do
        local key = row[1]; local desc = row[2]; local need_team = row[3]
        local is_pm = row[4]; local is_float = row[5]; local is_min = row[6]

        local p_val = get_val(p, key)
        local global_val = GlobalRecords.AllTime[key]

        if p_val and global_val and p_val == global_val and ((not is_min and p_val > 0) or (is_min and p_val < 9999)) then
            local val_str = tostring(p_val)
            if key == "max_gap_days" then
                val_str = p.max_gap_str
            elseif is_float then 
                val_str = string.format("%.2f", p_val):gsub("%.00$", ""):gsub("%.(%d)0$", ".%1"):gsub("%.", ",")
            elseif is_pm then 
                val_str = (p_val > 0 and "«+" .. p_val .. "»") or "«" .. p_val .. "»" 
            end
            
            if key == "mr_points_pct" then val_str = val_str .. "%" end
            
            if need_team then
                local t_code = p[key .. "_code"] or ""
                val_str = val_str .. " ([[" .. ModuleDataTeams.getName(t_code) .. "]])"
            end
            if key == "final_ga_real" then val_str = val_str .. " (" .. p.final_goals_real .. "+" .. p.final_assists_real .. ")" end

            table.insert(hist, "* " .. desc .. " — " .. val_str .. ".")
        end
    end

    local hc = get_hardcoded_historical(player)
    if hc then
        for line in hc:gmatch("[^\r\n]+") do table.insert(hist, "* " .. line:gsub("^%*?%s*", "")) end
    end

    local metrics_tourney = {
        {"goals", "Наибольшее число [[Голы|голов]]", false},
        {"max_goals_team", "Наибольшее число голов за одну команду", true},
        {"mega_tricks", "Наибольшее число [[Мега-трики|мега-триков]]", false},
        {"pm", "Наивысший [[показатель полезности]]", false, true},
        {"matches", "Наибольшее число [[Матчи|сыгранных матчей]]", false},
        {"pts_pct", "Наивысший [[процент набранных очков]]", false, false, true},
        {"md_prizes", "Наибольшее число [[Призы игровых дней|призов игровых дней]]", false},
        {"md_places", "Наибольшее число [[Призы игровых дней|призовых мест игровых дней]]", false},
        {"heel_goals", "Наибольшее число [[Голы пяточкой|голов пяточкой]]", false},
        {"mvp", "Наибольшее число признаний [[Игрок матча|игроком матча]]", false},
        {"max_team_mvp", "Наибольшее число признаний игроком матча за одну сборную", true},
        {"playoff_wins", "Наибольшее число [[Победы в плей-офф|побед в плей-офф]]", false},
        {"head_goals", "Наибольшее число [[Голы головой|голов головой]]", false},
        {"clearances", "Наибольшее число [[Выносы из пустых|выносов из пустых]]", false},
        {"gaa", "Лучший [[коэффициент пропущенных голов]]", false, false, true, true},
        {"playoff_goals", "Наибольшее число [[Голы в плей-офф|голов в плей-офф]]", false},
        {"winning_goals", "Наибольшее число [[Победные голы|победных голов]]", false},
        {"avg_goals", "Наивысшая [[средняя результативность]]", false, false, true},
        {"max_team_assists", "Наибольшее число [[Голевые передачи|голевых передач]] за одну команду", true},
        {"mvp_goalie", "Наибольшее число признаний игроком матча в качестве вратаря", false},
        {"mr_points", "Наибольшее количество очков [[мегарейтинг]]а", false, false, true},
        {"assists", "Наибольшее число [[Голевые передачи|голевых передач]]", false},
        {"avg_assists", "Наивысший показатель голевых передач в среднем за один матч", false, false, true},
        {"ga", "Наибольшее число очков по системе «гол+пас»", false},
        {"max_team_ga", "Наибольшее число очков по системе «гол+пас» за одну команду", true}
    }

    local tourney = {}
    for _, row in ipairs(metrics_tourney) do
        local key = row[1]; local desc = row[2]; local need_team = row[3]
        local is_pm = row[4]; local is_float = row[5]; local is_min = row[6]

        local global_val = GlobalRecords.PerTournament[key]
        local hits = {}
        for _, t in pairs(p.tournaments) do
            local v = t[key]
            if key == "gaa" and t.goalie_matches < 5 then v = nil end
            if (key == "avg_goals" or key == "avg_assists") and t.field_matches < 20 then v = nil end
            if key == "pts_pct" and t.matches < 20 then v = nil end

            if v and v == global_val and ((not is_min and v >= 0) or (is_min and v >= 0 and v < 9999)) then
                local str = tostring(t.year)
                if need_team then str = str .. ", [[" .. ModuleDataTeams.getName(t[key .. "_code"] or "") .. "]]" end
                table.insert(hits, str)
            end
        end

        if #hits > 0 then
            table.sort(hits)
            local val_str = tostring(global_val)
            if is_float then val_str = string.format("%.2f", global_val):gsub("%.00$", ""):gsub("%.(%d)0$", ".%1"):gsub("%.", ",")
            elseif is_pm then val_str = (global_val > 0 and "«+" .. global_val .. "»") or "«" .. global_val .. "»" end

            if key == "pts_pct" then val_str = val_str .. "%" end

            if key == "ga" then
                local first_t = p.tournaments[tonumber(string.match(hits[1], "%d+"))]
                val_str = val_str .. " (" .. first_t.goals .. "+" .. first_t.assists .. ")"
            elseif key == "max_team_ga" then
                local first_t = p.tournaments[tonumber(string.match(hits[1], "%d+"))]
                local tc = first_t.max_team_ga_code
                val_str = val_str .. " (" .. (first_t.team_goals and first_t.team_goals[tc] or 0) .. "+" .. (first_t.team_assists and first_t.team_assists[tc] or 0) .. ")"
            end

            table.insert(tourney, "* " .. desc .. " — " .. val_str .. " (" .. table.concat(hits, "; ") .. ").")
        end
    end

    local output = {}
    if #hist > 0 then
        table.insert(output, "; Исторические:")
        for _, h in ipairs(hist) do table.insert(output, h) end
    end
    if #tourney > 0 then
        if #hist > 0 then table.insert(output, "") end
        table.insert(output, "; За один турнир:")
        for _, t in ipairs(tourney) do table.insert(output, t) end
    end

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

-- ==========================================
-- ОСНОВНЫЕ ИСПОЛНИТЕЛЬНЫЕ ФУНКЦИИ
-- ==========================================
function PlayerAchievements.main(frame)
    local player = frame.args[1] or frame.args["игрок"] or ""
    player = mw.text.trim(player)
    if player == "" then return "" end

    initialize_data()
    
    local output = {}
    local ach_text = build_achievements_text(player)
    if ach_text and ach_text ~= "" then
        table.insert(output, "== Достижения ==")
        table.insert(output, ach_text)
    end

    local rec_text = build_records_text(player)
    if rec_text and rec_text ~= "" then
        if #output > 0 then table.insert(output, "") end
        table.insert(output, "== Рекорды ==")
        table.insert(output, "<small>'''Примечание'''. Все рекорды указаны на момент завершения [[ЧТМ-" .. Config.years[#Config.years] .. "]].</small>")
        table.insert(output, rec_text)
    end

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

-- ==========================================
-- НОВАЯ ФУНКЦИЯ ДЛЯ ГЕНЕРАЦИИ ПОЛНОЙ СТРАНИЦЫ
-- ==========================================
function PlayerAchievements.debug_records_page(frame)
    initialize_data()

    local all_players = {}
    for _, p in pairs(Players) do
        table.insert(all_players, p)
    end

    local all_tournaments = {}
    for _, p in pairs(Players) do
        for _, t in pairs(p.tournaments) do
            t.name = p.name
            table.insert(all_tournaments, t)
        end
    end

    local all_combos = {}
    for combo, c_data in pairs(GrandStats.PlayerTeam) do
        if c_data.team ~= "Нейтрал" then
            table.insert(all_combos, {
                name = c_data.name, team_code = c_data.team,
                goals = c_data.goals.total,
                mega_tricks = c_data.goals.hat_trick + c_data.goals.poker + c_data.goals.penta + c_data.goals.hexa,
                mvp = c_data.mvp.is_mvp, assists = c_data.assists.total,
                ga = c_data.goals.total + c_data.assists.total
            })
        end
    end

    local tour_combos = {}
    for year, ys in pairs(GrandStats.Years) do
        for combo, c_data in pairs(ys.PlayerTeam) do
             if c_data.team ~= "Нейтрал" then
                 table.insert(tour_combos, {
                     name = c_data.name, team_code = c_data.team, year = year,
                     goals = c_data.goals.total,
                     mega_tricks = c_data.goals.hat_trick + c_data.goals.poker + c_data.goals.penta + c_data.goals.hexa,
                     mvp = c_data.mvp.is_mvp, assists = c_data.assists.total,
                     ga = c_data.goals.total + c_data.assists.total
                 })
             end
        end
    end

    local out = {}
    local function add_h2(text) table.insert(out, "== " .. text .. " ==") end
    local function add_h3(text) table.insert(out, "=== " .. text .. " ===") end
    local function add_h4(text) table.insert(out, "==== " .. text .. " ====") end
    local function add_sep() table.insert(out, "----") end

    local function render_top_4(list, val_func, fmt_func, is_min, limit)
        local valid_items = {}
        for _, item in ipairs(list) do
            local val = val_func(item)
            if val and ((not is_min and val > 0) or (is_min and val >= 0 and val < 9999)) then
                if not limit or (is_min and val <= limit) or (not is_min and val >= limit) then
                    table.insert(valid_items, {item = item, val = val})
                end
            end
        end
        table.sort(valid_items, function(a, b)
            if is_min then return a.val < b.val end
            return a.val > b.val
        end)

        if #valid_items == 0 then return "* ''Нет данных''\n" end

        local result = {}
        local current_rank = 1
        local current_val = valid_items[1].val
        local tie_count = 0

        for i, v_item in ipairs(valid_items) do
            if v_item.val ~= current_val then
                current_rank = current_rank + tie_count
                current_val = v_item.val
                tie_count = 1
            else
                if i > 1 then tie_count = tie_count + 1 end
            end

            if current_rank > 4 then break end

            local is_first = (current_val == valid_items[1].val)
            table.insert(result, fmt_func(v_item.item, is_first, v_item.val))
        end
        return table.concat(result, "\n")
    end

    local function format_val(val, is_float, is_pm, is_pct)
        if type(val) == "string" then return val end
        if is_float then
            val = string.format("%.2f", val):gsub("%.00$", ""):gsub("%.(%d)0$", ".%1"):gsub("%.", ",")
        elseif is_pm then
            val = (val > 0 and "«+" .. val .. "»") or "«" .. val .. "»"
        end
        if is_pct then val = val .. "%" end
        return tostring(val)
    end

    local function fmt_at(p, is_first, val, is_float, is_pm, is_pct)
        local v = format_val(val, is_float, is_pm, is_pct)
        if is_first then return "* '''<u>[[" .. p.name .. "]]</u> (" .. v .. ")'''"
        else return "* [[" .. p.name .. "]] (" .. v .. ")" end
    end

    local function fmt_at_t(c, is_first, val)
        local v = val .. ", [[" .. ModuleDataTeams.getName(c.team_code) .. "]]"
        if is_first then return "* '''<u>[[" .. c.name .. "]]</u> (" .. v .. ")'''"
        else return "* [[" .. c.name .. "]] (" .. v .. ")" end
    end

    local function fmt_tour(t, is_first, val, is_float, is_pm, is_pct)
        local v = format_val(val, is_float, is_pm, is_pct)
        local y = string.sub(tostring(t.year), 3, 4)
        if is_first then return "* '''<u>[[" .. t.name .. "]]</u><sup>[['" .. y .. "]]</sup> (" .. v .. ")'''"
        else return "* [[" .. t.name .. "]]<sup>[['" .. y .. "]]</sup> (" .. v .. ")" end
    end
    
    local function fmt_tour_t(c, is_first, val)
        local v = val .. ", [[" .. ModuleDataTeams.getName(c.team_code) .. "]]"
        local y = string.sub(tostring(c.year), 3, 4)
        if is_first then return "* '''<u>[[" .. c.name .. "]]</u><sup>[['" .. y .. "]]</sup> (" .. v .. ")'''"
        else return "* [[" .. c.name .. "]]<sup>[['" .. y .. "]]</sup> (" .. v .. ")" end
    end

    local function add_block(title, list, val_func, fmt_wrapper, is_min, limit)
        table.insert(out, "; " .. title)
        table.insert(out, render_top_4(list, val_func, fmt_wrapper, is_min, limit))
    end

    -- =======================================
    -- СЛОЖНЫЙ ГЕНЕРАТОР (Хронология, годы, дубликаты)
    -- =======================================
    local function add_detailed_block(title, list, get_data_func, is_min, limit, show_occ)
        table.insert(out, "; " .. title)
        
        local valid_items = {}
        for _, item in ipairs(list) do
            local val, years, tcode = get_data_func(item)
            if val and ((not is_min and val > 0) or (is_min and val >= 0 and val < 9999)) then
                if not limit or (is_min and val <= limit) or (not is_min and val >= limit) then
                    table.insert(valid_items, {
                        item = item, val = val, years = years or {}, tcode = tcode,
                        count = years and #years or 1,
                        first_year = (years and #years > 0) and years[1] or 9999
                    })
                end
            end
        end

        table.sort(valid_items, function(a, b)
            if a.val ~= b.val then
                if is_min then return a.val < b.val else return a.val > b.val end
            end
            if a.count ~= b.count then return a.count > b.count end
            if a.first_year ~= b.first_year then return a.first_year < b.first_year end
            return a.item.name < b.item.name
        end)

        if #valid_items == 0 then table.insert(out, "* ''Нет данных''\n"); return end

        local result = {}
        local current_rank = 1
        local current_val = valid_items[1].val
        local tie_count = 0

        for i, v in ipairs(valid_items) do
            if v.val ~= current_val then
                current_rank = current_rank + tie_count
                current_val = v.val
                tie_count = 1
            else
                if i > 1 then tie_count = tie_count + 1 end
            end
            if current_rank > 4 then break end

            local is_first = (current_val == valid_items[1].val)
            local name_link = "[[" .. v.item.name .. "]]"
            if v.tcode and v.tcode ~= "" then name_link = name_link .. " ([[" .. ModuleDataTeams.getName(v.tcode) .. "]])" end
            
            local inner = tostring(v.val)
            if show_occ and v.count > 1 then
                inner = inner .. ", " .. v.count .. " " .. plural(v.count, "раз", "раза", "раз")
            end

            local prefix = is_first and ("'''<u>" .. name_link .. "</u> (" .. inner .. ")'''") or (name_link .. " (" .. inner .. ")")
            if v.years and #v.years > 0 then
                table.insert(result, "* " .. prefix .. ": " .. format_years_ranges(v.years) .. ".")
            else
                table.insert(result, "* " .. prefix .. ".")
            end
        end
        table.insert(out, table.concat(result, "\n"))
    end

    -- =======================================
    -- СБОРКА СТРАНИЦЫ
    -- =======================================

    add_h2("Исторические рекорды")
    
    add_h3("Финалы")
    add_detailed_block("[[Список игроков-чемпионов третьего мира|Чемпионские титулы]]", all_players, function(p) return p.champs, p.champs_years end)
    add_detailed_block("Чемпионские титулы в качестве капитана", all_players, function(p) return p.champ_captain, p.champ_captain_years end)
    add_detailed_block("[[Суперчемпионство|Суперчемпионства]]", all_players, function(p) return p.superchamps, p.superchamps_years end)
    add_detailed_block("Чемпионские титулы, выигранные подряд", all_players, function(p) return p.champ_streak, get_streak_years(p.champ_streak, p.champ_streak_end_year) end)
    add_detailed_block("Чемпионские титулы, выигранные подряд в составе одной команды", all_players, function(p) return p.champ_team_streak, get_streak_years(p.champ_team_streak, p.champ_team_streak_end_year), p.champ_team_streak_code end, false, 2)
    add_sep()
    add_detailed_block("Золотые голы", all_players, function(p) return p.golden_goals, p.golden_goals_years end)
    add_sep()
    add_block("Голы в финалах", all_players, function(p) return p.final_goals_real end, fmt_at)
    add_sep()
    add_detailed_block("Голы в одном финале", all_players, function(p) return p.max_final_goals, p.max_final_goals_occurences end, false, nil, true)
    add_sep()
    add_block("Голевые передачи в финалах", all_players, function(p) return p.final_assists_real end, fmt_at)
    add_sep()
    add_block("Гол + пас в финалах", all_players, function(p) return p.final_ga_real end, function(p, is_first, val)
        local v = val .. " (" .. p.final_goals_real .. "+" .. p.final_assists_real .. ")"
        if is_first then return "* '''<u>[[" .. p.name .. "]]</u> — " .. v .. "'''"
        else return "* [[" .. p.name .. "]] — " .. v end
    end)
    add_sep()
    add_block("Сыгранные финалы", all_players, function(p) return p.finals_played end, fmt_at)
    add_sep()
    add_detailed_block("Признание лучшим игроком финала", all_players, function(p) return p.mvp_final, p.mvp_final_years end)

    add_h3("Статистические показатели")
    add_h4("За всю историю")
    add_block("[[Голы|Забитые голы]]", all_players, function(p) return p.goals end, fmt_at)
    add_block("[[Список бомбардиров ЧТМ по сборным|Забитые голы за одну сборную]]", all_combos, function(c) return c.goals end, fmt_at_t)
    add_sep()
    add_block("Признание [[Список лучших игроков всех матчей ЧТМ|игроком матча]]", all_players, function(p) return p.mvp end, fmt_at)
    add_block("Признание [[Список лучших игроков матчей ЧТМ по сборным (таблица)|игроком матча за одну сборную]]", all_combos, function(c) return c.mvp end, fmt_at_t)
    add_block("Признание [[Игрок матча как вратарь|игроком матча в качестве вратаря]]", all_players, function(p) return p.mvp_goalie end, fmt_at)
    add_sep()
    add_detailed_block("Наивысшая индивидуальная результативность за матч", all_players, function(p) return p.max_goals_match, p.max_goals_match_occurences end, false, 4, true)
    add_block("[[Мега-трики ЧТМ|<abbr title=\"Количество раз, когда игрок забивал три и более голов в одном матче\">Мега-трики</abbr>]]", all_players, function(p) return p.mega_tricks end, fmt_at)
    add_block("[[Список мега-триков ЧТМ#Два и более мега-трика за одну сборную|Мега-трики за одну сборную]]", all_combos, function(c) return c.mega_tricks end, fmt_at_t, false, 3)
    add_block("[[Список мега-триков ЧТМ#Классические мега-трики|<abbr title=\"Количество раз, когда игрок забивал три и более голов подряд\">Классические мега-трики</abbr>]]", all_players, function(p) return p.classic_hat_tricks end, fmt_at)
    add_block("<abbr title=\"Количество раз, когда игрок забивал четыре и более голов в одном матче\">Покеры</abbr>", all_players, function(p) return p.pokers end, fmt_at)
    add_block("<abbr title=\"Количество раз, когда игрок забивал пять и более голов в одном матче\">Пента-трики</abbr>", all_players, function(p) return p.pentas end, fmt_at)
    add_block("<abbr title=\"Количество раз, когда игрок забивал шесть и более голов в одном матче\">Гекса-трики</abbr>", all_players, function(p) return p.hexas end, fmt_at)
    add_sep()
    add_block("Процент очков [[мегарейтинг]]а", all_players, function(p) return p.mr_points_pct end, function(p, f, v) return fmt_at(p, f, v, true, false, true) end)
    add_sep()
    add_block("Забитые [[пенальти]]", all_players, function(p) return p.pens_scored end, fmt_at)
    add_block("Забитые [[Пенальти#Только в игровое время|пенальти в игровое время]]", all_players, function(p) return p.pens_in_game_scored end, fmt_at, false, 3)
    add_block("[[Пенальти#Отбитые пенальти|Отбитые пенальти]]", all_players, function(p) return p.pens_saved end, fmt_at)
    add_sep()
    add_block("[[Возвращения|Пропущенные матчи]]", all_players, function(p) return p.max_streaks.missed_matches end, fmt_at)
    add_block("[[Возвращения|Временные паузы]] между пропущенными матчами", all_players, function(p) return p.max_gap_days end, function(p, f, v) return fmt_at(p, f, p.max_gap_str, false, false, false) end)
    add_sep()
    add_block("[[Вратарские голы]]", all_players, function(p) return p.goalie_goals end, fmt_at, false, 2)

    add_h4("Начиная с [[ЧТМ-2022]]")
    add_block("[[Показатель полезности]]", all_players, function(p) return p.pm end, function(p, f, v) return fmt_at(p, f, v, false, true) end)
    add_sep()
    add_block("[[Матчи|Сыгранные матчи]]", all_players, function(p) return p.matches end, fmt_at)
    add_block("Матчи в поле", all_players, function(p) return p.field_matches end, fmt_at)
    add_block("Матчи в воротах", all_players, function(p) return p.goalie_matches end, fmt_at)
    add_sep()
    add_block("[[Средняя результативность]] ''(не менее 20 матчей в поле)''", all_players, function(p) return p.field_matches >= 20 and p.avg_goals or nil end, function(p, f, v) return fmt_at(p, f, v, true, false) end)
    add_sep()
    add_block("[[Процент набранных очков]] ''(не менее 20 матчей)''", all_players, function(p) return p.matches >= 20 and p.pts_pct or nil end, function(p, f, v) return fmt_at(p, f, v, true, false, true) end)
    add_sep()
    add_block("[[Призы игровых дней]]", all_players, function(p) return p.md_prizes end, fmt_at)
    add_block("[[Призы игровых дней#Призовые места игровых дней|Призовые места игровых дней]]", all_players, function(p) return p.md_places end, fmt_at)
    add_sep()
    add_block("[[Голы головой]]", all_players, function(p) return p.head_goals end, fmt_at)
    add_sep()
    add_block("[[Выносы из пустых]]", all_players, function(p) return p.clearances end, fmt_at)
    add_sep()
    add_block("[[Коэффициент пропущенных голов]] ''(не менее 5 матчей в воротах)''", all_players, function(p) return p.goalie_matches >= 5 and p.gaa or nil end, function(p, f, v) return fmt_at(p, f, v, true, false) end, true)

    add_h4("Начиная с [[ЧТМ-2026]]")
    add_block("[[Передачи|Голевые передачи]]", all_players, function(p) return p.assists end, fmt_at)
    add_detailed_block("Наибольшее число голевых передач за матч", all_players, function(p) return p.max_assists_match, p.max_assists_match_occurences end, false, 3, true)
    add_block("Голевые передачи (в среднем за матч, ''не менее 20 матчей в поле'')", all_players, function(p) return p.field_matches >= 20 and p.avg_assists or nil end, function(p, f, v) return fmt_at(p, f, v, true, false) end)
    add_sep()
    add_block("[[Голы пяточкой]]", all_players, function(p) return p.heel_goals end, fmt_at, false, 2)

    add_h3("Индивидуальные призы")
    add_h4("За всю историю")
    add_detailed_block("[[Золотой Шар|Золотые Шары]]", all_players, function(p) return p.golden_spheres, p.golden_spheres_years end)
    add_block("Всего Шаров", all_players, function(p) return p.total_spheres end, fmt_at)
    add_sep()
    add_detailed_block("[[Золотой Башмак|Золотые Башмаки]]", all_players, function(p) return p.golden_shoes, p.golden_shoes_years end)
    add_block("Всего Башмаков", all_players, function(p) return p.total_shoes end, fmt_at)
    add_sep()
    add_detailed_block("Победы в зачёте забитых голов", all_players, function(p) return p.wins.goals, p.wins.goals_years end)
    add_sep()
    add_detailed_block("Победы в зачёте мега-триков", all_players, function(p) return p.wins.mega_tricks, p.wins.mega_tricks_years end)
    add_sep()
    add_detailed_block("Победы в зачёте MVP", all_players, function(p) return p.wins.mvp, p.wins.mvp_years end)
    add_detailed_block("Победы в зачёте MVP в качестве вратаря", all_players, function(p) return p.wins.mvp_goalie, p.wins.mvp_goalie_years end)

    add_h4("Начиная с [[ЧТМ-2022]]")
    add_detailed_block("Победы в зачёте показателя полезности", all_players, function(p) return p.wins.pm, p.wins.pm_years end)
    add_sep()
    add_detailed_block("Победы в зачёте голов головой", all_players, function(p) return p.wins.head_goals, p.wins.head_goals_years end)
    add_sep()
    add_detailed_block("Победы в зачёте выносов из пустых", all_players, function(p) return p.wins.clearances, p.wins.clearances_years end)
    add_sep()
    add_detailed_block("Победы в зачёте коэффициента пропущенных голов", all_players, function(p) return p.wins.gaa, p.wins.gaa_years end)

    add_h2("Рекорды за один турнир")
    add_h3("За всю историю")
    add_block("Забитые голы", all_tournaments, function(t) return t.goals end, fmt_tour)
    add_block("Забитые [[голы в плей-офф]]", all_tournaments, function(t) return t.playoff_goals end, fmt_tour)
    add_block("Забитые голы за одну команду", tour_combos, function(c) return c.goals end, fmt_tour_t)
    add_block("[[Победные голы]]", all_tournaments, function(t) return t.winning_goals end, fmt_tour)
    add_sep()
    add_block("Признание игроком матча", all_tournaments, function(t) return t.mvp end, fmt_tour)
    add_block("Признание игроком матча за одну сборную", tour_combos, function(c) return c.mvp end, fmt_tour_t)
    add_block("Признание игроком матча в качестве вратаря", all_tournaments, function(t) return t.mvp_goalie end, fmt_tour)
    add_sep()
    add_block("Мега-трики", all_tournaments, function(t) return t.mega_tricks end, fmt_tour)
    add_sep()
    add_block("Очки [[мегарейтинг]]а", all_tournaments, function(t) return t.mr_points end, function(t, f, v) return fmt_tour(t, f, v, true, false, false) end)

    add_h3("Начиная с [[ЧТМ-2022]]")
    add_block("Показатель полезности", all_tournaments, function(t) return t.pm end, function(t, f, v) return fmt_tour(t, f, v, false, true) end)
    add_sep()
    add_block("Сыгранные матчи", all_tournaments, function(t) return t.matches end, fmt_tour)
    add_sep()
    add_block("Средняя результативность ''(не менее 20 матчей в поле)''", all_tournaments, function(t) return t.field_matches >= 20 and t.avg_goals or nil end, function(t, f, v) return fmt_tour(t, f, v, true, false) end)
    add_sep()
    add_block("Процент набранных очков ''(не менее 20 матчей)''", all_tournaments, function(t) return t.matches >= 20 and t.pts_pct or nil end, function(t, f, v) return fmt_tour(t, f, v, true, false, true) end)
    add_sep()
    add_block("[[Победы в плей-офф]]", all_tournaments, function(t) return t.playoff_wins end, fmt_tour)
    add_sep()
    add_block("[[Призы игровых дней]]", all_tournaments, function(t) return t.md_prizes end, fmt_tour)
    add_block("[[Призы игровых дней#Призовые места игровых дней|Призовые места игровых дней]]", all_tournaments, function(t) return t.md_places end, fmt_tour)
    add_sep()
    add_block("Голы головой", all_tournaments, function(t) return t.head_goals end, fmt_tour)
    add_sep()
    add_block("Выносы из пустых", all_tournaments, function(t) return t.clearances end, fmt_tour)
    add_sep()
    add_block("Голы пяточкой", all_tournaments, function(t) return t.heel_goals end, fmt_tour)
    add_sep()
    add_block("[[Коэффициент пропущенных голов]] ''(не менее 5 матчей в воротах)''", all_tournaments, function(t) return t.goalie_matches >= 5 and t.gaa or nil end, function(t, f, v) return fmt_tour(t, f, v, true, false) end, true)

    add_h3("Начиная с [[ЧТМ-2026]]")
    add_block("Голевые передачи", all_tournaments, function(t) return t.assists end, fmt_tour)
    add_block("Голевые передачи за одну команду", tour_combos, function(c) return c.assists end, fmt_tour_t)
    add_block("Голевые передачи (в среднем за матч, ''не менее 20 матчей в поле'')", all_tournaments, function(t) return t.field_matches >= 20 and t.avg_assists or nil end, function(t, f, v) return fmt_tour(t, f, v, true, false) end)
    add_sep()
    add_block("Гол + пас", all_tournaments, function(t) return t.ga end, function(t, f, v) 
        local val = v .. " (" .. t.goals .. "+" .. t.assists .. ")"
        if f then return "* '''<u>[[" .. t.name .. "]]</u><sup>[['" .. string.sub(tostring(t.year), 3, 4) .. "]]</sup> (" .. val .. ")'''"
        else return "* [[" .. t.name .. "]]<sup>[['" .. string.sub(tostring(t.year), 3, 4) .. "]]</sup> (" .. val .. ")" end
    end)
    add_block("Гол + пас за одну команду", tour_combos, function(c) return c.ga end, function(c, f, v) 
        local val = v .. " (" .. c.goals .. "+" .. c.assists .. "), [[" .. ModuleDataTeams.getName(c.team_code) .. "]]"
        local y = string.sub(tostring(c.year), 3, 4)
        if f then return "* '''<u>[[" .. c.name .. "]]</u><sup>[['" .. y .. "]]</sup> (" .. val .. ")'''"
        else return "* [[" .. c.name .. "]]<sup>[['" .. y .. "]]</sup> (" .. val .. ")" end
    end)

    add_h2("Рекордные серии матчей")
    
    local function render_streaks_top_4(title, streak_list, limit)
        if not streak_list or #streak_list == 0 then return end
        
        table.sort(streak_list, function(a, b)
            if a.len ~= b.len then return a.len > b.len end
            if a.end_year ~= b.end_year then return a.end_year < b.end_year end
            return a.player < b.player
        end)
        
        table.insert(out, "; " .. title)
        
        local valid_items = {}
        for _, s in ipairs(streak_list) do
            if s.len > 0 and (not limit or s.len >= limit) then table.insert(valid_items, s) end
        end
        if #valid_items == 0 then table.insert(out, "* ''Нет данных''\n"); return end
        
        local result = {}
        local current_rank = 1
        local current_val = valid_items[1].len
        local tie_count = 1
        
        for i, s in ipairs(valid_items) do
            if i > 1 then
                if s.len ~= current_val then
                    current_rank = current_rank + tie_count
                    current_val = s.len
                    tie_count = 1
                else
                    tie_count = tie_count + 1
                end
            end
            
            if current_rank > 4 then break end
            
            local is_first = (current_val == valid_items[1].len)
            local active_str = s.is_active and ", серия продолжается" or ""
            local year_str = (s.start_year == s.end_year) and tostring(s.start_year) or (s.start_year .. "—" .. s.end_year)
            
            local team_str = ""
            if s.team then team_str = " (" .. ModuleDataTeams.getName(s.team) .. ")" end
            
            local text = string.format("[[%s]]%s — %d (%s%s)", s.player, team_str, s.len, year_str, active_str)
            
            if is_first then table.insert(result, "* '''<u>" .. text .. "</u>'''")
            else table.insert(result, "* " .. text) end
        end
        table.insert(out, table.concat(result, "\n"))
    end

    render_streaks_top_4("Матчи, сыгранные подряд", GlobalStreaks.all_completed["seq"])
    render_streaks_top_4("Серия матчей за одну команду", GlobalStreaks.all_team_completed)
    render_streaks_top_4("Победная серия", GlobalStreaks.all_completed["win"])
    render_streaks_top_4("Беспроигрышная серия", GlobalStreaks.all_completed["unbeaten"])
    render_streaks_top_4("Серия забитых голов", GlobalStreaks.all_completed["goals"])
    render_streaks_top_4("Серия дублей", GlobalStreaks.all_completed["double"], 4)
    render_streaks_top_4("Серия хет-триков", GlobalStreaks.all_completed["hat_trick"])
    render_streaks_top_4("Серия покеров", GlobalStreaks.all_completed["poker"], 2)
    render_streaks_top_4("Серия матчей, в которых команда игрока забивала голы", GlobalStreaks.all_completed["team_scored"])
    render_streaks_top_4("Серия матчей без пропущенных голов за одну команду", GlobalStreaks.all_goalie_completed, 2)
    render_streaks_top_4("Серия матчей хотя бы с одним очком по системе гол+пас", GlobalStreaks.all_completed["ga"])
    render_streaks_top_4("Серия матчей хотя бы с одним голом и одним пасом", GlobalStreaks.all_completed["both_ga"])

    return table.concat(out, "\n")
end

return PlayerAchievements