diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index e4bfc06..19e72a9 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -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. diff --git a/assets/data/version.json b/assets/data/version.json index 4c8ba51..4df9075 100644 --- a/assets/data/version.json +++ b/assets/data/version.json @@ -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", @@ -72,4 +99,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/export_presets.cfg b/export_presets.cfg index d3e8c25..86f631c 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -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}\"" - diff --git a/project.godot b/project.godot index 4d78a10..3ff8f88 100644 --- a/project.godot +++ b/project.godot @@ -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" diff --git a/scenes/lobby.gd b/scenes/lobby.gd index e8e34d5..4026195 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -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") diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index a46e8aa..6caba86 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -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] diff --git a/scenes/ui/admin_panel.tscn b/scenes/ui/admin_panel.tscn index abd7f49..d2ffb8f 100644 --- a/scenes/ui/admin_panel.tscn +++ b/scenes/ui/admin_panel.tscn @@ -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 diff --git a/scenes/ui/daily_reward_config_panel.tscn b/scenes/ui/daily_reward_config_panel.tscn new file mode 100644 index 0000000..3c29450 --- /dev/null +++ b/scenes/ui/daily_reward_config_panel.tscn @@ -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" diff --git a/scenes/ui/daily_reward_panel.tscn b/scenes/ui/daily_reward_panel.tscn new file mode 100644 index 0000000..243d622 --- /dev/null +++ b/scenes/ui/daily_reward_panel.tscn @@ -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 diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index 27b8b20..51409a4 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -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 diff --git a/scripts/ui/admin_panel.gd b/scripts/ui/admin_panel.gd index c949fd6..c78820a 100644 --- a/scripts/ui/admin_panel.gd +++ b/scripts/ui/admin_panel.gd @@ -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) diff --git a/scripts/ui/daily_reward_config_panel.gd b/scripts/ui/daily_reward_config_panel.gd new file mode 100644 index 0000000..d0676e1 --- /dev/null +++ b/scripts/ui/daily_reward_config_panel.gd @@ -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!" diff --git a/scripts/ui/daily_reward_config_panel.gd.uid b/scripts/ui/daily_reward_config_panel.gd.uid new file mode 100644 index 0000000..d6303f0 --- /dev/null +++ b/scripts/ui/daily_reward_config_panel.gd.uid @@ -0,0 +1 @@ +uid://v0h4qheiyh1k diff --git a/scripts/ui/daily_reward_panel.gd b/scripts/ui/daily_reward_panel.gd new file mode 100644 index 0000000..7292984 --- /dev/null +++ b/scripts/ui/daily_reward_panel.gd @@ -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() diff --git a/scripts/ui/daily_reward_panel.gd.uid b/scripts/ui/daily_reward_panel.gd.uid new file mode 100644 index 0000000..5dbc459 --- /dev/null +++ b/scripts/ui/daily_reward_panel.gd.uid @@ -0,0 +1 @@ +uid://332tmk1jxdw4 diff --git a/scripts/ui/leaderboard_panel.gd b/scripts/ui/leaderboard_panel.gd index 161fce2..876eee3 100644 --- a/scripts/ui/leaderboard_panel.gd +++ b/scripts/ui/leaderboard_panel.gd @@ -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 # ------------------------------------------------------------------------- diff --git a/server/nakama/tekton_admin.js b/server/nakama/tekton_admin.js index c3c310a..91afaa8 100644 --- a/server/nakama/tekton_admin.js +++ b/server/nakama/tekton_admin.js @@ -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 // =============================================================================