feat: add dailylogin feature
This commit is contained in:
@@ -15,6 +15,7 @@ signal profile_loaded(profile: Dictionary)
|
||||
signal profile_updated
|
||||
signal profile_update_failed(error: String)
|
||||
signal avatar_changed(url: String)
|
||||
signal stats_updated
|
||||
|
||||
# Profile data
|
||||
var profile: Dictionary = {}
|
||||
@@ -423,6 +424,7 @@ func save_wallet() -> void:
|
||||
|
||||
func update_stats(new_stats: Dictionary) -> bool:
|
||||
stats.merge(new_stats, true)
|
||||
emit_signal("stats_updated")
|
||||
|
||||
if not NakamaManager.session:
|
||||
return false
|
||||
|
||||
+117
-2
@@ -26,6 +26,18 @@ signal closed
|
||||
@onready var sync_lb_btn := %SyncLeaderboardBtn as Button
|
||||
@onready var reset_lb_btn := %ResetLBBtn as Button
|
||||
|
||||
# Tab: Daily Rewards
|
||||
@onready var month_option_btn := %MonthOptionBtn as OptionButton
|
||||
@onready var days_grid := %DaysGrid as GridContainer
|
||||
@onready var day_config_template := %DayConfigTemplate as VBoxContainer
|
||||
@onready var load_dr_btn := %LoadDRConfigBtn as Button
|
||||
@onready var save_dr_btn := %SaveDRConfigBtn as Button
|
||||
|
||||
var _daily_reward_config_data: Dictionary = {}
|
||||
var _current_dr_month: String = ""
|
||||
|
||||
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
||||
|
||||
# -- Data --
|
||||
var all_users: Array = []
|
||||
var lb_data: Array = []
|
||||
@@ -62,7 +74,7 @@ func _apply_plain_style() -> void:
|
||||
count_label.add_theme_color_override("font_color", CLR_HEADER)
|
||||
status_label.add_theme_color_override("font_color", CLR_DIM)
|
||||
|
||||
for btn: Button in [refresh_btn, close_btn, select_all_btn, deselect_btn]:
|
||||
for btn: Button in [refresh_btn, close_btn, select_all_btn, deselect_btn, load_dr_btn, save_dr_btn]:
|
||||
_style_button(btn, Color(0.2, 0.2, 0.24), CLR_TEXT)
|
||||
_style_button(ban_btn, Color(0.3, 0.2, 0.1), CLR_BTN_BAN)
|
||||
_style_button(unban_btn, Color(0.1, 0.22, 0.1), CLR_BTN_UNBAN)
|
||||
@@ -134,6 +146,11 @@ func _connect_signals() -> void:
|
||||
# LB actions
|
||||
lb_tree.button_clicked.connect(_on_lb_tree_button_clicked)
|
||||
sync_lb_btn.pressed.connect(_on_sync_leaderboard)
|
||||
|
||||
# DR actions
|
||||
load_dr_btn.pressed.connect(_load_daily_rewards_config)
|
||||
save_dr_btn.pressed.connect(_save_daily_rewards_config)
|
||||
month_option_btn.item_selected.connect(_on_dr_month_selected)
|
||||
|
||||
# =============================================================================
|
||||
# Core Panel Logic
|
||||
@@ -153,8 +170,10 @@ func _on_tab_changed(tab_index: int) -> void:
|
||||
_set_status("")
|
||||
if tab_index == 0:
|
||||
await _load_users()
|
||||
else:
|
||||
elif tab_index == 1:
|
||||
await _load_leaderboard()
|
||||
elif tab_index == 2:
|
||||
await _load_daily_rewards_config()
|
||||
|
||||
# =============================================================================
|
||||
# RPC Helper
|
||||
@@ -508,3 +527,99 @@ func _get_edit_icon() -> Texture2D:
|
||||
img.set_pixel(i, 11 - i + 4, CLR_TEXT)
|
||||
img.set_pixel(i, 12 - i + 4, CLR_TEXT)
|
||||
return ImageTexture.create_from_image(img)
|
||||
|
||||
# =============================================================================
|
||||
# TAB 3: DAILY REWARDS
|
||||
# =============================================================================
|
||||
func _load_daily_rewards_config() -> void:
|
||||
_set_status("Loading Daily Rewards Config...")
|
||||
var res := await _rpc("get_daily_reward_config_admin", {})
|
||||
if res.has("error"):
|
||||
_set_status("Failed to load DR config", CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
var config = res.get("config", {})
|
||||
if config.is_empty():
|
||||
for m in range(1, 13):
|
||||
var m_str = "%02d" % m
|
||||
var arr = []
|
||||
for d in range(30):
|
||||
arr.append({"type": "star", "amount": min(10 + d*5, 100)})
|
||||
config[m_str] = arr
|
||||
|
||||
_daily_reward_config_data = config
|
||||
|
||||
month_option_btn.clear()
|
||||
for i in range(1, 13):
|
||||
month_option_btn.add_item(MONTH_NAMES[i - 1])
|
||||
month_option_btn.set_item_metadata(i - 1, "%02d" % i)
|
||||
|
||||
if not _daily_reward_config_data.is_empty():
|
||||
_current_dr_month = "01"
|
||||
month_option_btn.select(0)
|
||||
_build_dr_grid()
|
||||
|
||||
_set_status("Config Loaded", CLR_STATUS_OK)
|
||||
|
||||
func _on_dr_month_selected(index: int) -> void:
|
||||
# Save current grid values into the dictionary before switching
|
||||
_save_current_grid_to_dict()
|
||||
|
||||
_current_dr_month = month_option_btn.get_item_metadata(index)
|
||||
_build_dr_grid()
|
||||
|
||||
func _save_current_grid_to_dict() -> void:
|
||||
if _current_dr_month.is_empty(): return
|
||||
var arr = []
|
||||
for child in days_grid.get_children():
|
||||
if child != day_config_template:
|
||||
var opt = child.get_node("Margin/VBox/TypeOptionBtn") as OptionButton
|
||||
var spin = child.get_node("Margin/VBox/AmountSpinBox") as SpinBox
|
||||
var item_type = opt.get_item_text(opt.selected) if opt.selected >= 0 else "star"
|
||||
arr.append({"type": item_type, "amount": int(spin.value)})
|
||||
if not arr.is_empty():
|
||||
_daily_reward_config_data[_current_dr_month] = arr
|
||||
|
||||
func _build_dr_grid() -> void:
|
||||
for child in days_grid.get_children():
|
||||
if child != day_config_template:
|
||||
child.queue_free()
|
||||
|
||||
var arr = _daily_reward_config_data.get(_current_dr_month, [])
|
||||
for i in range(arr.size()):
|
||||
var slot = day_config_template.duplicate()
|
||||
slot.visible = true
|
||||
days_grid.add_child(slot)
|
||||
|
||||
var lbl = slot.get_node("Margin/VBox/DayLabel") as Label
|
||||
var opt = slot.get_node("Margin/VBox/TypeOptionBtn") as OptionButton
|
||||
var spin = slot.get_node("Margin/VBox/AmountSpinBox") as SpinBox
|
||||
|
||||
lbl.text = "Day " + str(i + 1)
|
||||
var rdata = arr[i]
|
||||
if typeof(rdata) == TYPE_DICTIONARY:
|
||||
spin.value = rdata.get("amount", 0)
|
||||
var type_str = rdata.get("type", "star")
|
||||
var found = false
|
||||
for j in range(opt.item_count):
|
||||
if opt.get_item_text(j) == type_str:
|
||||
opt.select(j)
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
opt.add_item(type_str)
|
||||
opt.select(opt.item_count - 1)
|
||||
else:
|
||||
# Fallback for old int format
|
||||
spin.value = int(rdata)
|
||||
opt.select(0)
|
||||
|
||||
func _save_daily_rewards_config() -> void:
|
||||
_save_current_grid_to_dict()
|
||||
_set_status("Saving config...")
|
||||
var req = { "config": _daily_reward_config_data }
|
||||
var res = await _rpc("set_daily_reward_config", req)
|
||||
if res.has("error"):
|
||||
_set_status("Save failed: " + res.get("error"), CLR_STATUS_ERR)
|
||||
else:
|
||||
_set_status("Config saved successfully!", CLR_STATUS_OK)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
extends Control
|
||||
|
||||
@onready var close_btn = %CloseBtn
|
||||
@onready var save_btn = %SaveBtn
|
||||
@onready var status_lbl = %StatusLabel
|
||||
@onready var text_edit = %TextEdit
|
||||
|
||||
func _ready():
|
||||
close_btn.pressed.connect(func(): queue_free())
|
||||
save_btn.pressed.connect(_on_save)
|
||||
_load_config()
|
||||
|
||||
func _load_config():
|
||||
if not NakamaManager.session:
|
||||
status_lbl.text = "Not authenticated"
|
||||
return
|
||||
status_lbl.text = "Loading..."
|
||||
var res = await NakamaManager.client.rpc_async(NakamaManager.session, "get_daily_reward_config_admin", "{}")
|
||||
if res.is_exception():
|
||||
status_lbl.text = "Error: " + res.get_exception().message
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
if json.parse(res.payload) == OK:
|
||||
var config = json.get_data().get("config", {})
|
||||
if config.is_empty():
|
||||
# generate default 12 months for 2026/2027
|
||||
var year = 2026
|
||||
for m in range(1, 13):
|
||||
var m_str = "%d-%02d" % [year, m]
|
||||
var arr = []
|
||||
for d in range(30):
|
||||
arr.append(min(10 + d*5, 100)) # Reward is star currency, max 100
|
||||
config[m_str] = arr
|
||||
|
||||
text_edit.text = JSON.stringify(config, "\t")
|
||||
status_lbl.text = "Loaded"
|
||||
|
||||
func _on_save():
|
||||
var json = JSON.new()
|
||||
if json.parse(text_edit.text) != OK:
|
||||
status_lbl.text = "Invalid JSON syntax. Please check your formatting."
|
||||
return
|
||||
|
||||
status_lbl.text = "Saving..."
|
||||
var req = { "config": json.get_data() }
|
||||
var res = await NakamaManager.client.rpc_async(NakamaManager.session, "set_daily_reward_config", JSON.stringify(req))
|
||||
if res.is_exception():
|
||||
status_lbl.text = "Save error: " + res.get_exception().message
|
||||
else:
|
||||
status_lbl.text = "Config saved successfully!"
|
||||
@@ -0,0 +1 @@
|
||||
uid://v0h4qheiyh1k
|
||||
@@ -0,0 +1,151 @@
|
||||
extends Control
|
||||
|
||||
signal closed
|
||||
|
||||
@onready var close_btn = %CloseBtn
|
||||
@onready var grid_container = %GridContainer
|
||||
@onready var status_label = %StatusLabel
|
||||
@onready var claim_btn = %ClaimBtn
|
||||
@onready var slot_template = %RewardSlotTemplate
|
||||
@onready var month_label = %MonthLabel
|
||||
@onready var time_label = %TimeLabel
|
||||
@onready var reward_amount_label = %RewardAmount
|
||||
@onready var big_icon_label = $MainWindow/HBox/RightCol/MiddleDetails/VBox/BigIcon
|
||||
@onready var reward_name_label = $MainWindow/HBox/RightCol/MiddleDetails/VBox/RewardName
|
||||
|
||||
var _month_rewards: Array = []
|
||||
var _claimed_days: int = 0
|
||||
var _can_claim: bool = false
|
||||
var _today: String = ""
|
||||
|
||||
func _ready():
|
||||
close_btn.pressed.connect(func():
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
)
|
||||
claim_btn.pressed.connect(_on_claim_pressed)
|
||||
|
||||
var time_dict = Time.get_datetime_dict_from_system()
|
||||
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
||||
month_label.text = months[time_dict.month - 1] + " Sign-in"
|
||||
|
||||
func show_panel():
|
||||
show()
|
||||
status_label.text = "Loading rewards..."
|
||||
claim_btn.disabled = true
|
||||
claim_btn.text = "Loading..."
|
||||
_fetch_state()
|
||||
|
||||
func _fetch_state():
|
||||
if not NakamaManager.session:
|
||||
status_label.text = "Must be logged in to claim rewards."
|
||||
return
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(NakamaManager.session, "get_daily_reward_state", "{}")
|
||||
if result.is_exception():
|
||||
status_label.text = "Failed to load: " + result.get_exception().message
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
if json.parse(result.payload) == OK:
|
||||
var data = json.get_data()
|
||||
_month_rewards = data.get("month_rewards", [])
|
||||
var state = data.get("state", {})
|
||||
_claimed_days = state.get("claimed_days", 0)
|
||||
_can_claim = data.get("can_claim_today", false)
|
||||
_today = data.get("today_date", "")
|
||||
|
||||
_update_ui()
|
||||
else:
|
||||
status_label.text = "Error parsing data."
|
||||
|
||||
func _get_reward_display_data(type: String) -> Dictionary:
|
||||
if type == "gold": return {"icon": "💰", "name": "Gold"}
|
||||
elif type == "frag_common": return {"icon": "🧩", "name": "Common Fragment"}
|
||||
elif type == "frag_uncommon": return {"icon": "📦", "name": "Uncommon Fragment"}
|
||||
elif type == "frag_rare": return {"icon": "💎", "name": "Rare Fragment"}
|
||||
return {"icon": "⭐", "name": "Star Currency"}
|
||||
|
||||
func _update_ui():
|
||||
for child in grid_container.get_children():
|
||||
if child != slot_template:
|
||||
child.queue_free()
|
||||
|
||||
var today_reward_amount = 0
|
||||
var today_reward_type = "star"
|
||||
if _claimed_days < _month_rewards.size():
|
||||
var r = _month_rewards[_claimed_days]
|
||||
if typeof(r) == TYPE_DICTIONARY:
|
||||
today_reward_amount = r.get("amount", 0)
|
||||
today_reward_type = r.get("type", "star")
|
||||
else:
|
||||
today_reward_amount = int(r)
|
||||
|
||||
reward_amount_label.text = "x " + str(today_reward_amount)
|
||||
|
||||
var r_data = _get_reward_display_data(today_reward_type)
|
||||
big_icon_label.text = r_data.icon
|
||||
reward_name_label.text = r_data.name
|
||||
|
||||
for i in range(_month_rewards.size()):
|
||||
var reward_raw = _month_rewards[i]
|
||||
var reward_amount = 0
|
||||
var reward_type = "star"
|
||||
if typeof(reward_raw) == TYPE_DICTIONARY:
|
||||
reward_amount = reward_raw.get("amount", 0)
|
||||
reward_type = reward_raw.get("type", "star")
|
||||
else:
|
||||
reward_amount = int(reward_raw)
|
||||
|
||||
var r_slot_data = _get_reward_display_data(reward_type)
|
||||
|
||||
var is_claimed = i < _claimed_days
|
||||
var is_today = i == _claimed_days
|
||||
|
||||
var slot = slot_template.duplicate()
|
||||
slot.visible = true
|
||||
grid_container.add_child(slot)
|
||||
|
||||
var day_lbl = slot.get_node("DayNumber")
|
||||
var amt_lbl = slot.get_node("Amount")
|
||||
var icon_lbl = slot.get_node("IconLabel")
|
||||
var claimed_overlay = slot.get_node("ClaimedOverlay")
|
||||
var today_border = slot.get_node("TodayBorder")
|
||||
|
||||
day_lbl.text = str(i + 1)
|
||||
amt_lbl.text = str(reward_amount)
|
||||
icon_lbl.text = r_slot_data.icon
|
||||
|
||||
claimed_overlay.visible = is_claimed
|
||||
today_border.visible = is_today
|
||||
|
||||
if _can_claim:
|
||||
status_label.text = ""
|
||||
claim_btn.disabled = false
|
||||
claim_btn.text = "Sign In"
|
||||
else:
|
||||
status_label.text = "Come back tomorrow!"
|
||||
claim_btn.disabled = true
|
||||
claim_btn.text = "Signed-in"
|
||||
|
||||
func _on_claim_pressed():
|
||||
if not _can_claim or not NakamaManager.session:
|
||||
return
|
||||
|
||||
claim_btn.disabled = true
|
||||
claim_btn.text = "Claiming..."
|
||||
status_label.text = ""
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(NakamaManager.session, "claim_daily_reward", "{}")
|
||||
if result.is_exception():
|
||||
status_label.text = "Failed to claim: " + result.get_exception().message
|
||||
claim_btn.disabled = false
|
||||
claim_btn.text = "Sign In"
|
||||
return
|
||||
|
||||
# Refresh wallet
|
||||
if UserProfileManager.has_method("_reload_wallet"):
|
||||
UserProfileManager._reload_wallet()
|
||||
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
_fetch_state()
|
||||
@@ -0,0 +1 @@
|
||||
uid://332tmk1jxdw4
|
||||
@@ -51,6 +51,15 @@ func _ready() -> void:
|
||||
_update_tab_visuals()
|
||||
_setup_3d_preview()
|
||||
|
||||
# Listen to profile and stats changes to keep the panel updated
|
||||
UserProfileManager.profile_updated.connect(_on_profile_or_stats_changed)
|
||||
UserProfileManager.stats_updated.connect(_on_profile_or_stats_changed)
|
||||
UserProfileManager.avatar_changed.connect(func(_url): _on_profile_or_stats_changed())
|
||||
|
||||
func _on_profile_or_stats_changed() -> void:
|
||||
if visible:
|
||||
_fetch_leaderboard_data()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Show / Close
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -97,6 +106,7 @@ func _fetch_leaderboard_data() -> void:
|
||||
var native_data = await _fetch_native_leaderboard()
|
||||
|
||||
if native_data.size() > 0:
|
||||
_apply_local_overrides(native_data)
|
||||
leaderboard_data = native_data
|
||||
_calculate_win_rates()
|
||||
status_label.text = ""
|
||||
@@ -156,6 +166,7 @@ func _fetch_via_rpc() -> void:
|
||||
if json.parse(result.payload) == OK:
|
||||
var data = json.get_data()
|
||||
if data.has("leaderboard") and data.leaderboard.size() > 0:
|
||||
_apply_local_overrides(data.leaderboard)
|
||||
leaderboard_data = data.leaderboard
|
||||
_calculate_win_rates()
|
||||
status_label.text = ""
|
||||
@@ -174,6 +185,28 @@ func _calculate_win_rates() -> void:
|
||||
var won = entry.get("games_won", 0)
|
||||
entry["win_rate"] = float(won) / float(played) * 100.0 if played > 0 else 0.0
|
||||
|
||||
func _apply_local_overrides(data: Array) -> void:
|
||||
if not NakamaManager.session:
|
||||
return
|
||||
var my_id = NakamaManager.session.user_id
|
||||
for entry in data:
|
||||
if entry.get("user_id") == my_id:
|
||||
entry["display_name"] = UserProfileManager.get_display_name(entry.get("display_name", "Unknown"))
|
||||
entry["avatar_url"] = UserProfileManager.get_avatar_url()
|
||||
entry["loadout_character"] = UserProfileManager.profile.get("loadout_character", entry.get("loadout_character", "Copper"))
|
||||
|
||||
var local_score = UserProfileManager.stats.get("high_score", 0)
|
||||
if local_score >= entry.get("high_score", 0):
|
||||
entry["high_score"] = local_score
|
||||
|
||||
var local_played = UserProfileManager.stats.get("games_played", 0)
|
||||
if local_played >= entry.get("games_played", 0):
|
||||
entry["games_played"] = local_played
|
||||
|
||||
var local_won = UserProfileManager.stats.get("games_won", 0)
|
||||
if local_won >= entry.get("games_won", 0):
|
||||
entry["games_won"] = local_won
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Sorting / Display
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user