72b73c4dd6
Allows the game client to make HTTP API calls from a listen server (local lobby) instead of requiring a Steam dedicated server. CreateHTTPRequestScriptVM has the exact same API signature but works in both dedicated server and listen server contexts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
821 lines
33 KiB
Lua
821 lines
33 KiB
Lua
local ____lualib = require("lualib_bundle")
|
|
local Map = ____lualib.Map
|
|
local __TS__New = ____lualib.__TS__New
|
|
local Set = ____lualib.Set
|
|
local __TS__Number = ____lualib.__TS__Number
|
|
local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
|
|
local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign
|
|
local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite
|
|
local __TS__ObjectEntries = ____lualib.__TS__ObjectEntries
|
|
local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
|
|
local __TS__ArrayIncludes = ____lualib.__TS__ArrayIncludes
|
|
local __TS__ArrayEvery = ____lualib.__TS__ArrayEvery
|
|
local ____exports = {}
|
|
local getPurchasableCardPool, getPurchasablePoolByQuality, pickCardExactQuality, purchasableCardPoolCache, purchasablePoolByQualityCache, ARCADE_PACK_QUALITY_PICK_ATTEMPTS
|
|
local ____card_catalog = require("card_catalog")
|
|
local ALL_CARD_CATALOG_DEFS = ____card_catalog.ALL_CARD_CATALOG_DEFS
|
|
local ____CardSystem = require("cards.CardSystem")
|
|
local CardQuality = ____CardSystem.CardQuality
|
|
local CardSystem = ____CardSystem.CardSystem
|
|
local ____custom_game_events = require("custom_game_events")
|
|
local ensurePlayerCardSystem = ____custom_game_events.ensurePlayerCardSystem
|
|
local ____api_helper = require("api_helper")
|
|
local encodeApiBody = ____api_helper.encodeApiBody
|
|
local setApiHeaders = ____api_helper.setApiHeaders
|
|
local ____player_info = require("player_info")
|
|
local PlayerInfo = ____player_info.PlayerInfo
|
|
local ____server_config = require("server_config")
|
|
local SERVER_CONFIG = ____server_config.SERVER_CONFIG
|
|
local ____store_manager = require("store_manager")
|
|
local StoreManager = ____store_manager.StoreManager
|
|
function getPurchasableCardPool(self)
|
|
if purchasableCardPoolCache and #purchasableCardPoolCache > 0 then
|
|
return purchasableCardPoolCache
|
|
end
|
|
local pool = {}
|
|
for ____, def in ipairs(ALL_CARD_CATALOG_DEFS) do
|
|
do
|
|
local id = math.floor(__TS__Number(def.id))
|
|
if not __TS__NumberIsFinite(id) or id <= 0 or id == 404 then
|
|
goto __continue46
|
|
end
|
|
if def.defaultCard == true or def.purchasable == false then
|
|
goto __continue46
|
|
end
|
|
local runtimeData = CardSystem.cardData[id]
|
|
if (runtimeData and runtimeData.disabled) == true then
|
|
goto __continue46
|
|
end
|
|
pool[#pool + 1] = id
|
|
end
|
|
::__continue46::
|
|
end
|
|
if #pool == 0 then
|
|
for ____, ____value in ipairs(__TS__ObjectEntries(CardSystem.cardData)) do
|
|
local idKey = ____value[1]
|
|
local data = ____value[2]
|
|
do
|
|
local id = math.floor(__TS__Number(idKey))
|
|
if not __TS__NumberIsFinite(id) or id <= 0 or data.disabled == true or data.default == true or data.purchasable == false then
|
|
goto __continue52
|
|
end
|
|
pool[#pool + 1] = id
|
|
end
|
|
::__continue52::
|
|
end
|
|
end
|
|
purchasableCardPoolCache = pool
|
|
purchasablePoolByQualityCache = nil
|
|
return pool
|
|
end
|
|
function getPurchasablePoolByQuality(self)
|
|
if purchasablePoolByQualityCache then
|
|
return purchasablePoolByQualityCache
|
|
end
|
|
local byQuality = {}
|
|
for ____, cardId in ipairs(getPurchasableCardPool(nil)) do
|
|
local cardData = CardSystem.cardData[cardId]
|
|
local quality = math.max(
|
|
CardQuality.COMMON,
|
|
math.min(
|
|
CardQuality.MYTHIC,
|
|
math.floor(__TS__Number(cardData and cardData.quality or CardQuality.COMMON))
|
|
)
|
|
)
|
|
if not byQuality[quality] then
|
|
byQuality[quality] = {}
|
|
end
|
|
local ____byQuality_quality_11 = byQuality[quality]
|
|
____byQuality_quality_11[#____byQuality_quality_11 + 1] = cardId
|
|
end
|
|
purchasablePoolByQualityCache = byQuality
|
|
return byQuality
|
|
end
|
|
function pickCardExactQuality(self, quality, excludeIds)
|
|
local pool = getPurchasablePoolByQuality(nil)[quality]
|
|
if not pool or #pool == 0 then
|
|
return nil
|
|
end
|
|
local exclude = __TS__New(Set, excludeIds)
|
|
local available = __TS__ArrayFilter(
|
|
pool,
|
|
function(____, id) return not exclude:has(id) end
|
|
)
|
|
local pickFrom = #available > 0 and available or pool
|
|
return pickFrom[RandomInt(0, #pickFrom - 1) + 1]
|
|
end
|
|
____exports.ARCADE_PACK_DEFINITIONS = {arcade_pack_standard = {id = "arcade_pack_standard", cardsCount = 3, price_free = 25000, allowed_currencies = {"free_currency"}}, arcade_pack_premium = {id = "arcade_pack_premium", cardsCount = 3, price_donate = 80, allowed_currencies = {"donate_currency"}}}
|
|
--- Пороги pity: паков подряд без редкости → гарантия в следующем паке.
|
|
local ARCADE_PITY_STANDARD_MYTHIC_PACKS = 100
|
|
local ARCADE_PITY_STANDARD_LEGENDARY_PACKS = 50
|
|
local ARCADE_PITY_PREMIUM_MYTHIC_PACKS = 10
|
|
local ARCADE_SESSION_TTL_SECONDS = 600
|
|
local sessionsByPlayerId = __TS__New(Map)
|
|
local arcadePityByPlayerId = __TS__New(Map)
|
|
local arcadePityLoadedFromBackend = __TS__New(Set)
|
|
local function defaultArcadePityState(self)
|
|
return {standardPacksWithoutMythic = 0, standardPacksWithoutLegendary = 0, premiumPacksWithoutMythic = 0}
|
|
end
|
|
local function normalizeArcadePityState(self, raw)
|
|
local state = defaultArcadePityState(nil)
|
|
if not raw or type(raw) ~= "table" then
|
|
return state
|
|
end
|
|
local obj = raw
|
|
state.standardPacksWithoutMythic = math.max(
|
|
0,
|
|
math.floor(__TS__Number(obj.standardPacksWithoutMythic) or 0)
|
|
)
|
|
local ____math_max_2 = math.max
|
|
local ____math_floor_1 = math.floor
|
|
local ____obj_standardPacksWithoutLegendary_0 = obj.standardPacksWithoutLegendary
|
|
if ____obj_standardPacksWithoutLegendary_0 == nil then
|
|
____obj_standardPacksWithoutLegendary_0 = obj.standardPacksWithoutEpic
|
|
end
|
|
state.standardPacksWithoutLegendary = ____math_max_2(
|
|
0,
|
|
____math_floor_1(__TS__Number(____obj_standardPacksWithoutLegendary_0) or 0)
|
|
)
|
|
state.premiumPacksWithoutMythic = math.max(
|
|
0,
|
|
math.floor(__TS__Number(obj.premiumPacksWithoutMythic) or 0)
|
|
)
|
|
return state
|
|
end
|
|
local function decodeJsonBody(self, body)
|
|
do
|
|
local function ____catch()
|
|
return true, nil
|
|
end
|
|
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
|
return true, {json.decode(body)}
|
|
end)
|
|
if not ____try then
|
|
____hasReturned, ____returnValue = ____catch()
|
|
end
|
|
if ____hasReturned then
|
|
return ____returnValue
|
|
end
|
|
end
|
|
end
|
|
local function unwrapApiJsonObject(self, decoded)
|
|
if __TS__ArrayIsArray(decoded) and #decoded > 0 then
|
|
return decoded[1]
|
|
end
|
|
return decoded
|
|
end
|
|
local function parseArcadePityPayload(self, raw)
|
|
if not raw or type(raw) ~= "table" then
|
|
return nil
|
|
end
|
|
local root = raw
|
|
local pityRaw = root.arcade_pity
|
|
if pityRaw == nil or pityRaw == nil then
|
|
return normalizeArcadePityState(nil, nil)
|
|
end
|
|
return normalizeArcadePityState(nil, pityRaw)
|
|
end
|
|
local function applyArcadePityState(self, playerId, state)
|
|
arcadePityByPlayerId:set(playerId, state)
|
|
local info = PlayerInfo:GetPlayerInfo(playerId) or ({})
|
|
PlayerInfo:UpdatePlayerInfo(
|
|
playerId,
|
|
__TS__ObjectAssign({}, info, {arcade_pity = state})
|
|
)
|
|
end
|
|
local function saveArcadePityToBackend(self, playerId, state)
|
|
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
|
if not steamId then
|
|
return
|
|
end
|
|
local request = CreateHTTPRequestScriptVM(
|
|
"PUT",
|
|
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arcade_pity"
|
|
)
|
|
setApiHeaders(nil, request)
|
|
request:SetHTTPRequestRawPostBody(
|
|
"application/json",
|
|
encodeApiBody(nil, {arcade_pity = state})
|
|
)
|
|
request:Send(function(result)
|
|
if result.StatusCode < 200 or result.StatusCode >= 300 then
|
|
print((((("[ARCADE_PITY] PUT fail player=" .. tostring(playerId)) .. " steam=") .. tostring(steamId)) .. " code=") .. tostring(result.StatusCode))
|
|
end
|
|
end)
|
|
end
|
|
--- Загрузить pity с бэка при входе в игру / реконнект.
|
|
function ____exports.loadArcadePityForPlayer(self, playerId)
|
|
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
|
if not steamId then
|
|
return
|
|
end
|
|
local request = CreateHTTPRequestScriptVM(
|
|
"GET",
|
|
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arcade_pity"
|
|
)
|
|
setApiHeaders(nil, request)
|
|
request:Send(function(result)
|
|
if result.StatusCode < 200 or result.StatusCode >= 300 then
|
|
print((((("[ARCADE_PITY] GET fail player=" .. tostring(playerId)) .. " steam=") .. tostring(steamId)) .. " code=") .. tostring(result.StatusCode))
|
|
return
|
|
end
|
|
local bodyStr = result.Body ~= nil and tostring(result.Body) or ""
|
|
if #bodyStr == 0 then
|
|
return
|
|
end
|
|
local decoded = decodeJsonBody(nil, bodyStr)
|
|
local obj = unwrapApiJsonObject(nil, decoded)
|
|
local state = parseArcadePityPayload(nil, obj)
|
|
if state == nil then
|
|
print((("[ARCADE_PITY] GET parse invalid player=" .. tostring(playerId)) .. " steam=") .. tostring(steamId))
|
|
return
|
|
end
|
|
arcadePityLoadedFromBackend:add(playerId)
|
|
applyArcadePityState(nil, playerId, state)
|
|
print((((((("[ARCADE_PITY] GET ok player=" .. tostring(playerId)) .. " stdM=") .. tostring(state.standardPacksWithoutMythic)) .. " stdL=") .. tostring(state.standardPacksWithoutLegendary)) .. " premM=") .. tostring(state.premiumPacksWithoutMythic))
|
|
end)
|
|
end
|
|
local function getArcadePityState(self, playerId)
|
|
local state = arcadePityByPlayerId:get(playerId)
|
|
if not state then
|
|
local info = PlayerInfo:GetPlayerInfo(playerId)
|
|
state = normalizeArcadePityState(nil, info and info.arcade_pity)
|
|
arcadePityByPlayerId:set(playerId, state)
|
|
end
|
|
return state
|
|
end
|
|
local function saveArcadePityState(self, playerId, state)
|
|
applyArcadePityState(nil, playerId, state)
|
|
saveArcadePityToBackend(nil, playerId, state)
|
|
end
|
|
local function getCardQualityForPity(self, cardId)
|
|
local cardData = CardSystem.cardData[cardId]
|
|
return math.max(
|
|
CardQuality.COMMON,
|
|
math.min(
|
|
CardQuality.MYTHIC,
|
|
math.floor(__TS__Number(cardData and cardData.quality or CardQuality.COMMON))
|
|
)
|
|
)
|
|
end
|
|
local function packHasMythic(self, cardIds)
|
|
for ____, cardId in ipairs(cardIds) do
|
|
if getCardQualityForPity(nil, cardId) == CardQuality.MYTHIC then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
local function packHasLegendaryOrBetter(self, cardIds)
|
|
for ____, cardId in ipairs(cardIds) do
|
|
if getCardQualityForPity(nil, cardId) >= CardQuality.LEGENDARY then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
local function buildArcadePityPlan(self, packId, state)
|
|
local plan = {}
|
|
local slot = 0
|
|
if packId == "arcade_pack_premium" then
|
|
if state.premiumPacksWithoutMythic >= ARCADE_PITY_PREMIUM_MYTHIC_PACKS then
|
|
plan.mythicSlot = slot
|
|
slot = slot + 1
|
|
end
|
|
return plan
|
|
end
|
|
if state.standardPacksWithoutMythic >= ARCADE_PITY_STANDARD_MYTHIC_PACKS then
|
|
plan.mythicSlot = slot
|
|
slot = slot + 1
|
|
end
|
|
if state.standardPacksWithoutLegendary >= ARCADE_PITY_STANDARD_LEGENDARY_PACKS then
|
|
plan.legendarySlot = slot
|
|
slot = slot + 1
|
|
end
|
|
return plan
|
|
end
|
|
local function pickCardForcedQuality(self, quality, excludeIds)
|
|
do
|
|
local attempt = 0
|
|
while attempt < ARCADE_PACK_QUALITY_PICK_ATTEMPTS do
|
|
local cardId = pickCardExactQuality(nil, quality, excludeIds)
|
|
if cardId ~= nil then
|
|
return cardId
|
|
end
|
|
attempt = attempt + 1
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
--- Шансы редкости карты в паке аркады (сумма = 100%, шкала 1..10000).
|
|
local ARCADE_PACK_QUALITY_ROLL_MAX = 10000
|
|
local ARCADE_PACK_STANDARD_QUALITY_THRESHOLDS = {
|
|
{quality = CardQuality.COMMON, cumulative = 5500},
|
|
{quality = CardQuality.RARE, cumulative = 8000},
|
|
{quality = CardQuality.EPIC, cumulative = 9500},
|
|
{quality = CardQuality.LEGENDARY, cumulative = 9980},
|
|
{quality = CardQuality.MYTHIC, cumulative = 10000}
|
|
}
|
|
--- Премиум-пак: только эпик / легендарка / мифик.
|
|
local ARCADE_PACK_PREMIUM_QUALITY_THRESHOLDS = {{quality = CardQuality.EPIC, cumulative = 6000}, {quality = CardQuality.LEGENDARY, cumulative = 9800}, {quality = CardQuality.MYTHIC, cumulative = 10000}}
|
|
ARCADE_PACK_QUALITY_PICK_ATTEMPTS = 64
|
|
local function getArcadePackQualityWeights(self, packId)
|
|
local thresholds = packId == "arcade_pack_premium" and ARCADE_PACK_PREMIUM_QUALITY_THRESHOLDS or ARCADE_PACK_STANDARD_QUALITY_THRESHOLDS
|
|
local weights = {}
|
|
local prevCumulative = 0
|
|
for ____, entry in ipairs(thresholds) do
|
|
weights[#weights + 1] = {quality = entry.quality, weight = entry.cumulative - prevCumulative}
|
|
prevCumulative = entry.cumulative
|
|
end
|
|
return weights
|
|
end
|
|
local function rollArcadePackQualityFromWeights(self, weights)
|
|
local total = 0
|
|
for ____, w in ipairs(weights) do
|
|
total = total + w.weight
|
|
end
|
|
if total <= 0 then
|
|
local ____opt_12 = weights[#weights]
|
|
return ____opt_12 and ____opt_12.quality or CardQuality.COMMON
|
|
end
|
|
local roll = RandomInt(1, total)
|
|
for ____, w in ipairs(weights) do
|
|
if roll <= w.weight then
|
|
return w.quality
|
|
end
|
|
roll = roll - w.weight
|
|
end
|
|
return weights[#weights].quality
|
|
end
|
|
--- Редкости, для которых в каталоге есть хотя бы одна карта.
|
|
local function getStockedQualityWeights(self, packId)
|
|
local minQuality = packId == "arcade_pack_premium" and CardQuality.EPIC or CardQuality.COMMON
|
|
local byQuality = getPurchasablePoolByQuality(nil)
|
|
return __TS__ArrayFilter(
|
|
getArcadePackQualityWeights(nil, packId),
|
|
function(____, entry)
|
|
if entry.quality < minQuality then
|
|
return false
|
|
end
|
|
local pool = byQuality[entry.quality]
|
|
return pool ~= nil and #pool > 0
|
|
end
|
|
)
|
|
end
|
|
--- Сначала бросок по таблице шансов → карта строго этой редкости.
|
|
-- Если пул пуст — повторный бросок (без понижения/повышения тира).
|
|
local function pickRandomCardForPack(self, packId, excludeIds)
|
|
local tableWeights = getArcadePackQualityWeights(nil, packId)
|
|
do
|
|
local attempt = 0
|
|
while attempt < ARCADE_PACK_QUALITY_PICK_ATTEMPTS do
|
|
local rolledQuality = rollArcadePackQualityFromWeights(nil, tableWeights)
|
|
local cardId = pickCardExactQuality(nil, rolledQuality, excludeIds)
|
|
if cardId ~= nil then
|
|
return cardId
|
|
end
|
|
attempt = attempt + 1
|
|
end
|
|
end
|
|
local stockedWeights = getStockedQualityWeights(nil, packId)
|
|
if #stockedWeights == 0 then
|
|
return nil
|
|
end
|
|
do
|
|
local attempt = 0
|
|
while attempt < ARCADE_PACK_QUALITY_PICK_ATTEMPTS do
|
|
local rolledQuality = rollArcadePackQualityFromWeights(nil, stockedWeights)
|
|
local cardId = pickCardExactQuality(nil, rolledQuality, excludeIds)
|
|
if cardId ~= nil then
|
|
return cardId
|
|
end
|
|
attempt = attempt + 1
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
local function rollRandomCardIds(self, count, packId, pityPlan)
|
|
if #getPurchasableCardPool(nil) == 0 then
|
|
return {}
|
|
end
|
|
local forcedBySlot = __TS__New(Map)
|
|
if (pityPlan and pityPlan.mythicSlot) ~= nil and pityPlan.mythicSlot < count then
|
|
forcedBySlot:set(pityPlan.mythicSlot, CardQuality.MYTHIC)
|
|
end
|
|
if (pityPlan and pityPlan.legendarySlot) ~= nil and pityPlan.legendarySlot < count and not forcedBySlot:has(pityPlan.legendarySlot) then
|
|
forcedBySlot:set(pityPlan.legendarySlot, CardQuality.LEGENDARY)
|
|
end
|
|
local result = {}
|
|
do
|
|
local slot = 0
|
|
while slot < count do
|
|
local forcedQuality = forcedBySlot:get(slot)
|
|
local cardId
|
|
if forcedQuality ~= nil then
|
|
cardId = pickCardForcedQuality(nil, forcedQuality, result)
|
|
end
|
|
if cardId == nil then
|
|
cardId = pickRandomCardForPack(nil, packId, result)
|
|
end
|
|
if cardId ~= nil then
|
|
result[#result + 1] = cardId
|
|
end
|
|
slot = slot + 1
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
local function clearExpiredSession(self, playerId)
|
|
local session = sessionsByPlayerId:get(playerId)
|
|
if not session then
|
|
return
|
|
end
|
|
if GameRules:GetGameTime() - session.createdAt > ARCADE_SESSION_TTL_SECONDS then
|
|
sessionsByPlayerId:delete(playerId)
|
|
end
|
|
end
|
|
local function updateArcadePityAfterPack(self, playerId, packId, cardIds)
|
|
local state = getArcadePityState(nil, playerId)
|
|
local gotMythic = packHasMythic(nil, cardIds)
|
|
local gotLegendaryPlus = packHasLegendaryOrBetter(nil, cardIds)
|
|
if packId == "arcade_pack_premium" then
|
|
state.premiumPacksWithoutMythic = gotMythic and 0 or state.premiumPacksWithoutMythic + 1
|
|
else
|
|
state.standardPacksWithoutMythic = gotMythic and 0 or state.standardPacksWithoutMythic + 1
|
|
state.standardPacksWithoutLegendary = gotLegendaryPlus and 0 or state.standardPacksWithoutLegendary + 1
|
|
end
|
|
saveArcadePityState(nil, playerId, state)
|
|
end
|
|
local function sendArcadeEvent(self, playerId, eventName, payload)
|
|
local player = PlayerResource:GetPlayer(playerId)
|
|
if not player then
|
|
return
|
|
end
|
|
CustomGameEventManager:Send_ServerToPlayer(player, eventName, payload)
|
|
end
|
|
local function resolvePackPrice(self, pack, currency)
|
|
if currency == "dust_currency" and pack.price_dust ~= nil then
|
|
return math.floor(pack.price_dust)
|
|
end
|
|
if currency == "donate_currency" and pack.price_donate ~= nil then
|
|
return math.floor(pack.price_donate)
|
|
end
|
|
if currency == "free_currency" and pack.price_free ~= nil then
|
|
return math.floor(pack.price_free)
|
|
end
|
|
return -1
|
|
end
|
|
local function deductCurrency(self, store, playerId, currency, price)
|
|
if price <= 0 then
|
|
return true
|
|
end
|
|
if currency == "free_currency" then
|
|
return store:removeFreeCurrency(playerId, price)
|
|
end
|
|
if currency == "donate_currency" then
|
|
return store:removeDonateCurrency(playerId, price)
|
|
end
|
|
if currency == "dust_currency" then
|
|
return store:removeDustCurrency(playerId, price)
|
|
end
|
|
return false
|
|
end
|
|
local function refundCurrency(self, store, playerId, currency, price)
|
|
if price <= 0 then
|
|
return
|
|
end
|
|
if currency == "free_currency" then
|
|
store:addFreeCurrency(playerId, price)
|
|
elseif currency == "donate_currency" then
|
|
store:addDonateCurrency(playerId, price)
|
|
elseif currency == "dust_currency" then
|
|
store:addDustCurrency(playerId, price)
|
|
end
|
|
end
|
|
local function defaultArcadePackCredits(self)
|
|
return {standard = 0, premium = 0}
|
|
end
|
|
local function normalizeArcadePackCredits(self, raw)
|
|
local state = defaultArcadePackCredits(nil)
|
|
if not raw or type(raw) ~= "table" then
|
|
return state
|
|
end
|
|
local obj = raw
|
|
state.standard = math.max(
|
|
0,
|
|
math.floor(__TS__Number(obj.standard) or 0)
|
|
)
|
|
state.premium = math.max(
|
|
0,
|
|
math.floor(__TS__Number(obj.premium) or 0)
|
|
)
|
|
return state
|
|
end
|
|
local function applyArcadePackCredits(self, playerId, credits)
|
|
local info = PlayerInfo:GetPlayerInfo(playerId) or ({})
|
|
PlayerInfo:UpdatePlayerInfo(
|
|
playerId,
|
|
__TS__ObjectAssign({}, info, {arcade_pack_credits = credits})
|
|
)
|
|
end
|
|
local function getLocalArcadePackCredits(self, playerId)
|
|
local info = PlayerInfo:GetPlayerInfo(playerId)
|
|
return normalizeArcadePackCredits(nil, info and info.arcade_pack_credits)
|
|
end
|
|
local function getArcadePackCreditKey(self, packId)
|
|
if packId == "arcade_pack_standard" then
|
|
return "standard"
|
|
end
|
|
if packId == "arcade_pack_premium" then
|
|
return "premium"
|
|
end
|
|
return nil
|
|
end
|
|
local function tryConsumeArcadePackCredit(self, playerId, packId, onDone)
|
|
local creditKey = getArcadePackCreditKey(nil, packId)
|
|
if not creditKey then
|
|
onDone(nil, false)
|
|
return
|
|
end
|
|
local ____local = getLocalArcadePackCredits(nil, playerId)
|
|
if ____local[creditKey] <= 0 then
|
|
onDone(nil, false)
|
|
return
|
|
end
|
|
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
|
if not steamId then
|
|
onDone(nil, false)
|
|
return
|
|
end
|
|
local request = CreateHTTPRequestScriptVM(
|
|
"POST",
|
|
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arcade_pack_credits/consume"
|
|
)
|
|
setApiHeaders(nil, request)
|
|
request:SetHTTPRequestRawPostBody(
|
|
"application/json",
|
|
encodeApiBody(nil, {pack_id = packId})
|
|
)
|
|
request:Send(function(result)
|
|
if result.StatusCode < 200 or result.StatusCode >= 300 then
|
|
onDone(nil, false)
|
|
return
|
|
end
|
|
do
|
|
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
|
local decoded = decodeJsonBody(nil, result.Body)
|
|
local data = unwrapApiJsonObject(nil, decoded)
|
|
if (data and data.consumed) == true and data.arcade_pack_credits then
|
|
local credits = normalizeArcadePackCredits(nil, data.arcade_pack_credits)
|
|
applyArcadePackCredits(nil, playerId, credits)
|
|
onDone(nil, true)
|
|
return true
|
|
end
|
|
end)
|
|
if ____try and ____hasReturned then
|
|
return ____returnValue
|
|
end
|
|
end
|
|
onDone(nil, false)
|
|
end)
|
|
end
|
|
local function getCardLevelForPlayer(self, playerId, cardId)
|
|
local cardSystem = ensurePlayerCardSystem(nil, playerId)
|
|
if not cardSystem then
|
|
return 1
|
|
end
|
|
local level = cardSystem:GetCardLevel(cardId)
|
|
return __TS__NumberIsFinite(level) and math.max(
|
|
1,
|
|
math.floor(level)
|
|
) or 1
|
|
end
|
|
local function buildFlipPayload(self, playerId, cardId, slotIndex)
|
|
local cardData = CardSystem.cardData[cardId]
|
|
local quality = math.max(
|
|
1,
|
|
math.floor(__TS__Number(cardData and cardData.quality or CardQuality.COMMON))
|
|
)
|
|
local cardLevel = getCardLevelForPlayer(nil, playerId, cardId)
|
|
local store = StoreManager:getInstance()
|
|
local ownedBefore = store:getOwnedCardCopies(playerId, cardId)
|
|
local maxCopies = store:getCardMaxCopies(cardId)
|
|
local isDuplicate = ownedBefore >= maxCopies
|
|
local dustGranted = 0
|
|
if isDuplicate then
|
|
dustGranted = CardSystem:getDuplicateCardDustCompensation(quality, cardLevel)
|
|
if dustGranted > 0 then
|
|
store:addDustCurrency(playerId, dustGranted)
|
|
store:saveCurrencyToServer(playerId)
|
|
end
|
|
else
|
|
store:grantCardPurchaseWithoutPayment(playerId, cardId)
|
|
end
|
|
local rawIcon = cardData and cardData.icon
|
|
local icon = rawIcon ~= nil and #tostring(rawIcon) > 0 and tostring(rawIcon) or ("file://{images}/custom_game/cards/card_" .. tostring(cardId)) .. ".png"
|
|
local rawCardName = cardData and cardData.name
|
|
local cardName = rawCardName ~= nil and #tostring(rawCardName) > 0 and tostring(rawCardName) or ("card_" .. tostring(cardId)) .. "_name"
|
|
return {
|
|
slot_index = slotIndex,
|
|
card_id = cardId,
|
|
duplicate = isDuplicate and 1 or 0,
|
|
dust_granted = dustGranted,
|
|
card_level = cardLevel,
|
|
quality = quality,
|
|
icon = icon,
|
|
card_name = cardName
|
|
}
|
|
end
|
|
--- Выдать все карты пака сразу (порядок слотов важен для дубликатов).
|
|
local function grantArcadePackCards(self, playerId, cardIds)
|
|
local payloads = {}
|
|
do
|
|
local i = 0
|
|
while i < #cardIds do
|
|
payloads[#payloads + 1] = buildFlipPayload(nil, playerId, cardIds[i + 1], i)
|
|
i = i + 1
|
|
end
|
|
end
|
|
return payloads
|
|
end
|
|
local function handleBuyArcadePack(self, playerId, packId, selectedCurrency)
|
|
local pack = ____exports.ARCADE_PACK_DEFINITIONS[packId]
|
|
local store = StoreManager:getInstance()
|
|
if not pack then
|
|
store:sendPurchaseResult(playerId, false, "Неизвестный пак")
|
|
return
|
|
end
|
|
if not store:tryAcquireStorePurchaseLock(playerId) then
|
|
store:sendPurchaseResult(playerId, false, "Подождите, предыдущая покупка обрабатывается")
|
|
return
|
|
end
|
|
store:syncLatestCurrencyTotalsFromApi(
|
|
playerId,
|
|
function(____, currencyOk)
|
|
do
|
|
local function ____catch()
|
|
store:releaseStorePurchaseLock(playerId)
|
|
end
|
|
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
|
if not currencyOk then
|
|
store:sendPurchaseResult(playerId, false, "Не удалось проверить баланс")
|
|
store:releaseStorePurchaseLock(playerId)
|
|
return true
|
|
end
|
|
if not arcadePityLoadedFromBackend:has(playerId) then
|
|
____exports.loadArcadePityForPlayer(nil, playerId)
|
|
end
|
|
clearExpiredSession(nil, playerId)
|
|
if sessionsByPlayerId:has(playerId) then
|
|
store:sendPurchaseResult(playerId, false, "Сначала откройте карты из текущего пака")
|
|
store:releaseStorePurchaseLock(playerId)
|
|
return true
|
|
end
|
|
tryConsumeArcadePackCredit(
|
|
nil,
|
|
playerId,
|
|
packId,
|
|
function(____, usedCredit)
|
|
do
|
|
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
|
local currency = selectedCurrency or pack.allowed_currencies[1] or "dust_currency"
|
|
if not usedCredit and not __TS__ArrayIncludes(pack.allowed_currencies, currency) then
|
|
store:sendPurchaseResult(playerId, false, "Эта валюта недоступна для пака")
|
|
return true
|
|
end
|
|
local price = usedCredit and 0 or resolvePackPrice(nil, pack, currency)
|
|
if price < 0 then
|
|
store:sendPurchaseResult(playerId, false, "Некорректная цена пака")
|
|
return true
|
|
end
|
|
local pityState = getArcadePityState(nil, playerId)
|
|
local pityPlan = buildArcadePityPlan(nil, pack.id, pityState)
|
|
local cardIds = rollRandomCardIds(nil, pack.cardsCount, pack.id, pityPlan)
|
|
if #cardIds < pack.cardsCount then
|
|
store:sendPurchaseResult(playerId, false, "Нет доступных карт для пака")
|
|
return true
|
|
end
|
|
if not usedCredit and not deductCurrency(
|
|
nil,
|
|
store,
|
|
playerId,
|
|
currency,
|
|
price
|
|
) then
|
|
store:sendPurchaseResult(playerId, false, "Недостаточно валюты")
|
|
return true
|
|
end
|
|
if not usedCredit then
|
|
store:advanceCurrencyLoadSeq(playerId)
|
|
store:saveCurrencyToServer(playerId)
|
|
else
|
|
store:notifyArcadePackCredits(playerId)
|
|
end
|
|
store:updateCurrencyDisplay(playerId)
|
|
local flipPayloads = grantArcadePackCards(nil, playerId, cardIds)
|
|
updateArcadePityAfterPack(nil, playerId, pack.id, cardIds)
|
|
store:updateCurrencyDisplay(playerId)
|
|
local session = {
|
|
packId = pack.id,
|
|
flipPayloads = flipPayloads,
|
|
revealed = {},
|
|
createdAt = GameRules:GetGameTime()
|
|
}
|
|
do
|
|
local i = 0
|
|
while i < pack.cardsCount do
|
|
local ____session_revealed_28 = session.revealed
|
|
____session_revealed_28[#____session_revealed_28 + 1] = false
|
|
i = i + 1
|
|
end
|
|
end
|
|
sessionsByPlayerId:set(playerId, session)
|
|
local successMessage = usedCredit and "Открыт бесплатный пак из бандла" or "Пак куплен"
|
|
store:sendPurchaseResult(playerId, true, successMessage, pack.id)
|
|
sendArcadeEvent(nil, playerId, "store_arcade_pack_opened", {pack_id = pack.id, slot_count = pack.cardsCount, cards = flipPayloads, used_bundle_credit = usedCredit and 1 or 0})
|
|
end)
|
|
do
|
|
store:releaseStorePurchaseLock(playerId)
|
|
end
|
|
if ____try and ____hasReturned then
|
|
return ____returnValue
|
|
end
|
|
end
|
|
end
|
|
)
|
|
end)
|
|
if not ____try then
|
|
____hasReturned, ____returnValue = ____catch()
|
|
end
|
|
if ____hasReturned then
|
|
return ____returnValue
|
|
end
|
|
end
|
|
end
|
|
)
|
|
end
|
|
local function handleFlipArcadeCard(self, playerId, slotIndexRaw)
|
|
clearExpiredSession(nil, playerId)
|
|
local session = sessionsByPlayerId:get(playerId)
|
|
if not session then
|
|
sendArcadeEvent(nil, playerId, "store_arcade_flip_result", {success = 0, message = "Нет активного пака"})
|
|
return
|
|
end
|
|
local slotIndex = math.floor(__TS__Number(slotIndexRaw))
|
|
if not __TS__NumberIsFinite(slotIndex) or slotIndex < 0 or slotIndex >= #session.flipPayloads then
|
|
sendArcadeEvent(nil, playerId, "store_arcade_flip_result", {success = 0, message = "Некорректная карта"})
|
|
return
|
|
end
|
|
if session.revealed[slotIndex + 1] then
|
|
sendArcadeEvent(nil, playerId, "store_arcade_flip_result", {success = 0, message = "Карта уже открыта"})
|
|
return
|
|
end
|
|
session.revealed[slotIndex + 1] = true
|
|
local flipPayload = session.flipPayloads[slotIndex + 1]
|
|
sendArcadeEvent(
|
|
nil,
|
|
playerId,
|
|
"store_arcade_flip_result",
|
|
__TS__ObjectAssign({success = 1}, flipPayload)
|
|
)
|
|
local allRevealed = __TS__ArrayEvery(
|
|
session.revealed,
|
|
function(____, v) return v == true end
|
|
)
|
|
if allRevealed then
|
|
sessionsByPlayerId:delete(playerId)
|
|
sendArcadeEvent(nil, playerId, "store_arcade_pack_complete", {pack_id = session.packId})
|
|
end
|
|
end
|
|
local function setupStoreArcadePackListeners(self)
|
|
CustomGameEventManager:RegisterListener(
|
|
"store_buy_arcade_pack",
|
|
function(_source, event)
|
|
local playerId = event.PlayerID
|
|
local packId = tostring(event.pack_id or event.item_id or "")
|
|
local selectedCurrency = event.selected_currency
|
|
handleBuyArcadePack(nil, playerId, packId, selectedCurrency)
|
|
end
|
|
)
|
|
CustomGameEventManager:RegisterListener(
|
|
"store_arcade_flip_card",
|
|
function(_source, event)
|
|
local playerId = event.PlayerID
|
|
local ____event_slot_index_29 = event.slot_index
|
|
if ____event_slot_index_29 == nil then
|
|
____event_slot_index_29 = event.slotIndex
|
|
end
|
|
local ____event_slot_index_29_30 = ____event_slot_index_29
|
|
if ____event_slot_index_29_30 == nil then
|
|
____event_slot_index_29_30 = -1
|
|
end
|
|
local slotIndex = __TS__Number(____event_slot_index_29_30)
|
|
handleFlipArcadeCard(nil, playerId, slotIndex)
|
|
end
|
|
)
|
|
end
|
|
if IsServer() then
|
|
setupStoreArcadePackListeners(nil)
|
|
end
|
|
return ____exports
|