feat: add dailylogin feature

This commit is contained in:
2026-05-01 05:07:54 +08:00
parent 54be7bbb25
commit 21875cdf8a
17 changed files with 1262 additions and 34 deletions
+6
View File
@@ -1,5 +1,11 @@
## [NEXT]
## [2.2.1] — 2026-05-01
- Updated daily login logic to support multiple reward types (star, gold, fragments).
- Fixed leaderboard UI to dynamically update with player changes.
- Added full backend Nakama support for daily login claiming and administration.
- Refactored Admin Panel to include a bordered 30-day grid layout with January-December selection.
## [2.2.0] — 2026-04-30
- Redesigned Social Panel with a 3-tab layout (Search, Requests, Friends) to fix UI overlap issues.
- Fixed an issue where offline friend requests were not being delivered properly.
+28 -1
View File
@@ -1,7 +1,34 @@
{
"latest_version": "2.1.9",
"latest_version": "2.2.1",
"minimum_app_version": "2.1.0",
"releases": [
{
"version": "2.2.1",
"date": "2026-05-01",
"pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck",
"pck_size": 0,
"changelog": [
"Updated daily login logic to support multiple reward types (star, gold, fragments).",
"Fixed leaderboard UI to dynamically update with player changes.",
"Added full backend Nakama support for daily login claiming and administration.",
"Refactored Admin Panel to include a bordered 30-day grid layout with January-December selection."
]
},
{
"version": "2.2.0",
"date": "2026-04-30",
"pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck",
"pck_size": 0,
"changelog": [
"Redesigned Social Panel with a 3-tab layout (Search, Requests, Friends) to fix UI overlap issues.",
"Fixed an issue where offline friend requests were not being delivered properly.",
"Added persistent storage for Direct Messages, loading previous chat history when opening a DM.",
"Improved real-time Nakama socket listening to allow incoming DMs to be received instantly even when the chat tab is closed.",
"Fixed account state pollution where friend data leaked when switching users without restarting the client.",
"Corrected server-side RPCs for Nakama send_friend_request ensuring persistent push notifications.",
"Re-themed Social Panel DM UI to use dark brown fonts for high-contrast readability."
]
},
{
"version": "2.1.9",
"date": "2026-04-29",
+237 -28
View File
@@ -8,7 +8,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton_armageddon_v2.1.8.exe"
export_path="build/tekton_armageddon_v2.2.1.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -70,7 +70,6 @@ ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debu
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
[preset.1]
name="Android"
@@ -81,7 +80,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton-dash-armageddon-v.2.1.8.apk"
export_path="build/tekton-dash-armageddon-v.2.2.1.apk"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -307,7 +306,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton_armageddon_v2.1.8.zip"
export_path="build/tekton_armageddon_v2.2.1.zip"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -323,32 +322,228 @@ script_export_mode=2
[preset.2.options]
export/distribution_type=1
binary_format/architecture="universal"
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=0
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=true
binary_format/architecture="universal"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/identity=""
codesign/identity_type=0
codesign/custom_options=PackedStringArray()
application/modify_resources=false
application/liquid_glass_icon=""
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="2.1"
application/product_version="2.1"
application/company_name="DanchieGo"
application/product_name="Tekton Armageddon"
application/file_description=""
application/bundle_identifier=""
application/signature=""
application/app_category="Games"
application/short_version=""
application/version=""
application/copyright=""
application/trademarks=""
application/copyright_localized={}
application/min_macos_version_x86_64="10.12"
application/min_macos_version_arm64="11.00"
application/export_angle=0
display/high_res=true
shader_baker/enabled=true
application/additional_plist_content=""
xcode/platform_build="14C18"
xcode/sdk_version="13.1"
xcode/sdk_build="22C55"
xcode/sdk_name="macosx13.1"
xcode/xcode_version="1420"
xcode/xcode_build="14C18"
codesign/codesign=1
codesign/installer_identity=""
codesign/apple_team_id=""
codesign/identity=""
codesign/entitlements/custom_file=""
codesign/entitlements/allow_jit_code_execution=false
codesign/entitlements/allow_unsigned_executable_memory=false
codesign/entitlements/allow_dyld_environment_variables=false
codesign/entitlements/disable_library_validation=false
codesign/entitlements/audio_input=false
codesign/entitlements/camera=false
codesign/entitlements/location=false
codesign/entitlements/address_book=false
codesign/entitlements/calendars=false
codesign/entitlements/photos_library=false
codesign/entitlements/apple_events=false
codesign/entitlements/debugging=false
codesign/entitlements/app_sandbox/enabled=false
codesign/entitlements/app_sandbox/network_server=false
codesign/entitlements/app_sandbox/network_client=false
codesign/entitlements/app_sandbox/device_usb=false
codesign/entitlements/app_sandbox/device_bluetooth=false
codesign/entitlements/app_sandbox/files_downloads=0
codesign/entitlements/app_sandbox/files_pictures=0
codesign/entitlements/app_sandbox/files_music=0
codesign/entitlements/app_sandbox/files_movies=0
codesign/entitlements/app_sandbox/files_user_selected=0
codesign/entitlements/app_sandbox/helper_executables=[]
codesign/entitlements/additional=""
codesign/custom_options=PackedStringArray()
notarization/notarization=0
privacy/microphone_usage_description=""
privacy/microphone_usage_description_localized={}
privacy/camera_usage_description=""
privacy/camera_usage_description_localized={}
privacy/location_usage_description=""
privacy/location_usage_description_localized={}
privacy/address_book_usage_description=""
privacy/address_book_usage_description_localized={}
privacy/calendar_usage_description=""
privacy/calendar_usage_description_localized={}
privacy/photos_library_usage_description=""
privacy/photos_library_usage_description_localized={}
privacy/desktop_folder_usage_description=""
privacy/desktop_folder_usage_description_localized={}
privacy/documents_folder_usage_description=""
privacy/documents_folder_usage_description_localized={}
privacy/downloads_folder_usage_description=""
privacy/downloads_folder_usage_description_localized={}
privacy/network_volumes_usage_description=""
privacy/network_volumes_usage_description_localized={}
privacy/removable_volumes_usage_description=""
privacy/removable_volumes_usage_description_localized={}
privacy/tracking_enabled=false
privacy/tracking_domains=PackedStringArray()
privacy/collected_data/name/collected=false
privacy/collected_data/name/linked_to_user=false
privacy/collected_data/name/used_for_tracking=false
privacy/collected_data/name/collection_purposes=0
privacy/collected_data/email_address/collected=false
privacy/collected_data/email_address/linked_to_user=false
privacy/collected_data/email_address/used_for_tracking=false
privacy/collected_data/email_address/collection_purposes=0
privacy/collected_data/phone_number/collected=false
privacy/collected_data/phone_number/linked_to_user=false
privacy/collected_data/phone_number/used_for_tracking=false
privacy/collected_data/phone_number/collection_purposes=0
privacy/collected_data/physical_address/collected=false
privacy/collected_data/physical_address/linked_to_user=false
privacy/collected_data/physical_address/used_for_tracking=false
privacy/collected_data/physical_address/collection_purposes=0
privacy/collected_data/other_contact_info/collected=false
privacy/collected_data/other_contact_info/linked_to_user=false
privacy/collected_data/other_contact_info/used_for_tracking=false
privacy/collected_data/other_contact_info/collection_purposes=0
privacy/collected_data/health/collected=false
privacy/collected_data/health/linked_to_user=false
privacy/collected_data/health/used_for_tracking=false
privacy/collected_data/health/collection_purposes=0
privacy/collected_data/fitness/collected=false
privacy/collected_data/fitness/linked_to_user=false
privacy/collected_data/fitness/used_for_tracking=false
privacy/collected_data/fitness/collection_purposes=0
privacy/collected_data/payment_info/collected=false
privacy/collected_data/payment_info/linked_to_user=false
privacy/collected_data/payment_info/used_for_tracking=false
privacy/collected_data/payment_info/collection_purposes=0
privacy/collected_data/credit_info/collected=false
privacy/collected_data/credit_info/linked_to_user=false
privacy/collected_data/credit_info/used_for_tracking=false
privacy/collected_data/credit_info/collection_purposes=0
privacy/collected_data/other_financial_info/collected=false
privacy/collected_data/other_financial_info/linked_to_user=false
privacy/collected_data/other_financial_info/used_for_tracking=false
privacy/collected_data/other_financial_info/collection_purposes=0
privacy/collected_data/precise_location/collected=false
privacy/collected_data/precise_location/linked_to_user=false
privacy/collected_data/precise_location/used_for_tracking=false
privacy/collected_data/precise_location/collection_purposes=0
privacy/collected_data/coarse_location/collected=false
privacy/collected_data/coarse_location/linked_to_user=false
privacy/collected_data/coarse_location/used_for_tracking=false
privacy/collected_data/coarse_location/collection_purposes=0
privacy/collected_data/sensitive_info/collected=false
privacy/collected_data/sensitive_info/linked_to_user=false
privacy/collected_data/sensitive_info/used_for_tracking=false
privacy/collected_data/sensitive_info/collection_purposes=0
privacy/collected_data/contacts/collected=false
privacy/collected_data/contacts/linked_to_user=false
privacy/collected_data/contacts/used_for_tracking=false
privacy/collected_data/contacts/collection_purposes=0
privacy/collected_data/emails_or_text_messages/collected=false
privacy/collected_data/emails_or_text_messages/linked_to_user=false
privacy/collected_data/emails_or_text_messages/used_for_tracking=false
privacy/collected_data/emails_or_text_messages/collection_purposes=0
privacy/collected_data/photos_or_videos/collected=false
privacy/collected_data/photos_or_videos/linked_to_user=false
privacy/collected_data/photos_or_videos/used_for_tracking=false
privacy/collected_data/photos_or_videos/collection_purposes=0
privacy/collected_data/audio_data/collected=false
privacy/collected_data/audio_data/linked_to_user=false
privacy/collected_data/audio_data/used_for_tracking=false
privacy/collected_data/audio_data/collection_purposes=0
privacy/collected_data/gameplay_content/collected=false
privacy/collected_data/gameplay_content/linked_to_user=false
privacy/collected_data/gameplay_content/used_for_tracking=false
privacy/collected_data/gameplay_content/collection_purposes=0
privacy/collected_data/customer_support/collected=false
privacy/collected_data/customer_support/linked_to_user=false
privacy/collected_data/customer_support/used_for_tracking=false
privacy/collected_data/customer_support/collection_purposes=0
privacy/collected_data/other_user_content/collected=false
privacy/collected_data/other_user_content/linked_to_user=false
privacy/collected_data/other_user_content/used_for_tracking=false
privacy/collected_data/other_user_content/collection_purposes=0
privacy/collected_data/browsing_history/collected=false
privacy/collected_data/browsing_history/linked_to_user=false
privacy/collected_data/browsing_history/used_for_tracking=false
privacy/collected_data/browsing_history/collection_purposes=0
privacy/collected_data/search_history/collected=false
privacy/collected_data/search_history/linked_to_user=false
privacy/collected_data/search_history/used_for_tracking=false
privacy/collected_data/search_history/collection_purposes=0
privacy/collected_data/user_id/collected=false
privacy/collected_data/user_id/linked_to_user=false
privacy/collected_data/user_id/used_for_tracking=false
privacy/collected_data/user_id/collection_purposes=0
privacy/collected_data/device_id/collected=false
privacy/collected_data/device_id/linked_to_user=false
privacy/collected_data/device_id/used_for_tracking=false
privacy/collected_data/device_id/collection_purposes=0
privacy/collected_data/purchase_history/collected=false
privacy/collected_data/purchase_history/linked_to_user=false
privacy/collected_data/purchase_history/used_for_tracking=false
privacy/collected_data/purchase_history/collection_purposes=0
privacy/collected_data/product_interaction/collected=false
privacy/collected_data/product_interaction/linked_to_user=false
privacy/collected_data/product_interaction/used_for_tracking=false
privacy/collected_data/product_interaction/collection_purposes=0
privacy/collected_data/advertising_data/collected=false
privacy/collected_data/advertising_data/linked_to_user=false
privacy/collected_data/advertising_data/used_for_tracking=false
privacy/collected_data/advertising_data/collection_purposes=0
privacy/collected_data/other_usage_data/collected=false
privacy/collected_data/other_usage_data/linked_to_user=false
privacy/collected_data/other_usage_data/used_for_tracking=false
privacy/collected_data/other_usage_data/collection_purposes=0
privacy/collected_data/crash_data/collected=false
privacy/collected_data/crash_data/linked_to_user=false
privacy/collected_data/crash_data/used_for_tracking=false
privacy/collected_data/crash_data/collection_purposes=0
privacy/collected_data/performance_data/collected=false
privacy/collected_data/performance_data/linked_to_user=false
privacy/collected_data/performance_data/used_for_tracking=false
privacy/collected_data/performance_data/collection_purposes=0
privacy/collected_data/other_diagnostic_data/collected=false
privacy/collected_data/other_diagnostic_data/linked_to_user=false
privacy/collected_data/other_diagnostic_data/used_for_tracking=false
privacy/collected_data/other_diagnostic_data/collection_purposes=0
privacy/collected_data/environment_scanning/collected=false
privacy/collected_data/environment_scanning/linked_to_user=false
privacy/collected_data/environment_scanning/used_for_tracking=false
privacy/collected_data/environment_scanning/collection_purposes=0
privacy/collected_data/hands/collected=false
privacy/collected_data/hands/linked_to_user=false
privacy/collected_data/hands/used_for_tracking=false
privacy/collected_data/hands/collection_purposes=0
privacy/collected_data/head/collected=false
privacy/collected_data/head/linked_to_user=false
privacy/collected_data/head/used_for_tracking=false
privacy/collected_data/head/collection_purposes=0
privacy/collected_data/other_data_types/collected=false
privacy/collected_data/other_data_types/linked_to_user=false
privacy/collected_data/other_data_types/used_for_tracking=false
privacy/collected_data/other_data_types/collection_purposes=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
@@ -360,19 +555,34 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
ssh_remote_deploy/cleanup_script="#!/bin/bash
kill $(pgrep -x \"{exe_name}\")
rm -rf \"{temp_dir}\""
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/identity_type=0
application/modify_resources=false
application/console_wrapper_icon=""
application/file_version="2.1"
application/product_version="2.1"
application/company_name="DanchieGo"
application/product_name="Tekton Armageddon"
application/file_description=""
application/trademarks=""
[preset.3]
name="Linux/X11"
platform="Linux/X11"
platform="Linux"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton_armageddon_v2.1.8.x86_64"
export_path="build/tekton_armageddon_v2.2.1.x86_64"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -407,4 +617,3 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
ssh_remote_deploy/cleanup_script="#!/bin/bash
kill $(pgrep -x \"{exe_name}\")
rm -rf \"{temp_dir}\""
+1 -1
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Tekton Dash Armageddon"
config/version="2.1.8"
config/version="2.2.1"
run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus")
config/icon="res://icon.svg"
+23
View File
@@ -30,6 +30,7 @@ extends Control
@onready var shop_btn = %CartBtn
@onready var top_right_profile_btn = %ProfileBtn
@onready var banner1_btn = %Banner1
@onready var ticket_btn = $MainMenuPanel/MainMargin/MainHBox/RightCol/TopRightPanel/TicketBtn
# UI References - Room List
@onready var room_list_panel = $RoomListPanel
@@ -118,6 +119,7 @@ var current_match_id: String = ""
var leaderboard_panel_instance: Control
var shop_panel_instance: Control
var daily_reward_panel_instance: Control
# Bot name tracking keyed by slot index to avoid re-generating on each update
var _bot_names: Dictionary = {}
@@ -180,6 +182,8 @@ func _ready():
if leaderboard_btn:
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
if ticket_btn:
ticket_btn.pressed.connect(_on_ticket_pressed)
if quit_btn:
quit_btn.pressed.connect(_on_quit_pressed)
@@ -862,6 +866,25 @@ func _on_leaderboard_pressed() -> void:
main_menu_panel.hide()
leaderboard_panel_instance.show_panel()
func _on_ticket_pressed() -> void:
if not NakamaManager.session:
connection_status.text = "Must be logged in"
return
if not daily_reward_panel_instance:
var scene = load("res://scenes/ui/daily_reward_panel.tscn")
if scene:
daily_reward_panel_instance = scene.instantiate()
daily_reward_panel_instance.closed.connect(func():
if main_menu_panel: main_menu_panel.show()
)
add_child(daily_reward_panel_instance)
if daily_reward_panel_instance:
if main_menu_panel:
main_menu_panel.hide()
daily_reward_panel_instance.show_panel()
func _go_to_login() -> void:
if get_tree():
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
-1
View File
@@ -395,7 +395,6 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(44, 44)
layout_mode = 2
theme_override_fonts/font = ExtResource("5_pc087")
disabled = true
text = "BATTLE PASS"
[node name="SocialBtn" type="Button" parent="MainMenuPanel/MainMargin/MainHBox/RightCol/TopRightPanel" unique_id=82719328]
+100
View File
@@ -162,6 +162,106 @@ custom_minimum_size = Vector2(120, 36)
layout_mode = 2
text = "Reset All Scores"
[node name="Daily Rewards" type="VBoxContainer" parent="Margin/VBox/Tabs"]
visible = false
layout_mode = 2
theme_override_constants/separation = 8
metadata/_tab_index = 2
[node name="MonthHBox" type="HBoxContainer" parent="Margin/VBox/Tabs/Daily Rewards"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Daily Rewards/MonthHBox"]
layout_mode = 2
text = "Target Month:"
[node name="MonthOptionBtn" type="OptionButton" parent="Margin/VBox/Tabs/Daily Rewards/MonthHBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
[node name="DaysScroll" type="ScrollContainer" parent="Margin/VBox/Tabs/Daily Rewards"]
layout_mode = 2
size_flags_vertical = 3
[node name="DaysGrid" type="GridContainer" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
columns = 6
[node name="DayConfigTemplate" type="PanelContainer" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
[node name="ColorRect" type="ColorRect" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate"]
layout_mode = 2
color = Color(0.12, 0.12, 0.12, 1)
[node name="Border" type="ReferenceRect" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate"]
layout_mode = 2
border_color = Color(0.25, 0.25, 0.25, 1)
border_width = 1.0
editor_only = false
[node name="Margin" type="MarginContainer" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate"]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="VBox" type="VBoxContainer" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate/Margin"]
layout_mode = 2
[node name="DayLabel" type="Label" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate/Margin/VBox"]
layout_mode = 2
text = "Day 1"
horizontal_alignment = 1
[node name="TypeOptionBtn" type="OptionButton" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate/Margin/VBox"]
layout_mode = 2
alignment = 1
item_count = 5
popup/item_0/text = "star"
popup/item_0/id = 0
popup/item_1/text = "gold"
popup/item_1/id = 1
popup/item_2/text = "frag_common"
popup/item_2/id = 2
popup/item_3/text = "frag_uncommon"
popup/item_3/id = 3
popup/item_4/text = "frag_rare"
popup/item_4/id = 4
[node name="AmountSpinBox" type="SpinBox" parent="Margin/VBox/Tabs/Daily Rewards/DaysScroll/DaysGrid/DayConfigTemplate/Margin/VBox"]
layout_mode = 2
max_value = 10000.0
alignment = 1
[node name="DRActionBar" type="HBoxContainer" parent="Margin/VBox/Tabs/Daily Rewards"]
layout_mode = 2
theme_override_constants/separation = 8
alignment = 2
[node name="LoadDRConfigBtn" type="Button" parent="Margin/VBox/Tabs/Daily Rewards/DRActionBar"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 36)
layout_mode = 2
text = "Reload Config"
[node name="SaveDRConfigBtn" type="Button" parent="Margin/VBox/Tabs/Daily Rewards/DRActionBar"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 36)
layout_mode = 2
text = "Save Config"
[node name="StatusLabel" type="Label" parent="Margin/VBox"]
unique_name_in_owner = true
layout_mode = 2
+82
View File
@@ -0,0 +1,82 @@
[gd_scene format=3 uid="uid://sdnq3e6jnnby"]
[ext_resource type="Script" path="res://scripts/ui/daily_reward_config_panel.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2_theme"]
[node name="DailyRewardConfigPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.85)
[node name="Panel" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -400.0
offset_top = -300.0
offset_right = 400.0
offset_bottom = 300.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = -20.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 15
[node name="Label" type="Label" parent="Panel/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Daily Reward Config (Admin)"
horizontal_alignment = 1
[node name="TextEdit" type="TextEdit" parent="Panel/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_font_sizes/font_size = 14
[node name="StatusLabel" type="Label" parent="Panel/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
layout_mode = 2
alignment = 1
theme_override_constants/separation = 20
[node name="CloseBtn" type="Button" parent="Panel/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
text = "Close"
[node name="SaveBtn" type="Button" parent="Panel/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
text = "Save Config"
+273
View File
@@ -0,0 +1,273 @@
[gd_scene load_steps=5 format=3 uid="uid://dailyreward1234"]
[ext_resource type="Script" path="res://scripts/ui/daily_reward_panel.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg"]
bg_color = Color(0.1, 0.1, 0.1, 0.95)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yellow"]
bg_color = Color(0.95, 0.76, 0.2, 1)
[node name="DailyRewardPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.7)
[node name="MainWindow" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -450.0
offset_top = -280.0
offset_right = 450.0
offset_bottom = 280.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_bg")
[node name="HBox" type="HBoxContainer" parent="MainWindow"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="LeftCol" type="VBoxContainer" parent="MainWindow/HBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
[node name="HeaderMargin" type="MarginContainer" parent="MainWindow/HBox/LeftCol"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 10
[node name="HeaderHBox" type="HBoxContainer" parent="MainWindow/HBox/LeftCol/HeaderMargin"]
layout_mode = 2
theme_override_constants/separation = 15
[node name="MonthLabel" type="Label" parent="MainWindow/HBox/LeftCol/HeaderMargin/HeaderHBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 28
text = "Month Sign-in"
[node name="FlavorLabel" type="Label" parent="MainWindow/HBox/LeftCol/HeaderMargin/HeaderHBox"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
theme_override_font_sizes/font_size = 12
text = "A day's plan starts in the morning.
Sign in daily for rewards."
vertical_alignment = 1
autowrap_mode = 2
[node name="CloseBtn" type="Button" parent="MainWindow/HBox/LeftCol/HeaderMargin/HeaderHBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
text = "X"
[node name="ScrollContainer" type="ScrollContainer" parent="MainWindow/HBox/LeftCol"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="MarginContainer" type="MarginContainer" parent="MainWindow/HBox/LeftCol/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="GridContainer" type="GridContainer" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
columns = 7
[node name="RewardSlotTemplate" type="Control" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(75, 90)
layout_mode = 2
size_flags_horizontal = 3
[node name="Bg" type="ColorRect" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.15, 0.15, 0.15, 0.8)
[node name="DayNumber" type="Label" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
layout_mode = 1
offset_left = 5.0
offset_top = 0.0
theme_override_font_sizes/font_size = 40
theme_override_colors/font_color = Color(1, 1, 1, 0.15)
text = "1"
[node name="IconLabel" type="Label" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -20.0
offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 32
text = "⭐"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Amount" type="Label" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -40.0
offset_top = -23.0
offset_right = -5.0
grow_horizontal = 0
grow_vertical = 0
theme_override_font_sizes/font_size = 14
text = "10"
horizontal_alignment = 2
[node name="ClaimedOverlay" type="ColorRect" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
[node name="Checkmark" type="Label" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate/ClaimedOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_font_sizes/font_size = 60
theme_override_colors/font_color = Color(0.9, 0.9, 0.9, 1)
text = "✓"
horizontal_alignment = 1
vertical_alignment = 1
[node name="TodayBorder" type="ReferenceRect" parent="MainWindow/HBox/LeftCol/ScrollContainer/MarginContainer/GridContainer/RewardSlotTemplate"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
border_color = Color(1, 0.84, 0, 1)
border_width = 3.0
editor_only = false
[node name="RightCol" type="VBoxContainer" parent="MainWindow/HBox"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="TopTime" type="ColorRect" parent="MainWindow/HBox/RightCol"]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
color = Color(0.12, 0.12, 0.12, 1)
[node name="TimeLabel" type="Label" parent="MainWindow/HBox/RightCol/TopTime"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
text = "Daily Supply"
horizontal_alignment = 1
vertical_alignment = 1
[node name="MiddleDetails" type="ColorRect" parent="MainWindow/HBox/RightCol"]
layout_mode = 2
size_flags_vertical = 3
color = Color(0.18, 0.18, 0.18, 1)
[node name="VBox" type="VBoxContainer" parent="MainWindow/HBox/RightCol/MiddleDetails"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
alignment = 1
[node name="BigIcon" type="Label" parent="MainWindow/HBox/RightCol/MiddleDetails/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 100
text = "⭐"
horizontal_alignment = 1
[node name="RewardName" type="Label" parent="MainWindow/HBox/RightCol/MiddleDetails/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 20
theme_override_colors/font_color = Color(0.4, 0.8, 1, 1)
text = "Star Currency"
horizontal_alignment = 1
[node name="RewardAmount" type="Label" parent="MainWindow/HBox/RightCol/MiddleDetails/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 28
text = "x 0"
horizontal_alignment = 1
[node name="StatusLabel" type="Label" parent="MainWindow/HBox/RightCol/MiddleDetails/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.8, 0.4, 0.4, 1)
text = ""
horizontal_alignment = 1
[node name="BottomAction" type="PanelContainer" parent="MainWindow/HBox/RightCol"]
custom_minimum_size = Vector2(0, 100)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_yellow")
[node name="ClaimBtn" type="Button" parent="MainWindow/HBox/RightCol/BottomAction"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.1, 0.1, 0.1, 1)
theme_override_colors/font_pressed_color = Color(0, 0, 0, 1)
theme_override_colors/font_hover_color = Color(0.2, 0.2, 0.2, 1)
theme_override_colors/font_disabled_color = Color(0.3, 0.3, 0.3, 1)
theme_override_font_sizes/font_size = 28
text = "Sign In"
flat = true
+2
View File
@@ -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
View File
@@ -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)
@@ -135,6 +147,11 @@ func _connect_signals() -> void:
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)
+51
View File
@@ -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
+151
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://332tmk1jxdw4
+33
View File
@@ -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
# -------------------------------------------------------------------------
+155
View File
@@ -43,6 +43,12 @@ function InitModule(ctx, logger, nk, initializer) {
initializer.registerRpc("send_lobby_invite", rpcSendLobbyInvite);
initializer.registerRpc("send_friend_request", rpcSendFriendRequest);
// Daily Rewards RPCs
initializer.registerRpc("claim_daily_reward", rpcClaimDailyReward);
initializer.registerRpc("get_daily_reward_state", rpcGetDailyRewardState);
initializer.registerRpc("set_daily_reward_config", rpcSetDailyRewardConfig);
initializer.registerRpc("get_daily_reward_config_admin", rpcGetDailyRewardConfigAdmin);
// Steam auth hooks
initializer.registerAfterAuthenticateSteam(afterAuthenticateSteam);
@@ -593,6 +599,155 @@ function rpcPurchaseItem(ctx, logger, nk, payload) {
}
}
// =============================================================================
// Daily Rewards RPCs
// =============================================================================
function rpcClaimDailyReward(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
var now = new Date();
var currentMonth = now.toISOString().substring(5, 7); // e.g. "05"
var todayStr = now.toISOString().substring(0, 10);
var stateObjs = nk.storageRead([{ collection: "daily_rewards", key: "state", userId: ctx.userId }]);
var state = { claimed_days: 0, last_claim_date: "", month: "" };
if (stateObjs && stateObjs.length > 0) {
state = stateObjs[0].value;
}
if (state.month !== currentMonth) {
state.claimed_days = 0;
state.month = currentMonth;
}
if (state.last_claim_date === todayStr) {
throw new Error("Already claimed today");
}
var configObjs = nk.storageRead([{ collection: "config", key: "daily_rewards", userId: "00000000-0000-0000-0000-000000000000" }]);
var config = {};
if (configObjs && configObjs.length > 0) {
config = configObjs[0].value;
}
var monthRewards = config[currentMonth];
if (!monthRewards || monthRewards.length === 0) {
monthRewards = [];
for (var i = 0; i < 31; i++) {
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
}
}
var dayIndex = state.claimed_days;
if (dayIndex >= monthRewards.length) {
throw new Error("Already claimed all rewards for this month");
}
var rewardData = monthRewards[dayIndex];
if (typeof rewardData === "number") {
rewardData = { type: "star", amount: rewardData };
}
var rewardType = rewardData.type || "star";
var rewardAmount = rewardData.amount || 0;
if (rewardType === "star" || rewardType === "gold") {
var changes = {};
changes[rewardType] = rewardAmount;
nk.walletUpdate(ctx.userId, changes, {}, true);
} else if (rewardType.startsWith("frag_")) {
var invObjs = nk.storageRead([{ collection: "inventory", key: "fragments", userId: ctx.userId }]);
var frags = {};
if (invObjs && invObjs.length > 0) {
frags = invObjs[0].value;
}
frags[rewardType] = (frags[rewardType] || 0) + rewardAmount;
nk.storageWrite([{
collection: "inventory",
key: "fragments",
userId: ctx.userId,
value: frags,
permissionRead: 1,
permissionWrite: 0
}]);
}
state.claimed_days++;
state.last_claim_date = todayStr;
nk.storageWrite([{
collection: "daily_rewards",
key: "state",
userId: ctx.userId,
value: state,
permissionRead: 1,
permissionWrite: 0
}]);
return JSON.stringify({ success: true, reward_type: rewardType, reward_amount: rewardAmount, day: state.claimed_days });
}
function rpcGetDailyRewardState(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
var now = new Date();
var currentMonth = now.toISOString().substring(5, 7); // e.g. "05"
var todayStr = now.toISOString().substring(0, 10);
var stateObjs = nk.storageRead([{ collection: "daily_rewards", key: "state", userId: ctx.userId }]);
var state = { claimed_days: 0, last_claim_date: "", month: "" };
if (stateObjs && stateObjs.length > 0) {
state = stateObjs[0].value;
}
if (state.month !== currentMonth) {
state.claimed_days = 0;
state.month = currentMonth;
}
var configObjs = nk.storageRead([{ collection: "config", key: "daily_rewards", userId: "00000000-0000-0000-0000-000000000000" }]);
var config = {};
if (configObjs && configObjs.length > 0) {
config = configObjs[0].value;
}
var monthRewards = config[currentMonth];
if (!monthRewards || monthRewards.length === 0) {
monthRewards = [];
for (var i = 0; i < 31; i++) {
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
}
}
return JSON.stringify({
state: state,
month_rewards: monthRewards,
can_claim_today: state.last_claim_date !== todayStr && state.claimed_days < monthRewards.length,
today_date: todayStr
});
}
function rpcSetDailyRewardConfig(ctx, logger, nk, payload) {
requireAdmin(ctx, nk);
var request = JSON.parse(payload || "{}");
nk.storageWrite([{
collection: "config",
key: "daily_rewards",
userId: "00000000-0000-0000-0000-000000000000",
value: request.config,
permissionRead: 2,
permissionWrite: 0
}]);
return JSON.stringify({ success: true });
}
function rpcGetDailyRewardConfigAdmin(ctx, logger, nk, payload) {
requireAdmin(ctx, nk);
var configObjs = nk.storageRead([{ collection: "config", key: "daily_rewards", userId: "00000000-0000-0000-0000-000000000000" }]);
var config = {};
if (configObjs && configObjs.length > 0) {
config = configObjs[0].value;
}
return JSON.stringify({ config: config });
}
// =============================================================================
// User Profile RPCs
// =============================================================================