decdb74ade
Bump export_presets.cfg version to 2.3.5. Update CHANGELOG_DRAFT.md. Refactor lobby.gd into LobbyChat, LobbyMainMenu, LobbyRoomList, LobbyRoom. Move Nakama config to environment variables in nakama_manager.gd. Derive auth_manager.gd encryption key from OS.get_unique_id().sha256_text(). Remove Steam email auth fallback. Require auth ticket. Make GachaManager.pull() async in gacha_panel.gd. Remove dummy wallet seeding. Add store_type to IAP payload. Validate IAP receipts server-side in economy.lua. Register gacha module in main.lua. Clean backend_service.gd stubs. Fix featured_banners type safety in gacha_manager.gd. Guards non-array responses. Move tiles_armagedon_a1.res to assets/models/meshes/. Fix import fallback_path.
213 lines
6.8 KiB
Lua
213 lines
6.8 KiB
Lua
local nk = require("nakama")
|
|
|
|
local gacha = {}
|
|
|
|
-- Embedded Gacha Data from gacha_data.json
|
|
local GACHA_DATA = {
|
|
banners = {
|
|
star = {
|
|
name = "Star Banner",
|
|
currency = "star",
|
|
pull_1_cost = 160,
|
|
pull_10_cost = 1440,
|
|
pity_at = 90,
|
|
rates = {
|
|
common = 0.60,
|
|
uncommon = 0.25,
|
|
rare = 0.14,
|
|
real_prize = 0.01
|
|
}
|
|
},
|
|
gold = {
|
|
name = "Gold Banner",
|
|
currency = "gold",
|
|
pull_1_cost = 50,
|
|
pull_10_cost = 450,
|
|
pity_at = 90,
|
|
rates = {
|
|
common = 0.60,
|
|
uncommon = 0.25,
|
|
rare = 0.14,
|
|
real_prize = 0.01
|
|
}
|
|
}
|
|
},
|
|
fragments = {
|
|
frag_common = { name = "Common Fragment", rarity = "common", icon = "⬜" },
|
|
frag_uncommon = { name = "Uncommon Fragment", rarity = "uncommon", icon = "🟩" },
|
|
frag_rare = { name = "Rare Fragment", rarity = "rare", icon = "🟦" }
|
|
},
|
|
pools = {
|
|
common = {"frag_common"},
|
|
uncommon = {"frag_uncommon"},
|
|
rare = {"frag_rare"},
|
|
real_prize = {
|
|
"skin_gacha_rainbow_suit",
|
|
"skin_gacha_dragon_hat",
|
|
"skin_gacha_phantom_gloves",
|
|
"skin_gacha_neon_acc"
|
|
}
|
|
},
|
|
real_prize_catalog = {
|
|
skin_gacha_rainbow_suit = { name = "Rainbow Suit", category = "costume", rarity = "real_prize", character = "" },
|
|
skin_gacha_dragon_hat = { name = "Dragon Hat", category = "head", rarity = "real_prize", character = "" },
|
|
skin_gacha_phantom_gloves = { name = "Phantom Gloves", category = "glove", rarity = "real_prize", character = "" },
|
|
skin_gacha_neon_acc = { name = "Neon Accessory", category = "accessory", rarity = "real_prize", character = "" }
|
|
}
|
|
}
|
|
|
|
local function roll_rarity(rates)
|
|
local roll = math.random()
|
|
local cumulative = 0.0
|
|
local rarities = {"real_prize", "rare", "uncommon", "common"}
|
|
for _, rarity in ipairs(rarities) do
|
|
cumulative = cumulative + (rates[rarity] or 0.0)
|
|
if roll <= cumulative then
|
|
return rarity
|
|
end
|
|
end
|
|
return "common"
|
|
end
|
|
|
|
local function pick_from_pool(rarity)
|
|
local pool = GACHA_DATA.pools[rarity]
|
|
if not pool or #pool == 0 then return "" end
|
|
return pool[math.random(1, #pool)]
|
|
end
|
|
|
|
function gacha.rpc_perform_gacha_pull(context, payload)
|
|
if not context.user_id then error("Not authenticated") end
|
|
|
|
local request = nk.json_decode(payload)
|
|
local banner_id = request.banner_id
|
|
local count = request.count or 1
|
|
|
|
if not banner_id or banner_id == "" then error("Banner ID required") end
|
|
if count < 1 then error("Invalid count") end
|
|
|
|
local banner = GACHA_DATA.banners[banner_id]
|
|
if not banner then error("Unknown banner: " .. banner_id) end
|
|
|
|
local currency = banner.currency or "star"
|
|
local cost = banner["pull_" .. count .. "_cost"]
|
|
if not cost then cost = (banner.pull_1_cost or 999) * count end
|
|
local pity_at = banner.pity_at or 90
|
|
|
|
-- Read wallet
|
|
local status, account = pcall(nk.account_get_id, context.user_id)
|
|
if not status or not account then error("Could not read account") end
|
|
|
|
local wallet = account.wallet
|
|
if type(wallet) == "string" then wallet = nk.json_decode(wallet) end
|
|
local bal = wallet[currency] or 0
|
|
|
|
if bal < cost then
|
|
error("Insufficient currency")
|
|
end
|
|
|
|
-- Read pity and fragments from storage
|
|
local pity_counters = {}
|
|
local fragments = {}
|
|
local read_reqs = {
|
|
{ collection = "profiles", key = "pity_counters", user_id = context.user_id },
|
|
{ collection = "profiles", key = "fragments", user_id = context.user_id }
|
|
}
|
|
local status_read, read_objs = pcall(nk.storage_read, read_reqs)
|
|
if status_read and read_objs then
|
|
for _, obj in ipairs(read_objs) do
|
|
if obj.key == "pity_counters" then
|
|
pity_counters = obj.value
|
|
elseif obj.key == "fragments" then
|
|
fragments = obj.value
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Perform pulls
|
|
local results = {}
|
|
local inventory_writes = {}
|
|
local inventory_items = {}
|
|
|
|
for i = 1, count do
|
|
local pity = pity_counters[banner_id] or 0
|
|
local rarity
|
|
if pity + 1 >= pity_at then
|
|
rarity = "real_prize"
|
|
pity_counters[banner_id] = 0
|
|
else
|
|
rarity = roll_rarity(banner.rates)
|
|
pity_counters[banner_id] = (rarity == "real_prize") and 0 or (pity + 1)
|
|
end
|
|
|
|
local item_id = pick_from_pool(rarity)
|
|
|
|
local item_data = {}
|
|
if rarity == "real_prize" then
|
|
item_data = GACHA_DATA.real_prize_catalog[item_id] or {}
|
|
|
|
-- Add to inventory_writes
|
|
table.insert(inventory_writes, {
|
|
collection = "inventory",
|
|
key = item_id,
|
|
user_id = context.user_id,
|
|
value = { category = item_data.category or "accessory", purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z"), quantity = 1 },
|
|
permission_read = 1,
|
|
permission_write = 0
|
|
})
|
|
table.insert(inventory_items, item_id)
|
|
else
|
|
item_data = GACHA_DATA.fragments[item_id] or {}
|
|
fragments[item_id] = (fragments[item_id] or 0) + 1
|
|
end
|
|
|
|
table.insert(results, {
|
|
id = item_id,
|
|
rarity = rarity,
|
|
name = item_data.name or item_id
|
|
})
|
|
end
|
|
|
|
-- Write wallet
|
|
local changeset = {}
|
|
changeset[currency] = -cost
|
|
local s, err = pcall(function()
|
|
nk.wallet_update(context.user_id, changeset, {}, true)
|
|
end)
|
|
if not s then error("Failed to update wallet") end
|
|
|
|
-- Write storage (pity, fragments, inventory)
|
|
local write_objs = {
|
|
{
|
|
collection = "profiles",
|
|
key = "pity_counters",
|
|
user_id = context.user_id,
|
|
value = pity_counters,
|
|
permission_read = 1,
|
|
permission_write = 0
|
|
},
|
|
{
|
|
collection = "profiles",
|
|
key = "fragments",
|
|
user_id = context.user_id,
|
|
value = fragments,
|
|
permission_read = 1,
|
|
permission_write = 0
|
|
}
|
|
}
|
|
|
|
for _, iw in ipairs(inventory_writes) do
|
|
table.insert(write_objs, iw)
|
|
end
|
|
|
|
local s2, err2 = pcall(nk.storage_write, write_objs)
|
|
if not s2 then error("Failed to write storage: " .. tostring(err2)) end
|
|
|
|
return nk.json_encode({ success = true, results = results, new_pity = pity_counters[banner_id] })
|
|
end
|
|
|
|
nk.register_rpc(gacha.rpc_perform_gacha_pull, "perform_gacha_pull")
|
|
|
|
nk.logger_info("LUA TEST: gacha module loaded successfully")
|
|
|
|
return gacha
|