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:
@@ -22,8 +22,8 @@ var auth_mode: AuthMode = AuthMode.GUEST
|
||||
const SESSION_FILE := "user://auth_session.dat"
|
||||
const CREDENTIALS_FILE := "user://auth_credentials.dat"
|
||||
|
||||
# Encryption key for session storage (replace with your own!)
|
||||
const ENCRYPTION_KEY := "tekton_secret_key_change_me_123"
|
||||
# Encryption key for session storage (device-specific)
|
||||
var ENCRYPTION_KEY: String = OS.get_unique_id().sha256_text()
|
||||
|
||||
func _ready() -> void:
|
||||
# Try to restore session on startup
|
||||
@@ -368,7 +368,7 @@ func login_with_steam() -> bool:
|
||||
return true
|
||||
|
||||
func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
|
||||
# Try proper Steam ticket auth first
|
||||
# Proper Steam ticket auth
|
||||
var auth_ticket = steamworks.get_auth_session_ticket()
|
||||
if not auth_ticket.is_empty():
|
||||
print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...")
|
||||
@@ -376,22 +376,10 @@ func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
|
||||
if not session.is_exception():
|
||||
return session
|
||||
print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message)
|
||||
print("[AuthManager] Falling back to Steam ID custom auth (dev mode)...")
|
||||
|
||||
# Fallback: use Steam ID + username to create an email-style account (works without publisher key)
|
||||
var steam_id = str(steamworks.get_steam_user_id())
|
||||
var steam_name = steamworks.get_steam_user_name()
|
||||
if steam_id == "0" or steam_id.is_empty():
|
||||
return null
|
||||
|
||||
# Derive email and password from Steam credentials
|
||||
var email = steam_name.to_lower().replace(" ", "_") + "@steam.local"
|
||||
var password = steam_name # Default password = Steam username
|
||||
var username = steam_name
|
||||
|
||||
print("[AuthManager] Using Steam email auth: %s (%s)" % [email, username])
|
||||
var fallback_session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true)
|
||||
return fallback_session
|
||||
print("[AuthManager] Steam auth ticket is empty.")
|
||||
return null
|
||||
|
||||
# =============================================================================
|
||||
# Account Linking (Convert Guest to Full Account)
|
||||
|
||||
@@ -27,44 +27,44 @@ func _load_data() -> void:
|
||||
|
||||
func pull(banner_id: String, count: int) -> Array:
|
||||
if data.is_empty(): _load_data()
|
||||
var banner: Dictionary = data.get("banners", {}).get(banner_id, {})
|
||||
if banner.is_empty():
|
||||
push_error("[GachaManager] Unknown banner: " + banner_id)
|
||||
|
||||
if not NakamaManager.session:
|
||||
push_error("[GachaManager] Not authenticated")
|
||||
return []
|
||||
|
||||
var currency: String = banner.get("currency", "star")
|
||||
var cost_key: String = "pull_%d_cost" % count
|
||||
var cost: int = banner.get(cost_key, banner.get("pull_1_cost", 999) * count)
|
||||
var pity_at: int = banner.get("pity_at", 90)
|
||||
|
||||
# Deduct currency
|
||||
var bal: int = UserProfileManager.wallet.get(currency, 0)
|
||||
if bal < cost:
|
||||
push_warning("[GachaManager] Not enough %s (have %d need %d)" % [currency, bal, cost])
|
||||
var payload = JSON.stringify({
|
||||
"banner_id": banner_id,
|
||||
"count": count
|
||||
})
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
"perform_gacha_pull",
|
||||
payload
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
var msg = result.get_exception().message
|
||||
push_error("[GachaManager] Gacha pull failed: " + msg)
|
||||
return []
|
||||
UserProfileManager.wallet[currency] = bal - cost
|
||||
UserProfileManager.save_wallet()
|
||||
|
||||
var results: Array = []
|
||||
for _i in range(count):
|
||||
var pity: int = pity_counters.get(banner_id, 0)
|
||||
var rarity: String
|
||||
if pity + 1 >= pity_at:
|
||||
rarity = "real_prize"
|
||||
pity_counters[banner_id] = 0
|
||||
else:
|
||||
rarity = _roll_rarity(banner.get("rates", {}))
|
||||
pity_counters[banner_id] = (pity + 1) if rarity != "real_prize" else 0
|
||||
|
||||
var item_id: String = _pick_from_pool(rarity, banner_id)
|
||||
var item_data: Dictionary = _resolve_item(item_id, rarity)
|
||||
results.append({
|
||||
"id": item_id,
|
||||
"rarity": rarity,
|
||||
"name": item_data.get("name", item_id)
|
||||
})
|
||||
|
||||
var parsed = JSON.parse_string(result.payload)
|
||||
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("results"):
|
||||
return []
|
||||
|
||||
var results: Array = parsed.get("results", [])
|
||||
|
||||
if parsed.has("new_pity"):
|
||||
pity_counters[banner_id] = int(parsed.new_pity)
|
||||
|
||||
if UserProfileManager.has_method("_reload_wallet"):
|
||||
await UserProfileManager._reload_wallet()
|
||||
|
||||
for res in results:
|
||||
var item_id = res.get("id", "")
|
||||
var rarity = res.get("rarity", "")
|
||||
_grant_item(item_id, rarity)
|
||||
|
||||
|
||||
pull_completed.emit(results)
|
||||
return results
|
||||
|
||||
|
||||
@@ -360,7 +360,8 @@ func fetch_shop_catalog() -> void:
|
||||
if payload and payload.has("catalog"):
|
||||
shop_catalog = payload.catalog
|
||||
if payload.has("featured_banners"):
|
||||
featured_banners = payload.get("featured_banners", [])
|
||||
var _banners = payload.get("featured_banners", [])
|
||||
featured_banners = _banners if typeof(_banners) == TYPE_ARRAY else []
|
||||
emit_signal("profile_updated")
|
||||
|
||||
## Admin-only: grants a large amount of gold via a server-authoritative RPC.
|
||||
@@ -384,7 +385,8 @@ func buy_currency(package_id: String) -> bool:
|
||||
var payload = JSON.stringify({
|
||||
"package_id": package_id,
|
||||
"idempotency_key": str(randi()) + "_" + str(Time.get_ticks_usec()),
|
||||
"receipt": "mock_receipt_for_now"
|
||||
"receipt": "mock_receipt_for_now",
|
||||
"store_type": "test"
|
||||
})
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
extends Node
|
||||
|
||||
# Standard Nakama Configuration
|
||||
var nakama_server_key = "defaultkey"
|
||||
var nakama_host = "tektondash.vps.webdock.cloud"
|
||||
var nakama_port = 7350
|
||||
var nakama_scheme = "http"
|
||||
var nakama_server_key = OS.get_environment("NAKAMA_SERVER_KEY") if OS.has_environment("NAKAMA_SERVER_KEY") else ProjectSettings.get_setting("network/nakama/server_key", "defaultkey")
|
||||
var nakama_host = OS.get_environment("NAKAMA_HOST") if OS.has_environment("NAKAMA_HOST") else ProjectSettings.get_setting("network/nakama/host", "tektondash.vps.webdock.cloud")
|
||||
var nakama_port = OS.get_environment("NAKAMA_PORT").to_int() if OS.has_environment("NAKAMA_PORT") else ProjectSettings.get_setting("network/nakama/port", 7350)
|
||||
var nakama_scheme = OS.get_environment("NAKAMA_SCHEME") if OS.has_environment("NAKAMA_SCHEME") else ProjectSettings.get_setting("network/nakama/scheme", "http")
|
||||
|
||||
# Core Nakama Variables
|
||||
var client: NakamaClient
|
||||
|
||||
@@ -61,59 +61,7 @@ func _connect_nakama_signals() -> void:
|
||||
# No need to connect through BackendService
|
||||
pass
|
||||
|
||||
## Achievement Methods
|
||||
# All platforms use Nakama for achievements
|
||||
|
||||
func unlock_achievement(achievement_id: String) -> void:
|
||||
if nakama_backend:
|
||||
# Nakama achievement implementation
|
||||
pass
|
||||
|
||||
func set_achievement_progress(achievement_id: String, current: int, max: int) -> void:
|
||||
if nakama_backend:
|
||||
# Nakama progress implementation
|
||||
pass
|
||||
|
||||
func get_achievement_progress(achievement_id: String) -> Dictionary:
|
||||
if nakama_backend:
|
||||
# Nakama get progress implementation
|
||||
pass
|
||||
return {}
|
||||
|
||||
func get_all_achievements() -> Array:
|
||||
if nakama_backend:
|
||||
# Nakama get all achievements implementation
|
||||
pass
|
||||
return []
|
||||
|
||||
## Leaderboard Methods
|
||||
# All platforms use Nakama for leaderboards
|
||||
|
||||
func submit_leaderboard_score(leaderboard_id: String, score: int) -> void:
|
||||
if nakama_backend:
|
||||
# Nakama leaderboard submission - use UserProfileManager.submit_to_leaderboard()
|
||||
await UserProfileManager.submit_to_leaderboard()
|
||||
|
||||
func get_leaderboard_entries(leaderboard_id: String, range_start: int = 1, range_end: int = 10) -> void:
|
||||
if nakama_backend:
|
||||
# Nakama get leaderboard entries - use LeaderboardPanel._fetch_leaderboard_data()
|
||||
# This is handled by the UI panel directly
|
||||
pass
|
||||
|
||||
## Shop Methods
|
||||
# All platforms use Nakama for shop
|
||||
|
||||
func purchase_shop_item(item_id: String) -> void:
|
||||
if nakama_backend:
|
||||
# Nakama shop purchase - use UserProfileManager.purchase_item()
|
||||
# This is handled by the UI panel directly via ShopPanel
|
||||
pass
|
||||
|
||||
func get_shop_items() -> void:
|
||||
if nakama_backend:
|
||||
# Nakama get shop items - use UserProfileManager.fetch_shop_catalog()
|
||||
# This is handled by the UI panel directly via ShopPanel
|
||||
pass
|
||||
|
||||
## Utility Methods
|
||||
|
||||
|
||||
@@ -71,20 +71,13 @@ func _ready() -> void:
|
||||
close_result_btn.pressed.connect(func(): result_panel.hide())
|
||||
craft_btn.pressed.connect(_on_open_craft)
|
||||
result_panel.hide()
|
||||
_ensure_dummy_wallet()
|
||||
_switch_banner("star")
|
||||
|
||||
func show_panel() -> void:
|
||||
show()
|
||||
_refresh_ui()
|
||||
|
||||
# ─── Dummy wallet so editor testing works without Nakama ─────────────────────
|
||||
func _ensure_dummy_wallet() -> void:
|
||||
var w: Dictionary = UserProfileManager.wallet
|
||||
if w.get("star", 0) == 0:
|
||||
UserProfileManager.wallet["star"] = 3200
|
||||
if w.get("gold", 0) == 0:
|
||||
UserProfileManager.wallet["gold"] = 1500
|
||||
|
||||
|
||||
# ─── Banner switching ─────────────────────────────────────────────────────────
|
||||
func _switch_banner(id: String) -> void:
|
||||
@@ -179,7 +172,7 @@ func _do_pull(count: int) -> void:
|
||||
status_label.text = "Rolling..."
|
||||
|
||||
await get_tree().process_frame
|
||||
var results: Array = GachaManager.pull(_current_banner, count)
|
||||
var results: Array = await GachaManager.pull(_current_banner, count)
|
||||
|
||||
if results.is_empty():
|
||||
status_label.text = "❌ Not enough currency!"
|
||||
|
||||
Reference in New Issue
Block a user