Files
Dota-Zombie-Invasion/scripts/vscripts/blackshop.lua
T
2026-05-29 15:11:31 +07:00

600 lines
27 KiB
Lua

local ____lualib = require("lualib_bundle")
local __TS__Class = ____lualib.__TS__Class
local Set = ____lualib.Set
local __TS__New = ____lualib.__TS__New
local __TS__ArrayFind = ____lualib.__TS__ArrayFind
local __TS__ArrayForEach = ____lualib.__TS__ArrayForEach
local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
local __TS__ArrayFrom = ____lualib.__TS__ArrayFrom
local ____exports = {}
local ____entity_radius = require("utils.entity_radius")
local findAllByClassnameInRadius = ____entity_radius.findAllByClassnameInRadius
local ____CardSystem = require("cards.CardSystem")
local ShowCardSelectionToPlayer = ____CardSystem.ShowCardSelectionToPlayer
local ____crystal_currency = require("crystal_currency")
local CrystalCurrency = ____crystal_currency.CrystalCurrency
local ____black_shop_teleport = require("black_shop_teleport")
local isBlackShopTeleportDestination = ____black_shop_teleport.isBlackShopTeleportDestination
local resolveBlackShopTeleportPosition = ____black_shop_teleport.resolveBlackShopTeleportPosition
local BLACKSHOP_GOLD_TO_CRYSTAL_RATE = 100
local BLACKSHOP_CRYSTAL_TO_GOLD_RATE = 25
local ItemQuality = ItemQuality or ({})
ItemQuality.COMMON = 0
ItemQuality[ItemQuality.COMMON] = "COMMON"
ItemQuality.RARE = 1
ItemQuality[ItemQuality.RARE] = "RARE"
ItemQuality.EPIC = 2
ItemQuality[ItemQuality.EPIC] = "EPIC"
ItemQuality.LEGENDARY = 3
ItemQuality[ItemQuality.LEGENDARY] = "LEGENDARY"
ItemQuality.CURSED = 4
ItemQuality[ItemQuality.CURSED] = "CURSED"
ItemQuality.HEAVENLY = 5
ItemQuality[ItemQuality.HEAVENLY] = "HEAVENLY"
____exports.BlackShop = __TS__Class()
local BlackShop = ____exports.BlackShop
BlackShop.name = "BlackShop"
BlackShop.____file_path = "scripts/vscripts/blackshop.lua"
function BlackShop.prototype.____constructor(self)
self.shopItems = {}
self.spawnPoints = {}
self.purchasedUniqueItems = __TS__New(Set)
self.isUniqueItemSpawned = false
self.cardPurchaseBaseCost = 5
self.cardPurchaseCostByPlayer = {}
self:initializeItems()
self:findSpawnPoints()
self:SpawnItems()
end
function BlackShop.getInstance(self)
if not ____exports.BlackShop.instance then
____exports.BlackShop.instance = __TS__New(____exports.BlackShop)
end
return ____exports.BlackShop.instance
end
function BlackShop.prototype.initializeItems(self)
self.shopItems = {
{name = "item_blackshop_common_bonus_stats_str", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_bonus_stats_int", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_bonus_stats_agi", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_injector", cost = 3, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_king_crown", cost = 10, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_manaflare", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_spell_mask", cost = 7, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_stone_armor", cost = 10, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_boo_stuff", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_vigor_tincture", cost = 4, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_wind_dust", cost = 4, quality = ItemQuality.COMMON},
{name = "item_blackshop_common_blue_tallow", cost = 5, quality = ItemQuality.COMMON},
{name = "item_blackshop_rare_damage_dagger", cost = 7, quality = ItemQuality.RARE},
{name = "item_blackshop_rare_agility_cape", cost = 14, quality = ItemQuality.RARE},
{name = "item_blackshop_rare_egg_of_death", cost = 10, quality = ItemQuality.RARE},
{name = "item_blackshop_rare_granite_badge", cost = 12, quality = ItemQuality.RARE},
{name = "item_blackshop_rare_iron_resolve", cost = 13, quality = ItemQuality.RARE},
{name = "item_blackshop_rare_silver_eye", cost = 12, quality = ItemQuality.RARE, uniqueItem = true},
{name = "item_blackshop_rare_critical_havoc", cost = 11, quality = ItemQuality.RARE, uniqueItem = true},
{name = "item_blackshop_epic_power_of_grow", cost = 15, quality = ItemQuality.EPIC, uniqueItem = true},
{name = "item_blackshop_epic_critical_paladin_sword", cost = 8, quality = ItemQuality.EPIC, uniqueItem = true},
{name = "item_blackshop_epic_trinity_seal", cost = 20, quality = ItemQuality.EPIC, uniqueItem = true},
{name = "item_blackshop_epic_bulwark_plate", cost = 22, quality = ItemQuality.EPIC, uniqueItem = true},
{name = "item_blackshop_legendary_fire_summoner", cost = 18, quality = ItemQuality.LEGENDARY, uniqueItem = true},
{name = "item_blackshop_legendary_primordial_shard", cost = 32, quality = ItemQuality.LEGENDARY},
{
name = "item_blackshop_legendary_restock",
cost = 12,
quality = ItemQuality.LEGENDARY,
uniqueItem = true,
nonRefundable = true
},
{
name = "item_blackshop_legendary_twilight_mirror",
cost = 38,
quality = ItemQuality.LEGENDARY,
uniqueItem = true,
nonRefundable = true
},
{
name = "item_blackshop_legendary_astral_anchor",
cost = 36,
quality = ItemQuality.LEGENDARY,
uniqueItem = true,
nonRefundable = true
},
{name = "item_blackshop_legendary_fated_die", cost = 26, quality = ItemQuality.LEGENDARY},
{
name = "item_blackshop_cursed_the_hand_of_gluttony",
cost = 60,
quality = ItemQuality.CURSED,
uniqueItem = true,
nonRefundable = true
},
{name = "item_blackshop_cursed_martyrs_brand", cost = 52, quality = ItemQuality.CURSED},
{name = "item_blackshop_cursed_widow_chain", cost = 50, quality = ItemQuality.CURSED},
{name = "item_blackshop_cursed_glass_pact", cost = 46, quality = ItemQuality.CURSED},
{
name = "item_blackshop_heavenly_reset_to_zero",
cost = 15,
quality = ItemQuality.HEAVENLY,
uniqueItem = true,
nonRefundable = true
},
{
name = "item_blackshop_heavenly_font_of_mercy",
cost = 48,
quality = ItemQuality.HEAVENLY,
uniqueItem = true,
nonRefundable = true
},
{name = "item_blackshop_heavenly_sanctuary_veil", cost = 32, quality = ItemQuality.HEAVENLY, uniqueItem = true},
{name = "item_blackshop_heavenly_dawn_chorus", cost = 34, quality = ItemQuality.HEAVENLY, uniqueItem = true}
}
end
function BlackShop.prototype.findSpawnPoints(self)
self.spawnPoints = {}
do
local i = 1
while i <= 5 do
local entity = Entities:FindByName(
nil,
"blackshop_item_point_" .. tostring(i)
)
if entity and not entity:IsNull() then
local position = entity:GetOrigin()
local ____self_spawnPoints_0 = self.spawnPoints
____self_spawnPoints_0[#____self_spawnPoints_0 + 1] = position
end
i = i + 1
end
end
end
function BlackShop.prototype.SpawnItems(self)
self.isUniqueItemSpawned = false
__TS__ArrayForEach(
self.spawnPoints,
function(____, point, index)
local itemsNearby = findAllByClassnameInRadius("dota_item_drop", point, 100)
__TS__ArrayForEach(
itemsNearby,
function(____, entity)
if entity and not entity:IsNull() then
local containerItem = entity
local actualItem = containerItem:GetContainedItem()
if actualItem ~= nil and actualItem ~= nil then
local itemName = actualItem:GetAbilityName()
local shopItem = __TS__ArrayFind(
self.shopItems,
function(____, i) return i.name == itemName end
)
if shopItem and shopItem.uniqueItem then
self.isUniqueItemSpawned = true
end
end
end
end
)
if #itemsNearby == 0 then
local availableItems = __TS__ArrayFilter(
self.shopItems,
function(____, item)
if item.uniqueItem then
local isAvailable = not self.purchasedUniqueItems:has(item.name) and not self.isUniqueItemSpawned
return isAvailable
end
return not self.purchasedUniqueItems:has(item.name)
end
)
if #availableItems == 0 then
return
end
local randomItem = availableItems[RandomInt(0, #availableItems - 1) + 1]
if randomItem.uniqueItem then
self.isUniqueItemSpawned = true
end
local item = CreateItem(randomItem.name, nil, nil)
local drop = CreateItemOnPositionForLaunch(point, item)
drop:SetAbsOrigin(point)
drop:SetAngles(0, 0, 0)
local particleName
repeat
local ____switch21 = randomItem.quality
local ____cond21 = ____switch21 == ItemQuality.COMMON
if ____cond21 then
particleName = "particles/units/heroes/hero_ancient_apparition/ancient_apparition_freeze_stacks_smoke_b.vpcf"
break
end
____cond21 = ____cond21 or ____switch21 == ItemQuality.RARE
if ____cond21 then
particleName = "particles/units/heroes/hero_ancient_apparition/ancient_apparition_ice_blast_main.vpcf"
break
end
____cond21 = ____cond21 or ____switch21 == ItemQuality.EPIC
if ____cond21 then
particleName = "particles/econ/items/lanaya/lanaya_epit_trap/templar_assassin_epit_trap.vpcf"
break
end
____cond21 = ____cond21 or ____switch21 == ItemQuality.LEGENDARY
if ____cond21 then
particleName = "particles/econ/items/templar_assassin/ta_2022_immortal/ta_2022_immortal_trap_gold.vpcf"
break
end
____cond21 = ____cond21 or ____switch21 == ItemQuality.CURSED
if ____cond21 then
particleName = "particles/econ/items/shadow_fiend/sf_desolation/sf_base_attack_desolation.vpcf"
break
end
____cond21 = ____cond21 or ____switch21 == ItemQuality.HEAVENLY
if ____cond21 then
particleName = "particles/econ/items/templar_assassin/ta_2022_immortal/ta_2022_immortal_trap_crimson.vpcf"
break
end
do
particleName = "particles/default_item.vpcf"
end
until true
local particle = ParticleManager:CreateParticle(particleName, PATTACH_ABSORIGIN_FOLLOW, drop)
ParticleManager:SetParticleControl(particle, 0, point)
end
end
)
end
function BlackShop.prototype.RefreshShop(self)
__TS__ArrayForEach(
self.spawnPoints,
function(____, point, index)
local itemsNearby = findAllByClassnameInRadius("dota_item_drop", point, 100)
__TS__ArrayForEach(
itemsNearby,
function(____, item)
if item and not item:IsNull() then
local containerItem = item
local actualItem = containerItem:GetContainedItem()
if not actualItem or not actualItem._wasPurchased then
UTIL_Remove(item)
end
end
end
)
end
)
self:SpawnItems()
end
function BlackShop.prototype.OnItemPickup(self, item, player)
local itemName = item:GetAbilityName()
local shopItem = __TS__ArrayFind(
self.shopItems,
function(____, i) return i.name == itemName end
)
if not shopItem then
return false
end
if shopItem.uniqueItem then
self.purchasedUniqueItems:add(shopItem.name)
end
item._wasPurchased = true
return true
end
function BlackShop.prototype.getItemInfo(self, itemName)
return __TS__ArrayFind(
self.shopItems,
function(____, item) return item.name == itemName end
)
end
function BlackShop.prototype.getItemCost(self, itemName)
local item = self:getItemInfo(itemName)
return item and item.cost or 0
end
function BlackShop.prototype.OnItemPurchased(self, itemName)
local shopItem = __TS__ArrayFind(
self.shopItems,
function(____, i) return i.name == itemName end
)
if shopItem and shopItem.uniqueItem then
self.purchasedUniqueItems:add(itemName)
end
end
function BlackShop.prototype.ResetUniqueItems(self)
local currentItems = __TS__ArrayFrom(self.purchasedUniqueItems)
__TS__ArrayForEach(
currentItems,
function(____, itemName)
local item = __TS__ArrayFind(
self.shopItems,
function(____, i) return i.name == itemName end
)
if item and not item.nonRefundable then
self.purchasedUniqueItems:delete(itemName)
elseif item and item.nonRefundable then
end
end
)
self.isUniqueItemSpawned = false
self:RefreshShop()
end
function BlackShop.prototype.getCardPurchaseCost(self, playerId)
local currentCost = self.cardPurchaseCostByPlayer[playerId]
if currentCost ~= nil and currentCost > 0 then
return currentCost
end
self.cardPurchaseCostByPlayer[playerId] = self.cardPurchaseBaseCost
return self.cardPurchaseBaseCost
end
function BlackShop.prototype.buyCardForPlayer(self, playerId)
local player = PlayerResource:GetPlayer(playerId)
local hero = player and player:GetAssignedHero()
local currentCost = self:getCardPurchaseCost(playerId)
if not player or not hero then
return {success = false, spentCost = currentCost, nextCost = currentCost, errorMessage = "black_shop_not_enough_crystals"}
end
local crystalCurrency = CrystalCurrency:getInstance()
local currentCrystals = crystalCurrency:getCrystals(playerId)
if currentCrystals < currentCost then
return {success = false, spentCost = currentCost, nextCost = currentCost, errorMessage = "black_shop_not_enough_crystals"}
end
crystalCurrency:removeCrystals(playerId, currentCost)
ShowCardSelectionToPlayer(nil, playerId, 3, "black_shop_buy_card")
local nextCost = currentCost * 2
self.cardPurchaseCostByPlayer[playerId] = nextCost
return {success = true, spentCost = currentCost, nextCost = nextCost}
end
function BlackShop.prototype.SpawnItemAtPosition(self, position)
local availableItems = __TS__ArrayFilter(
self.shopItems,
function(____, item)
if item.uniqueItem then
return not self.purchasedUniqueItems:has(item.name) and not self.isUniqueItemSpawned
end
return not self.purchasedUniqueItems:has(item.name)
end
)
if #availableItems == 0 then
return
end
local randomItem = availableItems[RandomInt(0, #availableItems - 1) + 1]
if randomItem.uniqueItem then
self.isUniqueItemSpawned = true
end
local item = CreateItem(randomItem.name, nil, nil)
local drop = CreateItemOnPositionForLaunch(position, item)
drop:SetAbsOrigin(position)
drop:SetAngles(0, 0, 0)
local particleName
repeat
local ____switch52 = randomItem.quality
local ____cond52 = ____switch52 == ItemQuality.COMMON
if ____cond52 then
particleName = "particles/common_item.vpcf"
break
end
____cond52 = ____cond52 or ____switch52 == ItemQuality.RARE
if ____cond52 then
particleName = "particles/rare_item.vpcf"
break
end
____cond52 = ____cond52 or ____switch52 == ItemQuality.EPIC
if ____cond52 then
particleName = "particles/epic_item.vpcf"
break
end
____cond52 = ____cond52 or ____switch52 == ItemQuality.LEGENDARY
if ____cond52 then
particleName = "particles/legendary_item.vpcf"
break
end
____cond52 = ____cond52 or ____switch52 == ItemQuality.CURSED
if ____cond52 then
particleName = "particles/cursed_item.vpcf"
break
end
____cond52 = ____cond52 or ____switch52 == ItemQuality.HEAVENLY
if ____cond52 then
particleName = "particles/heavenly_item_effect.vpcf"
break
end
do
particleName = "particles/default_item.vpcf"
end
until true
local particle = ParticleManager:CreateParticle(particleName, PATTACH_ABSORIGIN_FOLLOW, drop)
ParticleManager:SetParticleControl(particle, 0, position)
end
--- Регистрация CGE для блекшопа — только из GameMode.Activate (или иного позднего входа).
-- При require() из addon_init CustomGameEventManager ещё nil — нельзя вызывать в main chunk.
function ____exports.registerBlackShopCustomGameEvents(self)
if not IsServer() then
return
end
CustomGameEventManager:RegisterListener(
"black_shop_refresh_request",
function(_, event)
local playerId = event.PlayerID
local ____opt_9 = PlayerResource:GetPlayer(playerId)
local hero = ____opt_9 and ____opt_9:GetAssignedHero()
local refreshCost = 50
if not hero then
return
end
local currentGold = hero:GetGold()
if currentGold >= refreshCost then
PlayerResource:SpendGold(playerId, refreshCost, DOTA_ModifyGold_AbilityGold)
____exports.BlackShop:getInstance():RefreshShop()
CustomGameEventManager:Send_ServerToAllClients("black_shop_refresh", {success = true})
else
CustomGameEventManager:Send_ServerToPlayer(
PlayerResource:GetPlayer(playerId),
"CreateIngameErrorMessage",
{gold = refreshCost - currentGold, message = "black_shop_not_enough_gold"}
)
end
end
)
CustomGameEventManager:RegisterListener(
"black_shop_request_card_price",
function(_, event)
local playerId = event.PlayerID
local player = PlayerResource:GetPlayer(playerId)
if not player then
return
end
local cost = ____exports.BlackShop:getInstance():getCardPurchaseCost(playerId)
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_card_cost_update", {cost = cost})
end
)
CustomGameEventManager:RegisterListener(
"black_shop_buy_card_request",
function(_, event)
local playerId = event.PlayerID
local player = PlayerResource:GetPlayer(playerId)
if not player then
return
end
local result = ____exports.BlackShop:getInstance():buyCardForPlayer(playerId)
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_card_cost_update", {cost = result.nextCost})
if not result.success then
local crystalCurrency = CrystalCurrency:getInstance()
local currentCrystals = crystalCurrency:getCrystals(playerId)
CustomGameEventManager:Send_ServerToPlayer(
player,
"CreateIngameErrorMessage",
{
crystals = math.max(0, result.spentCost - currentCrystals),
message = result.errorMessage or "black_shop_not_enough_crystals"
}
)
return
end
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_card_purchased", {success = true})
end
)
CustomGameEventManager:RegisterListener(
"black_shop_teleport_request",
function(_, event)
local playerId = event.PlayerID
if playerId == nil then
return
end
local player = PlayerResource:GetPlayer(playerId)
if not player then
return
end
local dest = event.destination
if not isBlackShopTeleportDestination(nil, dest) then
return
end
local hero = player:GetAssignedHero()
if not hero or hero:IsNull() then
return
end
if not hero:IsAlive() then
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_teleport_result", {success = false})
return
end
local teleportGoldCost = 100
local currentGold = hero:GetGold()
if currentGold < teleportGoldCost then
CustomGameEventManager:Send_ServerToPlayer(player, "CreateIngameErrorMessage", {gold = teleportGoldCost - currentGold, message = "black_shop_not_enough_gold"})
return
end
local pos = resolveBlackShopTeleportPosition(nil, dest)
if not pos then
return
end
PlayerResource:SpendGold(playerId, teleportGoldCost, DOTA_ModifyGold_AbilityGold)
FindClearSpaceForUnit(hero, pos, true)
local teleportFx = "particles/econ/events/compendium_2023/compendium_2023_teleport_lvl1.vpcf"
local origin = hero:GetAbsOrigin()
local pfx = ParticleManager:CreateParticle(teleportFx, PATTACH_WORLDORIGIN, hero)
ParticleManager:SetParticleControl(pfx, 0, origin)
Timers:CreateTimer(
0.5,
function()
ParticleManager:DestroyParticle(pfx, true)
ParticleManager:ReleaseParticleIndex(pfx)
end
)
EmitSoundOn("DOTA_Item.BlinkDagger.Activate", hero)
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_teleport_result", {success = true})
end
)
CustomGameEventManager:RegisterListener(
"black_shop_exchange_request",
function(_, event)
local playerId = event.PlayerID or event.player_id
if playerId == nil then
return
end
local player = PlayerResource:GetPlayer(playerId)
local hero = player and player:GetAssignedHero()
if not player or not hero then
return
end
local direction = event.direction
local crystals = math.floor(tonumber(event.crystals) or 0)
if not direction or crystals <= 0 then
return
end
local crystalCurrency = CrystalCurrency:getInstance()
if direction == "gold_to_crystal" then
local goldAmount = crystals * BLACKSHOP_GOLD_TO_CRYSTAL_RATE
local currentGold = hero:GetGold()
if currentGold < goldAmount then
CustomGameEventManager:Send_ServerToPlayer(player, "CreateIngameErrorMessage", {gold = goldAmount - currentGold, message = "black_shop_not_enough_gold"})
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_exchange_result", {success = false})
return
end
PlayerResource:SpendGold(playerId, goldAmount, DOTA_ModifyGold_AbilityGold)
crystalCurrency:addCrystals(playerId, crystals)
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_exchange_result", {success = true, direction = direction, crystals = crystals, gold = goldAmount})
else
local goldAmount = crystals * BLACKSHOP_CRYSTAL_TO_GOLD_RATE
local currentCrystals = crystalCurrency:getCrystals(playerId)
if currentCrystals < crystals then
CustomGameEventManager:Send_ServerToPlayer(player, "CreateIngameErrorMessage", {crystals = crystals - currentCrystals, message = "black_shop_not_enough_crystals"})
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_exchange_result", {success = false})
return
end
local removed = crystalCurrency:removeCrystals(playerId, crystals)
if not removed then
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_exchange_result", {success = false})
return
end
hero:ModifyGold(goldAmount, true, DOTA_ModifyGold_AbilityGold)
CustomGameEventManager:Send_ServerToPlayer(player, "black_shop_exchange_result", {success = true, direction = direction, crystals = crystals, gold = goldAmount})
end
end
)
CustomGameEventManager:RegisterListener(
"black_shop_exchange_sync_request",
function(_, event)
local playerId = event.PlayerID or event.player_id
if playerId == nil then
return
end
local player = PlayerResource:GetPlayer(playerId)
local hero = player and player:GetAssignedHero()
if not player or not hero then
return
end
local crystalCurrency = CrystalCurrency:getInstance()
CustomGameEventManager:Send_ServerToPlayer(
player,
"black_shop_exchange_sync",
{
gold = hero:GetGold(),
crystals = crystalCurrency:getCrystals(playerId)
}
)
end
)
end
--- Превью дропа / эффекты редкости предметов
function ____exports.precacheBlackshopParticles(self, context)
PrecacheResource("particle", "particles/econ/events/compendium_2023/compendium_2023_teleport_lvl1.vpcf", context)
PrecacheResource("particle", "particles/units/heroes/hero_ancient_apparition/ancient_apparition_freeze_stacks_smoke_b.vpcf", context)
PrecacheResource("particle", "particles/units/heroes/hero_ancient_apparition/ancient_apparition_ice_blast_main.vpcf", context)
PrecacheResource("particle", "particles/econ/items/lanaya/lanaya_epit_trap/templar_assassin_epit_trap.vpcf", context)
PrecacheResource("particle", "particles/econ/items/templar_assassin/ta_2022_immortal/ta_2022_immortal_trap_gold.vpcf", context)
PrecacheResource("particle", "particles/econ/items/shadow_fiend/sf_desolation/sf_base_attack_desolation.vpcf", context)
PrecacheResource("particle", "particles/econ/items/templar_assassin/ta_2022_immortal/ta_2022_immortal_trap_crimson.vpcf", context)
end
return ____exports