Files
2026-05-29 15:11:31 +07:00

512 lines
23 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 03).
____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