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>
1569 lines
57 KiB
Lua
1569 lines
57 KiB
Lua
local ____lualib = require("lualib_bundle")
|
||
local __TS__Class = ____lualib.__TS__Class
|
||
local __TS__New = ____lualib.__TS__New
|
||
local __TS__StringTrim = ____lualib.__TS__StringTrim
|
||
local __TS__ObjectKeys = ____lualib.__TS__ObjectKeys
|
||
local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite
|
||
local __TS__Delete = ____lualib.__TS__Delete
|
||
local __TS__ArraySort = ____lualib.__TS__ArraySort
|
||
local Set = ____lualib.Set
|
||
local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign
|
||
local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
|
||
local __TS__Decorate = ____lualib.__TS__Decorate
|
||
local ____exports = {}
|
||
local ____tstl_2Dutils = require("lib.tstl-utils")
|
||
local reloadable = ____tstl_2Dutils.reloadable
|
||
local ____api_helper = require("api_helper")
|
||
local setApiHeaders = ____api_helper.setApiHeaders
|
||
local ____server_config = require("server_config")
|
||
local SERVER_CONFIG = ____server_config.SERVER_CONFIG
|
||
local ____ArsenalCatalog = require("arsenal.ArsenalCatalog")
|
||
local ARSENAL_ITEMS = ____ArsenalCatalog.ARSENAL_ITEMS
|
||
local ARSENAL_ITEMS_MAP = ____ArsenalCatalog.ARSENAL_ITEMS_MAP
|
||
local ARSENAL_SLOTS = ____ArsenalCatalog.ARSENAL_SLOTS
|
||
local publishArsenalCatalog = ____ArsenalCatalog.publishArsenalCatalog
|
||
local ____ArsenalStats = require("arsenal.ArsenalStats")
|
||
local createArsenalItemInstance = ____ArsenalStats.createArsenalItemInstance
|
||
local getEffectiveItemStats = ____ArsenalStats.getEffectiveItemStats
|
||
local ____ArsenalStatRuntime = require("arsenal.ArsenalStatRuntime")
|
||
local setArsenalTotalsProvider = ____ArsenalStatRuntime.setArsenalTotalsProvider
|
||
local ____store_manager = require("store_manager")
|
||
local StoreManager = ____store_manager.StoreManager
|
||
local ____real_lobby_player = require("utils.real_lobby_player")
|
||
local isRealLobbyPlayer = ____real_lobby_player.isRealLobbyPlayer
|
||
require("arsenal.items.weapon")
|
||
require("arsenal.items.armor")
|
||
require("arsenal.items.helmet")
|
||
require("arsenal.items.boots")
|
||
require("arsenal.items.necklace")
|
||
require("arsenal.items.ring")
|
||
require("arsenal.items.dynamic_stats")
|
||
local LOG_PREFIX = "[ArsenalManager]"
|
||
local ARSENAL_ROLL_VERSION = 2
|
||
local ARSENAL_INVENTORY_PUT_DEBOUNCE = 0.12
|
||
--- Включить тяжёлый лог пересчёта тоталов (json.encode на каждое изменение) — только для отладки.
|
||
local ARSENAL_VERBOSE_TOTALS_LOG = false
|
||
--- Максимум экземпляров предметов в инвентаре (синхрон с UI `MIN_CATALOG_CELLS` и PUT API).
|
||
local ARSENAL_INVENTORY_MAX_INSTANCES = 207
|
||
____exports.ArsenalManagerClass = __TS__Class()
|
||
local ArsenalManagerClass = ____exports.ArsenalManagerClass
|
||
ArsenalManagerClass.name = "ArsenalManagerClass"
|
||
ArsenalManagerClass.____file_path = "scripts/vscripts/arsenal/ArsenalManager.lua"
|
||
function ArsenalManagerClass.prototype.____constructor(self)
|
||
self.loadouts = {}
|
||
self.instances = {}
|
||
self.selectedHero = {}
|
||
self.appliedFor = {}
|
||
self.lastPrintedTotals = {}
|
||
self.inventorySaveGeneration = {}
|
||
end
|
||
function ArsenalManagerClass.getInstance(self)
|
||
if not ____exports.ArsenalManagerClass._instance then
|
||
____exports.ArsenalManagerClass._instance = __TS__New(____exports.ArsenalManagerClass)
|
||
end
|
||
return ____exports.ArsenalManagerClass._instance
|
||
end
|
||
function ArsenalManagerClass.prototype.initialize(self)
|
||
publishArsenalCatalog(nil)
|
||
setArsenalTotalsProvider(
|
||
nil,
|
||
function(____, hero) return self:getHeroStatTotals(hero) end
|
||
)
|
||
self:registerListeners()
|
||
print(LOG_PREFIX .. " initialized")
|
||
end
|
||
function ArsenalManagerClass.prototype.registerListeners(self)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_equip_item",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleEquip(
|
||
playerId,
|
||
data.hero,
|
||
data.slot,
|
||
tostring(data.instanceId or "")
|
||
)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_unequip_item",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleUnequip(playerId, data.hero, data.slot)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_select_hero",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self.selectedHero[playerId] = tostring(data.hero or "")
|
||
self:syncToClient(playerId)
|
||
if self.selectedHero[playerId] and #self.selectedHero[playerId] > 0 then
|
||
self:refreshHeroArsenalStats(playerId, self.selectedHero[playerId])
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_request_sync",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:loadFromServer(playerId)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_upgrade_item",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleUpgradeItem(
|
||
playerId,
|
||
tostring(data.instanceId or "")
|
||
)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_toggle_pin",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleTogglePin(
|
||
playerId,
|
||
tostring(data.instanceId or "")
|
||
)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_toggle_favorite",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleToggleFavorite(
|
||
playerId,
|
||
tostring(data.instanceId or "")
|
||
)
|
||
end
|
||
)
|
||
CustomGameEventManager:RegisterListener(
|
||
"arsenal_disassemble_item",
|
||
function(_src, data)
|
||
local playerId = self:resolvePlayerId(data)
|
||
if playerId == nil then
|
||
return
|
||
end
|
||
self:handleDisassembleItem(
|
||
playerId,
|
||
tostring(data.instanceId or "")
|
||
)
|
||
end
|
||
)
|
||
ListenToGameEvent(
|
||
"game_rules_state_change",
|
||
function()
|
||
if not IsServer() then
|
||
return
|
||
end
|
||
local s = GameRules:State_Get()
|
||
if s == DOTA_GAMERULES_STATE_PRE_GAME or s == DOTA_GAMERULES_STATE_GAME_IN_PROGRESS then
|
||
do
|
||
local pid = 0
|
||
while pid < DOTA_MAX_PLAYERS do
|
||
do
|
||
if not isRealLobbyPlayer(nil, pid) then
|
||
goto __continue28
|
||
end
|
||
self:refreshHeroArsenalStats(pid, "")
|
||
self:scheduleArsenalStatRefresh(pid)
|
||
end
|
||
::__continue28::
|
||
pid = pid + 1
|
||
end
|
||
end
|
||
end
|
||
end,
|
||
nil
|
||
)
|
||
end
|
||
function ArsenalManagerClass.prototype.resolvePlayerId(self, data)
|
||
local ____opt_result_2
|
||
if data ~= nil then
|
||
____opt_result_2 = data.PlayerID
|
||
end
|
||
local ____opt_result_2_6 = ____opt_result_2
|
||
if ____opt_result_2_6 == nil then
|
||
local ____opt_result_5
|
||
if data ~= nil then
|
||
____opt_result_5 = data.playerId
|
||
end
|
||
____opt_result_2_6 = ____opt_result_5
|
||
end
|
||
local ____opt_result_2_6_10 = ____opt_result_2_6
|
||
if ____opt_result_2_6_10 == nil then
|
||
local ____opt_result_9
|
||
if data ~= nil then
|
||
____opt_result_9 = data.playerID
|
||
end
|
||
____opt_result_2_6_10 = ____opt_result_9
|
||
end
|
||
local raw = ____opt_result_2_6_10
|
||
if raw == nil or raw == nil then
|
||
return nil
|
||
end
|
||
local n = tonumber(tostring(raw))
|
||
if n == nil or n < 0 then
|
||
return nil
|
||
end
|
||
return n
|
||
end
|
||
function ArsenalManagerClass.prototype.normalizeArsenalStatKey(self, rawKey)
|
||
local key = string.lower(tostring(rawKey or ""))
|
||
local alias = {
|
||
battle_level = "battle_level",
|
||
bonus_damage = "bonus_damage",
|
||
attack_speed = "attack_speed",
|
||
bonus_armor = "bonus_armor",
|
||
max_health = "max_health",
|
||
max_mana = "max_mana",
|
||
health_regen = "health_regen",
|
||
mana_regen = "mana_regen",
|
||
bonus_health_regen = "health_regen",
|
||
bonus_mana_regen = "mana_regen",
|
||
hp_regen = "health_regen",
|
||
mp_regen = "mana_regen",
|
||
move_speed = "move_speed",
|
||
magic_resist = "magic_resist",
|
||
spell_amp = "spell_amp",
|
||
all_stats = "all_stats",
|
||
all_stats_pct = "all_stats_pct",
|
||
bonus_strength = "bonus_strength",
|
||
bonus_agility = "bonus_agility",
|
||
bonus_intellect = "bonus_intellect",
|
||
strength_pct = "strength_pct",
|
||
agility_pct = "agility_pct",
|
||
intellect_pct = "intellect_pct",
|
||
luck = "luck",
|
||
outgoing_damage_pct = "outgoing_damage_pct",
|
||
outgoing_damage = "outgoing_damage_pct",
|
||
damage_out_pct = "outgoing_damage_pct",
|
||
damage_outgoing_pct = "outgoing_damage_pct",
|
||
bonus_outgoing_damage_pct = "outgoing_damage_pct",
|
||
incoming_damage_reduction_pct = "incoming_damage_reduction_pct",
|
||
incoming_reduction_pct = "incoming_damage_reduction_pct",
|
||
damage_reduction_pct = "incoming_damage_reduction_pct",
|
||
attack_speed_pct = "attack_speed_pct",
|
||
attackspeed_pct = "attack_speed_pct",
|
||
attack_speed_percent = "attack_speed_pct",
|
||
move_speed_pct = "move_speed_pct",
|
||
movespeed_pct = "move_speed_pct",
|
||
move_speed_percent = "move_speed_pct",
|
||
base_damage_pct = "base_damage_pct",
|
||
bonus_damage_pct = "base_damage_pct",
|
||
attack_damage_pct = "base_damage_pct",
|
||
crit_mult = "crit_mult",
|
||
crit_multiplier = "crit_mult",
|
||
critical_multiplier = "crit_mult",
|
||
damage = "bonus_damage",
|
||
bonus_attack_damage = "bonus_damage",
|
||
attack_damage = "bonus_damage",
|
||
attackspeed = "attack_speed",
|
||
attack_speed_bonus = "attack_speed",
|
||
armor = "bonus_armor",
|
||
armor_bonus = "bonus_armor",
|
||
physical_armor = "bonus_armor",
|
||
health = "max_health",
|
||
bonus_health = "max_health",
|
||
mana = "max_mana",
|
||
bonus_mana = "max_mana",
|
||
movespeed = "move_speed",
|
||
movespeed_bonus = "move_speed",
|
||
move_speed_bonus = "move_speed",
|
||
magic_resistance = "magic_resist",
|
||
magical_resistance = "magic_resist",
|
||
magic_resistance_bonus = "magic_resist",
|
||
spell_amplify = "spell_amp",
|
||
spell_amplification = "spell_amp",
|
||
stats_all = "all_stats",
|
||
all_attributes_pct = "all_stats_pct",
|
||
attributes_pct = "all_stats_pct",
|
||
stats_pct = "all_stats_pct",
|
||
strength = "bonus_strength",
|
||
bonus_str = "bonus_strength",
|
||
str_bonus = "bonus_strength",
|
||
agility = "bonus_agility",
|
||
bonus_agi = "bonus_agility",
|
||
agi_bonus = "bonus_agility",
|
||
intellect = "bonus_intellect",
|
||
bonus_int = "bonus_intellect",
|
||
int_bonus = "bonus_intellect",
|
||
str_pct = "strength_pct",
|
||
strength_percent = "strength_pct",
|
||
bonus_strength_pct = "strength_pct",
|
||
agi_pct = "agility_pct",
|
||
agility_percent = "agility_pct",
|
||
bonus_agility_pct = "agility_pct",
|
||
int_pct = "intellect_pct",
|
||
intellect_percent = "intellect_pct",
|
||
bonus_intellect_pct = "intellect_pct"
|
||
}
|
||
return alias[key]
|
||
end
|
||
function ArsenalManagerClass.prototype.refreshHeroArsenalStats(self, playerId, _heroName)
|
||
local player = PlayerResource:GetPlayer(playerId)
|
||
if not player then
|
||
return
|
||
end
|
||
local assigned = player.GetAssignedHero and player:GetAssignedHero()
|
||
local hero = assigned and IsValidEntity(assigned) and assigned:IsRealHero() and assigned or PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if not hero or not IsValidEntity(hero) or not hero:IsRealHero() then
|
||
return
|
||
end
|
||
local heroMemo = hero
|
||
heroMemo.__arsenalHeroStatTotalsMemoTime = nil
|
||
heroMemo.__arsenalHeroStatTotalsMemo = nil
|
||
if not hero:HasModifier(____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME) then
|
||
hero:AddNewModifier(
|
||
hero,
|
||
getModifierSourceAbility(nil, hero),
|
||
____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME,
|
||
{}
|
||
)
|
||
end
|
||
local mod = hero:FindModifierByName(____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME)
|
||
if mod then
|
||
mod:ForceRefresh()
|
||
end
|
||
hero:CalculateStatBonus(true)
|
||
end
|
||
function ArsenalManagerClass.prototype.scheduleArsenalStatRefresh(self, playerId)
|
||
if not IsServer() then
|
||
return
|
||
end
|
||
local function run()
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
return nil
|
||
end
|
||
Timers:CreateTimer(0, run)
|
||
Timers:CreateTimer(0.15, run)
|
||
end
|
||
function ArsenalManagerClass.prototype.isArsenalEditPhase(self)
|
||
return GameRules:State_Get() == DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP
|
||
end
|
||
function ArsenalManagerClass.prototype.sendError(self, playerId, token)
|
||
local player = PlayerResource:GetPlayer(playerId)
|
||
if not player then
|
||
return
|
||
end
|
||
CustomGameEventManager:Send_ServerToPlayer(player, "create_error_message", {message = token})
|
||
end
|
||
function ArsenalManagerClass.prototype.ensureInstances(self, playerId)
|
||
if not self.instances[playerId] then
|
||
self.instances[playerId] = {}
|
||
end
|
||
return self.instances[playerId]
|
||
end
|
||
function ArsenalManagerClass.prototype.getInstance(self, playerId, instanceId)
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
return nil
|
||
end
|
||
local direct = bag[instanceId]
|
||
if direct ~= nil then
|
||
return direct
|
||
end
|
||
for key in pairs(bag) do
|
||
do
|
||
local row = bag[key]
|
||
if not row then
|
||
goto __continue50
|
||
end
|
||
if row.instanceId == instanceId or row.instance_id == instanceId then
|
||
return row
|
||
end
|
||
end
|
||
::__continue50::
|
||
end
|
||
return nil
|
||
end
|
||
function ArsenalManagerClass.prototype.countStatLines(self, stats)
|
||
if not stats or type(stats) ~= "table" then
|
||
return 0
|
||
end
|
||
local n = 0
|
||
local row = stats
|
||
for k in pairs(row) do
|
||
local idx = tonumber(k)
|
||
if idx ~= nil and idx > 0 and row[k] ~= nil then
|
||
n = n + 1
|
||
end
|
||
end
|
||
return n
|
||
end
|
||
function ArsenalManagerClass.prototype.getInstanceByCatalogItem(self, playerId, itemName)
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
return nil
|
||
end
|
||
for id in pairs(bag) do
|
||
local inst = bag[id]
|
||
if inst ~= nil and inst.itemName == itemName then
|
||
return inst
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
function ArsenalManagerClass.prototype.nextSerialForPlayer(self, playerId)
|
||
local maxS = 0
|
||
local bag = self.instances[playerId]
|
||
if bag ~= nil then
|
||
for id in pairs(bag) do
|
||
local ____opt_11 = bag[id]
|
||
local s = ____opt_11 and ____opt_11.serial or 0
|
||
if s > maxS then
|
||
maxS = s
|
||
end
|
||
end
|
||
end
|
||
return maxS + 1
|
||
end
|
||
function ArsenalManagerClass.prototype.ensureInstanceIdentityMeta(self, _playerId, _inst)
|
||
return false
|
||
end
|
||
function ArsenalManagerClass.prototype.resolveInventoryOwnerDisplayName(self, playerId)
|
||
local pid = playerId
|
||
if not PlayerResource:IsValidPlayerID(pid) then
|
||
return "Unknown"
|
||
end
|
||
local nm = PlayerResource:GetPlayerName(pid)
|
||
local s = __TS__StringTrim(tostring(nm or ""))
|
||
return #s > 0 and s or "Unknown"
|
||
end
|
||
function ArsenalManagerClass.prototype.createNewInstance(self, playerId, itemName, rollQuality, rollContext)
|
||
local def = ARSENAL_ITEMS_MAP[itemName]
|
||
if not def then
|
||
return nil
|
||
end
|
||
local quality = rollQuality or def.quality
|
||
local bag = self:ensureInstances(playerId)
|
||
if #__TS__ObjectKeys(bag) >= ARSENAL_INVENTORY_MAX_INSTANCES then
|
||
print((((LOG_PREFIX .. " createNewInstance: inventory cap ") .. tostring(ARSENAL_INVENTORY_MAX_INSTANCES)) .. " player=") .. tostring(playerId))
|
||
return nil
|
||
end
|
||
local instanceId = "ars_" .. DoUniqueString("i")
|
||
local serial = self:nextSerialForPlayer(playerId)
|
||
local created = createArsenalItemInstance(
|
||
nil,
|
||
instanceId,
|
||
serial,
|
||
itemName,
|
||
quality,
|
||
rollContext
|
||
)
|
||
self:setRollVersion(created, ARSENAL_ROLL_VERSION)
|
||
created.ownerName = self:resolveInventoryOwnerDisplayName(playerId)
|
||
bag[instanceId] = created
|
||
return instanceId
|
||
end
|
||
function ArsenalManagerClass.prototype.repairBrokenInstance(self, playerId, instanceId)
|
||
local ____opt_13 = self.instances[playerId]
|
||
local inst = ____opt_13 and ____opt_13[instanceId]
|
||
if not inst then
|
||
return false
|
||
end
|
||
if self:countStatLines(inst.stats) >= 7 then
|
||
return false
|
||
end
|
||
local def = ARSENAL_ITEMS_MAP[inst.itemName]
|
||
if not def then
|
||
return false
|
||
end
|
||
local oldFlags = inst
|
||
local wasPinned = not not oldFlags.pinned
|
||
local wasFavorite = not not oldFlags.favorite
|
||
local rerolled = createArsenalItemInstance(
|
||
nil,
|
||
instanceId,
|
||
inst.serial,
|
||
inst.itemName,
|
||
inst.quality or def.quality
|
||
)
|
||
rerolled.upgradeLevel = math.max(
|
||
0,
|
||
math.min(
|
||
5,
|
||
math.floor(inst.upgradeLevel or 0)
|
||
)
|
||
)
|
||
if wasPinned then
|
||
rerolled.pinned = true
|
||
end
|
||
if wasFavorite then
|
||
rerolled.favorite = true
|
||
end
|
||
self:setRollVersion(rerolled, ARSENAL_ROLL_VERSION)
|
||
local oldMeta = inst
|
||
rerolled.globalSerial = oldMeta.globalSerial
|
||
rerolled.ownerName = oldMeta.ownerName
|
||
self:ensureInstances(playerId)[instanceId] = rerolled
|
||
return true
|
||
end
|
||
function ArsenalManagerClass.prototype.normalizePinned(self, inst)
|
||
local raw = inst.pinned
|
||
inst.pinned = raw == true or raw == 1 or raw == "1"
|
||
end
|
||
function ArsenalManagerClass.prototype.normalizeFavorite(self, inst)
|
||
local raw = inst.favorite
|
||
inst.favorite = raw == true or raw == 1 or raw == "1"
|
||
end
|
||
function ArsenalManagerClass.prototype.getRollVersion(self, inst)
|
||
local raw = inst.rollVersion
|
||
if type(raw) ~= "number" or not __TS__NumberIsFinite(raw) then
|
||
return 0
|
||
end
|
||
return math.floor(raw)
|
||
end
|
||
function ArsenalManagerClass.prototype.setRollVersion(self, inst, version)
|
||
inst.rollVersion = version
|
||
end
|
||
function ArsenalManagerClass.prototype.rerollInstanceForCurrentVersion(self, playerId, instanceId)
|
||
local ____opt_15 = self.instances[playerId]
|
||
local inst = ____opt_15 and ____opt_15[instanceId]
|
||
if not inst then
|
||
return false
|
||
end
|
||
if self:getRollVersion(inst) >= ARSENAL_ROLL_VERSION then
|
||
return false
|
||
end
|
||
local def = ARSENAL_ITEMS_MAP[inst.itemName]
|
||
if not def then
|
||
return false
|
||
end
|
||
local oldFlags = inst
|
||
local wasPinned = not not oldFlags.pinned
|
||
local wasFavorite = not not oldFlags.favorite
|
||
local rerolled = createArsenalItemInstance(
|
||
nil,
|
||
instanceId,
|
||
inst.serial,
|
||
inst.itemName,
|
||
inst.quality or def.quality
|
||
)
|
||
rerolled.upgradeLevel = math.max(
|
||
0,
|
||
math.min(
|
||
5,
|
||
math.floor(inst.upgradeLevel or 0)
|
||
)
|
||
)
|
||
if wasPinned then
|
||
rerolled.pinned = true
|
||
end
|
||
if wasFavorite then
|
||
rerolled.favorite = true
|
||
end
|
||
self:setRollVersion(rerolled, ARSENAL_ROLL_VERSION)
|
||
local oldMeta = inst
|
||
rerolled.globalSerial = oldMeta.globalSerial
|
||
rerolled.ownerName = oldMeta.ownerName
|
||
self:ensureInstances(playerId)[instanceId] = rerolled
|
||
return true
|
||
end
|
||
function ArsenalManagerClass.prototype.normalizeInventoryFlags(self, playerId)
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
return
|
||
end
|
||
for id in pairs(bag) do
|
||
local inst = bag[id]
|
||
if inst ~= nil then
|
||
self:normalizePinned(inst)
|
||
self:normalizeFavorite(inst)
|
||
if not not inst.favorite then
|
||
inst.pinned = true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.debugPrintInventoryOwnerMeta(self, playerId)
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
print(((LOG_PREFIX .. " owner-meta player=") .. tostring(playerId)) .. ": inventory empty")
|
||
return
|
||
end
|
||
for instanceId in pairs(bag) do
|
||
local inst = bag[instanceId]
|
||
local ownerName = tostring(inst and inst.ownerName or "")
|
||
local globalSerial = inst and inst.globalSerial or 0
|
||
local markMissing = (not ownerName or #ownerName <= 0 or ownerName == "Unknown") and "MISSING_OWNER" or "OK"
|
||
print((((((((((((LOG_PREFIX .. " owner-meta player=") .. tostring(playerId)) .. " instance=") .. instanceId) .. " item=") .. (inst and inst.itemName or "?")) .. " globalSerial=") .. tostring(globalSerial)) .. " ownerName=\"") .. ownerName) .. "\" status=") .. markMissing)
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.getArsenalUpgradeCost(self, quality, currentLevelBeforeUpgrade)
|
||
local lv = math.max(
|
||
0,
|
||
math.min(
|
||
4,
|
||
math.floor(currentLevelBeforeUpgrade)
|
||
)
|
||
)
|
||
local commonCurve = {
|
||
100,
|
||
200,
|
||
400,
|
||
800,
|
||
1600
|
||
}
|
||
local rarityMult = {
|
||
common = 1,
|
||
rare = 2,
|
||
epic = 4,
|
||
legendary = 8,
|
||
mythic = 16
|
||
}
|
||
local base = commonCurve[lv + 1] or commonCurve[1]
|
||
local mult = rarityMult[quality] or 1
|
||
return base * mult
|
||
end
|
||
function ArsenalManagerClass.prototype.getDisassembleShardReward(self, quality, upgradeLevel)
|
||
local base = {
|
||
common = 15,
|
||
rare = 35,
|
||
epic = 70,
|
||
legendary = 120,
|
||
mythic = 200
|
||
}
|
||
local b = base[quality] or 15
|
||
local lv = math.max(
|
||
0,
|
||
math.min(
|
||
5,
|
||
math.floor(upgradeLevel)
|
||
)
|
||
)
|
||
return b + lv * 20
|
||
end
|
||
function ArsenalManagerClass.prototype.isInstanceEquippedInAnyLoadout(self, playerId, instanceId)
|
||
local all = self.loadouts[playerId]
|
||
if not all then
|
||
return false
|
||
end
|
||
for heroName in pairs(all) do
|
||
do
|
||
local hl = all[heroName]
|
||
if not hl then
|
||
goto __continue106
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
if hl[slot] == instanceId then
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
::__continue106::
|
||
end
|
||
return false
|
||
end
|
||
function ArsenalManagerClass.prototype.removeInstanceFromAllLoadouts(self, playerId, instanceId)
|
||
local all = self.loadouts[playerId]
|
||
if not all then
|
||
return
|
||
end
|
||
for heroName in pairs(all) do
|
||
do
|
||
local hl = all[heroName]
|
||
if not hl then
|
||
goto __continue114
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
if hl[slot] == instanceId then
|
||
__TS__Delete(hl, slot)
|
||
end
|
||
end
|
||
end
|
||
::__continue114::
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.removeInstanceFromHeroLoadoutExceptSlot(self, playerId, heroName, keepSlot, instanceId)
|
||
local ____opt_23 = self.loadouts[playerId]
|
||
local hl = ____opt_23 and ____opt_23[heroName]
|
||
if not hl then
|
||
return
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
do
|
||
if slot == keepSlot then
|
||
goto __continue122
|
||
end
|
||
if hl[slot] == instanceId then
|
||
__TS__Delete(hl, slot)
|
||
end
|
||
end
|
||
::__continue122::
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.migrateLoadoutsFromLegacyItemNames(self, playerId)
|
||
local all = self.loadouts[playerId]
|
||
if not all then
|
||
return
|
||
end
|
||
for heroName in pairs(all) do
|
||
do
|
||
local hl = all[heroName]
|
||
if not hl then
|
||
goto __continue128
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
do
|
||
local v = hl[slot]
|
||
if not v then
|
||
goto __continue130
|
||
end
|
||
if self:getInstance(playerId, v) then
|
||
goto __continue130
|
||
end
|
||
if ARSENAL_ITEMS_MAP[v] then
|
||
local inst = self:getInstanceByCatalogItem(playerId, v)
|
||
if inst then
|
||
hl[slot] = inst.instanceId
|
||
end
|
||
end
|
||
end
|
||
::__continue130::
|
||
end
|
||
end
|
||
::__continue128::
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.ingestInventoryPayload(self, playerId, payload)
|
||
if not payload or type(payload) ~= "table" then
|
||
self.instances[playerId] = {}
|
||
return
|
||
end
|
||
if payload.instances and type(payload.instances) == "table" then
|
||
local raw = payload.instances
|
||
local deduped = {}
|
||
for rawKey in pairs(raw) do
|
||
do
|
||
local row = raw[rawKey]
|
||
if not row or type(row) ~= "table" then
|
||
goto __continue140
|
||
end
|
||
local ____tostring_27 = tostring
|
||
local ____row_instanceId_25 = row.instanceId
|
||
if ____row_instanceId_25 == nil then
|
||
____row_instanceId_25 = row.instance_id
|
||
end
|
||
local ____row_instanceId_25_26 = ____row_instanceId_25
|
||
if ____row_instanceId_25_26 == nil then
|
||
____row_instanceId_25_26 = rawKey
|
||
end
|
||
local canonicalId = ____tostring_27(____row_instanceId_25_26)
|
||
if not canonicalId or #canonicalId <= 0 then
|
||
goto __continue140
|
||
end
|
||
local normalizedRow = row
|
||
normalizedRow.instanceId = canonicalId
|
||
deduped[canonicalId] = normalizedRow
|
||
end
|
||
::__continue140::
|
||
end
|
||
self:trimInstancesBagToMax(deduped, ARSENAL_INVENTORY_MAX_INSTANCES)
|
||
self.instances[playerId] = deduped
|
||
return
|
||
end
|
||
self.instances[playerId] = {}
|
||
end
|
||
function ArsenalManagerClass.prototype.countCatalogInstances(self, playerId)
|
||
local n = 0
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
return 0
|
||
end
|
||
for id in pairs(bag) do
|
||
local it = bag[id]
|
||
if it ~= nil and ARSENAL_ITEMS_MAP[it.itemName] ~= nil then
|
||
n = n + 1
|
||
end
|
||
end
|
||
return n
|
||
end
|
||
function ArsenalManagerClass.prototype.trimInstancesBagToMax(self, bag, max)
|
||
local keys = __TS__ObjectKeys(bag)
|
||
if #keys <= max then
|
||
return
|
||
end
|
||
__TS__ArraySort(
|
||
keys,
|
||
function(____, a, b)
|
||
local ____opt_28 = bag[a]
|
||
local sa = ____opt_28 and ____opt_28.serial or 0
|
||
local ____opt_30 = bag[b]
|
||
local sb = ____opt_30 and ____opt_30.serial or 0
|
||
return sb - sa
|
||
end
|
||
)
|
||
do
|
||
local i = max
|
||
while i < #keys do
|
||
local k = keys[i + 1]
|
||
if k ~= nil then
|
||
__TS__Delete(bag, k)
|
||
end
|
||
i = i + 1
|
||
end
|
||
end
|
||
print(((((LOG_PREFIX .. " trimInstancesBagToMax: removed ") .. tostring(#keys - max)) .. " oldest instances (cap=") .. tostring(max)) .. ")")
|
||
end
|
||
function ArsenalManagerClass.prototype.handleEquip(self, playerId, heroName, slot, instanceId)
|
||
if not self:isArsenalEditPhase() then
|
||
self:sendError(playerId, "#arsenal_locked_in_game")
|
||
return
|
||
end
|
||
if not heroName or not slot or not instanceId then
|
||
print((((((((LOG_PREFIX .. " equip: bad args player=") .. tostring(playerId)) .. " hero=") .. heroName) .. " slot=") .. slot) .. " id=") .. instanceId)
|
||
return
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
self:sendError(playerId, "#arsenal_item_not_owned")
|
||
return
|
||
end
|
||
local def = ARSENAL_ITEMS_MAP[inst.itemName]
|
||
if not def or def.slot ~= slot then
|
||
print((((LOG_PREFIX .. " equip: slot mismatch item=") .. inst.itemName) .. " slot=") .. slot)
|
||
return
|
||
end
|
||
self:removeInstanceFromHeroLoadoutExceptSlot(playerId, heroName, slot, instanceId)
|
||
if not self.loadouts[playerId] then
|
||
self.loadouts[playerId] = {}
|
||
end
|
||
if not self.loadouts[playerId][heroName] then
|
||
self.loadouts[playerId][heroName] = {}
|
||
end
|
||
self.loadouts[playerId][heroName][slot] = instanceId
|
||
self:syncToClient(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, heroName)
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
print((((((((LOG_PREFIX .. " equip player=") .. tostring(playerId)) .. " hero=") .. heroName) .. " slot=") .. slot) .. " instance=") .. instanceId)
|
||
end
|
||
function ArsenalManagerClass.prototype.handleUnequip(self, playerId, heroName, slot)
|
||
if not self:isArsenalEditPhase() then
|
||
self:sendError(playerId, "#arsenal_locked_in_game")
|
||
return
|
||
end
|
||
if not heroName or not slot then
|
||
return
|
||
end
|
||
local ____opt_32 = self.loadouts[playerId]
|
||
local heroLoadout = ____opt_32 and ____opt_32[heroName]
|
||
if not heroLoadout or heroLoadout[slot] == nil then
|
||
return
|
||
end
|
||
__TS__Delete(heroLoadout, slot)
|
||
self:syncToClient(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, heroName)
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
print((((((LOG_PREFIX .. " unequip player=") .. tostring(playerId)) .. " hero=") .. heroName) .. " slot=") .. slot)
|
||
end
|
||
function ArsenalManagerClass.prototype.handleUpgradeItem(self, playerId, instanceId)
|
||
if not instanceId then
|
||
return
|
||
end
|
||
if not self:isArsenalEditPhase() then
|
||
self:sendError(playerId, "#arsenal_locked_in_game")
|
||
return
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
self:sendError(playerId, "#arsenal_item_not_owned")
|
||
return
|
||
end
|
||
if inst.upgradeLevel >= 5 then
|
||
return
|
||
end
|
||
local cost = self:getArsenalUpgradeCost(inst.quality, inst.upgradeLevel)
|
||
local store = StoreManager:getInstance()
|
||
if cost > 0 then
|
||
if store:getDustCurrency(playerId) < cost then
|
||
self:sendError(playerId, "Недостаточно пыли для улучшения")
|
||
return
|
||
end
|
||
if not store:removeDustCurrency(playerId, cost) then
|
||
self:sendError(playerId, "Недостаточно пыли для улучшения")
|
||
return
|
||
end
|
||
store:saveCurrencyToServer(playerId)
|
||
end
|
||
inst.upgradeLevel = inst.upgradeLevel + 1
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
local needStats = false
|
||
for heroName in pairs(self.loadouts[playerId] or ({})) do
|
||
do
|
||
local loadout = self.loadouts[playerId][heroName]
|
||
if not loadout then
|
||
goto __continue173
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
if loadout[slot] == instanceId then
|
||
needStats = true
|
||
break
|
||
end
|
||
end
|
||
if needStats then
|
||
break
|
||
end
|
||
end
|
||
::__continue173::
|
||
end
|
||
if needStats then
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
print((((((((LOG_PREFIX .. " upgrade player=") .. tostring(playerId)) .. " instance=") .. instanceId) .. " level=") .. tostring(inst.upgradeLevel)) .. " cost=") .. tostring(cost))
|
||
end
|
||
function ArsenalManagerClass.prototype.handleTogglePin(self, playerId, instanceId)
|
||
if not instanceId then
|
||
return
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
self:sendError(playerId, "#arsenal_item_not_owned")
|
||
return
|
||
end
|
||
local cur = not not inst.pinned
|
||
inst.pinned = not cur
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
print((((((LOG_PREFIX .. " pin=") .. tostring(not cur)) .. " player=") .. tostring(playerId)) .. " instance=") .. instanceId)
|
||
end
|
||
function ArsenalManagerClass.prototype.handleToggleFavorite(self, playerId, instanceId)
|
||
if not instanceId then
|
||
return
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
self:sendError(playerId, "#arsenal_item_not_owned")
|
||
return
|
||
end
|
||
local cur = not not inst.favorite
|
||
local next = not cur
|
||
inst.favorite = next
|
||
if next then
|
||
inst.pinned = true
|
||
end
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
print((((((((LOG_PREFIX .. " favorite=") .. tostring(next)) .. " pin=") .. tostring(not not inst.pinned)) .. " player=") .. tostring(playerId)) .. " instance=") .. instanceId)
|
||
end
|
||
function ArsenalManagerClass.prototype.handleDisassembleItem(self, playerId, instanceId)
|
||
if not instanceId then
|
||
return
|
||
end
|
||
if not self:isArsenalEditPhase() then
|
||
self:sendError(playerId, "#arsenal_locked_in_game")
|
||
return
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
self:sendError(playerId, "#arsenal_item_not_owned")
|
||
return
|
||
end
|
||
if not not inst.pinned then
|
||
self:sendError(playerId, "#arsenal_disassemble_pinned")
|
||
return
|
||
end
|
||
if self:isInstanceEquippedInAnyLoadout(playerId, instanceId) then
|
||
self:sendError(playerId, "#arsenal_disassemble_equipped")
|
||
return
|
||
end
|
||
local shards = self:getDisassembleShardReward(inst.quality, inst.upgradeLevel or 0)
|
||
__TS__Delete(
|
||
self:ensureInstances(playerId),
|
||
instanceId
|
||
)
|
||
self:removeInstanceFromAllLoadouts(playerId, instanceId)
|
||
StoreManager:getInstance():addDustCurrency(playerId, shards)
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
print((((((LOG_PREFIX .. " disassemble player=") .. tostring(playerId)) .. " instance=") .. instanceId) .. " shards=") .. tostring(shards))
|
||
end
|
||
function ArsenalManagerClass.prototype.applyLoadout(self, player, hero)
|
||
if not player or not hero then
|
||
return
|
||
end
|
||
local playerId = player:GetPlayerID()
|
||
if playerId < 0 then
|
||
return
|
||
end
|
||
local heroName = hero:GetUnitName()
|
||
local ____opt_34 = self.loadouts[playerId]
|
||
local heroLoadout = ____opt_34 and ____opt_34[heroName]
|
||
if not heroLoadout then
|
||
return
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
do
|
||
local instanceId = heroLoadout[slot]
|
||
if not instanceId then
|
||
goto __continue198
|
||
end
|
||
if not self:getInstance(playerId, instanceId) then
|
||
goto __continue198
|
||
end
|
||
end
|
||
::__continue198::
|
||
end
|
||
if not hero:HasModifier(____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME) then
|
||
hero:AddNewModifier(
|
||
hero,
|
||
getModifierSourceAbility(nil, hero),
|
||
____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME,
|
||
{}
|
||
)
|
||
end
|
||
local mod = hero:FindModifierByName(____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME)
|
||
if mod then
|
||
mod:ForceRefresh()
|
||
end
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
if not self.appliedFor[playerId] then
|
||
self.appliedFor[playerId] = __TS__New(Set)
|
||
end
|
||
self.appliedFor[playerId]:add(heroName)
|
||
print((((LOG_PREFIX .. " applyLoadout player=") .. tostring(playerId)) .. " hero=") .. heroName)
|
||
end
|
||
function ArsenalManagerClass.prototype.clearLoadout(self, player, hero)
|
||
if not player or not hero then
|
||
return
|
||
end
|
||
local playerId = player:GetPlayerID()
|
||
if playerId < 0 then
|
||
return
|
||
end
|
||
local heroName = hero:GetUnitName()
|
||
local ____opt_36 = self.loadouts[playerId]
|
||
local heroLoadout = ____opt_36 and ____opt_36[heroName]
|
||
if not heroLoadout then
|
||
return
|
||
end
|
||
hero:RemoveModifierByName(____exports.ArsenalManagerClass.DYNAMIC_MODIFIER_NAME)
|
||
local ____opt_38 = self.appliedFor[playerId]
|
||
if ____opt_38 ~= nil then
|
||
____opt_38:delete(heroName)
|
||
end
|
||
if self.lastPrintedTotals[playerId] then
|
||
self.lastPrintedTotals[playerId][heroName] = ""
|
||
end
|
||
end
|
||
function ArsenalManagerClass.prototype.tryGrantStarterPackIfEmpty(self, playerId)
|
||
local changed = false
|
||
for ____, def in ipairs(ARSENAL_ITEMS) do
|
||
if not self:getInstanceByCatalogItem(playerId, def.itemName) then
|
||
changed = self:createNewInstance(playerId, def.itemName) ~= nil or changed
|
||
end
|
||
end
|
||
local bag = self.instances[playerId]
|
||
if bag ~= nil then
|
||
for instanceId in pairs(bag) do
|
||
changed = self:repairBrokenInstance(playerId, instanceId) or changed
|
||
end
|
||
end
|
||
if not changed then
|
||
print(((LOG_PREFIX .. " starter/test grant skipped: player=") .. tostring(playerId)) .. ", inventory already valid")
|
||
return
|
||
end
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
print((LOG_PREFIX .. " starter/test grant applied player=") .. tostring(playerId))
|
||
end
|
||
function ArsenalManagerClass.prototype.regenerateAllCatalogItemsForTesting(self, playerId)
|
||
self.instances[playerId] = {}
|
||
self.loadouts[playerId] = {}
|
||
self.appliedFor[playerId] = __TS__New(Set)
|
||
for ____, def in ipairs(ARSENAL_ITEMS) do
|
||
self:createNewInstance(playerId, def.itemName)
|
||
end
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
print((LOG_PREFIX .. " regenerated full random arsenal for player=") .. tostring(playerId))
|
||
end
|
||
function ArsenalManagerClass.prototype.grantArsenalItem(self, playerId, itemName, count)
|
||
if count == nil then
|
||
count = 1
|
||
end
|
||
if not ARSENAL_ITEMS_MAP[itemName] then
|
||
print((LOG_PREFIX .. " grantArsenalItem: unknown item ") .. itemName)
|
||
return false
|
||
end
|
||
local n = math.max(
|
||
1,
|
||
math.floor(count)
|
||
)
|
||
do
|
||
local i = 0
|
||
while i < n do
|
||
self:createNewInstance(playerId, itemName, nil)
|
||
i = i + 1
|
||
end
|
||
end
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
print((((((LOG_PREFIX .. " grant player=") .. tostring(playerId)) .. " item=") .. itemName) .. " x") .. tostring(n))
|
||
return true
|
||
end
|
||
function ArsenalManagerClass.prototype.grantArsenalItemDetailed(self, playerId, itemName, count, rollQuality, rollContext, options)
|
||
if count == nil then
|
||
count = 1
|
||
end
|
||
if not ARSENAL_ITEMS_MAP[itemName] then
|
||
print((LOG_PREFIX .. " grantArsenalItemDetailed: unknown item ") .. itemName)
|
||
return {}
|
||
end
|
||
local n = math.max(
|
||
1,
|
||
math.floor(count)
|
||
)
|
||
local created = {}
|
||
do
|
||
local i = 0
|
||
while i < n do
|
||
do
|
||
local instanceId = self:createNewInstance(playerId, itemName, rollQuality, rollContext)
|
||
if not instanceId then
|
||
goto __continue226
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
local defQ = ARSENAL_ITEMS_MAP[itemName].quality
|
||
created[#created + 1] = {instanceId = instanceId, itemName = itemName, quality = inst and inst.quality or rollQuality or defQ}
|
||
end
|
||
::__continue226::
|
||
i = i + 1
|
||
end
|
||
end
|
||
if not (options and options.deferPersistence) then
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
end
|
||
print((((((LOG_PREFIX .. " grant detailed player=") .. tostring(playerId)) .. " item=") .. itemName) .. " x=") .. tostring(#created))
|
||
return created
|
||
end
|
||
function ArsenalManagerClass.prototype.flushArsenalInventoryToClientAndApi(self, playerId)
|
||
local pid = playerId
|
||
self.inventorySaveGeneration[pid] = (self.inventorySaveGeneration[pid] or 0) + 1
|
||
self:syncToClient(pid)
|
||
self:sendInventoryPutRequest(pid)
|
||
end
|
||
function ArsenalManagerClass.prototype.getOwnedCount(self, playerId, itemName)
|
||
local bag = self.instances[playerId]
|
||
if not bag then
|
||
return 0
|
||
end
|
||
local n = 0
|
||
for id in pairs(bag) do
|
||
local it = bag[id]
|
||
if it ~= nil and it.itemName == itemName then
|
||
n = n + 1
|
||
end
|
||
end
|
||
return n
|
||
end
|
||
function ArsenalManagerClass.prototype.getHeroLoadout(self, playerId, heroName)
|
||
local ____opt_44 = self.loadouts[playerId]
|
||
return ____opt_44 and ____opt_44[heroName] or ({})
|
||
end
|
||
function ArsenalManagerClass.prototype.getInventoryInstance(self, playerId, instanceId)
|
||
return self:getInstance(playerId, instanceId)
|
||
end
|
||
function ArsenalManagerClass.prototype.isInstanceTradeLocked(self, playerId, instanceId)
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
return true
|
||
end
|
||
local rawPinned = inst.pinned
|
||
local rawFavorite = inst.favorite
|
||
local pinned = rawPinned == true or rawPinned == 1 or rawPinned == "1"
|
||
local favorite = rawFavorite == true or rawFavorite == 1 or rawFavorite == "1"
|
||
return pinned or favorite
|
||
end
|
||
function ArsenalManagerClass.prototype.removeInstanceFromAllLoadoutsForMarket(self, playerId, instanceId)
|
||
self:removeInstanceFromAllLoadouts(playerId, instanceId)
|
||
self:syncToClient(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
function ArsenalManagerClass.prototype.buildInventoryPayloadForMarket(self, playerId)
|
||
local source = self.instances[playerId] or ({})
|
||
local normalized = {}
|
||
for key in pairs(source) do
|
||
do
|
||
local inst = source[key]
|
||
if not inst then
|
||
goto __continue241
|
||
end
|
||
normalized[key] = __TS__ObjectAssign({}, inst, {
|
||
instanceId = inst.instanceId,
|
||
instance_id = inst.instanceId,
|
||
itemName = inst.itemName,
|
||
item_name = inst.itemName,
|
||
upgradeLevel = inst.upgradeLevel,
|
||
upgrade_level = inst.upgradeLevel,
|
||
globalSerial = inst.globalSerial,
|
||
global_serial = inst.globalSerial,
|
||
ownerName = inst.ownerName,
|
||
owner_name = inst.ownerName
|
||
})
|
||
end
|
||
::__continue241::
|
||
end
|
||
return {instances = normalized}
|
||
end
|
||
function ArsenalManagerClass.prototype.removeInventoryInstanceForMarket(self, playerId, instanceId)
|
||
local bag = self:ensureInstances(playerId)
|
||
local inst = bag[instanceId]
|
||
if not inst then
|
||
return nil
|
||
end
|
||
__TS__Delete(bag, instanceId)
|
||
self:removeInstanceFromAllLoadouts(playerId, instanceId)
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
self:saveLoadoutsToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
return inst
|
||
end
|
||
function ArsenalManagerClass.prototype.upsertInventoryInstanceFromMarket(self, playerId, instance)
|
||
if not instance or not instance.instanceId then
|
||
return
|
||
end
|
||
self:ensureInstances(playerId)[instance.instanceId] = instance
|
||
self:syncToClient(playerId)
|
||
self:saveInventoryToServer(playerId)
|
||
self:refreshHeroArsenalStats(playerId, "")
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
function ArsenalManagerClass.prototype.getHeroStatTotals(self, hero)
|
||
local empty = {
|
||
battle_level = 0,
|
||
bonus_damage = 0,
|
||
attack_speed = 0,
|
||
bonus_armor = 0,
|
||
max_health = 0,
|
||
max_mana = 0,
|
||
health_regen = 0,
|
||
mana_regen = 0,
|
||
move_speed = 0,
|
||
magic_resist = 0,
|
||
spell_amp = 0,
|
||
all_stats = 0,
|
||
all_stats_pct = 0,
|
||
bonus_strength = 0,
|
||
bonus_agility = 0,
|
||
bonus_intellect = 0,
|
||
strength_pct = 0,
|
||
agility_pct = 0,
|
||
intellect_pct = 0,
|
||
luck = 0,
|
||
outgoing_damage_pct = 0,
|
||
incoming_damage_reduction_pct = 0,
|
||
attack_speed_pct = 0,
|
||
move_speed_pct = 0,
|
||
base_damage_pct = 0,
|
||
crit_mult = 0
|
||
}
|
||
if not hero then
|
||
return empty
|
||
end
|
||
local playerId = hero.GetPlayerID ~= nil and hero:GetPlayerID() or hero:GetPlayerOwnerID()
|
||
if playerId < 0 then
|
||
return empty
|
||
end
|
||
local gt = GameRules:GetGameTime()
|
||
local heroMemo = hero
|
||
local memo = heroMemo.__arsenalHeroStatTotalsMemo
|
||
if memo ~= nil and heroMemo.__arsenalHeroStatTotalsMemoTime == gt then
|
||
return memo
|
||
end
|
||
local heroName = hero:GetUnitName()
|
||
local ____opt_46 = self.loadouts[playerId]
|
||
local heroLoadout = ____opt_46 and ____opt_46[heroName]
|
||
if not heroLoadout then
|
||
heroMemo.__arsenalHeroStatTotalsMemoTime = gt
|
||
heroMemo.__arsenalHeroStatTotalsMemo = empty
|
||
return empty
|
||
end
|
||
for ____, slot in ipairs(ARSENAL_SLOTS) do
|
||
do
|
||
local instanceId = heroLoadout[slot]
|
||
if not instanceId then
|
||
goto __continue253
|
||
end
|
||
local inst = self:getInstance(playerId, instanceId)
|
||
if not inst then
|
||
goto __continue253
|
||
end
|
||
local lines = getEffectiveItemStats(nil, inst)
|
||
for ____, line in ipairs(lines) do
|
||
do
|
||
local normalizedKey = self:normalizeArsenalStatKey(tostring(line.key))
|
||
if not normalizedKey then
|
||
print((((((LOG_PREFIX .. " unknown stat key dropped: \"") .. tostring(line.key)) .. "\" item=") .. inst.itemName) .. " instance=") .. instanceId)
|
||
goto __continue256
|
||
end
|
||
empty[normalizedKey] = (empty[normalizedKey] or 0) + line.value
|
||
end
|
||
::__continue256::
|
||
end
|
||
end
|
||
::__continue253::
|
||
end
|
||
if ARSENAL_VERBOSE_TOTALS_LOG then
|
||
local totalsHash = json.encode(empty)
|
||
if not self.lastPrintedTotals[playerId] then
|
||
self.lastPrintedTotals[playerId] = {}
|
||
end
|
||
local prevHash = self.lastPrintedTotals[playerId][heroName]
|
||
if prevHash ~= totalsHash then
|
||
self.lastPrintedTotals[playerId][heroName] = totalsHash
|
||
print((((((((((LOG_PREFIX .. " totals player=") .. tostring(playerId)) .. " hero=") .. heroName) .. " ") .. ((((((("dmg=" .. tostring(empty.bonus_damage)) .. " as=") .. tostring(empty.attack_speed)) .. " aspct=") .. tostring(empty.attack_speed_pct)) .. " armor=") .. tostring(empty.bonus_armor)) .. " ") .. ((("hp=" .. tostring(empty.max_health)) .. " mp=") .. tostring(empty.max_mana)) .. " ") .. ((((((((("ms=" .. tostring(empty.move_speed)) .. " mspct=") .. tostring(empty.move_speed_pct)) .. " mr=") .. tostring(empty.magic_resist)) .. " amp=") .. tostring(empty.spell_amp)) .. " all=") .. tostring(empty.all_stats)) .. " ") .. ((((((("all%=" .. tostring(empty.all_stats_pct)) .. " str%=") .. tostring(empty.strength_pct)) .. " agi%=") .. tostring(empty.agility_pct)) .. " int%=") .. tostring(empty.intellect_pct)) .. " ") .. (((((((("luck=" .. tostring(empty.luck)) .. " out%=") .. tostring(empty.outgoing_damage_pct)) .. " in_red%=") .. tostring(empty.incoming_damage_reduction_pct)) .. " bdmg%=") .. tostring(empty.base_damage_pct)) .. " crit%=") .. tostring(empty.crit_mult))
|
||
end
|
||
end
|
||
heroMemo.__arsenalHeroStatTotalsMemoTime = gt
|
||
heroMemo.__arsenalHeroStatTotalsMemo = empty
|
||
return empty
|
||
end
|
||
function ArsenalManagerClass.prototype.syncToClient(self, playerId)
|
||
CustomNetTables:SetTableValue(
|
||
"arsenal_loadouts",
|
||
tostring(playerId),
|
||
{loadouts = self.loadouts[playerId] or ({}), selectedHero = self.selectedHero[playerId] or ""}
|
||
)
|
||
CustomNetTables:SetTableValue(
|
||
"arsenal_inventory",
|
||
tostring(playerId),
|
||
{instances = self.instances[playerId] or ({})}
|
||
)
|
||
end
|
||
function ArsenalManagerClass.prototype.syncLoadoutsToClientOnly(self, playerId)
|
||
CustomNetTables:SetTableValue(
|
||
"arsenal_loadouts",
|
||
tostring(playerId),
|
||
{loadouts = self.loadouts[playerId] or ({}), selectedHero = self.selectedHero[playerId] or ""}
|
||
)
|
||
end
|
||
function ArsenalManagerClass.prototype.saveLoadoutsToServer(self, playerId)
|
||
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
||
if not steamId then
|
||
return
|
||
end
|
||
local data = self.loadouts[playerId] or ({})
|
||
local request = CreateHTTPRequestScriptVM(
|
||
"PUT",
|
||
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_loadouts"
|
||
)
|
||
setApiHeaders(nil, request)
|
||
request:SetHTTPRequestRawPostBody(
|
||
"application/json",
|
||
json.encode({arsenal_loadouts = data})
|
||
)
|
||
request:Send(function(result)
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
print(((LOG_PREFIX .. " arsenal_loadouts PUT ok (player=") .. tostring(playerId)) .. ")")
|
||
else
|
||
print((LOG_PREFIX .. " arsenal_loadouts PUT fail: StatusCode=") .. tostring(result.StatusCode))
|
||
end
|
||
end)
|
||
end
|
||
function ArsenalManagerClass.prototype.saveInventoryToServer(self, playerId)
|
||
local pid = playerId
|
||
local g = (self.inventorySaveGeneration[pid] or 0) + 1
|
||
self.inventorySaveGeneration[pid] = g
|
||
Timers:CreateTimer(
|
||
ARSENAL_INVENTORY_PUT_DEBOUNCE,
|
||
function()
|
||
if self.inventorySaveGeneration[pid] ~= g then
|
||
return nil
|
||
end
|
||
self:sendInventoryPutRequest(pid)
|
||
return nil
|
||
end
|
||
)
|
||
end
|
||
function ArsenalManagerClass.prototype.sendInventoryPutRequest(self, playerId)
|
||
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
||
if not steamId then
|
||
return
|
||
end
|
||
local data = {instances = self.instances[playerId] or ({})}
|
||
local request = CreateHTTPRequestScriptVM(
|
||
"PUT",
|
||
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_inventory"
|
||
)
|
||
setApiHeaders(nil, request)
|
||
request:SetHTTPRequestRawPostBody(
|
||
"application/json",
|
||
json.encode({arsenal_inventory = data})
|
||
)
|
||
request:Send(function(result)
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
print(((LOG_PREFIX .. " arsenal_inventory PUT ok (player=") .. tostring(playerId)) .. ")")
|
||
else
|
||
print((LOG_PREFIX .. " arsenal_inventory PUT fail: StatusCode=") .. tostring(result.StatusCode))
|
||
end
|
||
end)
|
||
end
|
||
function ArsenalManagerClass.prototype.loadFromServer(self, playerId)
|
||
local steamId = PlayerResource:GetSteamAccountID(playerId)
|
||
if not steamId then
|
||
return
|
||
end
|
||
local loadoutsReq = CreateHTTPRequestScriptVM(
|
||
"GET",
|
||
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_loadouts"
|
||
)
|
||
setApiHeaders(nil, loadoutsReq)
|
||
loadoutsReq:Send(function(result)
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
do
|
||
local function ____catch(e)
|
||
print((LOG_PREFIX .. " loadouts decode err: ") .. tostring(e))
|
||
end
|
||
local ____try, ____hasReturned = pcall(function()
|
||
local decoded = {json.decode(result.Body)}
|
||
local resp = nil
|
||
if __TS__ArrayIsArray(decoded) and #decoded > 0 then
|
||
resp = decoded[1]
|
||
elseif decoded and type(decoded) == "table" then
|
||
resp = decoded
|
||
end
|
||
if resp and type(resp) == "table" and resp.arsenal_loadouts ~= nil then
|
||
self.loadouts[playerId] = resp.arsenal_loadouts or ({})
|
||
else
|
||
self.loadouts[playerId] = self.loadouts[playerId] or ({})
|
||
end
|
||
self:syncLoadoutsToClientOnly(playerId)
|
||
local selected = self.selectedHero[playerId]
|
||
if selected and #selected > 0 then
|
||
self:refreshHeroArsenalStats(playerId, selected)
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
print((LOG_PREFIX .. " loaded arsenal_loadouts for player=") .. tostring(playerId))
|
||
end)
|
||
if not ____try then
|
||
____catch(____hasReturned)
|
||
end
|
||
end
|
||
else
|
||
print((LOG_PREFIX .. " arsenal_loadouts GET fail: StatusCode=") .. tostring(result.StatusCode))
|
||
end
|
||
end)
|
||
local invReq = CreateHTTPRequestScriptVM(
|
||
"GET",
|
||
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_inventory"
|
||
)
|
||
setApiHeaders(nil, invReq)
|
||
invReq:Send(function(result)
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
do
|
||
local function ____catch(e)
|
||
print((LOG_PREFIX .. " inventory decode err: ") .. tostring(e))
|
||
end
|
||
local ____try, ____hasReturned = pcall(function()
|
||
local decoded = {json.decode(result.Body)}
|
||
local resp = nil
|
||
if __TS__ArrayIsArray(decoded) and #decoded > 0 then
|
||
resp = decoded[1]
|
||
elseif decoded and type(decoded) == "table" then
|
||
resp = decoded
|
||
end
|
||
if resp and type(resp) == "table" and resp.arsenal_inventory ~= nil then
|
||
self:ingestInventoryPayload(playerId, resp.arsenal_inventory)
|
||
else
|
||
self.instances[playerId] = self.instances[playerId] or ({})
|
||
end
|
||
self:normalizeInventoryFlags(playerId)
|
||
self:debugPrintInventoryOwnerMeta(playerId)
|
||
local repaired = false
|
||
local rerolledByVersion = false
|
||
local bag = self.instances[playerId]
|
||
if bag ~= nil then
|
||
for instanceId in pairs(bag) do
|
||
repaired = self:repairBrokenInstance(playerId, instanceId) or repaired
|
||
rerolledByVersion = self:rerollInstanceForCurrentVersion(playerId, instanceId) or rerolledByVersion
|
||
end
|
||
end
|
||
if repaired or rerolledByVersion then
|
||
self.inventorySaveGeneration[playerId] = (self.inventorySaveGeneration[playerId] or 0) + 1
|
||
self:sendInventoryPutRequest(playerId)
|
||
end
|
||
self:migrateLoadoutsFromLegacyItemNames(playerId)
|
||
self:syncToClient(playerId)
|
||
local selected = self.selectedHero[playerId]
|
||
if selected and #selected > 0 then
|
||
self:refreshHeroArsenalStats(playerId, selected)
|
||
self:scheduleArsenalStatRefresh(playerId)
|
||
end
|
||
print((((((LOG_PREFIX .. " loaded arsenal_inventory for player=") .. tostring(playerId)) .. " repaired=") .. tostring(repaired)) .. " rerolledByVersion=") .. tostring(rerolledByVersion))
|
||
end)
|
||
if not ____try then
|
||
____catch(____hasReturned)
|
||
end
|
||
end
|
||
else
|
||
print((LOG_PREFIX .. " arsenal_inventory GET fail: StatusCode=") .. tostring(result.StatusCode))
|
||
end
|
||
end)
|
||
end
|
||
ArsenalManagerClass.DYNAMIC_MODIFIER_NAME = "modifier_arsenal_dynamic_stats"
|
||
ArsenalManagerClass = __TS__Decorate(ArsenalManagerClass, ArsenalManagerClass, {reloadable}, {kind = "class", name = "ArsenalManagerClass"})
|
||
____exports.ArsenalManagerClass = ArsenalManagerClass
|
||
____exports.ArsenalManager = ____exports.ArsenalManagerClass:getInstance()
|
||
function ____exports.grantArsenalItem(self, playerId, itemName, count)
|
||
if count == nil then
|
||
count = 1
|
||
end
|
||
return ____exports.ArsenalManager:grantArsenalItem(playerId, itemName, count)
|
||
end
|
||
function ____exports.grantArsenalItemDetailed(self, playerId, itemName, count, rollQuality, rollContext, options)
|
||
if count == nil then
|
||
count = 1
|
||
end
|
||
return ____exports.ArsenalManager:grantArsenalItemDetailed(
|
||
playerId,
|
||
itemName,
|
||
count,
|
||
rollQuality,
|
||
rollContext,
|
||
options
|
||
)
|
||
end
|
||
function ____exports.flushArsenalInventoryToClientAndApi(self, playerId)
|
||
____exports.ArsenalManager:flushArsenalInventoryToClientAndApi(playerId)
|
||
end
|
||
return ____exports
|