local ____lualib = require("lualib_bundle") local __TS__ArrayMap = ____lualib.__TS__ArrayMap local __TS__Class = ____lualib.__TS__Class local ____exports = {} local ____battle_pass_server = require("battle_pass_server") local BattlePassServer = ____battle_pass_server.BattlePassServer local ____difficulty_manager = require("difficulty_manager") local Difficulty = ____difficulty_manager.Difficulty local ____game_stats_tracker = require("game_stats_tracker") local GameStatsTracker = ____game_stats_tracker.GameStatsTracker local ____match_end_combat_stats = require("match_end_combat_stats") local MatchEndCombatStats = ____match_end_combat_stats.MatchEndCombatStats local ____store_manager = require("store_manager") local StoreManager = ____store_manager.StoreManager local ____ArsenalCatalog = require("arsenal.ArsenalCatalog") local ARSENAL_ITEMS = ____ArsenalCatalog.ARSENAL_ITEMS local ____ArsenalStats = require("arsenal.ArsenalStats") local contractRewardMultToDropStatBias01 = ____ArsenalStats.contractRewardMultToDropStatBias01 local ____ArsenalManager = require("arsenal.ArsenalManager") local grantArsenalItemDetailed = ____ArsenalManager.grantArsenalItemDetailed local flushArsenalInventoryToClientAndApi = ____ArsenalManager.flushArsenalInventoryToClientAndApi local ____luck = require("utils.luck") local rollLuckChance = ____luck.rollLuckChance local getLuck = ____luck.getLuck --- За каждые N единиц удачи героя — доп. предмет арсенала: на Death Sentence любое качество, иначе только epic и ниже. local ARSENAL_BONUS_DROP_LUCK_PER_ITEM = 100 local ARSENAL_FULL_RANDOM_QUALITIES = { "common", "rare", "epic", "legendary", "mythic" } --- Бонус за удачу вне DS: только common / rare / epic. local ARSENAL_LUCK_BONUS_CAP_EPIC_QUALITIES = {"common", "rare", "epic"} --- Качество бонусного дропа за удачу (полный спектр только на смертельном приговоре). local function rollArsenalLuckBonusDropQuality(self) if Difficulty.leader == "death_sentence" then return ARSENAL_FULL_RANDOM_QUALITIES[RandomInt(0, #ARSENAL_FULL_RANDOM_QUALITIES - 1) + 1] end return ARSENAL_LUCK_BONUS_CAP_EPIC_QUALITIES[RandomInt(0, #ARSENAL_LUCK_BONUS_CAP_EPIC_QUALITIES - 1) + 1] end --- Фиксированное число строк таблицы (слоты Radiant 0–3). ____exports.MATCH_END_PLAYER_SLOTS = 4 --- Множитель к итоговой награде за победу, если у игрока куплен Battle Pass Premium (+50%). local MATCH_END_PREMIUM_REWARD_MULT = 1.5 --- Случайный шаблон из каталога (любой слот); редкость экземпляра задаётся отдельно при выдаче. local function pickRandomArsenalTemplateItemName(self) local n = #ARSENAL_ITEMS if n <= 0 then return nil end local idx = RandomInt(0, n - 1) local ____opt_0 = ARSENAL_ITEMS[idx + 1] return ____opt_0 and ____opt_0.itemName or nil end local function rollArsenalDropQuality(self, playerId) local hero = PlayerResource:GetSelectedHeroEntity(playerId) if Difficulty.leader == "easy" then if hero and rollLuckChance(nil, hero, 0.12) then return "rare" end return "common" end if Difficulty.leader == "normal" then if hero and rollLuckChance(nil, hero, 0.3) then return "rare" end return "common" end if Difficulty.leader == "hard" then if hero and rollLuckChance(nil, hero, 0.18) then return "epic" end return "rare" end if Difficulty.leader == "impossible" then if hero and rollLuckChance(nil, hero, 0.3) then return "legendary" end return "epic" end local snap = Difficulty:getWinningContractSnapshot() local rarity = snap and snap.rarity or "epic" local rewardMult = snap and snap.rewardMultiplier or Difficulty:getDeathSentenceContractRewardMultiplier() local multBias = contractRewardMultToDropStatBias01(nil, rewardMult) local mythicChance = 8 if rarity == "common" then mythicChance = 2 end if rarity == "rare" then mythicChance = 4 end if rarity == "epic" then mythicChance = 10 end if rarity == "legendary" then mythicChance = 18 end if rarity == "mythic" then mythicChance = 26 end mythicChance = math.min( 32, mythicChance + math.floor(multBias * 6) ) if hero and rollLuckChance(nil, hero, mythicChance / 100) then return "mythic" end local legendaryRoll = math.min(0.72, 0.55 + multBias * 0.22) if hero and rollLuckChance(nil, hero, legendaryRoll) then return "legendary" end return "epic" end local function getArsenalWinDropCount(self) local diffMult = Difficulty:getNpcStatScale() local scaled = math.ceil(diffMult * 0.75) local base = math.max(1, scaled) local bonusTiers = math.floor(diffMult / 5) local bonus = bonusTiers > 0 and bonusTiers or 0 return math.max(1, base + bonus) end local function grantArsenalWinDrops(self, playerId) local granted = {} local deferredPersistence = false local diffMult = Difficulty:getNpcStatScale() local dropCount = getArsenalWinDropCount(nil) local arsenalRollContext = Difficulty.leader == "death_sentence" and ({contractDropStatBias = contractRewardMultToDropStatBias01( nil, Difficulty:getDeathSentenceContractRewardMultiplier() )}) or nil local baseScaled = math.ceil(diffMult * 0.75) local baseOnly = math.max(1, baseScaled) local bonusTiers = math.floor(diffMult / 5) print((((((((((("[MatchEndRewards] player=" .. tostring(playerId)) .. " roll arsenal drops: count=") .. tostring(dropCount)) .. " (base=") .. tostring(baseOnly)) .. ", bonus_tiers_5=") .. tostring(bonusTiers)) .. "), difficulty=") .. Difficulty.leader) .. ", mult=") .. tostring(diffMult)) do local i = 0 while i < dropCount do local quality = rollArsenalDropQuality(nil, playerId) local itemName = pickRandomArsenalTemplateItemName(nil) if itemName then deferredPersistence = true local detailed = grantArsenalItemDetailed( nil, playerId, itemName, 1, quality, arsenalRollContext, {deferPersistence = true} ) local created = detailed[1] if created ~= nil then granted[#granted + 1] = {item_name = created.itemName, instance_id = created.instanceId, quality = quality} end print((((((((("[MatchEndRewards] player=" .. tostring(playerId)) .. " drop ") .. tostring(i + 1)) .. "/") .. tostring(dropCount)) .. ": quality=") .. quality) .. ", item=") .. itemName) else print((((((("[MatchEndRewards] player=" .. tostring(playerId)) .. " drop ") .. tostring(i + 1)) .. "/") .. tostring(dropCount)) .. ": empty pool for quality=") .. quality) end i = i + 1 end end local hero = PlayerResource:GetSelectedHeroEntity(playerId) local bonusFromLuck = hero and math.floor(getLuck(nil, hero) / ARSENAL_BONUS_DROP_LUCK_PER_ITEM) or 0 if bonusFromLuck > 0 then print(((((("[MatchEndRewards] player=" .. tostring(playerId)) .. " luck bonus arsenal drops: ") .. tostring(bonusFromLuck)) .. " (luck=") .. tostring(hero and getLuck(nil, hero) or 0)) .. ")") end do local b = 0 while b < bonusFromLuck do local quality = rollArsenalLuckBonusDropQuality(nil) local itemName = pickRandomArsenalTemplateItemName(nil) if itemName then deferredPersistence = true local detailed = grantArsenalItemDetailed( nil, playerId, itemName, 1, quality, arsenalRollContext, {deferPersistence = true} ) local created = detailed[1] if created ~= nil then granted[#granted + 1] = {item_name = created.itemName, instance_id = created.instanceId, quality = quality} end print((((((((("[MatchEndRewards] player=" .. tostring(playerId)) .. " luck-bonus drop ") .. tostring(b + 1)) .. "/") .. tostring(bonusFromLuck)) .. ": quality=") .. quality) .. ", item=") .. itemName) end b = b + 1 end end if deferredPersistence then flushArsenalInventoryToClientAndApi(nil, playerId) end print((("[MatchEndRewards] player=" .. tostring(playerId)) .. " arsenal drops result: ") .. (#granted > 0 and table.concat( __TS__ArrayMap( granted, function(____, x) return ((x.item_name .. "(") .. x.instance_id) .. ")" end ), ", " ) or "none")) return granted end --- Итог награды за матч: одно округление в конце (синхронно с клиентом по payload). function ____exports.computeMatchRewardAmount(self, difficultyScale, creepKills, deaths) local preScale = 50 + creepKills * 0.1 local scaled = preScale * difficultyScale local deathMultiplier = math.max(0, 1 - deaths * 0.1) local raw = scaled * deathMultiplier return math.floor(math.max(0, raw)) end --- Только имя из движка; пустую строку отдаём в payload — отображение добирает клиент (GetPlayerInfo). local function getPlayerDisplayName(self, playerId) local raw = PlayerResource:GetPlayerName(playerId) if raw ~= nil and raw ~= nil and tostring(raw) ~= "" then return tostring(raw) end return "" end local function buildEmptyRow(self, slot, _outcome, _grantsBlocked) local rewardAmount = 0 return { player_id = slot, player_name = "", hero_name = "", creep_kills = 0, deaths = 0, damage_dealt = 0, damage_taken = 0, heal_to_allies = 0, free_currency = 0, bp_level = 1, bp_experience = 0, quests_completed_count = 0, reward_amount = rewardAmount, slot_empty = true, breakdown = { base_linear = 0, creep_term = 0, quest_term = 0, death_penalty = 0, total = rewardAmount } } end ____exports.MatchEndRewards = __TS__Class() local MatchEndRewards = ____exports.MatchEndRewards MatchEndRewards.name = "MatchEndRewards" MatchEndRewards.____file_path = "scripts/vscripts/match_end_rewards.lua" function MatchEndRewards.prototype.____constructor(self) end function MatchEndRewards.buildPayload(self, outcome) --- В DS итоговый множик награды = контракт (полосы 3–7+ по редкости); иначе — шкала сложности. local difficultyScale = Difficulty.leader == "death_sentence" and Difficulty:getDeathSentenceContractRewardMultiplier() or Difficulty:getNpcStatScale() local difficultyKey = Difficulty.leader or "normal" local stats = GameStatsTracker:getInstance() local grantsBlocked = stats:shouldBlockMatchEndRewards() local rows = {} local store = StoreManager:getInstance() local bp = BattlePassServer:getInstance() local combat = MatchEndCombatStats:getInstance() do local slot = 0 while slot < ____exports.MATCH_END_PLAYER_SLOTS do do local playerId = slot local key = tostring(slot) if not PlayerResource:IsValidPlayerID(playerId) or not PlayerResource:IsValidPlayer(playerId) or PlayerResource:IsFakeClient(playerId) then rows[key] = buildEmptyRow(nil, slot, outcome, grantsBlocked) goto __continue40 end local hero = PlayerResource:GetSelectedHeroEntity(playerId) if not hero then rows[key] = buildEmptyRow(nil, slot, outcome, grantsBlocked) goto __continue40 end local creepKills = PlayerResource:GetNearbyCreepDeaths(playerId) local deaths = PlayerResource:GetDeaths(playerId) --- Как defension: реальный урон через DamageFilter (match_end_combat_stats). local damageDealt = combat:getOutgoingDamageSum(playerId) local damageTaken = combat:getIncomingDamageSum(playerId) local healToAllies = combat:getHealToAlliesSum(playerId) local questsCompleted = bp:getNpcQuestsCompletedForPlayer(playerId) local snap = bp:getBattlePassSnapshotForMatchEnd(playerId) local baseLinear = 50 local creepTerm = creepKills * 0.1 local questTerm = 0 local deathMultiplier = math.max(0, 1 - deaths * 0.1) local totalBeforeDeathPenalty = (baseLinear + creepTerm + questTerm) * difficultyScale local deathPenalty = math.max(0, totalBeforeDeathPenalty - totalBeforeDeathPenalty * deathMultiplier) local computedTotal = ____exports.computeMatchRewardAmount(nil, difficultyScale, creepKills, deaths) local rewardAmount = computedTotal if outcome == "loss" or grantsBlocked then rewardAmount = 0 end local hasPremium = snap.has_premium == true local totalBeforePremium = nil if outcome == "win" and not grantsBlocked and rewardAmount > 0 and hasPremium then totalBeforePremium = rewardAmount rewardAmount = math.floor(rewardAmount * MATCH_END_PREMIUM_REWARD_MULT) end rows[key] = { player_id = playerId, player_name = getPlayerDisplayName(nil, playerId), hero_name = hero:GetUnitName(), creep_kills = creepKills, deaths = deaths, damage_dealt = damageDealt, damage_taken = damageTaken, heal_to_allies = healToAllies, free_currency = store:getFreeCurrency(playerId), bp_level = snap.level, bp_experience = snap.experience, quests_completed_count = questsCompleted, reward_amount = rewardAmount, has_battle_pass_premium = hasPremium, slot_empty = false, breakdown = { base_linear = baseLinear, creep_term = creepTerm, quest_term = questTerm, death_penalty = deathPenalty, total_before_premium = totalBeforePremium, total = rewardAmount } } end ::__continue40:: slot = slot + 1 end end return {outcome = outcome, difficulty_scale = difficultyScale, difficulty_key = difficultyKey, players = rows} end function MatchEndRewards.previewForDevPlayer(self, playerId, outcome) if outcome == nil then outcome = "win" end if not IsServer() then return end do local function ____catch() return true end local ____try, ____hasReturned, ____returnValue = pcall(function() local tools = _G.IsInToolsMode if not tools or not tools(nil) then return true end end) if not ____try then ____hasReturned, ____returnValue = ____catch() end if ____hasReturned then return ____returnValue end end local player = PlayerResource:GetPlayer(playerId) if not player then return end local payload = ____exports.MatchEndRewards:buildPayload(outcome) CustomGameEventManager:Send_ServerToPlayer(player, "match_end_screen", {outcome = outcome, payload = payload}) end function MatchEndRewards.registerStateListenerForMatchEnd(self) ListenToGameEvent( "game_rules_state_change", function() if GameRules:State_Get() == DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then ____exports.MatchEndRewards.dispatched = false end end, nil ) end function MatchEndRewards.dispatch(self, outcome, onFullyDispatched) if not IsServer() then return end if ____exports.MatchEndRewards.dispatched then return end ____exports.MatchEndRewards.dispatched = true local stats = GameStatsTracker:getInstance() local grantsBlocked = stats:shouldBlockMatchEndRewards() local payload = ____exports.MatchEndRewards:buildPayload(outcome) local screenSent = false local function sendScreen() if screenSent then return end screenSent = true CustomGameEventManager:Send_ServerToAllClients("match_end_screen", {outcome = outcome, payload = payload}) end local function finish() sendScreen(nil) if onFullyDispatched then onFullyDispatched(nil) end end do local function ____catch(e) print("[MatchEndRewards] reward dispatch error: " .. tostring(e)) end local ____try, ____hasReturned, ____returnValue = pcall(function() if outcome == "loss" and not grantsBlocked and Difficulty.leader == "death_sentence" then Difficulty:applyDeathSentenceContractDurabilityOnLoss(function() finish(nil) end) return true end if outcome == "win" and not grantsBlocked then local store = StoreManager:getInstance() local bp = BattlePassServer:getInstance() local hasAnyRealWinner = false local winnersBySlot = {} do local slot = 0 while slot < ____exports.MATCH_END_PLAYER_SLOTS do do local row = payload.players[tostring(slot)] if not row or row.slot_empty then goto __continue65 end hasAnyRealWinner = true winnersBySlot[#winnersBySlot + 1] = slot local pid = row.player_id local amt = row.reward_amount if amt > 0 then bp:addExperience(pid, amt) store:grantFreeCurrencyMatchEndReward(pid, amt) end row.arsenal_drops = grantArsenalWinDrops(nil, pid) print((((((("[MatchEndRewards] payload row slot=" .. tostring(slot)) .. " pid=") .. tostring(pid)) .. ": reward=") .. tostring(amt)) .. ", arsenal_drops=") .. (row.arsenal_drops and #row.arsenal_drops > 0 and table.concat( __TS__ArrayMap( row.arsenal_drops, function(____, x) return ((x.item_name .. "(") .. x.instance_id) .. ")" end ), ", " ) or "none")) end ::__continue65:: slot = slot + 1 end end if hasAnyRealWinner then local pendingContractSaves = 0 for ____, slot in ipairs(winnersBySlot) do do local row = payload.players[tostring(slot)] if not row or row.slot_empty then goto __continue70 end local pid = row.player_id local contract = Difficulty:grantMatchEndContractIfEligible(pid) if not contract then goto __continue70 end local info = { instance_id = contract.instanceId, serial = contract.serial, rarity = contract.rarity, title_index = contract.titleIndex, reward_multiplier = contract.rewardMultiplier, trait_id = contract.traitId, complication_ids = {unpack(contract.complicationIds)}, durability = contract.durability, durability_max = contract.durabilityMax } pendingContractSaves = pendingContractSaves + 1 Difficulty:saveContractRosterForPlayer( pid, function(____, ok) if ok then row.contract_drop = info print((((((("[MatchEndRewards] payload row slot=" .. tostring(slot)) .. ": contract_drop saved id=") .. info.instance_id) .. ", rarity=") .. info.rarity) .. ", title=") .. tostring(info.title_index)) else print((((("[MatchEndRewards] contract_drop save failed slot=" .. tostring(slot)) .. " pid=") .. tostring(row.player_id)) .. " id=") .. info.instance_id) end pendingContractSaves = pendingContractSaves - 1 if pendingContractSaves <= 0 then finish(nil) end end ) end ::__continue70:: end if pendingContractSaves > 0 then return true end print("[MatchEndRewards] no contract drop: difficulty=" .. Difficulty.leader) end end end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end finish(nil) end MatchEndRewards.dispatched = false if IsServer() then ____exports.MatchEndRewards:registerStateListenerForMatchEnd() end return ____exports