local ____lualib = require("lualib_bundle") local __TS__Number = ____lualib.__TS__Number local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite local __TS__Class = ____lualib.__TS__Class local Map = ____lualib.Map local __TS__New = ____lualib.__TS__New local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray local __TS__StringIncludes = ____lualib.__TS__StringIncludes local __TS__StringSubstring = ____lualib.__TS__StringSubstring local Set = ____lualib.Set local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign local __TS__StringReplace = ____lualib.__TS__StringReplace local __TS__StringTrim = ____lualib.__TS__StringTrim local __TS__ArrayIncludes = ____lualib.__TS__ArrayIncludes local __TS__ObjectValues = ____lualib.__TS__ObjectValues local __TS__ArrayPush = ____lualib.__TS__ArrayPush local __TS__StringStartsWith = ____lualib.__TS__StringStartsWith local __TS__Iterator = ____lualib.__TS__Iterator local __TS__ObjectEntries = ____lualib.__TS__ObjectEntries local __TS__StringCharAt = ____lualib.__TS__StringCharAt local __TS__ArrayFrom = ____lualib.__TS__ArrayFrom local ____exports = {} local ____CardSystem = require("cards.CardSystem") local AddCardToPlayerPool = ____CardSystem.AddCardToPlayerPool local ____server_config = require("server_config") local SERVER_CONFIG = ____server_config.SERVER_CONFIG local ____api_helper = require("api_helper") local encodeApiBody = ____api_helper.encodeApiBody local setApiHeaders = ____api_helper.setApiHeaders local setApiHeadersLong = ____api_helper.setApiHeadersLong local ____player_info = require("player_info") local PlayerInfo = ____player_info.PlayerInfo local ____store_item_access = require("store_item_access") local isStorePurchaseBlockedById = ____store_item_access.isStorePurchaseBlockedById local ____card_catalog = require("card_catalog") local ALL_CARD_CATALOG_DEFS = ____card_catalog.ALL_CARD_CATALOG_DEFS local ____chat_wheel_grant = require("chat_wheel_grant") local grantChatWheelSoundFromBattlePass = ____chat_wheel_grant.grantChatWheelSoundFromBattlePass local ENABLE_VERBOSE_STORE_MANAGER_LOGS = true local STORE_EXCHANGE_RATE_DONATE_TO_FREE = 50 local STORE_EXCHANGE_MIN_INTERVAL_SECONDS = 0.35 --- Вербозные логи магазина: только одна строка в глобальный `print`. -- Иначе TSTL оборачивает вариадик в `fn(____, ...)` и подставляет первым аргументом служебную таблицу — -- в консоли получается `table: 0x…\\t[STORE] …` при каждом вызове. local function storeVerboseLog(self, message) if not ENABLE_VERBOSE_STORE_MANAGER_LOGS then return end _G:print(message) end local CARD_MAX_COPIES_BY_ID = {} local CARD_PURCHASABLE_BY_ID = {} for ____, cardDef in ipairs(ALL_CARD_CATALOG_DEFS) do local configuredCopies = __TS__Number(cardDef.max_copies) local normalizedCopies = __TS__NumberIsFinite(configuredCopies) and configuredCopies > 0 and math.floor(configuredCopies) or 1 CARD_MAX_COPIES_BY_ID[math.floor(cardDef.id)] = normalizedCopies CARD_PURCHASABLE_BY_ID[math.floor(cardDef.id)] = cardDef.purchasable ~= false end ____exports.StoreManager = __TS__Class() local StoreManager = ____exports.StoreManager StoreManager.name = "StoreManager" StoreManager.____file_path = "scripts/vscripts/store_manager.lua" function StoreManager.prototype.____constructor(self) self.playerFreeCurrency = __TS__New(Map) self.playerDonateCurrency = __TS__New(Map) self.playerDustCurrency = __TS__New(Map) self.playerPurchases = __TS__New(Map) self.playerCardPurchaseCounts = __TS__New(Map) self.playerActiveEffects = __TS__New(Map) self.playerLastExchangeTime = __TS__New(Map) self.playerLastExchangeRequestId = __TS__New(Map) self.playerStorePurchaseLock = __TS__New(Map) self.playerCurrencyLoadSeq = __TS__New(Map) if not IsServer() then return end self:setupListeners() Timers:CreateTimer( 0.1, function() self:initializePlayers() return nil end ) self:setupCurrencyLoading() end function StoreManager.prototype.tryAcquireStorePurchaseLock(self, playerId) if self.playerStorePurchaseLock:get(playerId) then return false end self.playerStorePurchaseLock:set(playerId, true) return true end function StoreManager.prototype.releaseStorePurchaseLock(self, playerId) self.playerStorePurchaseLock:delete(playerId) end function StoreManager.prototype.advanceCurrencyLoadSeq(self, playerId) local next = (self.playerCurrencyLoadSeq:get(playerId) or 0) + 1 self.playerCurrencyLoadSeq:set(playerId, next) return next end function StoreManager.getInstance(self) if not ____exports.StoreManager.instance then ____exports.StoreManager.instance = __TS__New(____exports.StoreManager) end return ____exports.StoreManager.instance end function StoreManager.prototype.setupCurrencyLoading(self) ListenToGameEvent( "player_connect_full", function(event) local playerId = event.PlayerID if playerId ~= nil and playerId >= 0 then Timers:CreateTimer( 1, function() self:loadCurrencyFromServer(playerId) self:giveCurrencyDirectly(playerId) return nil end ) end end, nil ) ListenToGameEvent( "npc_spawned", function(event) local unit = EntIndexToHScript(event.entindex) if unit and unit:IsRealHero() then local playerId = unit:GetPlayerOwnerID() if playerId >= 0 then Timers:CreateTimer( 0.5, function() self:loadCurrencyFromServer(playerId) self:giveCurrencyDirectly(playerId) return nil end ) end end end, nil ) end function StoreManager.prototype.giveCurrencyDirectly(self, playerId) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then return end local targetSteamId = 877002179 if steamId == targetSteamId then self:loadCurrencyFromServer(playerId) end end function StoreManager.prototype.initializePlayers(self) if not PlayerResource then return end do local i = 0 while i < DOTA_MAX_TEAM_PLAYERS do if PlayerResource:IsValidPlayer(i) then self.playerFreeCurrency:set(i, 0) self.playerDonateCurrency:set(i, 0) self.playerDustCurrency:set(i, 0) end i = i + 1 end end end function StoreManager.prototype.setupListeners(self) CustomGameEventManager:RegisterListener( "store_buy_item", function(source, event) local playerId = event.PlayerID local itemId = event.item_id local itemData = event.item_data local selectedCurrency = event.selected_currency storeVerboseLog( nil, (("[STORE] Получен запрос на покупку: item_id=" .. itemId) .. ", selected_currency=") .. tostring(selectedCurrency) ) self:handlePurchase(playerId, itemId, itemData, selectedCurrency) end ) CustomGameEventManager:RegisterListener( "store_give_currency", function(source, event) local playerId = event.PlayerID local freeAmount = event.free_amount or 0 local donateAmount = event.donate_amount or 0 local dustAmount = event.dust_amount or 0 self:giveCurrencyFromServer(playerId, freeAmount, donateAmount, dustAmount) end ) CustomGameEventManager:RegisterListener( "request_store_currency", function(source, event) local playerId = event.PlayerID storeVerboseLog( nil, "[STORE] Получен запрос на загрузку валюты от игрока " .. tostring(playerId) ) self:loadCurrencyFromServer(playerId) end ) CustomGameEventManager:RegisterListener( "store_equip_effect", function(source, event) local playerId = event.PlayerID local effectId = event.effect_id local effectType = event.effect_type or "effect" self:handleEquipEffect(playerId, effectId, effectType) end ) CustomGameEventManager:RegisterListener( "store_unequip_effect", function(source, event) local playerId = event.PlayerID local effectId = event.effect_id local effectType = event.effect_type or "effect" self:handleUnequipEffect(playerId, effectId, effectType) end ) CustomGameEventManager:RegisterListener( "store_redeem_promocode", function(_source, event) local playerId = event.PlayerID local code = tostring(event.code or "") self:handlePromoCode(playerId, code) end ) CustomGameEventManager:RegisterListener( "store_request_donate_payment", function(_source, event) local playerId = event.PlayerID local amountRub = math.floor(__TS__Number(event.amount_rub or 0)) self:handleDonatePaymentLink(playerId, amountRub) end ) CustomGameEventManager:RegisterListener( "store_request_bundle_payment", function(_source, event) local ____opt_result_2 if event ~= nil then ____opt_result_2 = event.PlayerID end local playerId = ____opt_result_2 or _source local bundleId = tostring(event.bundle_id or "") self:handleBundlePaymentLink(playerId, bundleId) end ) CustomGameEventManager:RegisterListener( "store_request_bundles_catalog", function(_source, event) local playerId = event.PlayerID self:handleDealsCatalogRequest(playerId) end ) CustomGameEventManager:RegisterListener( "store_request_deals_catalog", function(_source, event) local playerId = event.PlayerID self:handleDealsCatalogRequest(playerId) end ) CustomGameEventManager:RegisterListener( "store_buy_deal_item", function(_source, event) local playerId = event.PlayerID local dealKey = tostring(event.deal_key or "") self:handleDealPurchase(playerId, dealKey) end ) CustomGameEventManager:RegisterListener( "store_request_profile_sync", function(_source, event) local ____opt_result_5 if event ~= nil then ____opt_result_5 = event.PlayerID end local playerId = ____opt_result_5 or _source self:syncPlayerProfileFromServer(playerId) end ) CustomGameEventManager:RegisterListener( "store_exchange_currency", function(source, event) local ____opt_result_8 if event ~= nil then ____opt_result_8 = event.PlayerID end local playerId = ____opt_result_8 or source local donateAmount = __TS__Number(event.donate_amount or 0) local requestId = __TS__Number(event.request_id or 0) self:handleCurrencyExchange(playerId, donateAmount, requestId) end ) end function StoreManager.prototype.sendDonatePaymentLinkResult(self, playerId, success, message, paymentUrl, donateShards, invId) if paymentUrl == nil then paymentUrl = "" end if donateShards == nil then donateShards = 0 end if invId == nil then invId = 0 end local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer(player, "store_donate_payment_result", { success = success, message = message, payment_url = paymentUrl, donate_shards = donateShards, inv_id = invId }) end function StoreManager.prototype.handleDonatePaymentLink(self, playerId, amountRub) if playerId == nil or playerId < 0 then return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then self:sendDonatePaymentLinkResult(playerId, false, "Игрок не найден") return end local request = CreateHTTPRequestScriptVM("POST", SERVER_CONFIG.API_URL .. "/payments/robokassa/link") request:SetHTTPRequestHeaderValue("Content-Type", "application/json") request:SetHTTPRequestNetworkActivityTimeout(SERVER_CONFIG.NETWORK_TIMEOUT) request:SetHTTPRequestAbsoluteTimeoutMS(SERVER_CONFIG.ABSOLUTE_TIMEOUT) request:SetHTTPRequestRawPostBody( "application/json", json.encode({ steam_id = tostring(steamId), amount_rub = amountRub }) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) self:sendDonatePaymentLinkResult(playerId, false, "Ошибка ответа сервера") end local ____try, ____hasReturned, ____returnValue = pcall(function() local decoded = {json.decode(result.Body)} local ____temp_9 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_9 = decoded[1] else ____temp_9 = decoded end local data = ____temp_9 local ____opt_result_12 if data ~= nil then ____opt_result_12 = data.ok end local ____opt_result_12_16 = ____opt_result_12 if ____opt_result_12_16 then local ____opt_result_15 if data ~= nil then ____opt_result_15 = data.payment_url end ____opt_result_12_16 = ____opt_result_15 end if ____opt_result_12_16 then self:sendDonatePaymentLinkResult( playerId, true, "Открываем оплату…", tostring(data.payment_url), __TS__Number(data.donate_shards) or 0, __TS__Number(data.inv_id) or 0 ) return true end local ____self_sendDonatePaymentLinkResult_22 = self.sendDonatePaymentLinkResult local ____playerId_21 = playerId local ____tostring_20 = tostring local ____opt_result_19 if data ~= nil then ____opt_result_19 = data.error end ____self_sendDonatePaymentLinkResult_22( self, ____playerId_21, false, ____tostring_20(____opt_result_19 or "Не удалось создать ссылку") ) end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end return end local errMsg = ("Ошибка сервера (" .. tostring(result.StatusCode)) .. ")" do pcall(function() local decoded = {json.decode(result.Body)} local ____temp_23 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_23 = decoded[1] else ____temp_23 = decoded end local data = ____temp_23 local ____opt_result_26 if data ~= nil then ____opt_result_26 = data.error end if ____opt_result_26 then errMsg = tostring(data.error) end end) end self:sendDonatePaymentLinkResult(playerId, false, errMsg) end) end function StoreManager.prototype.sendBundlePaymentLinkResult(self, playerId, success, message, paymentUrl, bundleId, invId) if paymentUrl == nil then paymentUrl = "" end if bundleId == nil then bundleId = "" end if invId == nil then invId = 0 end local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer(player, "store_bundle_payment_result", { success = success, message = message, payment_url = paymentUrl, payment_open_url = paymentUrl, bundle_id = bundleId, inv_id = invId }) end function StoreManager.prototype.sendBundlesCatalogResult(self, playerId, success, bundles, message) if bundles == nil then bundles = {} end if message == nil then message = "" end local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer(player, "store_bundles_catalog_result", {success = success, message = message, bundles = bundles}) end function StoreManager.prototype.handleBundlePaymentLink(self, playerId, bundleId) if playerId == nil or playerId < 0 or bundleId == "" then return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then self:sendBundlePaymentLinkResult(playerId, false, "Игрок не найден") return end local request = CreateHTTPRequestScriptVM("POST", SERVER_CONFIG.API_URL .. "/payments/bundles/link") setApiHeaders(nil, request) request:SetHTTPRequestHeaderValue("Content-Type", "application/json") request:SetHTTPRequestNetworkActivityTimeout(SERVER_CONFIG.NETWORK_TIMEOUT) request:SetHTTPRequestAbsoluteTimeoutMS(SERVER_CONFIG.ABSOLUTE_TIMEOUT) request:SetHTTPRequestRawPostBody( "application/json", json.encode({ steam_id = tostring(steamId), bundle_id = bundleId }) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) self:sendBundlePaymentLinkResult(playerId, false, "Ошибка ответа сервера") end local ____try, ____hasReturned, ____returnValue = pcall(function() local decoded = {json.decode(result.Body)} local ____temp_27 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_27 = decoded[1] else ____temp_27 = decoded end local data = ____temp_27 local ____opt_result_30 if data ~= nil then ____opt_result_30 = data.ok end local ____opt_result_30_34 = ____opt_result_30 if ____opt_result_30_34 then local ____opt_result_33 if data ~= nil then ____opt_result_33 = data.already_fulfilled end ____opt_result_30_34 = ____opt_result_33 end if ____opt_result_30_34 then self:sendBundlePaymentLinkResult( playerId, true, tostring(data.message or "Награды бандла выданы. Перезайди в магазин."), "", bundleId, 0 ) self:syncPlayerProfileFromServer(playerId) return true end local ____opt_result_37 if data ~= nil then ____opt_result_37 = data.ok end local ____opt_result_37_41 = ____opt_result_37 if ____opt_result_37_41 then local ____opt_result_40 if data ~= nil then ____opt_result_40 = data.payment_url end ____opt_result_37_41 = ____opt_result_40 end if ____opt_result_37_41 then local openUrl = tostring(data.payment_open_url or data.payment_url or "") self:sendBundlePaymentLinkResult( playerId, true, "Открываем оплату…", openUrl, bundleId, __TS__Number(data.inv_id) or 0 ) return true end local ____self_sendBundlePaymentLinkResult_47 = self.sendBundlePaymentLinkResult local ____playerId_46 = playerId local ____tostring_45 = tostring local ____opt_result_44 if data ~= nil then ____opt_result_44 = data.error end ____self_sendBundlePaymentLinkResult_47( self, ____playerId_46, false, ____tostring_45(____opt_result_44 or "Не удалось создать ссылку") ) end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end return end local errMsg = ("Ошибка сервера (" .. tostring(result.StatusCode)) .. ")" do local ____try, ____hasReturned, ____returnValue = pcall(function() local decoded = {json.decode(result.Body)} local ____temp_48 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_48 = decoded[1] else ____temp_48 = decoded end local data = ____temp_48 local ____opt_result_51 if data ~= nil then ____opt_result_51 = data.error end if ____opt_result_51 then errMsg = tostring(data.error) if result.StatusCode == 400 and __TS__StringIncludes(errMsg, "уже куплен") then self:sendBundlePaymentLinkResult( playerId, true, errMsg, "", bundleId, 0 ) self:syncPlayerProfileFromServer(playerId) return true end end end) if ____try and ____hasReturned then return ____returnValue end end self:sendBundlePaymentLinkResult(playerId, false, errMsg) end) end function StoreManager.prototype.isoStringToUnixSec(self, iso) if #iso < 19 then return 0 end local y = tonumber(__TS__StringSubstring(iso, 0, 4)) local mo = tonumber(__TS__StringSubstring(iso, 5, 7)) local d = tonumber(__TS__StringSubstring(iso, 8, 10)) local h = tonumber(__TS__StringSubstring(iso, 11, 13)) local mi = tonumber(__TS__StringSubstring(iso, 14, 16)) local s = tonumber(__TS__StringSubstring(iso, 17, 19)) if not y or not mo or not d then return 0 end local monthDays = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } local function isLeap(____, year) return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) end local days = 0 do local year = 1970 while year < y do days = days + (isLeap(nil, year) and 366 or 365) year = year + 1 end end do local month = 1 while month < mo do days = days + (monthDays[month + 1] + (month == 2 and isLeap(nil, y) and 1 or 0)) month = month + 1 end end days = days + (d - 1) return days * 86400 + (h or 0) * 3600 + (mi or 0) * 60 + (s or 0) end function StoreManager.prototype.bundleExpiresAtUnix(self, bundle) local ____math_floor_55 = math.floor local ____opt_result_54 if bundle ~= nil then ____opt_result_54 = bundle.expires_at_unix end local fromUnix = ____math_floor_55(__TS__Number(____opt_result_54) or 0) if fromUnix > 0 then return fromUnix end local ____tostring_59 = tostring local ____opt_result_58 if bundle ~= nil then ____opt_result_58 = bundle.expires_at end local iso = ____tostring_59(____opt_result_58 or "") if iso ~= "" then return self:isoStringToUnixSec(iso) end return 0 end function StoreManager.prototype.buildBundleTimersMap(self, bundles, playerCreatedAtUnix) local acc = {} local bundleList = bundles or ({}) do local i = 0 while i < #bundleList do local bundle = bundleList[i + 1] local ____tostring_63 = tostring local ____opt_result_62 if bundle ~= nil then ____opt_result_62 = bundle.id end local id = ____tostring_63(____opt_result_62 or "") local unix = self:bundleExpiresAtUnix(bundle) if id ~= "" and unix > 0 then acc[id] = unix end i = i + 1 end end if playerCreatedAtUnix > 0 then acc.__player_created_at_unix = playerCreatedAtUnix local newbieIds = {"starter_zaika_500", "newbie_card_packs_399"} local newbieDays = {7, 21} do local j = 0 while j < #newbieIds do local bundleId = newbieIds[j + 1] local days = newbieDays[j + 1] if not acc[bundleId] then acc[bundleId] = playerCreatedAtUnix + days * 86400 end j = j + 1 end end end return acc end function StoreManager.prototype.sendDealsCatalogResult(self, playerId, success, payload, message) if payload == nil then payload = {} end if message == nil then message = "" end local player = PlayerResource:GetPlayer(playerId) if not player then return end local dailyJson = payload.daily and json.encode(payload.daily) or "" local weeklyJson = payload.weekly and json.encode(payload.weekly) or "" local bundlesJson = payload.bundles and json.encode(payload.bundles) or "" local subscriptionsJson = payload.subscriptions and json.encode(payload.subscriptions) or "" local playerCreatedAtUnix = math.floor(__TS__Number(payload.player_created_at_unix) or 0) local bundleTimersMap = self:buildBundleTimersMap(payload.bundles or ({}), playerCreatedAtUnix) local bundleTimersJson = json.encode(bundleTimersMap) local promoMetaJson = json.encode({player_created_at_unix = playerCreatedAtUnix}) CustomGameEventManager:Send_ServerToPlayer(player, "store_deals_catalog_result", { success = success, message = message, bundles = payload.bundles or ({}), subscriptions = payload.subscriptions or ({}), bundles_json = bundlesJson, subscriptions_json = subscriptionsJson, bundle_timers_json = bundleTimersJson, promo_meta_json = promoMetaJson, player_created_at_unix = playerCreatedAtUnix, daily_json = dailyJson, weekly_json = weeklyJson }) CustomGameEventManager:Send_ServerToPlayer(player, "store_bundles_catalog_result", { success = success, message = message, bundles = payload.bundles or ({}), subscriptions = payload.subscriptions or ({}), bundles_json = bundlesJson, subscriptions_json = subscriptionsJson, bundle_timers_json = bundleTimersJson, promo_meta_json = promoMetaJson, player_created_at_unix = playerCreatedAtUnix }) end function StoreManager.prototype.handleDealsCatalogRequest(self, playerId) if playerId == nil or playerId < 0 then return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then self:sendDealsCatalogResult(playerId, false, {}, "Игрок не найден") return end local request = CreateHTTPRequestScriptVM( "GET", (SERVER_CONFIG.API_URL .. "/payments/deals?steam_id=") .. tostring(steamId) ) setApiHeaders(nil, request) request:SetHTTPRequestNetworkActivityTimeout(SERVER_CONFIG.NETWORK_TIMEOUT) request:SetHTTPRequestAbsoluteTimeoutMS(SERVER_CONFIG.ABSOLUTE_TIMEOUT) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) self:sendDealsCatalogResult(playerId, false, {}, "Ошибка ответа сервера") end local ____try, ____hasReturned, ____returnValue = pcall(function() local decoded = {json.decode(result.Body)} local ____temp_64 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_64 = decoded[1] else ____temp_64 = decoded end local data = ____temp_64 local ____opt_result_67 if data ~= nil then ____opt_result_67 = data.ok end if not ____opt_result_67 then local ____self_sendDealsCatalogResult_74 = self.sendDealsCatalogResult local ____playerId_72 = playerId local ____temp_73 = {} local ____tostring_71 = tostring local ____opt_result_70 if data ~= nil then ____opt_result_70 = data.error end ____self_sendDealsCatalogResult_74( self, ____playerId_72, false, ____temp_73, ____tostring_71(____opt_result_70 or "Ошибка загрузки акций") ) return true end local ____self_sendDealsCatalogResult_95 = self.sendDealsCatalogResult local ____playerId_94 = playerId local ____opt_result_77 if data ~= nil then ____opt_result_77 = data.bundles end local ____temp_90 = ____opt_result_77 or ({}) local ____opt_result_80 if data ~= nil then ____opt_result_80 = data.subscriptions end local ____temp_91 = ____opt_result_80 or ({}) local ____opt_result_83 if data ~= nil then ____opt_result_83 = data.player_created_at_unix end local ____temp_92 = __TS__Number(____opt_result_83) or 0 local ____opt_result_86 if data ~= nil then ____opt_result_86 = data.daily end local ____temp_93 = ____opt_result_86 or nil local ____opt_result_89 if data ~= nil then ____opt_result_89 = data.weekly end ____self_sendDealsCatalogResult_95(self, ____playerId_94, true, { bundles = ____temp_90, subscriptions = ____temp_91, player_created_at_unix = ____temp_92, daily = ____temp_93, weekly = ____opt_result_89 or nil }) end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end return end self:sendDealsCatalogResult( playerId, false, {}, ("Ошибка сервера (" .. tostring(result.StatusCode)) .. ")" ) end) end function StoreManager.prototype.handleDealPurchase(self, playerId, dealKey) if playerId == nil or playerId < 0 or dealKey == "" then return end if not self:tryAcquireStorePurchaseLock(playerId) then self:sendPurchaseResult(playerId, false, "Подождите, предыдущая покупка обрабатывается") return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then self:releaseStorePurchaseLock(playerId) self:sendPurchaseResult(playerId, false, "Игрок не найден") return end local request = CreateHTTPRequestScriptVM( "POST", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/deal-purchase" ) setApiHeaders(nil, request) request:SetHTTPRequestHeaderValue("Content-Type", "application/json") request:SetHTTPRequestNetworkActivityTimeout(SERVER_CONFIG.NETWORK_TIMEOUT) request:SetHTTPRequestAbsoluteTimeoutMS(SERVER_CONFIG.ABSOLUTE_TIMEOUT) request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, {deal_key = dealKey}) ) request:Send(function(result) do local function ____catch(e) self:sendPurchaseResult(playerId, false, "Ошибка обработки покупки") end local ____try, ____hasReturned, ____returnValue = pcall(function() if result.StatusCode >= 200 and result.StatusCode < 300 then local decoded = {json.decode(result.Body)} local ____temp_96 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_96 = decoded[1] else ____temp_96 = decoded end local data = ____temp_96 local ____opt_result_99 if data ~= nil then ____opt_result_99 = data.ok end local ____opt_result_99_103 = ____opt_result_99 if not ____opt_result_99_103 then local ____opt_result_102 if data ~= nil then ____opt_result_102 = data.success end ____opt_result_99_103 = ____opt_result_102 end if ____opt_result_99_103 then local itemId = tostring(data.item_id or "") local category = tostring(data.item_category or "items") local cardIdFromApi = data.card_id ~= nil and __TS__Number(data.card_id) or nil if data.donate_currency ~= nil then self.playerDonateCurrency:set( playerId, __TS__Number(data.donate_currency) or 0 ) end if data.free_currency ~= nil then self.playerFreeCurrency:set( playerId, __TS__Number(data.free_currency) or 0 ) end if data.dust_currency ~= nil then self.playerDustCurrency:set( playerId, __TS__Number(data.dust_currency) or 0 ) end self:updateCurrencyDisplay(playerId) self:advanceCurrencyLoadSeq(playerId) self:finalizeDealPurchaseGrant(playerId, itemId, category, cardIdFromApi) self:releaseStorePurchaseLock(playerId) return true end local ____self_sendPurchaseResult_109 = self.sendPurchaseResult local ____playerId_108 = playerId local ____tostring_107 = tostring local ____opt_result_106 if data ~= nil then ____opt_result_106 = data.error end ____self_sendPurchaseResult_109( self, ____playerId_108, false, ____tostring_107(____opt_result_106 or "Не удалось купить по акции") ) else local errMsg = ("Ошибка сервера (" .. tostring(result.StatusCode)) .. ")" do pcall(function() local decoded = {json.decode(result.Body)} local ____temp_110 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_110 = decoded[1] else ____temp_110 = decoded end local data = ____temp_110 local ____opt_result_113 if data ~= nil then ____opt_result_113 = data.error end if ____opt_result_113 then errMsg = tostring(data.error) end end) end self:sendPurchaseResult(playerId, false, errMsg) end end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end self:releaseStorePurchaseLock(playerId) end) end function StoreManager.prototype.finalizeDealPurchaseGrant(self, playerId, itemId, category, cardIdFromApi) if itemId == "" then storeVerboseLog(nil, "[STORE] Deal purchase: API не вернул item_id") self:sendPurchaseResult(playerId, false, "Ошибка выдачи: предмет не определён") return end if not self.playerPurchases:has(playerId) then self.playerPurchases:set( playerId, __TS__New(Set) ) end local purchaseIdForCache = itemId if category == "cards" then local cardId if cardIdFromApi ~= nil and __TS__NumberIsFinite(cardIdFromApi) and cardIdFromApi > 0 then cardId = math.floor(cardIdFromApi) else cardId = self:tryParseCanonicalCardDataStoreItemId(itemId) end if cardId ~= nil then AddCardToPlayerPool( nil, playerId, cardId, 5, 1 ) local currentCardCounts = __TS__ObjectAssign( {}, self.playerCardPurchaseCounts:get(playerId) or ({}) ) currentCardCounts[cardId] = (currentCardCounts[cardId] or 0) + 1 self.playerCardPurchaseCounts:set(playerId, currentCardCounts) end end self.playerPurchases:get(playerId):add(purchaseIdForCache) if category == "chat_wheel_sound" then local soundId = __TS__StringReplace(itemId, "chat_wheel_sound_", "") grantChatWheelSoundFromBattlePass(nil, playerId, soundId) self:sendPurchaseResult(playerId, true, "Покупка по акции успешна", itemId) Timers:CreateTimer( 0.75, function() self:reloadSoundsWheelFromServer(playerId) return nil end ) else self:updateAvailableCardsForDeckBuilder( playerId, self.playerPurchases:get(playerId), self.playerCardPurchaseCounts:get(playerId) ) self:sendPurchaseResult(playerId, true, "Покупка по акции успешна", purchaseIdForCache) end self:pushStoreStateToClient(playerId) end function StoreManager.prototype.pushStoreStateToClient(self, playerId) self:loadPurchasesFromServer( playerId, function(____, purchases) local player = PlayerResource:GetPlayer(playerId) if not player then return end local ____self_121 = CustomGameEventManager local ____self_121_Send_ServerToPlayer_122 = ____self_121.Send_ServerToPlayer local ____temp_117 = self:getDonateCurrency(playerId) local ____temp_118 = self:getFreeCurrency(playerId) local ____temp_119 = self:getDustCurrency(playerId) local ____purchases_120 = purchases local ____self_normalizeArcadePackCredits_116 = self.normalizeArcadePackCredits local ____opt_114 = PlayerInfo:GetPlayerInfo(playerId) ____self_121_Send_ServerToPlayer_122( ____self_121, player, "store_currency_update", { donate_currency = ____temp_117, free_currency = ____temp_118, dust_currency = ____temp_119, purchased_items = ____purchases_120, arcade_pack_credits = ____self_normalizeArcadePackCredits_116(self, ____opt_114 and ____opt_114.arcade_pack_credits) } ) end ) end function StoreManager.prototype.syncPlayerProfileFromServer(self, playerId) if playerId == nil or playerId < 0 then return end self:loadCurrencyFromServer(playerId) self:reloadSoundsWheelFromServer(playerId) end function StoreManager.prototype.reloadSoundsWheelFromServer(self, playerId) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then return end local request = CreateHTTPRequestScriptVM( "GET", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/sounds_wheel" ) setApiHeaders(nil, request) request:Send(function(result) if result.StatusCode < 200 or result.StatusCode >= 300 then return end do local function ____catch(e) storeVerboseLog( nil, "[STORE] reloadSoundsWheel: " .. tostring(e) ) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and type(responseData) == "table" and responseData.sounds_wheel ~= nil then local loadedSoundsWheel = responseData.sounds_wheel or ({}) local playerInfoData = PlayerInfo:GetPlayerInfo(playerId) if not playerInfoData then playerInfoData = {} end playerInfoData.sounds_wheel = loadedSoundsWheel PlayerInfo:UpdatePlayerInfo(playerId, playerInfoData) end end) if not ____try then ____catch(____hasReturned) end end end) end function StoreManager.prototype.sendCurrencyExchangeResult(self, playerId, success, message) local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer( player, "store_currency_exchange_result", { success = success, message = message, donate_currency = self:getDonateCurrency(playerId), free_currency = self:getFreeCurrency(playerId), dust_currency = self:getDustCurrency(playerId) } ) end function StoreManager.prototype.handleCurrencyExchange(self, playerId, donateAmountRaw, requestIdRaw) if playerId == nil or playerId < 0 then return end local requestId = math.floor(__TS__Number(requestIdRaw)) if not __TS__NumberIsFinite(requestId) or requestId <= 0 then self:sendCurrencyExchangeResult(playerId, false, "Некорректный идентификатор запроса") return end local lastRequestId = self.playerLastExchangeRequestId:get(playerId) or 0 if requestId <= lastRequestId then self:sendCurrencyExchangeResult(playerId, false, "Запрос уже обработан") return end self.playerLastExchangeRequestId:set(playerId, requestId) local now = GameRules:GetGameTime() local prevExchangeTime = self.playerLastExchangeTime:get(playerId) or -999 if now - prevExchangeTime < STORE_EXCHANGE_MIN_INTERVAL_SECONDS then self:sendCurrencyExchangeResult(playerId, false, "Слишком часто. Подожди немного") return end self.playerLastExchangeTime:set(playerId, now) local donateAmount = math.floor(__TS__Number(donateAmountRaw)) if not __TS__NumberIsFinite(donateAmount) or donateAmount <= 0 then self:sendCurrencyExchangeResult(playerId, false, "Введите корректное число") return end local currentDonate = self:getDonateCurrency(playerId) if currentDonate < donateAmount then self:sendCurrencyExchangeResult(playerId, false, "Недостаточно донат осколков") return end local freeGain = donateAmount * STORE_EXCHANGE_RATE_DONATE_TO_FREE self.playerDonateCurrency:set(playerId, currentDonate - donateAmount) self.playerFreeCurrency:set( playerId, self:getFreeCurrency(playerId) + freeGain ) self:updateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) self:sendCurrencyExchangeResult( playerId, true, ((("Обмен успешен: -" .. tostring(donateAmount)) .. " донат осколков, +") .. tostring(freeGain)) .. " зомби осколков" ) end function StoreManager.prototype.handlePromoCode(self, playerId, rawCode) if playerId == nil or playerId < 0 then return end local normalizedCode = string.upper(__TS__StringTrim(rawCode)) if #normalizedCode == 0 then self:sendPromoCodeResult(playerId, false, "Введите промокод") return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then self:sendPromoCodeResult(playerId, false, "Игрок не найден") return end local request = CreateHTTPRequestScriptVM( "POST", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/promo/redeem" ) setApiHeaders(nil, request) request:SetHTTPRequestRawPostBody( "application/json", json.encode({code = normalizedCode}) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(_e) self:loadCurrencyFromServer(playerId) self:sendPromoCodeResult(playerId, true, "Промокод применён") end local ____try, ____hasReturned = pcall(function() local prevFree = self:getFreeCurrency(playerId) local prevDonate = self:getDonateCurrency(playerId) local decoded = {json.decode(result.Body)} local ____temp_123 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_123 = decoded[1] else ____temp_123 = decoded end local responseData = ____temp_123 local data = responseData local prevDust = self:getDustCurrency(playerId) local ____opt_124 = data and data.rewards local freeAmount = __TS__Number(____opt_124 and ____opt_124.free_currency or 0) local ____opt_128 = data and data.rewards local donateAmount = __TS__Number(____opt_128 and ____opt_128.donate_currency or 0) local ____opt_132 = data and data.rewards local dustAmount = __TS__Number(____opt_132 and ____opt_132.dust_currency or 0) if data and data.currency then local nextFree = __TS__Number(data.currency.free_currency or 0) local nextDonate = __TS__Number(data.currency.donate_currency or 0) local nextDust = __TS__Number(data.currency.dust_currency or 0) self.playerFreeCurrency:set(playerId, nextFree) self.playerDonateCurrency:set(playerId, nextDonate) self.playerDustCurrency:set(playerId, nextDust) self:updateCurrencyDisplay(playerId) if freeAmount == 0 and donateAmount == 0 and dustAmount == 0 then freeAmount = math.max(0, nextFree - prevFree) donateAmount = math.max(0, nextDonate - prevDonate) dustAmount = math.max(0, nextDust - prevDust) end else self:loadCurrencyFromServer(playerId) end local rewardParts = {} if freeAmount > 0 then rewardParts[#rewardParts + 1] = ("+" .. tostring(freeAmount)) .. " зомби-осколков" end if donateAmount > 0 then rewardParts[#rewardParts + 1] = ("+" .. tostring(donateAmount)) .. " донат" end if dustAmount > 0 then rewardParts[#rewardParts + 1] = ("+" .. tostring(dustAmount)) .. " пыли" end if #rewardParts > 0 then self:sendPromoCodeResult( playerId, true, "Промокод применён: " .. table.concat(rewardParts, ", ") ) else self:sendPromoCodeResult(playerId, true, "Промокод применён") end end) if not ____try then ____catch(____hasReturned) end end else local errorMessage = ("Ошибка активации промокода (HTTP " .. tostring(result.StatusCode)) .. ")" do local function ____catch(_e) if result.StatusCode == 0 then errorMessage = "Сервер недоступен: не удалось отправить запрос" elseif result.StatusCode == 404 then errorMessage = "Эндпоинт промокода не найден на сервере (404)" elseif result.StatusCode >= 500 then errorMessage = "Сервер промокодов временно недоступен" end end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local ____temp_138 if __TS__ArrayIsArray(decoded) and #decoded > 0 then ____temp_138 = decoded[1] else ____temp_138 = decoded end local responseData = ____temp_138 if responseData and responseData.error then errorMessage = responseData.error end end) if not ____try then ____catch(____hasReturned) end end self:sendPromoCodeResult(playerId, false, errorMessage) end end) end function StoreManager.prototype.normalizeArcadePackCredits(self, raw) local credits = {standard = 0, premium = 0} if not raw or type(raw) ~= "table" then return credits end local obj = raw credits.standard = math.max( 0, math.floor(__TS__Number(obj.standard) or 0) ) credits.premium = math.max( 0, math.floor(__TS__Number(obj.premium) or 0) ) return credits end function StoreManager.prototype.applyArcadePackCreditsFromApi(self, playerId, raw) if raw == nil then return false end local credits = self:normalizeArcadePackCredits(raw) local playerInfoData = PlayerInfo:GetPlayerInfo(playerId) or ({}) playerInfoData = __TS__ObjectAssign({}, playerInfoData, {arcade_pack_credits = credits}) PlayerInfo:UpdatePlayerInfo(playerId, playerInfoData) return true end function StoreManager.prototype.notifyArcadePackCredits(self, playerId) local info = PlayerInfo:GetPlayerInfo(playerId) local credits = self:normalizeArcadePackCredits(info and info.arcade_pack_credits) local player = PlayerResource:GetPlayer(playerId) if not player then return end CustomGameEventManager:Send_ServerToPlayer( player, "store_currency_update", { donate_currency = self:getDonateCurrency(playerId), free_currency = self:getFreeCurrency(playerId), dust_currency = self:getDustCurrency(playerId), arcade_pack_credits = credits } ) end function StoreManager.prototype.handleLoginPlayerApiResponse(self, playerId, responseData) if not responseData or type(responseData) ~= "table" then return end self:applyShopCurrencyFromApiPayload(playerId, responseData) local reward = responseData.subscription_daily_reward if not reward or not reward.granted then return end local player = PlayerResource:GetPlayer(playerId) if not player then return end local donateAmount = math.max( 0, math.floor(__TS__Number(reward.donate_currency) or 0) ) local freeAmount = math.max( 0, math.floor(__TS__Number(reward.free_currency) or 0) ) if donateAmount <= 0 and freeAmount <= 0 then return end CustomGameEventManager:Send_ServerToPlayer(player, "store_subscription_daily_reward", {granted = 1, donate_currency = donateAmount, free_currency = freeAmount}) storeVerboseLog( nil, ((((("[STORE] Ежедневная подписка UI: +" .. tostring(donateAmount)) .. " donate, +") .. tostring(freeAmount)) .. " free (player ") .. tostring(playerId)) .. ")" ) end function StoreManager.prototype.applyShopCurrencyFromApiPayload(self, playerId, responseData) if not responseData or type(responseData) ~= "table" then return false end local changed = false if responseData.free_currency ~= nil then self.playerFreeCurrency:set( playerId, math.max( 0, math.floor(__TS__Number(responseData.free_currency) or 0) ) ) changed = true end if responseData.donate_currency ~= nil then self.playerDonateCurrency:set( playerId, math.max( 0, math.floor(__TS__Number(responseData.donate_currency) or 0) ) ) changed = true end if responseData.dust_currency ~= nil then self.playerDustCurrency:set( playerId, math.max( 0, math.floor(__TS__Number(responseData.dust_currency) or 0) ) ) changed = true end local arcadeChanged = self:applyArcadePackCreditsFromApi(playerId, responseData.arcade_pack_credits) if arcadeChanged then changed = true end if not changed then return false end if arcadeChanged then self:notifyArcadePackCredits(playerId) else self:updateCurrencyDisplay(playerId) end return true end function StoreManager.prototype.syncLatestCurrencyTotalsFromApi(self, playerId, onDone) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then storeVerboseLog( nil, "[STORE] syncLatestCurrencyTotals: нет Steam ID для " .. tostring(playerId) ) onDone(nil, false) return end local request = CreateHTTPRequestScriptVM( "GET", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/currency" ) setApiHeaders(nil, request) request:Send(function(result) if result.StatusCode < 200 or result.StatusCode >= 300 then storeVerboseLog( nil, "[STORE] syncLatestCurrencyTotals: HTTP " .. tostring(result.StatusCode) ) onDone(nil, false) return end do local function ____catch(e) storeVerboseLog( nil, "[STORE] syncLatestCurrencyTotals: parse error " .. tostring(e) ) end local ____try, ____hasReturned, ____returnValue = pcall(function() local decoded = {json.decode(result.Body)} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and self:applyShopCurrencyFromApiPayload(playerId, responseData) then onDone(nil, true) return true end end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end onDone(nil, false) end) end function StoreManager.prototype.handlePurchase(self, playerId, itemId, itemData, selectedCurrency) if not itemData then self:sendPurchaseResult(playerId, false, "Данные о товаре не переданы") return end if isStorePurchaseBlockedById(nil, itemId) then storeVerboseLog(nil, ("[STORE] Покупка отклонена: " .. itemId) .. " запрещён для покупки в магазине (батлпасс/сезон)") self:sendPurchaseResult(playerId, false, "Предмет недоступен для покупки в магазине") return end if not self:tryAcquireStorePurchaseLock(playerId) then storeVerboseLog( nil, ("[STORE] Покупка отклонена: игрок " .. tostring(playerId)) .. " — уже обрабатывается другая покупка (антидубликат)" ) self:sendPurchaseResult(playerId, false, "Подождите, предыдущая покупка обрабатывается") return end self:syncLatestCurrencyTotalsFromApi( playerId, function(____, currencyOk) do local ____try, ____hasReturned, ____returnValue = pcall(function() if not currencyOk then storeVerboseLog( nil, ("[STORE] Покупка отклонена: не удалось синхронизировать баланс с API (игрок " .. tostring(playerId)) .. ")" ) self:sendPurchaseResult(playerId, false, "Не удалось проверить баланс на сервере. Попробуйте через секунду.") return true end local item = itemData local player = PlayerResource:GetPlayer(playerId) local playerName = player and PlayerResource:GetPlayerName(playerId) or "Player " .. tostring(playerId) local steamId = PlayerResource:GetSteamAccountID(playerId) local currency local price local allowedCurrencies = {} if item.allowed_currencies then for ____, value in ipairs(__TS__ObjectValues(item.allowed_currencies)) do if value == "free_currency" or value == "donate_currency" or value == "dust_currency" then if not __TS__ArrayIncludes(allowedCurrencies, value) then allowedCurrencies[#allowedCurrencies + 1] = value end end end end if #allowedCurrencies == 0 then __TS__ArrayPush(allowedCurrencies, "donate_currency", "free_currency") end if selectedCurrency then storeVerboseLog(nil, "[STORE] Используется выбранная валюта: " .. selectedCurrency) if not __TS__ArrayIncludes(allowedCurrencies, selectedCurrency) then storeVerboseLog( nil, (((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") пытается купить \"") .. item.name) .. "\" за недоступную валюту ") .. selectedCurrency ) self:sendPurchaseResult(playerId, false, "Эта валюта недоступна для данного предмета") return true end currency = selectedCurrency if currency == "free_currency" and item.price_free ~= nil then price = item.price_free storeVerboseLog( nil, "[STORE] Цена в free_currency: " .. tostring(price) ) elseif currency == "donate_currency" and item.price_donate ~= nil then price = item.price_donate storeVerboseLog( nil, "[STORE] Цена в donate_currency: " .. tostring(price) ) elseif currency == "dust_currency" and item.price_dust ~= nil then price = item.price_dust storeVerboseLog( nil, "[STORE] Цена в dust_currency: " .. tostring(price) ) else price = item.price storeVerboseLog( nil, "[STORE] Используется цена по умолчанию: " .. tostring(price) ) end else storeVerboseLog(nil, "[STORE] Валюта не выбрана, используется система по умолчанию") currency = item.currency or "donate_currency" if not __TS__ArrayIncludes(allowedCurrencies, currency) then currency = allowedCurrencies[1] end price = item.price end local priceRounded = math.floor(__TS__Number(price)) if not __TS__NumberIsFinite(priceRounded) or priceRounded < 0 then storeVerboseLog( nil, (("[STORE] Некорректная цена для \"" .. item.name) .. "\": raw=") .. tostring(price) ) self:sendPurchaseResult(playerId, false, "Некорректная цена") return true end price = priceRounded if item.category == "cards" and item.cardId ~= nil then local parsedCardId = __TS__Number(item.cardId) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then self:sendPurchaseResult(playerId, false, "Карта не найдена") return true end local cardId = math.floor(parsedCardId) if CARD_PURCHASABLE_BY_ID[cardId] == false then self:sendPurchaseResult(playerId, false, "Эту карту нельзя купить") return true end local maxCopies = self:getCardMaxCopies(cardId) local ownedCardCounts = self.playerCardPurchaseCounts:get(playerId) or ({}) local ownedCopies = math.max( 0, math.floor(__TS__Number(ownedCardCounts[cardId] or 0)) ) if ownedCopies >= maxCopies then storeVerboseLog( nil, ((((((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") достиг лимита карты ") .. tostring(cardId)) .. " (") .. tostring(ownedCopies)) .. "/") .. tostring(maxCopies)) .. ")" ) self:sendPurchaseResult(playerId, false, "Достигнут лимит копий карты") return true end end local parsedUniqueCardId = item.category == "cards" and item.cardId ~= nil and math.floor(__TS__Number(item.cardId)) or nil local cardAllowsMultipleCopies = parsedUniqueCardId ~= nil and __TS__NumberIsFinite(parsedUniqueCardId) and parsedUniqueCardId > 0 and self:getCardMaxCopies(parsedUniqueCardId) > 1 if item.unique and not cardAllowsMultipleCopies then local purchasedItems = self.playerPurchases:get(playerId) or __TS__New(Set) if item.category == "chat_wheel_sound" then local soundId = __TS__StringReplace(itemId, "chat_wheel_sound_", "") local playerInfo = PlayerInfo:GetPlayerInfo(playerId) local soundsWheel = playerInfo and playerInfo.sounds_wheel local hasInWheel = not not (soundsWheel and soundsWheel[soundId]) if purchasedItems:has(itemId) then if not hasInWheel then storeVerboseLog(nil, ("[STORE] chat_wheel_sound repair: " .. itemId) .. " в покупках, нет в sounds_wheel — успех без списания") self:sendPurchaseResult(playerId, true, "Звук уже в покупках", itemId) return true end storeVerboseLog( nil, ((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") звук чат-вилла уже в колесе (") .. soundId) .. ")" ) self:sendPurchaseResult(playerId, false, "Этот звук уже куплен") return true end if hasInWheel then storeVerboseLog( nil, ((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") звук ") .. soundId) .. " уже в sounds_wheel" ) self:sendPurchaseResult(playerId, false, "Этот звук уже куплен") return true end elseif purchasedItems:has(itemId) then storeVerboseLog( nil, ((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") пытается купить уже купленный уникальный предмет \"") .. item.name) .. "\"" ) self:sendPurchaseResult(playerId, false, "Этот предмет уже куплен") return true end end local hasEnoughCurrency = false local currentAmount = 0 if currency == "free_currency" then currentAmount = self:getFreeCurrency(playerId) hasEnoughCurrency = currentAmount >= price elseif currency == "donate_currency" then currentAmount = self.playerDonateCurrency:get(playerId) or 0 hasEnoughCurrency = currentAmount >= price elseif currency == "dust_currency" then currentAmount = self:getDustCurrency(playerId) hasEnoughCurrency = currentAmount >= price end if not hasEnoughCurrency then storeVerboseLog( nil, (((((((((("[STORE] Покупка отклонена: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") пытается купить \"") .. item.name) .. "\" за ") .. tostring(price)) .. " ") .. currency) .. ", но у него только ") .. tostring(currentAmount) ) self:sendPurchaseResult(playerId, false, "Недостаточно валюты") return true end if currency == "free_currency" then if price > 0 then local success = self:removeFreeCurrency(playerId, price) if not success then storeVerboseLog( nil, (((((("[STORE] Ошибка списания: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") - не удалось списать ") .. tostring(price)) .. " ") .. currency ) self:sendPurchaseResult(playerId, false, "Ошибка списания валюты") return true end local remainingAmount = self:getFreeCurrency(playerId) storeVerboseLog( nil, (((((((((((("[STORE] Покупка успешна: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") купил \"") .. item.name) .. "\" за ") .. tostring(price)) .. " зомби осколков. Было: ") .. tostring(currentAmount)) .. ", Снято: ") .. tostring(price)) .. ", Осталось: ") .. tostring(remainingAmount) ) end self:updateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) elseif currency == "donate_currency" then if price > 0 then local currentDonate = self.playerDonateCurrency:get(playerId) or 0 self.playerDonateCurrency:set(playerId, currentDonate - price) local remainingAmount = self.playerDonateCurrency:get(playerId) or 0 storeVerboseLog( nil, (((((((((((("[STORE] Покупка успешна: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") купил \"") .. item.name) .. "\" за ") .. tostring(price)) .. " донат осколков. Было: ") .. tostring(currentDonate)) .. ", Снято: ") .. tostring(price)) .. ", Осталось: ") .. tostring(remainingAmount) ) end self:updateDonateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) elseif currency == "dust_currency" then if price > 0 then local success = self:removeDustCurrency(playerId, price) if not success then storeVerboseLog( nil, ((((("[STORE] Ошибка списания: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") - не удалось списать ") .. tostring(price)) .. " пыли" ) self:sendPurchaseResult(playerId, false, "Ошибка списания валюты") return true end local remainingAmount = self:getDustCurrency(playerId) storeVerboseLog( nil, (((((((((((("[STORE] Покупка успешна: " .. playerName) .. " (SteamID: ") .. tostring(steamId)) .. ") купил \"") .. item.name) .. "\" за ") .. tostring(price)) .. " пыли. Было: ") .. tostring(currentAmount)) .. ", Снято: ") .. tostring(price)) .. ", Осталось: ") .. tostring(remainingAmount) ) end self:updateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) end self:advanceCurrencyLoadSeq(playerId) if item.category == "cards" and item.cardId ~= nil then AddCardToPlayerPool( nil, playerId, item.cardId, 5, 1 ) elseif item.category == "cards" and not item.cardId then self:sendPurchaseResult(playerId, false, "Карта не найдена") if currency == "free_currency" then self:addFreeCurrency(playerId, price) elseif currency == "donate_currency" then local currentDonate = self.playerDonateCurrency:get(playerId) or 0 self.playerDonateCurrency:set(playerId, currentDonate + price) elseif currency == "dust_currency" then self:addDustCurrency(playerId, price) end return true elseif item.category == "chat_wheel_sound" then storeVerboseLog(nil, ("[STORE] Покупка звука чат-вилла " .. itemId) .. " обрабатывается через chat_wheel_buy_sound") end local priceFreeRecorded = currency == "free_currency" and price or 0 local priceDonateRecorded = currency == "donate_currency" and price or 0 local priceDustRecorded = currency == "dust_currency" and price or 0 local persistedItemId = self:buildPersistedPurchaseItemId(itemId, item.category, item.cardId) self:savePurchaseToServer( playerId, persistedItemId, item.category, item.cardId, priceFreeRecorded, priceDonateRecorded, priceDustRecorded ) if not self.playerPurchases:has(playerId) then self.playerPurchases:set( playerId, __TS__New(Set) ) end local purchaseIdForCache = item.category == "cards" and item.cardId ~= nil and persistedItemId or itemId self.playerPurchases:get(playerId):add(purchaseIdForCache) if item.category == "cards" and item.cardId ~= nil then local parsedCardId = __TS__Number(item.cardId) if __TS__NumberIsFinite(parsedCardId) and parsedCardId > 0 then local cardId = math.floor(parsedCardId) local currentCardCounts = __TS__ObjectAssign( {}, self.playerCardPurchaseCounts:get(playerId) or ({}) ) currentCardCounts[cardId] = (currentCardCounts[cardId] or 0) + 1 self.playerCardPurchaseCounts:set(playerId, currentCardCounts) end end self:updateAvailableCardsForDeckBuilder( playerId, self.playerPurchases:get(playerId), self.playerCardPurchaseCounts:get(playerId) ) local purchaseResultItemId = item.category == "cards" and item.cardId ~= nil and persistedItemId or itemId self:sendPurchaseResult(playerId, true, "Покупка успешна", purchaseResultItemId) end) do self:releaseStorePurchaseLock(playerId) end if ____try and ____hasReturned then return ____returnValue end end end ) end function StoreManager.prototype.handleEquipEffect(self, playerId, effectId, effectType) local player = PlayerResource:GetPlayer(playerId) if not player then return end local purchases = self.playerPurchases:get(playerId) if not purchases or not purchases:has(effectId) then storeVerboseLog( nil, (("[STORE] Игрок " .. tostring(playerId)) .. " пытается надеть некупленный эффект ") .. effectId ) return end local activeEffects = self.playerActiveEffects:get(playerId) if not activeEffects then activeEffects = {} end activeEffects[effectType] = effectId self.playerActiveEffects:set(playerId, activeEffects) self:saveActiveEffectsToServer(playerId, activeEffects) local playerInfo = CustomNetTables:GetTableValue( "player_info", tostring(playerId) ) or ({}) playerInfo.active_effects = activeEffects CustomNetTables:SetTableValue( "player_info", tostring(playerId), playerInfo ) self:sendActiveEffectsUpdate(playerId, activeEffects) storeVerboseLog( nil, (((("[STORE] Игрок " .. tostring(playerId)) .. " надел эффект ") .. effectId) .. " типа ") .. effectType ) end function StoreManager.prototype.handleUnequipEffect(self, playerId, effectId, effectType) local player = PlayerResource:GetPlayer(playerId) if not player then return end local purchases = self.playerPurchases:get(playerId) if not purchases or not purchases:has(effectId) then storeVerboseLog( nil, (("[STORE] Игрок " .. tostring(playerId)) .. " пытается снять некупленный эффект ") .. effectId ) return end local activeEffects = self.playerActiveEffects:get(playerId) if not activeEffects then self:loadActiveEffectsFromServer( playerId, function(____, loadedEffects) if loadedEffects[effectType] == effectId then loadedEffects[effectType] = nil self.playerActiveEffects:set(playerId, loadedEffects) self:saveActiveEffectsToServer(playerId, loadedEffects) CustomNetTables:SetTableValue( "player_info", tostring(playerId), {active_effects = loadedEffects} ) self:sendActiveEffectsUpdate(playerId, loadedEffects) storeVerboseLog( nil, (((("[STORE] Игрок " .. tostring(playerId)) .. " снял эффект ") .. effectId) .. " типа ") .. effectType ) else storeVerboseLog( nil, ((((((("[STORE] Игрок " .. tostring(playerId)) .. " пытается снять неактивный эффект ") .. effectId) .. " типа ") .. effectType) .. " (активен: ") .. tostring(loadedEffects[effectType])) .. ")" ) end end ) return end if activeEffects[effectType] ~= effectId then storeVerboseLog( nil, ((((((("[STORE] Игрок " .. tostring(playerId)) .. " пытается снять неактивный эффект ") .. effectId) .. " типа ") .. effectType) .. " (активен: ") .. tostring(activeEffects[effectType])) .. ")" ) return end activeEffects[effectType] = nil self:saveActiveEffectsToServer(playerId, activeEffects) self:sendActiveEffectsUpdate(playerId, activeEffects) storeVerboseLog( nil, (((("[STORE] Игрок " .. tostring(playerId)) .. " снял эффект ") .. effectId) .. " типа ") .. effectType ) end function StoreManager.prototype.saveActiveEffectsToServer(self, playerId, activeEffects) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then return end local request = CreateHTTPRequestScriptVM( "PUT", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/active_effects" ) setApiHeaders(nil, request) local dataToSend = {active_effects = activeEffects} request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, dataToSend) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then storeVerboseLog( nil, (("[STORE] Активные эффекты сохранены на сервере для игрока " .. tostring(playerId)) .. ": ") .. json.encode(activeEffects) ) else storeVerboseLog( nil, "[STORE] Ошибка сохранения активных эффектов: StatusCode=" .. tostring(result.StatusCode) ) end end) end function StoreManager.prototype.sendActiveEffectsUpdate(self, playerId, activeEffects) local player = PlayerResource:GetPlayer(playerId) if player then CustomGameEventManager:Send_ServerToPlayer(player, "store_active_effects_update", {active_effects = activeEffects}) end end function StoreManager.prototype.getPlayerActiveEffects(self, playerId) local activeEffects = self.playerActiveEffects:get(playerId) if activeEffects then return activeEffects end return {} end function StoreManager.prototype.getPlayerActiveEffect(self, playerId, effectType) local activeEffects = self.playerActiveEffects:get(playerId) if activeEffects and activeEffects[effectType] then return activeEffects[effectType] end return nil end function StoreManager.prototype.isEffectEquipped(self, playerId, effectId, effectType) local activeEffects = self.playerActiveEffects:get(playerId) if not activeEffects then return false end if effectType then return activeEffects[effectType] == effectId else for ____type in pairs(activeEffects) do if activeEffects[____type] == effectId then return true end end end return false end function StoreManager.prototype.loadActiveEffectsFromServer(self, playerId, callback) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then callback(nil, {}) return end local request = CreateHTTPRequestScriptVM( "GET", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/active_effects" ) setApiHeaders(nil, request) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) storeVerboseLog( nil, "[STORE] Ошибка парсинга активных эффектов: " .. tostring(e) ) callback(nil, {}) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and type(responseData) == "table" and responseData.active_effects ~= nil then local loadedEffects = responseData.active_effects or ({}) self.playerActiveEffects:set(playerId, loadedEffects) local playerInfo = CustomNetTables:GetTableValue( "player_info", tostring(playerId) ) or ({}) playerInfo.active_effects = loadedEffects CustomNetTables:SetTableValue( "player_info", tostring(playerId), playerInfo ) storeVerboseLog( nil, (("[STORE] Загружены активные эффекты для игрока " .. tostring(playerId)) .. ": ") .. json.encode(loadedEffects) ) callback(nil, loadedEffects) else callback(nil, {}) end end) if not ____try then ____catch(____hasReturned) end end else storeVerboseLog( nil, "[STORE] Ошибка загрузки активных эффектов: StatusCode=" .. tostring(result.StatusCode) ) callback(nil, {}) end end) end function StoreManager.prototype.savePurchaseToServer(self, playerId, itemId, category, cardId, priceFree, priceDonate, priceDust) if priceFree == nil then priceFree = 0 end if priceDonate == nil then priceDonate = 0 end if priceDust == nil then priceDust = 0 end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then return end local request = CreateHTTPRequestScriptVM( "POST", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/purchases" ) setApiHeaders(nil, request) local dataToSend = { item_id = itemId, item_category = category, card_id = cardId or nil, price_free = priceFree, price_donate = priceDonate, price_dust = priceDust } request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, dataToSend) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then storeVerboseLog(nil, ("[STORE] Покупка " .. itemId) .. " сохранена на сервере") else storeVerboseLog( nil, (("[STORE] Ошибка сохранения покупки " .. itemId) .. ": StatusCode=") .. tostring(result.StatusCode) ) end end) end function StoreManager.prototype.sendPurchaseResult(self, playerId, success, message, itemId) local player = PlayerResource:GetPlayer(playerId) if player then CustomGameEventManager:Send_ServerToPlayer(player, "store_purchase_result", {success = success, message = message, item_id = itemId or ""}) end end function StoreManager.prototype.sendPromoCodeResult(self, playerId, success, message) local player = PlayerResource:GetPlayer(playerId) if player then CustomGameEventManager:Send_ServerToPlayer(player, "store_promocode_result", {success = success, message = message}) end end function StoreManager.prototype.registerChatWheelSoundFromBattlePass(self, playerId, soundId) local itemId = "chat_wheel_sound_" .. soundId if not self.playerPurchases:has(playerId) then self.playerPurchases:set( playerId, __TS__New(Set) ) end self.playerPurchases:get(playerId):add(itemId) self:updateAvailableCardsForDeckBuilder( playerId, self.playerPurchases:get(playerId) ) self:savePurchaseToServer(playerId, itemId, "chat_wheel_sound", nil) self:sendPurchaseResult(playerId, true, "Награда Battle Pass: звук чат-колеса", itemId) end function StoreManager.prototype.registerCardFromBattlePass(self, playerId, cardIdRaw) local parsedCardId = __TS__Number(cardIdRaw) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then return end local cardId = math.floor(parsedCardId) local itemId = "card_data_" .. tostring(cardId) if not self.playerPurchases:has(playerId) then self.playerPurchases:set( playerId, __TS__New(Set) ) end self.playerPurchases:get(playerId):add(itemId) local currentCardCounts = __TS__ObjectAssign( {}, self.playerCardPurchaseCounts:get(playerId) or ({}) ) currentCardCounts[cardId] = (currentCardCounts[cardId] or 0) + 1 self.playerCardPurchaseCounts:set(playerId, currentCardCounts) self:updateAvailableCardsForDeckBuilder( playerId, self.playerPurchases:get(playerId), currentCardCounts ) self:savePurchaseToServer( playerId, itemId, "cards", cardId, 0, 0 ) self:sendPurchaseResult(playerId, true, "Награда Battle Pass: карта добавлена", itemId) end function StoreManager.prototype.buildCardPurchaseCountsFromPurchaseList(self, purchases) local counts = {} local cardPrefix = "card_data_" for ____, purchaseId in __TS__Iterator(purchases) do do if not __TS__StringStartsWith(purchaseId, cardPrefix) then goto __continue308 end local rawCardId = __TS__StringSubstring(purchaseId, #cardPrefix) local suffixDelimiterIndex = (string.find(rawCardId, "_", nil, true) or 0) - 1 if suffixDelimiterIndex >= 0 then rawCardId = __TS__StringSubstring(rawCardId, 0, suffixDelimiterIndex) end local parsedCardId = __TS__Number(rawCardId) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then goto __continue308 end local cardId = math.floor(parsedCardId) counts[cardId] = (counts[cardId] or 0) + 1 end ::__continue308:: end return counts end function StoreManager.prototype.getCardMaxCopies(self, cardId) local configured = CARD_MAX_COPIES_BY_ID[cardId] if __TS__NumberIsFinite(configured) and configured > 0 then return math.floor(configured) end return 1 end function StoreManager.prototype.buildPersistedPurchaseItemId(self, itemId, category, cardId) if category ~= "cards" or cardId == nil then return itemId end local parsedCardId = __TS__Number(cardId) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then return itemId end local normalizedCardId = math.floor(parsedCardId) local nonce = (tostring(math.floor(GameRules:GetGameTime() * 1000)) .. "_") .. tostring(RandomInt(100000, 999999)) return (("card_data_" .. tostring(normalizedCardId)) .. "__") .. nonce end function StoreManager.prototype.updateAvailableCardsForDeckBuilder(self, playerId, purchases, providedCardCounts) local purchasedCardIds = {} local cardCounts = providedCardCounts or self:buildCardPurchaseCountsFromPurchaseList(purchases) storeVerboseLog( nil, "[STORE] Обновление списка купленных карт для deck builder. Всего покупок: " .. tostring(purchases.size) ) for ____, ____value in ipairs(__TS__ObjectEntries(cardCounts)) do local cardIdRaw = ____value[1] local copiesRaw = ____value[2] do local cardId = __TS__Number(cardIdRaw) local copies = math.max( 0, math.floor(__TS__Number(copiesRaw)) ) if not __TS__NumberIsFinite(cardId) or cardId <= 0 or copies <= 0 then goto __continue319 end do local i = 0 while i < copies do purchasedCardIds[#purchasedCardIds + 1] = cardId i = i + 1 end end storeVerboseLog( nil, (("[STORE] Добавлена карта " .. tostring(cardId)) .. " в количестве ") .. tostring(copies) ) end ::__continue319:: end local player = PlayerResource:GetPlayer(playerId) if player then CustomNetTables:SetTableValue( "cards", "purchased_cards_" .. tostring(playerId), purchasedCardIds ) storeVerboseLog( nil, ((("[STORE] Обновлен список купленных карт для deck builder: " .. tostring(#purchasedCardIds)) .. " карт: [") .. table.concat(purchasedCardIds, ", ")) .. "]" ) else storeVerboseLog( nil, ("[STORE] Ошибка: игрок " .. tostring(playerId)) .. " не найден для обновления списка купленных карт" ) end end function StoreManager.prototype.getFreeCurrency(self, playerId) return self.playerFreeCurrency:get(playerId) or 0 end function StoreManager.prototype.addFreeCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getFreeCurrency(playerId) self.playerFreeCurrency:set(playerId, current + amount) self:updateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) return true end function StoreManager.prototype.removeFreeCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getFreeCurrency(playerId) if current < amount then return false end self.playerFreeCurrency:set(playerId, current - amount) self:updateCurrencyDisplay(playerId) return true end function StoreManager.prototype.getDonateCurrency(self, playerId) return self.playerDonateCurrency:get(playerId) or 0 end function StoreManager.prototype.addDonateCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getDonateCurrency(playerId) self.playerDonateCurrency:set(playerId, current + amount) self:updateDonateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) return true end function StoreManager.prototype.removeDonateCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getDonateCurrency(playerId) if current < amount then return false end self.playerDonateCurrency:set(playerId, current - amount) self:updateDonateCurrencyDisplay(playerId) return true end function StoreManager.prototype.getDustCurrency(self, playerId) return self.playerDustCurrency:get(playerId) or 0 end function StoreManager.prototype.addDustCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getDustCurrency(playerId) self.playerDustCurrency:set(playerId, current + amount) self:updateCurrencyDisplay(playerId) self:saveCurrencyToServer(playerId) return true end function StoreManager.prototype.addArcadePackCredits(self, playerId, standard, premium) local addStandard = math.max( 0, math.floor(standard or 0) ) local addPremium = math.max( 0, math.floor(premium or 0) ) if addStandard <= 0 and addPremium <= 0 then return false end local info = PlayerInfo:GetPlayerInfo(playerId) or ({}) local credits = self:normalizeArcadePackCredits(info.arcade_pack_credits) credits.standard = credits.standard + addStandard credits.premium = credits.premium + addPremium PlayerInfo:UpdatePlayerInfo( playerId, __TS__ObjectAssign({}, info, {arcade_pack_credits = credits}) ) self:notifyArcadePackCredits(playerId) return true end function StoreManager.prototype.removeDustCurrency(self, playerId, amount) if amount <= 0 then return false end local current = self:getDustCurrency(playerId) if current < amount then return false end self.playerDustCurrency:set(playerId, current - amount) self:updateCurrencyDisplay(playerId) return true end function StoreManager.prototype.tryConsumeDonateCurrency(self, playerId, amount) if not self:removeDonateCurrency(playerId, amount) then return false end self:saveCurrencyToServer(playerId) return true end function StoreManager.prototype.hasUnlockedHero(self, playerId, heroName, storeItemId) local purchases = self.playerPurchases:get(playerId) if not purchases or purchases.size == 0 then return false end local targetItemId = storeItemId or self:transformHeroNameToStoreItemId(heroName) if not targetItemId then return false end return purchases:has(targetItemId) end function StoreManager.prototype.hasPurchasedItem(self, playerId, itemId) local purchases = self.playerPurchases:get(playerId) if not purchases or purchases.size == 0 then return false end if purchases:has(itemId) then return true end local canonCardId = self:tryParseCanonicalCardDataStoreItemId(itemId) if canonCardId ~= nil then return self:hasPurchasedCardById(playerId, canonCardId) end return false end function StoreManager.prototype.tryParseCanonicalCardDataStoreItemId(self, itemId) local prefix = "card_data_" if not __TS__StringStartsWith(itemId, prefix) then return nil end local suffix = __TS__StringSubstring(itemId, #prefix) local digits = "" do local i = 0 while i < #suffix do local ch = __TS__StringCharAt(suffix, i) if ch >= "0" and ch <= "9" then digits = digits .. ch else break end i = i + 1 end end if #digits == 0 or #digits ~= #suffix then return nil end local cid = tonumber(digits) if cid == nil or cid <= 0 then return nil end return math.floor(cid) end function StoreManager.prototype.hasPurchasedCardById(self, playerId, cardIdRaw) return self:getOwnedCardCopies(playerId, cardIdRaw) > 0 end function StoreManager.prototype.getOwnedCardCopies(self, playerId, cardIdRaw) local parsedCardId = __TS__Number(cardIdRaw) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then return 0 end local cardId = math.floor(parsedCardId) local cardCounts = self.playerCardPurchaseCounts:get(playerId) return math.max( 0, math.floor(__TS__Number(cardCounts and cardCounts[cardId] or 0)) ) end function StoreManager.prototype.grantCardPurchaseWithoutPayment(self, playerId, cardIdRaw) local parsedCardId = __TS__Number(cardIdRaw) if not __TS__NumberIsFinite(parsedCardId) or parsedCardId <= 0 then return nil end local cardId = math.floor(parsedCardId) if CARD_PURCHASABLE_BY_ID[cardId] == false then return nil end local maxCopies = self:getCardMaxCopies(cardId) local ownedCopies = self:getOwnedCardCopies(playerId, cardId) if ownedCopies >= maxCopies then return nil end AddCardToPlayerPool( nil, playerId, cardId, 5, 1 ) local persistedItemId = self:buildPersistedPurchaseItemId( "card_data_" .. tostring(cardId), "cards", cardId ) self:savePurchaseToServer( playerId, persistedItemId, "cards", cardId, 0, 0, 0 ) if not self.playerPurchases:has(playerId) then self.playerPurchases:set( playerId, __TS__New(Set) ) end self.playerPurchases:get(playerId):add(persistedItemId) local currentCardCounts = __TS__ObjectAssign( {}, self.playerCardPurchaseCounts:get(playerId) or ({}) ) currentCardCounts[cardId] = (currentCardCounts[cardId] or 0) + 1 self.playerCardPurchaseCounts:set(playerId, currentCardCounts) self:updateAvailableCardsForDeckBuilder( playerId, self.playerPurchases:get(playerId), currentCardCounts ) return persistedItemId end function StoreManager.prototype.transformHeroNameToStoreItemId(self, heroName) if __TS__StringStartsWith(heroName, ____exports.StoreManager.HERO_STORE_PREFIX) then return "hero_" .. __TS__StringSubstring(heroName, #____exports.StoreManager.HERO_STORE_PREFIX) end if #heroName > 0 then return heroName end return nil end function StoreManager.prototype.updateDonateCurrencyDisplay(self, playerId) self:updateCurrencyDisplay(playerId) end function StoreManager.prototype.updateCurrencyDisplay(self, playerId) local donateAmount = self:getDonateCurrency(playerId) local freeAmount = self:getFreeCurrency(playerId) local dustAmount = self:getDustCurrency(playerId) local info = PlayerInfo:GetPlayerInfo(playerId) local arcadePackCredits = self:normalizeArcadePackCredits(info and info.arcade_pack_credits) local player = PlayerResource:GetPlayer(playerId) if player then CustomGameEventManager:Send_ServerToPlayer(player, "store_currency_update", {donate_currency = donateAmount, free_currency = freeAmount, dust_currency = dustAmount, arcade_pack_credits = arcadePackCredits}) end end function StoreManager.prototype.grantShopExtrasViaApi(self, playerId, extras, logSuccess) self:sendShopExtrasGiveRequest( playerId, { free = math.max( 0, math.floor(extras.freeAmount or 0) ), donate = math.max( 0, math.floor(extras.donateAmount or 0) ), dust = math.max( 0, math.floor(extras.dustAmount or 0) ), arcadeStandard = math.max( 0, math.floor(extras.arcadePackStandard or 0) ), arcadePremium = math.max( 0, math.floor(extras.arcadePackPremium or 0) ) }, logSuccess, false ) end function StoreManager.prototype.sendShopExtrasGiveRequest(self, playerId, amounts, logSuccess, alreadyRetriedAfterEnsure) local ____amounts_149 = amounts local free = ____amounts_149.free local donate = ____amounts_149.donate local dust = ____amounts_149.dust local arcadeStandard = ____amounts_149.arcadeStandard local arcadePremium = ____amounts_149.arcadePremium if free <= 0 and donate <= 0 and dust <= 0 and arcadeStandard <= 0 and arcadePremium <= 0 then return end local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then storeVerboseLog( nil, "[STORE] grantShopExtrasViaApi: нет Steam ID для " .. tostring(playerId) ) return end local body = {free_currency = free, donate_currency = donate, dust_currency = dust} if arcadeStandard > 0 or arcadePremium > 0 then body.arcade_pack_credits = {standard = arcadeStandard, premium = arcadePremium} end local request = CreateHTTPRequestScriptVM( "POST", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/currency/give" ) setApiHeadersLong(nil, request) request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, body) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then storeVerboseLog(nil, logSuccess) do local function ____catch(e) storeVerboseLog( nil, "[STORE] grantShopExtrasViaApi: ошибка парсинга: " .. tostring(e) ) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData then self:applyShopCurrencyFromApiPayload(playerId, responseData) end end) if not ____try then ____catch(____hasReturned) end end return end if result.StatusCode == 404 and not alreadyRetriedAfterEnsure then storeVerboseLog(nil, "[STORE] grantShopExtrasViaApi 404 — создаём player и повторяем") self:ensurePlayerRowOnApi( playerId, function(____, ok) if ok then self:sendShopExtrasGiveRequest(playerId, amounts, logSuccess, true) else storeVerboseLog(nil, "[STORE] grantShopExtrasViaApi: не удалось создать игрока на API") end end ) return end local bodyStr = result.Body ~= nil and tostring(result.Body) or "" storeVerboseLog( nil, (("[STORE] grantShopExtrasViaApi: HTTP " .. tostring(result.StatusCode)) .. " body=") .. bodyStr ) end) end function StoreManager.prototype.saveCurrencyToServer(self, playerId) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then return end local freeCurrency = self:getFreeCurrency(playerId) local donateCurrency = self:getDonateCurrency(playerId) local dustCurrency = self:getDustCurrency(playerId) local request = CreateHTTPRequestScriptVM( "PUT", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/currency" ) setApiHeaders(nil, request) local dataToSend = {free_currency = freeCurrency, donate_currency = donateCurrency, dust_currency = dustCurrency} request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, dataToSend) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then else end end) end function StoreManager.prototype.ensurePlayerRowOnApi(self, playerId, done) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then storeVerboseLog( nil, "[STORE] ensurePlayerRowOnApi: нет Steam ID для " .. tostring(playerId) ) done(nil, false) return end local request = CreateHTTPRequestScriptVM("POST", SERVER_CONFIG.API_URL .. "/player") setApiHeadersLong(nil, request) request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody( nil, { steam_id = steamId, player_name = PlayerResource:GetPlayerName(playerId) or "" } ) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then done(nil, true) return end local bodyStr = result.Body ~= nil and tostring(result.Body) or "" storeVerboseLog( nil, (("[STORE] ensurePlayerRowOnApi: HTTP " .. tostring(result.StatusCode)) .. " body=") .. bodyStr ) done(nil, false) end) end function StoreManager.prototype.giveCurrencyFromServer(self, playerId, freeAmount, donateAmount, dustAmount) if dustAmount == nil then dustAmount = 0 end self:sendGiveCurrencyRequest( playerId, freeAmount, donateAmount, dustAmount, false ) end function StoreManager.prototype.sendGiveCurrencyRequest(self, playerId, freeAmount, donateAmount, dustAmount, alreadyRetriedAfterEnsure) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then storeVerboseLog( nil, "[STORE] giveCurrency: нет Steam ID для " .. tostring(playerId) ) return end if freeAmount <= 0 and donateAmount <= 0 and dustAmount <= 0 then return end local request = CreateHTTPRequestScriptVM( "POST", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/currency/give" ) setApiHeadersLong(nil, request) request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, {free_currency = freeAmount, donate_currency = donateAmount, dust_currency = dustAmount}) ) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) storeVerboseLog( nil, "[STORE] currency/give: ошибка парсинга: " .. tostring(e) ) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and self:applyShopCurrencyFromApiPayload(playerId, responseData) then storeVerboseLog( nil, (("[STORE] currency/give OK: free=" .. tostring(responseData.free_currency)) .. " donate=") .. tostring(responseData.donate_currency) ) else storeVerboseLog( nil, "[STORE] currency/give: неожиданный JSON: " .. tostring(result.Body) ) end end) if not ____try then ____catch(____hasReturned) end end elseif result.StatusCode == 404 and not alreadyRetriedAfterEnsure then storeVerboseLog(nil, "[STORE] currency/give 404 (нет строки player) — POST /player и повтор") self:ensurePlayerRowOnApi( playerId, function(____, ok) if ok then self:sendGiveCurrencyRequest( playerId, freeAmount, donateAmount, dustAmount, true ) else storeVerboseLog(nil, "[STORE] Не удалось создать игрока на API — осколки не начислены") end end ) else local bodyStr = result.Body ~= nil and tostring(result.Body) or "" storeVerboseLog( nil, (("[STORE] Ошибка currency/give: HTTP " .. tostring(result.StatusCode)) .. " body=") .. bodyStr ) end end) end function StoreManager.prototype.grantDustCurrencyMatchEndReward(self, playerId, amount) local n = math.floor(amount) if n <= 0 then return end self:giveCurrencyFromServer(playerId, 0, 0, n) end function StoreManager.prototype.grantFreeCurrencyMatchEndReward(self, playerId, amount) local n = math.floor(amount) if n <= 0 then return end self:giveCurrencyFromServer(playerId, n, 0) end function StoreManager.prototype.loadCurrencyFromServer(self, playerId) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then storeVerboseLog( nil, "[STORE] Ошибка: Steam ID не найден для игрока " .. tostring(playerId) ) return end storeVerboseLog( nil, ((("[STORE] Загрузка валюты для игрока " .. tostring(playerId)) .. " (SteamID: ") .. tostring(steamId)) .. ")" ) local loadSeq = self:advanceCurrencyLoadSeq(playerId) local request = CreateHTTPRequestScriptVM( "GET", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/currency" ) setApiHeaders(nil, request) request:Send(function(result) if (self.playerCurrencyLoadSeq:get(playerId) or 0) ~= loadSeq then storeVerboseLog( nil, ((("[STORE] Пропуск устаревшего ответа валюты для игрока " .. tostring(playerId)) .. " (seq ") .. tostring(loadSeq)) .. ")" ) return end storeVerboseLog( nil, (("[STORE] Ответ сервера: StatusCode=" .. tostring(result.StatusCode)) .. ", Body=") .. tostring(result.Body) ) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) storeVerboseLog( nil, "[STORE] Ошибка парсинга ответа: " .. tostring(e) ) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} storeVerboseLog( nil, "[STORE] Декодированный ответ: " .. json.encode(decoded) ) local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and (responseData.free_currency ~= nil or responseData.donate_currency ~= nil or responseData.dust_currency ~= nil) then storeVerboseLog( nil, (((("[STORE] Валюта с сервера: free=" .. tostring(responseData.free_currency)) .. ", donate=") .. tostring(responseData.donate_currency)) .. ", dust=") .. tostring(responseData.dust_currency) ) if responseData.free_currency ~= nil then self.playerFreeCurrency:set(playerId, responseData.free_currency) storeVerboseLog( nil, "[STORE] Установлены зомби осколки: " .. tostring(responseData.free_currency) ) end if responseData.donate_currency ~= nil then self.playerDonateCurrency:set(playerId, responseData.donate_currency) storeVerboseLog( nil, "[STORE] Установлена донат валюта: " .. tostring(responseData.donate_currency) ) end if responseData.dust_currency ~= nil then self.playerDustCurrency:set(playerId, responseData.dust_currency) storeVerboseLog( nil, "[STORE] Установлена пыль: " .. tostring(responseData.dust_currency) ) end self:loadPurchasesFromServer( playerId, function(____, purchases) self:loadActiveEffectsFromServer( playerId, function(____, loadedEffects) local player = PlayerResource:GetPlayer(playerId) if player then local eventData = { donate_currency = responseData.donate_currency or 0, free_currency = responseData.free_currency or 0, dust_currency = responseData.dust_currency or 0, purchased_items = purchases, arcade_pack_credits = self:normalizeArcadePackCredits(responseData.arcade_pack_credits) } storeVerboseLog( nil, "[STORE] Отправка события клиенту: " .. json.encode(eventData) ) CustomGameEventManager:Send_ServerToPlayer(player, "store_currency_update", eventData) self:sendActiveEffectsUpdate(playerId, loadedEffects) else storeVerboseLog( nil, ("[STORE] Ошибка: Игрок " .. tostring(playerId)) .. " не найден для отправки события" ) end end ) end ) else storeVerboseLog( nil, "[STORE] Ошибка: Неверный формат ответа от сервера. responseData: " .. json.encode(responseData) ) end end) if not ____try then ____catch(____hasReturned) end end else storeVerboseLog( nil, "[STORE] Ошибка HTTP: StatusCode=" .. tostring(result.StatusCode) ) end end) end function StoreManager.prototype.loadPurchasesFromServer(self, playerId, callback) local steamId = PlayerResource:GetSteamAccountID(playerId) if not steamId then callback(nil, {}) return end local request = CreateHTTPRequestScriptVM( "GET", ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/purchases" ) setApiHeaders(nil, request) request:Send(function(result) if result.StatusCode >= 200 and result.StatusCode < 300 then do local function ____catch(e) storeVerboseLog( nil, "[STORE] Ошибка парсинга списка покупок: " .. tostring(e) ) callback(nil, {}) end local ____try, ____hasReturned = pcall(function() local decoded = {json.decode(result.Body)} local purchases = {} local responseData = nil if __TS__ArrayIsArray(decoded) and #decoded > 0 then responseData = decoded[1] elseif decoded and type(decoded) == "table" then responseData = decoded end if responseData and type(responseData) == "table" and responseData.purchases ~= nil then local purchasesData = responseData.purchases if __TS__ArrayIsArray(purchasesData) then purchases = purchasesData end end local previousPurchases = self.playerPurchases:get(playerId) or __TS__New(Set) local purchasesSet = __TS__New(Set, purchases) for ____, p in __TS__Iterator(previousPurchases) do purchasesSet:add(p) end local cardCounts = self:buildCardPurchaseCountsFromPurchaseList(purchasesSet) self.playerPurchases:set(playerId, purchasesSet) self.playerCardPurchaseCounts:set(playerId, cardCounts) self:updateAvailableCardsForDeckBuilder(playerId, purchasesSet, cardCounts) local mergedPurchasesArray = __TS__ArrayFrom(purchasesSet) storeVerboseLog( nil, (((("[STORE] Загружено с API " .. tostring(#purchases)) .. " покупок, после слияния с сессией ") .. tostring(#mergedPurchasesArray)) .. " для игрока ") .. tostring(playerId) ) callback(nil, mergedPurchasesArray) end) if not ____try then ____catch(____hasReturned) end end else storeVerboseLog( nil, "[STORE] Ошибка загрузки покупок: StatusCode=" .. tostring(result.StatusCode) ) callback(nil, {}) end end) end StoreManager.HERO_STORE_PREFIX = "npc_dota_hero_" ____exports.StoreManager:getInstance() return ____exports