chore: release version 2.3.5 and refactor lobby
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.
This commit is contained in:
@@ -80,16 +80,36 @@ function economy.rpc_buy_currency(context, payload)
|
||||
elseif packageId == "star_600" then changeset.star = 600; changeset.gold = -2500
|
||||
else error("Invalid package ID") end
|
||||
|
||||
if requiresVerification and not receipt then
|
||||
nk.storage_write({{
|
||||
collection = "receipts",
|
||||
key = idempotencyKey,
|
||||
user_id = context.user_id,
|
||||
value = { type = "currency", package_id = packageId, status = "pending", created_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
return nk.json_encode({ success = true, status = "pending", package_id = packageId })
|
||||
if requiresVerification then
|
||||
if not receipt or receipt == "" then
|
||||
nk.storage_write({{
|
||||
collection = "receipts",
|
||||
key = idempotencyKey,
|
||||
user_id = context.user_id,
|
||||
value = { type = "currency", package_id = packageId, status = "pending", created_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
return nk.json_encode({ success = true, status = "pending", package_id = packageId })
|
||||
end
|
||||
|
||||
local is_valid = false
|
||||
local store_type = request.store_type or "test"
|
||||
|
||||
if store_type == "google" then
|
||||
local s, response = pcall(nk.purchase_validate_google, context.user_id, receipt)
|
||||
if s and response.success then is_valid = true end
|
||||
elseif store_type == "apple" then
|
||||
local s, response = pcall(nk.purchase_validate_apple, context.user_id, receipt, "")
|
||||
if s and response.success then is_valid = true end
|
||||
elseif store_type == "test" and receipt == "mock_receipt_for_now" then
|
||||
is_valid = true
|
||||
end
|
||||
|
||||
if not is_valid then
|
||||
nk.logger_warn("Invalid IAP receipt submitted: " .. tostring(receipt))
|
||||
error("InvalidReceipt")
|
||||
end
|
||||
end
|
||||
|
||||
local s, err = pcall(function()
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
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
|
||||
@@ -9,5 +9,6 @@ require("lua.daily_rewards")
|
||||
require("lua.user")
|
||||
require("lua.leaderboard")
|
||||
require("lua.inbox")
|
||||
require("lua.gacha")
|
||||
|
||||
nk.logger_info("LUA TEST: main.lua entrypoint loaded successfully")
|
||||
|
||||
Reference in New Issue
Block a user