forked from xiangwang25/whale-town-front-v2
Compare commits
2 Commits
dev
...
feature-so
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3accf6b71 | ||
|
|
9a3248abd6 |
@@ -107,6 +107,8 @@ const HUD_FRIEND_LIST_TOGGLE = "hud_friend_list_toggle"
|
|||||||
const HUD_SETTINGS_REQUESTED = "hud_settings_requested"
|
const HUD_SETTINGS_REQUESTED = "hud_settings_requested"
|
||||||
const HUD_BACKPACK_TOGGLE = "hud_backpack_toggle"
|
const HUD_BACKPACK_TOGGLE = "hud_backpack_toggle"
|
||||||
const HUD_MAP_TOGGLE = "hud_map_toggle"
|
const HUD_MAP_TOGGLE = "hud_map_toggle"
|
||||||
|
const HUD_TASK_BOOK_REQUESTED = "hud_task_book_requested"
|
||||||
|
const HUD_ADMIN_TEST_LAB_REQUESTED = "hud_admin_test_lab_requested"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 商城事件
|
# 商城事件
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func patch_json(endpoint: String, payload: Dictionary, callback: Callable, authe
|
|||||||
func put_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
func put_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
||||||
request_json(endpoint, payload, callback, HTTPClient.METHOD_PUT, authenticated)
|
request_json(endpoint, payload, callback, HTTPClient.METHOD_PUT, authenticated)
|
||||||
|
|
||||||
|
func delete_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
||||||
|
request_json(endpoint, payload, callback, HTTPClient.METHOD_DELETE, authenticated)
|
||||||
|
|
||||||
func request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int = HTTPClient.METHOD_GET, authenticated: bool = true) -> void:
|
func request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int = HTTPClient.METHOD_GET, authenticated: bool = true) -> void:
|
||||||
var request := HTTPRequest.new()
|
var request := HTTPRequest.new()
|
||||||
request.timeout = REQUEST_TIMEOUT
|
request.timeout = REQUEST_TIMEOUT
|
||||||
|
|||||||
128
_Core/managers/TaskManager.gd
Normal file
128
_Core/managers/TaskManager.gd
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# 玩家任务书状态与后端任务接口的唯一入口。
|
||||||
|
signal board_changed(board: Dictionary)
|
||||||
|
signal board_failed(message: String)
|
||||||
|
signal reward_claimed(task_id: String, wallet: Dictionary)
|
||||||
|
|
||||||
|
var _board: Dictionary = {}
|
||||||
|
var _loading_board: bool = false
|
||||||
|
var _account_generation: int = -1
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
if auth_manager != null and auth_manager.has_signal("auth_state_changed"):
|
||||||
|
var callback := Callable(self, "_on_auth_state_changed")
|
||||||
|
if not auth_manager.is_connected("auth_state_changed", callback):
|
||||||
|
auth_manager.connect("auth_state_changed", callback)
|
||||||
|
call_deferred("_refresh_after_ready")
|
||||||
|
|
||||||
|
func get_board() -> Dictionary:
|
||||||
|
return _board.duplicate(true)
|
||||||
|
|
||||||
|
func is_loaded() -> bool:
|
||||||
|
return not _board.is_empty()
|
||||||
|
|
||||||
|
func refresh_board() -> void:
|
||||||
|
if not _is_authenticated() or _loading_board:
|
||||||
|
return
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
board_failed.emit("任务服务不可用")
|
||||||
|
return
|
||||||
|
_loading_board = true
|
||||||
|
_account_generation = _current_account_generation()
|
||||||
|
api.call("get_json", "/tasks/board", Callable(self, "_on_board_response").bind(_account_generation), true)
|
||||||
|
|
||||||
|
func report_activity(activity: String, target_id: String = "") -> void:
|
||||||
|
if not _is_authenticated():
|
||||||
|
return
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
return
|
||||||
|
var payload := {"activity": activity}
|
||||||
|
if not target_id.strip_edges().is_empty():
|
||||||
|
payload["target_id"] = target_id.strip_edges()
|
||||||
|
var request_generation := _current_account_generation()
|
||||||
|
api.call("post_json", "/tasks/activities", payload, Callable(self, "_on_activity_response").bind(request_generation), true)
|
||||||
|
|
||||||
|
func claim_task(task_id: String) -> void:
|
||||||
|
if task_id.strip_edges().is_empty() or not _is_authenticated():
|
||||||
|
return
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
board_failed.emit("任务服务不可用")
|
||||||
|
return
|
||||||
|
var request_generation := _current_account_generation()
|
||||||
|
api.call("post_json", "/tasks/%s/claim" % task_id.uri_encode(), {}, Callable(self, "_on_claim_response").bind(task_id, request_generation), true)
|
||||||
|
|
||||||
|
func _refresh_after_ready() -> void:
|
||||||
|
if _is_authenticated():
|
||||||
|
refresh_board()
|
||||||
|
|
||||||
|
func _on_auth_state_changed(is_authenticated: bool, _user: Dictionary) -> void:
|
||||||
|
if not is_authenticated:
|
||||||
|
_board.clear()
|
||||||
|
_loading_board = false
|
||||||
|
_account_generation = -1
|
||||||
|
board_changed.emit({})
|
||||||
|
return
|
||||||
|
refresh_board()
|
||||||
|
|
||||||
|
func _on_board_response(success: bool, response: Dictionary, error_info: Dictionary, request_generation: int) -> void:
|
||||||
|
if request_generation != _current_account_generation():
|
||||||
|
return
|
||||||
|
_loading_board = false
|
||||||
|
if not success:
|
||||||
|
board_failed.emit(str(error_info.get("message", "任务书读取失败")))
|
||||||
|
return
|
||||||
|
_apply_board_response(response)
|
||||||
|
|
||||||
|
func _on_activity_response(success: bool, response: Dictionary, _error_info: Dictionary, request_generation: int) -> void:
|
||||||
|
if not success or request_generation != _current_account_generation():
|
||||||
|
return
|
||||||
|
_apply_board_response(response)
|
||||||
|
|
||||||
|
func _on_claim_response(success: bool, response: Dictionary, error_info: Dictionary, task_id: String, request_generation: int) -> void:
|
||||||
|
if request_generation != _current_account_generation():
|
||||||
|
return
|
||||||
|
if not success:
|
||||||
|
board_failed.emit(str(error_info.get("message", "奖励领取失败")))
|
||||||
|
return
|
||||||
|
var data_variant: Variant = response.get("data", {})
|
||||||
|
if not (data_variant is Dictionary):
|
||||||
|
board_failed.emit("任务奖励响应格式错误")
|
||||||
|
return
|
||||||
|
var data: Dictionary = data_variant as Dictionary
|
||||||
|
var board_variant: Variant = data.get("board", {})
|
||||||
|
if board_variant is Dictionary:
|
||||||
|
_apply_board(board_variant as Dictionary)
|
||||||
|
var wallet_variant: Variant = data.get("wallet", {})
|
||||||
|
if wallet_variant is Dictionary:
|
||||||
|
var wallet: Dictionary = wallet_variant as Dictionary
|
||||||
|
var player_state := get_node_or_null("/root/PlayerStateManager")
|
||||||
|
if player_state != null and player_state.has_method("apply_wallet"):
|
||||||
|
player_state.call("apply_wallet", wallet)
|
||||||
|
reward_claimed.emit(task_id, wallet.duplicate(true))
|
||||||
|
|
||||||
|
func _apply_board_response(response: Dictionary) -> void:
|
||||||
|
var data_variant: Variant = response.get("data", {})
|
||||||
|
if data_variant is Dictionary:
|
||||||
|
_apply_board(data_variant as Dictionary)
|
||||||
|
else:
|
||||||
|
board_failed.emit("任务书响应格式错误")
|
||||||
|
|
||||||
|
func _apply_board(board: Dictionary) -> void:
|
||||||
|
_board = board.duplicate(true)
|
||||||
|
board_changed.emit(get_board())
|
||||||
|
|
||||||
|
func _api_client() -> Node:
|
||||||
|
return get_node_or_null("/root/ApiClient")
|
||||||
|
|
||||||
|
func _is_authenticated() -> bool:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
return auth_manager != null and auth_manager.has_method("is_authenticated") and bool(auth_manager.call("is_authenticated"))
|
||||||
|
|
||||||
|
func _current_account_generation() -> int:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
return int(auth_manager.call("get_account_generation")) if auth_manager != null and auth_manager.has_method("get_account_generation") else 0
|
||||||
1
_Core/managers/TaskManager.gd.uid
Normal file
1
_Core/managers/TaskManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://byurfu7d8qyfb
|
||||||
135
_Core/managers/TestLabManager.gd
Normal file
135
_Core/managers/TestLabManager.gd
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# 游戏内管理员测试实验室的唯一 API 入口。
|
||||||
|
# 客户端只把 role=9 用于显示入口;实际授权仍由后端 TestLabGuard 完成。
|
||||||
|
|
||||||
|
signal status_changed(status: Dictionary)
|
||||||
|
signal status_failed(message: String)
|
||||||
|
signal operation_completed(message: String)
|
||||||
|
|
||||||
|
var _status: Dictionary = {}
|
||||||
|
var _loading_status: bool = false
|
||||||
|
var _account_generation: int = -1
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
if auth_manager != null and auth_manager.has_signal("auth_state_changed"):
|
||||||
|
var callback := Callable(self, "_on_auth_state_changed")
|
||||||
|
if not auth_manager.is_connected("auth_state_changed", callback):
|
||||||
|
auth_manager.connect("auth_state_changed", callback)
|
||||||
|
call_deferred("_refresh_after_ready")
|
||||||
|
|
||||||
|
func get_status() -> Dictionary:
|
||||||
|
return _status.duplicate(true)
|
||||||
|
|
||||||
|
func is_loaded() -> bool:
|
||||||
|
return not _status.is_empty()
|
||||||
|
|
||||||
|
func is_admin() -> bool:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
if auth_manager == null or not auth_manager.has_method("is_authenticated") or not bool(auth_manager.call("is_authenticated")):
|
||||||
|
return false
|
||||||
|
if not auth_manager.has_method("get_current_user"):
|
||||||
|
return false
|
||||||
|
var user_variant: Variant = auth_manager.call("get_current_user")
|
||||||
|
if not (user_variant is Dictionary):
|
||||||
|
return false
|
||||||
|
var user: Dictionary = user_variant as Dictionary
|
||||||
|
return int(user.get("role", 0)) == 9
|
||||||
|
|
||||||
|
func refresh_status() -> void:
|
||||||
|
if not is_admin() or _loading_status:
|
||||||
|
return
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
status_failed.emit("测试实验室服务不可用")
|
||||||
|
return
|
||||||
|
_loading_status = true
|
||||||
|
_account_generation = _current_account_generation()
|
||||||
|
api.call("get_json", "/admin/test-lab/status", Callable(self, "_on_status_response").bind(_account_generation), true)
|
||||||
|
|
||||||
|
func create_actor(nickname: String, map_id: String, x: int, y: int, skin_id: String) -> void:
|
||||||
|
_mutate("post_json", "/admin/test-lab/actors", {
|
||||||
|
"nickname": nickname.strip_edges(), "mapId": map_id, "x": x, "y": y, "skinId": skin_id
|
||||||
|
}, "测试假人已上线")
|
||||||
|
|
||||||
|
func update_actor(user_id: String, online: bool, map_id: String, x: int, y: int, skin_id: String) -> void:
|
||||||
|
_mutate("patch_json", "/admin/test-lab/actors/%s" % user_id.uri_encode(), {
|
||||||
|
"online": online, "mapId": map_id, "x": x, "y": y, "skinId": skin_id
|
||||||
|
}, "假人状态已同步")
|
||||||
|
|
||||||
|
func set_room_policy(user_id: String, policy: String) -> void:
|
||||||
|
_mutate("patch_json", "/admin/test-lab/actors/%s/room-policy" % user_id.uri_encode(), {
|
||||||
|
"roomVisitPolicy": policy
|
||||||
|
}, "房间访问策略已更新")
|
||||||
|
|
||||||
|
func send_message(user_id: String, scope: String, content: String, target_user_id: String = "") -> void:
|
||||||
|
var payload := {"scope": scope, "content": content.strip_edges()}
|
||||||
|
if scope == "private":
|
||||||
|
payload["targetUserId"] = target_user_id
|
||||||
|
_mutate("post_json", "/admin/test-lab/actors/%s/messages" % user_id.uri_encode(), payload, "测试消息已发送")
|
||||||
|
|
||||||
|
func social_action(user_id: String, action: String, target_user_id: String) -> void:
|
||||||
|
_mutate("post_json", "/admin/test-lab/actors/%s/social" % user_id.uri_encode(), {
|
||||||
|
"action": action, "targetUserId": target_user_id
|
||||||
|
}, "社交操作已执行")
|
||||||
|
|
||||||
|
func delete_actor(user_id: String) -> void:
|
||||||
|
_mutate("delete_json", "/admin/test-lab/actors/%s" % user_id.uri_encode(), {}, "测试假人已删除")
|
||||||
|
|
||||||
|
func clear_all() -> void:
|
||||||
|
_mutate("delete_json", "/admin/test-lab/actors", {}, "测试实验室已清空")
|
||||||
|
|
||||||
|
func _mutate(method_name: String, endpoint: String, payload: Dictionary, success_message: String) -> void:
|
||||||
|
if not is_admin():
|
||||||
|
status_failed.emit("仅管理员可使用测试实验室")
|
||||||
|
return
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
status_failed.emit("测试实验室服务不可用")
|
||||||
|
return
|
||||||
|
var request_generation := _current_account_generation()
|
||||||
|
api.call(method_name, endpoint, payload, Callable(self, "_on_mutation_response").bind(success_message, request_generation), true)
|
||||||
|
|
||||||
|
func _refresh_after_ready() -> void:
|
||||||
|
if is_admin():
|
||||||
|
refresh_status()
|
||||||
|
|
||||||
|
func _on_auth_state_changed(is_authenticated: bool, _user: Dictionary) -> void:
|
||||||
|
if not is_authenticated or not is_admin():
|
||||||
|
_status.clear()
|
||||||
|
_loading_status = false
|
||||||
|
_account_generation = -1
|
||||||
|
status_changed.emit({})
|
||||||
|
return
|
||||||
|
refresh_status()
|
||||||
|
|
||||||
|
func _on_status_response(success: bool, response: Dictionary, error_info: Dictionary, request_generation: int) -> void:
|
||||||
|
if request_generation != _current_account_generation():
|
||||||
|
return
|
||||||
|
_loading_status = false
|
||||||
|
if not success:
|
||||||
|
status_failed.emit(str(error_info.get("message", "测试实验室读取失败")))
|
||||||
|
return
|
||||||
|
var data_variant: Variant = response.get("data", {})
|
||||||
|
if not (data_variant is Dictionary):
|
||||||
|
status_failed.emit("测试实验室响应格式错误")
|
||||||
|
return
|
||||||
|
_status = (data_variant as Dictionary).duplicate(true)
|
||||||
|
status_changed.emit(get_status())
|
||||||
|
|
||||||
|
func _on_mutation_response(success: bool, _response: Dictionary, error_info: Dictionary, success_message: String, request_generation: int) -> void:
|
||||||
|
if request_generation != _current_account_generation():
|
||||||
|
return
|
||||||
|
if not success:
|
||||||
|
status_failed.emit(str(error_info.get("message", "测试实验室操作失败")))
|
||||||
|
return
|
||||||
|
operation_completed.emit(success_message)
|
||||||
|
refresh_status()
|
||||||
|
|
||||||
|
func _api_client() -> Node:
|
||||||
|
return get_node_or_null("/root/ApiClient")
|
||||||
|
|
||||||
|
func _current_account_generation() -> int:
|
||||||
|
var auth_manager := get_node_or_null("/root/AuthManager")
|
||||||
|
return int(auth_manager.call("get_account_generation")) if auth_manager != null and auth_manager.has_method("get_account_generation") else 0
|
||||||
1
_Core/managers/TestLabManager.gd.uid
Normal file
1
_Core/managers/TestLabManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cntd1faiax3br
|
||||||
@@ -35,6 +35,10 @@ SettingsManager="*res://_Core/managers/SettingsManager.gd"
|
|||||||
NotificationSoundManager="*res://_Core/managers/NotificationSoundManager.gd"
|
NotificationSoundManager="*res://_Core/managers/NotificationSoundManager.gd"
|
||||||
SocialManager="*res://_Core/managers/SocialManager.gd"
|
SocialManager="*res://_Core/managers/SocialManager.gd"
|
||||||
InteractionManager="*res://_Core/managers/InteractionManager.gd"
|
InteractionManager="*res://_Core/managers/InteractionManager.gd"
|
||||||
|
TaskManager="*res://_Core/managers/TaskManager.gd"
|
||||||
|
TaskBookPanel="*res://scenes/ui/TaskBookPanel.gd"
|
||||||
|
TestLabManager="*res://_Core/managers/TestLabManager.gd"
|
||||||
|
AdminTestLabPanel="*res://scenes/ui/AdminTestLabPanel.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ func _ready() -> void:
|
|||||||
_connect_cafe_companion_events()
|
_connect_cafe_companion_events()
|
||||||
_register_interactables()
|
_register_interactables()
|
||||||
_discover_destination()
|
_discover_destination()
|
||||||
|
_report_task_map_visit()
|
||||||
|
|
||||||
|
func _report_task_map_visit() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "map_visited", "whale_cafe")
|
||||||
|
|
||||||
func _discover_destination() -> void:
|
func _discover_destination() -> void:
|
||||||
var destination_id := SceneManager.get_next_destination_id()
|
var destination_id := SceneManager.get_next_destination_id()
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ func _ready() -> void:
|
|||||||
_configure_camera()
|
_configure_camera()
|
||||||
_discover_destination()
|
_discover_destination()
|
||||||
call_deferred("_show_registration_welcome")
|
call_deferred("_show_registration_welcome")
|
||||||
|
_report_task_map_visit()
|
||||||
|
|
||||||
|
func _report_task_map_visit() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "map_visited", "square")
|
||||||
|
|
||||||
func _show_registration_welcome() -> void:
|
func _show_registration_welcome() -> void:
|
||||||
var auth_manager: Node = get_node_or_null("/root/AuthManager")
|
var auth_manager: Node = get_node_or_null("/root/AuthManager")
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ func _ready() -> void:
|
|||||||
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
|
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
|
||||||
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
|
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
|
||||||
_discover_destination()
|
_discover_destination()
|
||||||
|
_report_task_map_visit()
|
||||||
|
|
||||||
|
func _report_task_map_visit() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "map_visited", "work_zone")
|
||||||
|
|
||||||
func _discover_destination() -> void:
|
func _discover_destination() -> void:
|
||||||
var destination_id := SceneManager.get_next_destination_id()
|
var destination_id := SceneManager.get_next_destination_id()
|
||||||
@@ -135,6 +141,11 @@ func _on_object_interacted(data: Dictionary) -> void:
|
|||||||
if buildingId == "virtual_whale_recruitment_board":
|
if buildingId == "virtual_whale_recruitment_board":
|
||||||
_open_course_board()
|
_open_course_board()
|
||||||
return
|
return
|
||||||
|
if buildingId == "whale_job_center":
|
||||||
|
var taskBook := get_node_or_null("/root/TaskBookPanel")
|
||||||
|
if taskBook != null and taskBook.has_method("show_panel"):
|
||||||
|
taskBook.call("show_panel")
|
||||||
|
return
|
||||||
if not [
|
if not [
|
||||||
"whale_job_center",
|
"whale_job_center",
|
||||||
"ai_service_station",
|
"ai_service_station",
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ func _physics_process(_delta: float) -> void:
|
|||||||
|
|
||||||
# 处理玩家交互,展示气泡并向全局事件系统广播。
|
# 处理玩家交互,展示气泡并向全局事件系统广播。
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "facility_interacted", "npc")
|
||||||
show_bubble(dialogue)
|
show_bubble(dialogue)
|
||||||
var eventSystem: Node = get_node_or_null("/root/EventSystem")
|
var eventSystem: Node = get_node_or_null("/root/EventSystem")
|
||||||
if eventSystem != null:
|
if eventSystem != null:
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ func _ready() -> void:
|
|||||||
add_child(interactable)
|
add_child(interactable)
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "notice_viewed")
|
||||||
|
taskManager.call("report_activity", "facility_interacted", "notice_board")
|
||||||
var root: Window = get_tree().root
|
var root: Window = get_tree().root
|
||||||
if root.has_node(NOTICE_DIALOG_NAME):
|
if root.has_node(NOTICE_DIALOG_NAME):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func _ready() -> void:
|
|||||||
add_child(interactable)
|
add_child(interactable)
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "facility_interacted", "welcome_board")
|
||||||
var root: Window = get_tree().root
|
var root: Window = get_tree().root
|
||||||
if root.has_node(WELCOME_DIALOG_NAME):
|
if root.has_node(WELCOME_DIALOG_NAME):
|
||||||
return
|
return
|
||||||
|
|||||||
685
scenes/ui/AdminTestLabPanel.gd
Normal file
685
scenes/ui/AdminTestLabPanel.gd
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
# 仅 role=9 可打开的游戏内测试实验室。服务端会再次验证管理员身份。
|
||||||
|
|
||||||
|
const PANEL_SIZE: Vector2 = Vector2(1180, 820)
|
||||||
|
const MAP_OPTIONS: Array[Dictionary] = [
|
||||||
|
{"id": "whale_port", "label": "鲸鱼港广场"},
|
||||||
|
{"id": "work_zone", "label": "打工区"},
|
||||||
|
{"id": "whale_cafe", "label": "鲸鱼咖啡馆"},
|
||||||
|
]
|
||||||
|
const SKIN_OPTIONS: Array[Dictionary] = [
|
||||||
|
{"id": "classic_whale", "label": "经典鲸鱼"},
|
||||||
|
{"id": "human_whale_directional_v2_8x4", "label": "海风行者"},
|
||||||
|
{"id": "girl_sailor_turnaround_v2_8x4", "label": "海风少女"},
|
||||||
|
{"id": "panda_hero_8x4", "label": "熊猫侠"},
|
||||||
|
{"id": "ordinary_man_male_8x4", "label": "普通人(男)"},
|
||||||
|
]
|
||||||
|
const POLICY_OPTIONS: Array[Dictionary] = [
|
||||||
|
{"id": "public", "label": "公开访问"},
|
||||||
|
{"id": "friends", "label": "仅好友"},
|
||||||
|
{"id": "closed", "label": "关闭访问"},
|
||||||
|
]
|
||||||
|
const SCOPE_OPTIONS: Array[Dictionary] = [
|
||||||
|
{"id": "local", "label": "当前地图公共消息"},
|
||||||
|
{"id": "global", "label": "全服测试消息"},
|
||||||
|
{"id": "private", "label": "私聊选定玩家"},
|
||||||
|
]
|
||||||
|
const TEXT_COLOR: Color = Color(0.84, 0.92, 0.96)
|
||||||
|
const MUTED_COLOR: Color = Color(0.53, 0.68, 0.75)
|
||||||
|
const SURFACE_COLOR: Color = Color(0.035, 0.105, 0.16, 0.98)
|
||||||
|
const RAISED_COLOR: Color = Color(0.065, 0.16, 0.23, 1.0)
|
||||||
|
const ACCENT_COLOR: Color = Color(0.20, 0.80, 0.86, 1.0)
|
||||||
|
const DANGER_COLOR: Color = Color(0.82, 0.29, 0.25, 1.0)
|
||||||
|
|
||||||
|
var _root: Control
|
||||||
|
var _panel: PanelContainer
|
||||||
|
var _status_label: Label
|
||||||
|
var _notice_label: Label
|
||||||
|
var _actor_list: VBoxContainer
|
||||||
|
var _selected_label: Label
|
||||||
|
var _target_select: OptionButton
|
||||||
|
var _create_nickname: LineEdit
|
||||||
|
var _create_map: OptionButton
|
||||||
|
var _create_x: SpinBox
|
||||||
|
var _create_y: SpinBox
|
||||||
|
var _create_skin: OptionButton
|
||||||
|
var _actor_online: CheckButton
|
||||||
|
var _actor_map: OptionButton
|
||||||
|
var _actor_x: SpinBox
|
||||||
|
var _actor_y: SpinBox
|
||||||
|
var _actor_skin: OptionButton
|
||||||
|
var _policy_select: OptionButton
|
||||||
|
var _message_scope: OptionButton
|
||||||
|
var _message_input: TextEdit
|
||||||
|
var _selected_actor_id: String = ""
|
||||||
|
var _is_open: bool = false
|
||||||
|
var _clear_confirm_deadline_ms: int = 0
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 48
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
add_to_group("whaletown_escape_dismissible")
|
||||||
|
_build_ui()
|
||||||
|
_connect_services()
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
var event_system := get_node_or_null("/root/EventSystem")
|
||||||
|
if event_system != null:
|
||||||
|
event_system.call("disconnect_event", EventNames.HUD_ADMIN_TEST_LAB_REQUESTED, _on_open_requested, self)
|
||||||
|
var manager := get_node_or_null("/root/TestLabManager")
|
||||||
|
if manager != null:
|
||||||
|
_disconnect_signal(manager, "status_changed", Callable(self, "_on_status_changed"))
|
||||||
|
_disconnect_signal(manager, "status_failed", Callable(self, "_on_status_failed"))
|
||||||
|
_disconnect_signal(manager, "operation_completed", Callable(self, "_on_operation_completed"))
|
||||||
|
|
||||||
|
func show_panel() -> void:
|
||||||
|
if not _is_admin():
|
||||||
|
hide_panel()
|
||||||
|
return
|
||||||
|
_is_open = true
|
||||||
|
_root.visible = true
|
||||||
|
_set_notice("正在读取测试实验室…", MUTED_COLOR)
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
if manager.has_method("is_loaded") and bool(manager.call("is_loaded")):
|
||||||
|
var status_variant: Variant = manager.call("get_status")
|
||||||
|
if status_variant is Dictionary:
|
||||||
|
_on_status_changed(status_variant as Dictionary)
|
||||||
|
if manager.has_method("refresh_status"):
|
||||||
|
manager.call("refresh_status")
|
||||||
|
|
||||||
|
func hide_panel() -> void:
|
||||||
|
_is_open = false
|
||||||
|
if is_instance_valid(_root):
|
||||||
|
_root.visible = false
|
||||||
|
|
||||||
|
func is_escape_dismissible() -> bool:
|
||||||
|
return _is_open
|
||||||
|
|
||||||
|
func get_escape_priority() -> int:
|
||||||
|
return 850
|
||||||
|
|
||||||
|
func request_escape_close() -> void:
|
||||||
|
hide_panel()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not _is_open or get_viewport().is_input_handled():
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
hide_panel()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
func _connect_services() -> void:
|
||||||
|
var event_system := get_node_or_null("/root/EventSystem")
|
||||||
|
if event_system != null:
|
||||||
|
event_system.call("connect_event", EventNames.HUD_ADMIN_TEST_LAB_REQUESTED, _on_open_requested, self)
|
||||||
|
var manager := _manager()
|
||||||
|
if manager == null:
|
||||||
|
return
|
||||||
|
_connect_signal(manager, "status_changed", Callable(self, "_on_status_changed"))
|
||||||
|
_connect_signal(manager, "status_failed", Callable(self, "_on_status_failed"))
|
||||||
|
_connect_signal(manager, "operation_completed", Callable(self, "_on_operation_completed"))
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
_root = Control.new()
|
||||||
|
_root.name = "AdminTestLabRoot"
|
||||||
|
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_root.visible = false
|
||||||
|
add_child(_root)
|
||||||
|
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
dim.color = Color(0.005, 0.02, 0.04, 0.78)
|
||||||
|
dim.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
dim.gui_input.connect(func(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
||||||
|
hide_panel()
|
||||||
|
)
|
||||||
|
_root.add_child(dim)
|
||||||
|
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.custom_minimum_size = PANEL_SIZE
|
||||||
|
_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
_panel.offset_left = -PANEL_SIZE.x * 0.5
|
||||||
|
_panel.offset_top = -PANEL_SIZE.y * 0.5
|
||||||
|
_panel.offset_right = PANEL_SIZE.x * 0.5
|
||||||
|
_panel.offset_bottom = PANEL_SIZE.y * 0.5
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_panel.add_theme_stylebox_override("panel", _style(SURFACE_COLOR, 24, Color(0.18, 0.48, 0.58, 0.86), 1))
|
||||||
|
_root.add_child(_panel)
|
||||||
|
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 24)
|
||||||
|
margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
margin.add_theme_constant_override("margin_right", 24)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 20)
|
||||||
|
_panel.add_child(margin)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.add_theme_constant_override("separation", 14)
|
||||||
|
margin.add_child(layout)
|
||||||
|
layout.add_child(_build_header())
|
||||||
|
_notice_label = Label.new()
|
||||||
|
_notice_label.visible = false
|
||||||
|
_notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_notice_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
layout.add_child(_notice_label)
|
||||||
|
var body := HSplitContainer.new()
|
||||||
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
body.split_offset = 358
|
||||||
|
layout.add_child(body)
|
||||||
|
body.add_child(_build_roster_column())
|
||||||
|
body.add_child(_build_control_column())
|
||||||
|
var footer := HBoxContainer.new()
|
||||||
|
footer.add_theme_constant_override("separation", 10)
|
||||||
|
layout.add_child(footer)
|
||||||
|
var hint := Label.new()
|
||||||
|
hint.text = "仅开发/测试环境可用;所有动作由服务端再次校验。"
|
||||||
|
hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hint.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
hint.add_theme_font_size_override("font_size", 13)
|
||||||
|
footer.add_child(hint)
|
||||||
|
footer.add_child(_button("刷新", ACCENT_COLOR, _on_refresh_pressed, Vector2(86, 38)))
|
||||||
|
footer.add_child(_button("清空实验室", DANGER_COLOR, _on_clear_pressed, Vector2(126, 38)))
|
||||||
|
var esc := Label.new()
|
||||||
|
esc.text = "[Esc] 关闭"
|
||||||
|
esc.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
esc.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
esc.add_theme_font_size_override("font_size", 13)
|
||||||
|
footer.add_child(esc)
|
||||||
|
|
||||||
|
func _build_header() -> Control:
|
||||||
|
var header := PanelContainer.new()
|
||||||
|
header.custom_minimum_size = Vector2(0, 92)
|
||||||
|
header.add_theme_stylebox_override("panel", _style(Color(0.045, 0.19, 0.27, 1.0), 16, ACCENT_COLOR.darkened(0.30), 1))
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 20)
|
||||||
|
margin.add_theme_constant_override("margin_top", 12)
|
||||||
|
margin.add_theme_constant_override("margin_right", 14)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 12)
|
||||||
|
header.add_child(margin)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
margin.add_child(row)
|
||||||
|
var title_box := VBoxContainer.new()
|
||||||
|
title_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(title_box)
|
||||||
|
var eyebrow := Label.new()
|
||||||
|
eyebrow.text = "管理员 · 多人互动验证"
|
||||||
|
eyebrow.add_theme_color_override("font_color", Color(0.51, 0.86, 0.89, 1.0))
|
||||||
|
eyebrow.add_theme_font_size_override("font_size", 13)
|
||||||
|
title_box.add_child(eyebrow)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "测试实验室"
|
||||||
|
title.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
title_box.add_child(title)
|
||||||
|
_status_label = Label.new()
|
||||||
|
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_status_label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
_status_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
row.add_child(_status_label)
|
||||||
|
row.add_child(_button("关闭", Color(0.20, 0.36, 0.44, 1.0), hide_panel, Vector2(78, 38)))
|
||||||
|
return header
|
||||||
|
|
||||||
|
func _build_roster_column() -> Control:
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.custom_minimum_size = Vector2(336, 0)
|
||||||
|
column.add_theme_constant_override("separation", 12)
|
||||||
|
var create_title := Label.new()
|
||||||
|
create_title.text = "投放假人"
|
||||||
|
create_title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
create_title.add_theme_font_size_override("font_size", 20)
|
||||||
|
column.add_child(create_title)
|
||||||
|
var create_box := PanelContainer.new()
|
||||||
|
create_box.add_theme_stylebox_override("panel", _style(RAISED_COLOR, 14, Color(0.16, 0.36, 0.46, 0.72), 1))
|
||||||
|
column.add_child(create_box)
|
||||||
|
var create_margin := MarginContainer.new()
|
||||||
|
create_margin.add_theme_constant_override("margin_left", 14)
|
||||||
|
create_margin.add_theme_constant_override("margin_top", 14)
|
||||||
|
create_margin.add_theme_constant_override("margin_right", 14)
|
||||||
|
create_margin.add_theme_constant_override("margin_bottom", 14)
|
||||||
|
create_box.add_child(create_margin)
|
||||||
|
var create_form := VBoxContainer.new()
|
||||||
|
create_form.add_theme_constant_override("separation", 8)
|
||||||
|
create_margin.add_child(create_form)
|
||||||
|
_create_nickname = _line_edit("显示昵称,例如 社交测试鲸")
|
||||||
|
create_form.add_child(_field("昵称", _create_nickname))
|
||||||
|
_create_map = _option(MAP_OPTIONS)
|
||||||
|
create_form.add_child(_field("地图", _create_map))
|
||||||
|
var create_position := HBoxContainer.new()
|
||||||
|
create_position.add_theme_constant_override("separation", 8)
|
||||||
|
_create_x = _spin(1280)
|
||||||
|
_create_y = _spin(960)
|
||||||
|
create_position.add_child(_field("X", _create_x))
|
||||||
|
create_position.add_child(_field("Y", _create_y))
|
||||||
|
create_form.add_child(create_position)
|
||||||
|
_create_skin = _option(SKIN_OPTIONS)
|
||||||
|
create_form.add_child(_field("皮肤", _create_skin))
|
||||||
|
create_form.add_child(_button("创建并上线", ACCENT_COLOR, _on_create_pressed, Vector2(0, 40)))
|
||||||
|
var roster_title := Label.new()
|
||||||
|
roster_title.text = "假人编队"
|
||||||
|
roster_title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
roster_title.add_theme_font_size_override("font_size", 20)
|
||||||
|
column.add_child(roster_title)
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
column.add_child(scroll)
|
||||||
|
_actor_list = VBoxContainer.new()
|
||||||
|
_actor_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_actor_list.add_theme_constant_override("separation", 7)
|
||||||
|
scroll.add_child(_actor_list)
|
||||||
|
return column
|
||||||
|
|
||||||
|
func _build_control_column() -> Control:
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 12)
|
||||||
|
var title_row := HBoxContainer.new()
|
||||||
|
column.add_child(title_row)
|
||||||
|
_selected_label = Label.new()
|
||||||
|
_selected_label.text = "选择一名假人开始控制"
|
||||||
|
_selected_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_selected_label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
_selected_label.add_theme_font_size_override("font_size", 20)
|
||||||
|
title_row.add_child(_selected_label)
|
||||||
|
title_row.add_child(_button("删除假人", DANGER_COLOR, _on_delete_selected_pressed, Vector2(96, 34)))
|
||||||
|
var controls_scroll := ScrollContainer.new()
|
||||||
|
controls_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
controls_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
column.add_child(controls_scroll)
|
||||||
|
var controls := VBoxContainer.new()
|
||||||
|
controls.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
controls.add_theme_constant_override("separation", 12)
|
||||||
|
controls_scroll.add_child(controls)
|
||||||
|
controls.add_child(_build_presence_section())
|
||||||
|
controls.add_child(_build_room_section())
|
||||||
|
controls.add_child(_build_social_section())
|
||||||
|
return column
|
||||||
|
|
||||||
|
func _build_presence_section() -> Control:
|
||||||
|
var section := _section("在线与位置")
|
||||||
|
var content := section.get_node("Margin/Layout/Content") as VBoxContainer
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
_actor_online = CheckButton.new()
|
||||||
|
_actor_online.text = "在线"
|
||||||
|
_actor_online.button_pressed = true
|
||||||
|
_actor_online.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
row.add_child(_actor_online)
|
||||||
|
row.add_spacer(false)
|
||||||
|
row.add_child(_button("同步状态", ACCENT_COLOR, _on_update_pressed, Vector2(104, 36)))
|
||||||
|
content.add_child(row)
|
||||||
|
_actor_map = _option(MAP_OPTIONS)
|
||||||
|
content.add_child(_field("地图", _actor_map))
|
||||||
|
var position := HBoxContainer.new()
|
||||||
|
position.add_theme_constant_override("separation", 10)
|
||||||
|
_actor_x = _spin(1280)
|
||||||
|
_actor_y = _spin(960)
|
||||||
|
position.add_child(_field("X", _actor_x))
|
||||||
|
position.add_child(_field("Y", _actor_y))
|
||||||
|
content.add_child(position)
|
||||||
|
_actor_skin = _option(SKIN_OPTIONS)
|
||||||
|
content.add_child(_field("皮肤", _actor_skin))
|
||||||
|
return section
|
||||||
|
|
||||||
|
func _build_room_section() -> Control:
|
||||||
|
var section := _section("房间访问")
|
||||||
|
var content := section.get_node("Margin/Layout/Content") as VBoxContainer
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
_policy_select = _option(POLICY_OPTIONS)
|
||||||
|
_policy_select.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(_policy_select)
|
||||||
|
row.add_child(_button("保存权限", Color(0.20, 0.42, 0.55, 1.0), _on_policy_pressed, Vector2(102, 36)))
|
||||||
|
content.add_child(row)
|
||||||
|
return section
|
||||||
|
|
||||||
|
func _build_social_section() -> Control:
|
||||||
|
var section := _section("社交注入")
|
||||||
|
var content := section.get_node("Margin/Layout/Content") as VBoxContainer
|
||||||
|
var target_hint := Label.new()
|
||||||
|
target_hint.text = "目标真实在线玩家(私聊、好友和拉黑操作必选)"
|
||||||
|
target_hint.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
target_hint.add_theme_font_size_override("font_size", 13)
|
||||||
|
content.add_child(target_hint)
|
||||||
|
_target_select = OptionButton.new()
|
||||||
|
_target_select.custom_minimum_size = Vector2(0, 36)
|
||||||
|
_target_select.add_item("暂无在线真实玩家")
|
||||||
|
_target_select.add_theme_stylebox_override("normal", _style(Color(0.03, 0.10, 0.15, 1.0), 10, Color(0.20, 0.42, 0.52, 0.8), 1))
|
||||||
|
_target_select.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
content.add_child(_target_select)
|
||||||
|
_message_scope = _option(SCOPE_OPTIONS)
|
||||||
|
content.add_child(_field("消息范围", _message_scope))
|
||||||
|
_message_input = TextEdit.new()
|
||||||
|
_message_input.custom_minimum_size = Vector2(0, 78)
|
||||||
|
_message_input.placeholder_text = "测试消息不会同步到 Zulip、任务或鲸币。"
|
||||||
|
_message_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||||
|
_message_input.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
_message_input.add_theme_color_override("font_placeholder_color", MUTED_COLOR)
|
||||||
|
_message_input.add_theme_stylebox_override("normal", _style(Color(0.025, 0.085, 0.13, 1.0), 10, Color(0.20, 0.42, 0.52, 0.8), 1))
|
||||||
|
content.add_child(_message_input)
|
||||||
|
content.add_child(_button("发送测试消息", ACCENT_COLOR, _on_message_pressed, Vector2(0, 38)))
|
||||||
|
var social_row := HBoxContainer.new()
|
||||||
|
social_row.add_theme_constant_override("separation", 8)
|
||||||
|
social_row.add_child(_button("发起好友申请", Color(0.20, 0.42, 0.55, 1.0), func() -> void: _on_social_pressed("friend_request"), Vector2(116, 36)))
|
||||||
|
social_row.add_child(_button("接受", Color(0.20, 0.42, 0.55, 1.0), func() -> void: _on_social_pressed("friend_accept"), Vector2(72, 36)))
|
||||||
|
social_row.add_child(_button("拒绝", Color(0.20, 0.42, 0.55, 1.0), func() -> void: _on_social_pressed("friend_reject"), Vector2(72, 36)))
|
||||||
|
social_row.add_child(_button("拉黑", DANGER_COLOR, func() -> void: _on_social_pressed("block"), Vector2(72, 36)))
|
||||||
|
content.add_child(social_row)
|
||||||
|
return section
|
||||||
|
|
||||||
|
func _section(title_text: String) -> PanelContainer:
|
||||||
|
var section := PanelContainer.new()
|
||||||
|
section.add_theme_stylebox_override("panel", _style(RAISED_COLOR, 14, Color(0.16, 0.36, 0.46, 0.72), 1))
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.name = "Margin"
|
||||||
|
margin.add_theme_constant_override("margin_left", 14)
|
||||||
|
margin.add_theme_constant_override("margin_top", 12)
|
||||||
|
margin.add_theme_constant_override("margin_right", 14)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 14)
|
||||||
|
section.add_child(margin)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.name = "Layout"
|
||||||
|
layout.add_theme_constant_override("separation", 9)
|
||||||
|
margin.add_child(layout)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = title_text
|
||||||
|
title.add_theme_color_override("font_color", Color(0.68, 0.91, 0.94, 1.0))
|
||||||
|
title.add_theme_font_size_override("font_size", 16)
|
||||||
|
layout.add_child(title)
|
||||||
|
var content := VBoxContainer.new()
|
||||||
|
content.name = "Content"
|
||||||
|
content.add_theme_constant_override("separation", 7)
|
||||||
|
layout.add_child(content)
|
||||||
|
return section
|
||||||
|
|
||||||
|
func _field(label_text: String, control: Control) -> VBoxContainer:
|
||||||
|
var field := VBoxContainer.new()
|
||||||
|
field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
field.add_theme_constant_override("separation", 3)
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = label_text
|
||||||
|
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
label.add_theme_font_size_override("font_size", 12)
|
||||||
|
field.add_child(label)
|
||||||
|
control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
field.add_child(control)
|
||||||
|
return field
|
||||||
|
|
||||||
|
func _line_edit(placeholder: String) -> LineEdit:
|
||||||
|
var input := LineEdit.new()
|
||||||
|
input.placeholder_text = placeholder
|
||||||
|
input.custom_minimum_size = Vector2(0, 36)
|
||||||
|
input.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
input.add_theme_color_override("font_placeholder_color", MUTED_COLOR)
|
||||||
|
input.add_theme_stylebox_override("normal", _style(Color(0.025, 0.085, 0.13, 1.0), 10, Color(0.20, 0.42, 0.52, 0.8), 1))
|
||||||
|
input.add_theme_stylebox_override("focus", _style(Color(0.025, 0.085, 0.13, 1.0), 10, ACCENT_COLOR, 1))
|
||||||
|
return input
|
||||||
|
|
||||||
|
func _spin(initial_value: int) -> SpinBox:
|
||||||
|
var spin := SpinBox.new()
|
||||||
|
spin.min_value = -8192
|
||||||
|
spin.max_value = 8192
|
||||||
|
spin.step = 1
|
||||||
|
spin.value = initial_value
|
||||||
|
spin.custom_minimum_size = Vector2(0, 36)
|
||||||
|
spin.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
return spin
|
||||||
|
|
||||||
|
func _option(items: Array[Dictionary]) -> OptionButton:
|
||||||
|
var select := OptionButton.new()
|
||||||
|
select.custom_minimum_size = Vector2(0, 36)
|
||||||
|
select.add_theme_stylebox_override("normal", _style(Color(0.025, 0.085, 0.13, 1.0), 10, Color(0.20, 0.42, 0.52, 0.8), 1))
|
||||||
|
select.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
for item: Dictionary in items:
|
||||||
|
select.add_item(str(item.get("label", "")))
|
||||||
|
select.set_item_metadata(select.item_count - 1, str(item.get("id", "")))
|
||||||
|
return select
|
||||||
|
|
||||||
|
func _button(text_value: String, color: Color, callback: Callable, size: Vector2) -> Button:
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = text_value
|
||||||
|
button.custom_minimum_size = size
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
button.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||||
|
button.add_theme_stylebox_override("normal", _style(color, 10))
|
||||||
|
button.add_theme_stylebox_override("hover", _style(color.lightened(0.12), 10))
|
||||||
|
button.add_theme_stylebox_override("pressed", _style(color.darkened(0.12), 10))
|
||||||
|
button.pressed.connect(callback)
|
||||||
|
return button
|
||||||
|
|
||||||
|
func _style(color: Color, radius: int, border_color: Color = Color.TRANSPARENT, border_width: int = 0) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = color
|
||||||
|
style.corner_radius_top_left = radius
|
||||||
|
style.corner_radius_top_right = radius
|
||||||
|
style.corner_radius_bottom_left = radius
|
||||||
|
style.corner_radius_bottom_right = radius
|
||||||
|
if border_width > 0:
|
||||||
|
style.border_width_left = border_width
|
||||||
|
style.border_width_top = border_width
|
||||||
|
style.border_width_right = border_width
|
||||||
|
style.border_width_bottom = border_width
|
||||||
|
style.border_color = border_color
|
||||||
|
return style
|
||||||
|
|
||||||
|
func _on_open_requested(_data: Variant = null) -> void:
|
||||||
|
show_panel()
|
||||||
|
|
||||||
|
func _on_refresh_pressed() -> void:
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("refresh_status")
|
||||||
|
|
||||||
|
func _on_create_pressed() -> void:
|
||||||
|
if _create_nickname.text.strip_edges().is_empty():
|
||||||
|
_set_notice("请输入测试假人的显示昵称", DANGER_COLOR.lightened(0.18))
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("create_actor", _create_nickname.text, _selected_option(_create_map), int(_create_x.value), int(_create_y.value), _selected_option(_create_skin))
|
||||||
|
|
||||||
|
func _on_update_pressed() -> void:
|
||||||
|
if not _ensure_selected_actor():
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("update_actor", _selected_actor_id, _actor_online.button_pressed, _selected_option(_actor_map), int(_actor_x.value), int(_actor_y.value), _selected_option(_actor_skin))
|
||||||
|
|
||||||
|
func _on_policy_pressed() -> void:
|
||||||
|
if not _ensure_selected_actor():
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("set_room_policy", _selected_actor_id, _selected_option(_policy_select))
|
||||||
|
|
||||||
|
func _on_message_pressed() -> void:
|
||||||
|
if not _ensure_selected_actor():
|
||||||
|
return
|
||||||
|
var content := _message_input.text.strip_edges()
|
||||||
|
if content.is_empty():
|
||||||
|
_set_notice("请输入测试消息", DANGER_COLOR.lightened(0.18))
|
||||||
|
return
|
||||||
|
var scope := _selected_option(_message_scope)
|
||||||
|
var target_id := _selected_target_id()
|
||||||
|
if scope == "private" and target_id.is_empty():
|
||||||
|
_set_notice("私聊需要先选择在线真实玩家", DANGER_COLOR.lightened(0.18))
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("send_message", _selected_actor_id, scope, content, target_id)
|
||||||
|
|
||||||
|
func _on_social_pressed(action: String) -> void:
|
||||||
|
if not _ensure_selected_actor():
|
||||||
|
return
|
||||||
|
var target_id := _selected_target_id()
|
||||||
|
if target_id.is_empty():
|
||||||
|
_set_notice("请先选择在线真实玩家", DANGER_COLOR.lightened(0.18))
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("social_action", _selected_actor_id, action, target_id)
|
||||||
|
|
||||||
|
func _on_clear_pressed() -> void:
|
||||||
|
var now_ms: int = Time.get_ticks_msec()
|
||||||
|
if now_ms > _clear_confirm_deadline_ms:
|
||||||
|
_clear_confirm_deadline_ms = now_ms + 4000
|
||||||
|
_set_notice("再次点击“清空实验室”以确认;这会删除全部测试账号和关联测试数据。", DANGER_COLOR.lightened(0.18))
|
||||||
|
return
|
||||||
|
_clear_confirm_deadline_ms = 0
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("clear_all")
|
||||||
|
|
||||||
|
func _on_delete_selected_pressed() -> void:
|
||||||
|
if not _ensure_selected_actor():
|
||||||
|
return
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null:
|
||||||
|
manager.call("delete_actor", _selected_actor_id)
|
||||||
|
|
||||||
|
func _on_status_changed(status: Dictionary) -> void:
|
||||||
|
if not _is_open:
|
||||||
|
return
|
||||||
|
var enabled: bool = bool(status.get("enabled", false))
|
||||||
|
var environment: String = str(status.get("environment", "未知"))
|
||||||
|
var actors_variant: Variant = status.get("actors", [])
|
||||||
|
var actor_count: int = (actors_variant as Array).size() if actors_variant is Array else 0
|
||||||
|
var max_actors: int = int(status.get("maxActors", 20))
|
||||||
|
_status_label.text = "%s · 假人 %d/%d" % [environment, actor_count, max_actors]
|
||||||
|
_set_notice("实验室已启用" if enabled else "测试实验室未启用(仅 development/test 且 TEST_LAB_ENABLED=true)", ACCENT_COLOR if enabled else DANGER_COLOR.lightened(0.18))
|
||||||
|
_render_actor_list(actors_variant)
|
||||||
|
_render_online_players(status.get("onlinePlayers", []))
|
||||||
|
|
||||||
|
func _on_status_failed(message: String) -> void:
|
||||||
|
if _is_open:
|
||||||
|
_set_notice(message, DANGER_COLOR.lightened(0.18))
|
||||||
|
|
||||||
|
func _on_operation_completed(message: String) -> void:
|
||||||
|
if _is_open:
|
||||||
|
_message_input.text = ""
|
||||||
|
_set_notice(message, ACCENT_COLOR)
|
||||||
|
|
||||||
|
func _render_actor_list(actors_variant: Variant) -> void:
|
||||||
|
_clear_children(_actor_list)
|
||||||
|
if not (actors_variant is Array) or (actors_variant as Array).is_empty():
|
||||||
|
var empty := Label.new()
|
||||||
|
empty.text = "尚未投放测试假人"
|
||||||
|
empty.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
empty.add_theme_font_size_override("font_size", 14)
|
||||||
|
_actor_list.add_child(empty)
|
||||||
|
_selected_actor_id = ""
|
||||||
|
_selected_label.text = "选择一名假人开始控制"
|
||||||
|
return
|
||||||
|
var found_selected: bool = false
|
||||||
|
for actor_variant: Variant in actors_variant as Array:
|
||||||
|
if not (actor_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var actor: Dictionary = actor_variant as Dictionary
|
||||||
|
var actor_id: String = str(actor.get("userId", ""))
|
||||||
|
if actor_id == _selected_actor_id:
|
||||||
|
found_selected = true
|
||||||
|
_actor_list.add_child(_actor_row(actor))
|
||||||
|
if not found_selected:
|
||||||
|
var first_actor_variant: Variant = (actors_variant as Array)[0]
|
||||||
|
if first_actor_variant is Dictionary:
|
||||||
|
_select_actor(first_actor_variant as Dictionary)
|
||||||
|
|
||||||
|
func _actor_row(actor: Dictionary) -> Button:
|
||||||
|
var actor_id: String = str(actor.get("userId", ""))
|
||||||
|
var nickname: String = str(actor.get("nickname", actor.get("username", "测试假人")))
|
||||||
|
var map_id: String = str(actor.get("mapId", ""))
|
||||||
|
var online: bool = bool(actor.get("online", false))
|
||||||
|
var selected: bool = actor_id == _selected_actor_id
|
||||||
|
var button := Button.new()
|
||||||
|
button.custom_minimum_size = Vector2(0, 64)
|
||||||
|
button.text = "%s · %s\n%s · %d, %d" % ["●" if online else "○", nickname, map_id, int(actor.get("x", 0)), int(actor.get("y", 0))]
|
||||||
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
button.add_theme_font_size_override("font_size", 14)
|
||||||
|
button.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
button.add_theme_stylebox_override("normal", _style(Color(0.08, 0.23, 0.30, 1.0) if selected else Color(0.05, 0.14, 0.20, 1.0), 10, ACCENT_COLOR if selected else Color(0.16, 0.36, 0.46, 0.52), 1))
|
||||||
|
button.add_theme_stylebox_override("hover", _style(Color(0.10, 0.29, 0.37, 1.0), 10, ACCENT_COLOR.darkened(0.10), 1))
|
||||||
|
button.pressed.connect(func() -> void: _select_actor(actor))
|
||||||
|
return button
|
||||||
|
|
||||||
|
func _select_actor(actor: Dictionary) -> void:
|
||||||
|
_selected_actor_id = str(actor.get("userId", ""))
|
||||||
|
var nickname: String = str(actor.get("nickname", actor.get("username", "测试假人")))
|
||||||
|
_selected_label.text = "控制 · %s" % nickname
|
||||||
|
_actor_online.button_pressed = bool(actor.get("online", false))
|
||||||
|
_actor_x.value = int(actor.get("x", 1280))
|
||||||
|
_actor_y.value = int(actor.get("y", 960))
|
||||||
|
_select_option(_actor_map, str(actor.get("mapId", "whale_port")))
|
||||||
|
_select_option(_actor_skin, str(actor.get("skinId", "classic_whale")))
|
||||||
|
_select_option(_policy_select, str(actor.get("roomVisitPolicy", "public")))
|
||||||
|
var manager := _manager()
|
||||||
|
if manager != null and manager.has_method("get_status"):
|
||||||
|
var status_variant: Variant = manager.call("get_status")
|
||||||
|
if status_variant is Dictionary:
|
||||||
|
_render_actor_list((status_variant as Dictionary).get("actors", []))
|
||||||
|
|
||||||
|
func _render_online_players(players_variant: Variant) -> void:
|
||||||
|
_target_select.clear()
|
||||||
|
if not (players_variant is Array) or (players_variant as Array).is_empty():
|
||||||
|
_target_select.add_item("暂无在线真实玩家")
|
||||||
|
_target_select.set_item_metadata(0, "")
|
||||||
|
return
|
||||||
|
for player_variant: Variant in players_variant as Array:
|
||||||
|
if not (player_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var player: Dictionary = player_variant as Dictionary
|
||||||
|
var user_id: String = str(player.get("userId", ""))
|
||||||
|
var username: String = str(player.get("username", user_id))
|
||||||
|
var map_id: String = str(player.get("mapId", ""))
|
||||||
|
_target_select.add_item("%s · %s" % [username, map_id])
|
||||||
|
_target_select.set_item_metadata(_target_select.item_count - 1, user_id)
|
||||||
|
|
||||||
|
func _selected_option(select: OptionButton) -> String:
|
||||||
|
if select.selected < 0:
|
||||||
|
return ""
|
||||||
|
return str(select.get_item_metadata(select.selected))
|
||||||
|
|
||||||
|
func _selected_target_id() -> String:
|
||||||
|
return _selected_option(_target_select)
|
||||||
|
|
||||||
|
func _select_option(select: OptionButton, value: String) -> void:
|
||||||
|
for index: int in range(select.item_count):
|
||||||
|
if str(select.get_item_metadata(index)) == value:
|
||||||
|
select.select(index)
|
||||||
|
return
|
||||||
|
if select.item_count > 0:
|
||||||
|
select.select(0)
|
||||||
|
|
||||||
|
func _ensure_selected_actor() -> bool:
|
||||||
|
if not _selected_actor_id.is_empty():
|
||||||
|
return true
|
||||||
|
_set_notice("请先从左侧选择测试假人", DANGER_COLOR.lightened(0.18))
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _set_notice(message: String, color: Color) -> void:
|
||||||
|
_notice_label.visible = not message.strip_edges().is_empty()
|
||||||
|
_notice_label.text = message
|
||||||
|
_notice_label.add_theme_color_override("font_color", color)
|
||||||
|
|
||||||
|
func _clear_children(node: Node) -> void:
|
||||||
|
for child: Node in node.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
func _is_admin() -> bool:
|
||||||
|
var manager := _manager()
|
||||||
|
return manager != null and manager.has_method("is_admin") and bool(manager.call("is_admin"))
|
||||||
|
|
||||||
|
func _manager() -> Node:
|
||||||
|
return get_node_or_null("/root/TestLabManager")
|
||||||
|
|
||||||
|
func _connect_signal(source: Node, signal_name: StringName, callback: Callable) -> void:
|
||||||
|
if source.has_signal(signal_name) and not source.is_connected(signal_name, callback):
|
||||||
|
source.connect(signal_name, callback)
|
||||||
|
|
||||||
|
func _disconnect_signal(source: Node, signal_name: StringName, callback: Callable) -> void:
|
||||||
|
if source.has_signal(signal_name) and source.is_connected(signal_name, callback):
|
||||||
|
source.disconnect(signal_name, callback)
|
||||||
1
scenes/ui/AdminTestLabPanel.gd.uid
Normal file
1
scenes/ui/AdminTestLabPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cma7is74nvb3u
|
||||||
@@ -62,6 +62,9 @@ func show_panel() -> void:
|
|||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
set_process(true)
|
set_process(true)
|
||||||
_releaseMovementInputState()
|
_releaseMovementInputState()
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "course_board_opened")
|
||||||
_fetchCourses()
|
_fetchCourses()
|
||||||
_animatePanel(true)
|
_animatePanel(true)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ func _draw() -> void:
|
|||||||
_draw_friends()
|
_draw_friends()
|
||||||
"settings":
|
"settings":
|
||||||
_draw_settings()
|
_draw_settings()
|
||||||
|
"admin":
|
||||||
|
_draw_admin()
|
||||||
_:
|
_:
|
||||||
_draw_map()
|
_draw_map()
|
||||||
|
|
||||||
@@ -101,6 +103,13 @@ func _draw_settings() -> void:
|
|||||||
_draw_arc(center, 4.9, 0.0, TAU, 32, LINE_WIDTH)
|
_draw_arc(center, 4.9, 0.0, TAU, 32, LINE_WIDTH)
|
||||||
_draw_arc(center, 2.0, 0.0, TAU, 24, DETAIL_WIDTH)
|
_draw_arc(center, 2.0, 0.0, TAU, 24, DETAIL_WIDTH)
|
||||||
|
|
||||||
|
func _draw_admin() -> void:
|
||||||
|
_draw_round_rect(Rect2(7.0, 5.8, 13.0, 15.6), 3.0)
|
||||||
|
_draw_line(Vector2(9.5, 11.0), Vector2(17.5, 11.0), DETAIL_WIDTH)
|
||||||
|
_draw_line(Vector2(9.5, 14.5), Vector2(15.3, 14.5), DETAIL_WIDTH)
|
||||||
|
_draw_arc(Vector2(13.5, 20.5), 3.5, PI, TAU, 16, DETAIL_WIDTH)
|
||||||
|
_draw_red_dot(Vector2(20.2, 6.0))
|
||||||
|
|
||||||
func _draw_round_rect(rect: Rect2, radius: float, width: float = LINE_WIDTH) -> void:
|
func _draw_round_rect(rect: Rect2, radius: float, width: float = LINE_WIDTH) -> void:
|
||||||
var left := rect.position.x
|
var left := rect.position.x
|
||||||
var top := rect.position.y
|
var top := rect.position.y
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ extends Control
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
const HUD_MARGIN: Vector2 = Vector2(16, 16)
|
const HUD_MARGIN: Vector2 = Vector2(16, 16)
|
||||||
const SHORTCUT_BAR_SIZE: Vector2 = Vector2(406, 86)
|
const SHORTCUT_BAR_SIZE: Vector2 = Vector2(476, 86)
|
||||||
const PLAYER_PROFILE_BUTTON_SIZE: Vector2 = Vector2(210, 78)
|
const PLAYER_PROFILE_BUTTON_SIZE: Vector2 = Vector2(210, 78)
|
||||||
const PLAYER_AVATAR_SIZE: Vector2 = Vector2(58, 58)
|
const PLAYER_AVATAR_SIZE: Vector2 = Vector2(58, 58)
|
||||||
const HUD_SEPARATION: int = 18
|
const HUD_SEPARATION: int = 18
|
||||||
@@ -24,6 +24,10 @@ var _walletLabel: Label
|
|||||||
var _avatarLabel: Label
|
var _avatarLabel: Label
|
||||||
var _avatarPanel: PanelContainer
|
var _avatarPanel: PanelContainer
|
||||||
var _walletRefreshTimer: Timer
|
var _walletRefreshTimer: Timer
|
||||||
|
var _taskShortcutButton: Button
|
||||||
|
var _taskRewardBadge: Label
|
||||||
|
var _shortcutRow: HBoxContainer
|
||||||
|
var _adminShortcutButton: Button
|
||||||
|
|
||||||
var _currentUsername: String = "玩家"
|
var _currentUsername: String = "玩家"
|
||||||
var _walletBalance: int = 0
|
var _walletBalance: int = 0
|
||||||
@@ -37,6 +41,7 @@ func _ready() -> void:
|
|||||||
_subscribe_to_events()
|
_subscribe_to_events()
|
||||||
_load_current_user()
|
_load_current_user()
|
||||||
_refresh_wallet()
|
_refresh_wallet()
|
||||||
|
call_deferred("_refresh_admin_shortcut")
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
var eventSystem := _get_event_system()
|
var eventSystem := _get_event_system()
|
||||||
@@ -59,6 +64,11 @@ func _exit_tree() -> void:
|
|||||||
var callback := Callable(self, "_on_auth_state_changed")
|
var callback := Callable(self, "_on_auth_state_changed")
|
||||||
if authManager.is_connected("auth_state_changed", callback):
|
if authManager.is_connected("auth_state_changed", callback):
|
||||||
authManager.disconnect("auth_state_changed", callback)
|
authManager.disconnect("auth_state_changed", callback)
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_signal("board_changed"):
|
||||||
|
var taskCallback := Callable(self, "_on_task_board_changed")
|
||||||
|
if taskManager.is_connected("board_changed", taskCallback):
|
||||||
|
taskManager.disconnect("board_changed", taskCallback)
|
||||||
|
|
||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
var rootRow := HBoxContainer.new()
|
var rootRow := HBoxContainer.new()
|
||||||
@@ -87,16 +97,35 @@ func _build_shortcut_bar() -> PanelContainer:
|
|||||||
panel.add_child(margin)
|
panel.add_child(margin)
|
||||||
|
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
|
_shortcutRow = row
|
||||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
margin.add_child(row)
|
margin.add_child(row)
|
||||||
|
|
||||||
row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed))
|
row.add_child(_create_shortcut_button("map", "地图", _on_map_pressed))
|
||||||
row.add_child(_create_shortcut_button("task", "任务", _on_task_pressed))
|
_taskShortcutButton = _create_shortcut_button("task", "任务", _on_task_pressed)
|
||||||
|
_taskShortcutButton.tooltip_text = "打开任务书"
|
||||||
|
_taskRewardBadge = Label.new()
|
||||||
|
_taskRewardBadge.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_taskRewardBadge.visible = false
|
||||||
|
_taskRewardBadge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_taskRewardBadge.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_taskRewardBadge.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
_taskRewardBadge.add_theme_font_size_override("font_size", 12)
|
||||||
|
_taskRewardBadge.add_theme_stylebox_override("normal", _create_pill_style(Color(0.92, 0.30, 0.24), 12))
|
||||||
|
_taskRewardBadge.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
|
_taskRewardBadge.offset_left = -20
|
||||||
|
_taskRewardBadge.offset_top = 1
|
||||||
|
_taskRewardBadge.offset_right = 4
|
||||||
|
_taskRewardBadge.offset_bottom = 25
|
||||||
|
_taskRewardBadge.z_index = 1
|
||||||
|
_taskShortcutButton.add_child(_taskRewardBadge)
|
||||||
|
row.add_child(_taskShortcutButton)
|
||||||
row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed))
|
row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed))
|
||||||
row.add_child(_create_shortcut_button("friends", "好友", _on_friends_pressed))
|
row.add_child(_create_shortcut_button("friends", "好友", _on_friends_pressed))
|
||||||
row.add_child(_create_shortcut_button("activity", "通知", _on_notifications_pressed))
|
row.add_child(_create_shortcut_button("activity", "通知", _on_notifications_pressed))
|
||||||
row.add_child(_create_shortcut_button("settings", "设置", _on_settings_pressed))
|
row.add_child(_create_shortcut_button("settings", "设置", _on_settings_pressed))
|
||||||
|
_refresh_admin_shortcut()
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
|
|
||||||
@@ -240,6 +269,15 @@ func _subscribe_to_events() -> void:
|
|||||||
var callback := Callable(self, "_on_auth_state_changed")
|
var callback := Callable(self, "_on_auth_state_changed")
|
||||||
if not authManager.is_connected("auth_state_changed", callback):
|
if not authManager.is_connected("auth_state_changed", callback):
|
||||||
authManager.connect("auth_state_changed", callback)
|
authManager.connect("auth_state_changed", callback)
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_signal("board_changed"):
|
||||||
|
var taskCallback := Callable(self, "_on_task_board_changed")
|
||||||
|
if not taskManager.is_connected("board_changed", taskCallback):
|
||||||
|
taskManager.connect("board_changed", taskCallback)
|
||||||
|
if taskManager.has_method("get_board"):
|
||||||
|
var boardVariant: Variant = taskManager.call("get_board")
|
||||||
|
if boardVariant is Dictionary:
|
||||||
|
_on_task_board_changed(boardVariant as Dictionary)
|
||||||
|
|
||||||
func _load_current_user() -> void:
|
func _load_current_user() -> void:
|
||||||
var authManager := get_node_or_null("/root/AuthManager")
|
var authManager := get_node_or_null("/root/AuthManager")
|
||||||
@@ -263,9 +301,11 @@ func _on_auth_state_changed(isAuthenticated: bool, user: Dictionary) -> void:
|
|||||||
if not isAuthenticated:
|
if not isAuthenticated:
|
||||||
_set_username("玩家")
|
_set_username("玩家")
|
||||||
_reset_wallet()
|
_reset_wallet()
|
||||||
|
_refresh_admin_shortcut()
|
||||||
return
|
return
|
||||||
_apply_auth_user_payload({"user": user})
|
_apply_auth_user_payload({"user": user})
|
||||||
_refresh_wallet()
|
_refresh_wallet()
|
||||||
|
_refresh_admin_shortcut()
|
||||||
|
|
||||||
func _apply_auth_user_payload(data: Dictionary) -> void:
|
func _apply_auth_user_payload(data: Dictionary) -> void:
|
||||||
var userVariant: Variant = data.get("user", {})
|
var userVariant: Variant = data.get("user", {})
|
||||||
@@ -273,10 +313,51 @@ func _apply_auth_user_payload(data: Dictionary) -> void:
|
|||||||
var username := str((userVariant as Dictionary).get("username", "")).strip_edges()
|
var username := str((userVariant as Dictionary).get("username", "")).strip_edges()
|
||||||
if not username.is_empty():
|
if not username.is_empty():
|
||||||
_set_username(username)
|
_set_username(username)
|
||||||
|
_refresh_admin_shortcut()
|
||||||
|
|
||||||
func _on_auth_logout(_data: Variant = null) -> void:
|
func _on_auth_logout(_data: Variant = null) -> void:
|
||||||
_set_username("玩家")
|
_set_username("玩家")
|
||||||
_reset_wallet()
|
_reset_wallet()
|
||||||
|
_on_task_board_changed({})
|
||||||
|
_refresh_admin_shortcut()
|
||||||
|
|
||||||
|
func _on_task_board_changed(board: Dictionary) -> void:
|
||||||
|
if not is_instance_valid(_taskShortcutButton):
|
||||||
|
return
|
||||||
|
var claimableCount: int = _count_claimable_task_rewards(board)
|
||||||
|
if claimableCount > 0:
|
||||||
|
_taskShortcutButton.tooltip_text = "任务书:有 %d 项奖励可领取" % claimableCount
|
||||||
|
_taskShortcutButton.modulate = Color(1.0, 0.855, 0.470, 1.0)
|
||||||
|
if is_instance_valid(_taskRewardBadge):
|
||||||
|
_taskRewardBadge.text = str(mini(claimableCount, 9)) if claimableCount < 10 else "9+"
|
||||||
|
_taskRewardBadge.visible = true
|
||||||
|
return
|
||||||
|
_taskShortcutButton.tooltip_text = "打开任务书"
|
||||||
|
_taskShortcutButton.modulate = Color.WHITE
|
||||||
|
if is_instance_valid(_taskRewardBadge):
|
||||||
|
_taskRewardBadge.visible = false
|
||||||
|
|
||||||
|
func _count_claimable_task_rewards(board: Dictionary) -> int:
|
||||||
|
var claimableCount: int = 0
|
||||||
|
claimableCount += _count_claimable_tasks(board.get("newbie_tasks", []))
|
||||||
|
claimableCount += _count_claimable_tasks(board.get("weekly_tasks", []))
|
||||||
|
var weeklyBonusVariant: Variant = board.get("weekly_bonus", {})
|
||||||
|
if weeklyBonusVariant is Dictionary:
|
||||||
|
var weeklyBonus: Dictionary = weeklyBonusVariant as Dictionary
|
||||||
|
if bool(weeklyBonus.get("claimable", false)):
|
||||||
|
claimableCount += 1
|
||||||
|
return claimableCount
|
||||||
|
|
||||||
|
func _count_claimable_tasks(tasksVariant: Variant) -> int:
|
||||||
|
if not (tasksVariant is Array):
|
||||||
|
return 0
|
||||||
|
var claimableCount: int = 0
|
||||||
|
for taskVariant: Variant in tasksVariant as Array:
|
||||||
|
if taskVariant is Dictionary:
|
||||||
|
var task: Dictionary = taskVariant as Dictionary
|
||||||
|
if bool(task.get("claimable", false)):
|
||||||
|
claimableCount += 1
|
||||||
|
return claimableCount
|
||||||
|
|
||||||
func _on_appearance_avatar_changed(_data: Dictionary) -> void:
|
func _on_appearance_avatar_changed(_data: Dictionary) -> void:
|
||||||
_apply_current_avatar()
|
_apply_current_avatar()
|
||||||
@@ -386,7 +467,9 @@ func _on_notifications_pressed() -> void:
|
|||||||
socialManager.call("toggle_notifications")
|
socialManager.call("toggle_notifications")
|
||||||
|
|
||||||
func _on_task_pressed() -> void:
|
func _on_task_pressed() -> void:
|
||||||
_emit_status_message("任务入口稍后接入")
|
var eventSystem := _get_event_system()
|
||||||
|
if eventSystem != null:
|
||||||
|
eventSystem.call("emit_event", EventNames.HUD_TASK_BOOK_REQUESTED, {})
|
||||||
|
|
||||||
func _on_backpack_pressed() -> void:
|
func _on_backpack_pressed() -> void:
|
||||||
var eventSystem := _get_event_system()
|
var eventSystem := _get_event_system()
|
||||||
@@ -408,6 +491,35 @@ func _on_avatar_pressed() -> void:
|
|||||||
if eventSystem != null:
|
if eventSystem != null:
|
||||||
eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {})
|
eventSystem.call("emit_event", EventNames.HUD_SETTINGS_REQUESTED, {})
|
||||||
|
|
||||||
|
func _on_admin_test_lab_pressed() -> void:
|
||||||
|
var eventSystem := _get_event_system()
|
||||||
|
if eventSystem != null:
|
||||||
|
eventSystem.call("emit_event", EventNames.HUD_ADMIN_TEST_LAB_REQUESTED, {})
|
||||||
|
|
||||||
|
func _refresh_admin_shortcut() -> void:
|
||||||
|
if not is_instance_valid(_shortcutRow):
|
||||||
|
return
|
||||||
|
var shouldShow: bool = _is_current_user_admin()
|
||||||
|
if shouldShow and not is_instance_valid(_adminShortcutButton):
|
||||||
|
_adminShortcutButton = _create_shortcut_button("admin", "管理", _on_admin_test_lab_pressed)
|
||||||
|
_adminShortcutButton.tooltip_text = "管理员测试实验室"
|
||||||
|
_shortcutRow.add_child(_adminShortcutButton)
|
||||||
|
elif not shouldShow and is_instance_valid(_adminShortcutButton):
|
||||||
|
_adminShortcutButton.queue_free()
|
||||||
|
_adminShortcutButton = null
|
||||||
|
|
||||||
|
func _is_current_user_admin() -> bool:
|
||||||
|
var authManager := get_node_or_null("/root/AuthManager")
|
||||||
|
if authManager == null or not authManager.has_method("is_authenticated") or not bool(authManager.call("is_authenticated")):
|
||||||
|
return false
|
||||||
|
if not authManager.has_method("get_current_user"):
|
||||||
|
return false
|
||||||
|
var userVariant: Variant = authManager.call("get_current_user")
|
||||||
|
if not (userVariant is Dictionary):
|
||||||
|
return false
|
||||||
|
var user: Dictionary = userVariant as Dictionary
|
||||||
|
return int(user.get("role", 0)) == 9
|
||||||
|
|
||||||
func _emit_status_message(message: String) -> void:
|
func _emit_status_message(message: String) -> void:
|
||||||
var eventSystem := _get_event_system()
|
var eventSystem := _get_event_system()
|
||||||
if eventSystem != null:
|
if eventSystem != null:
|
||||||
|
|||||||
337
scenes/ui/TaskBookPanel.gd
Normal file
337
scenes/ui/TaskBookPanel.gd
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const PANEL_SIZE := Vector2(980, 860)
|
||||||
|
const TEXT_COLOR := Color(0.10, 0.18, 0.27)
|
||||||
|
const MUTED_COLOR := Color(0.38, 0.48, 0.58)
|
||||||
|
const ACCENT_COLOR := Color(0.10, 0.49, 0.85)
|
||||||
|
const COIN_COLOR := Color(0.94, 0.58, 0.08)
|
||||||
|
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
|
||||||
|
const WELCOME_DIALOG_NAME := "WelcomeDialog"
|
||||||
|
|
||||||
|
var _root: Control
|
||||||
|
var _panel: PanelContainer
|
||||||
|
var _content: VBoxContainer
|
||||||
|
var _status_label: Label
|
||||||
|
var _is_open: bool = false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 32
|
||||||
|
add_to_group("whaletown_escape_dismissible")
|
||||||
|
_build_ui()
|
||||||
|
_connect_services()
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
var event_system := get_node_or_null("/root/EventSystem")
|
||||||
|
if event_system != null:
|
||||||
|
event_system.call("disconnect_event", EventNames.HUD_TASK_BOOK_REQUESTED, _on_task_book_requested, self)
|
||||||
|
var task_manager := get_node_or_null("/root/TaskManager")
|
||||||
|
if task_manager != null:
|
||||||
|
var board_callback := Callable(self, "_on_board_changed")
|
||||||
|
if task_manager.has_signal("board_changed") and task_manager.is_connected("board_changed", board_callback):
|
||||||
|
task_manager.disconnect("board_changed", board_callback)
|
||||||
|
var failed_callback := Callable(self, "_on_board_failed")
|
||||||
|
if task_manager.has_signal("board_failed") and task_manager.is_connected("board_failed", failed_callback):
|
||||||
|
task_manager.disconnect("board_failed", failed_callback)
|
||||||
|
|
||||||
|
func show_panel() -> void:
|
||||||
|
_is_open = true
|
||||||
|
_root.visible = true
|
||||||
|
var task_manager := get_node_or_null("/root/TaskManager")
|
||||||
|
if task_manager != null:
|
||||||
|
if task_manager.has_method("is_loaded") and bool(task_manager.call("is_loaded")):
|
||||||
|
var board_variant: Variant = task_manager.call("get_board")
|
||||||
|
if board_variant is Dictionary:
|
||||||
|
_render_board(board_variant as Dictionary)
|
||||||
|
if task_manager.has_method("refresh_board"):
|
||||||
|
task_manager.call("refresh_board")
|
||||||
|
|
||||||
|
func hide_panel() -> void:
|
||||||
|
_is_open = false
|
||||||
|
_root.visible = false
|
||||||
|
|
||||||
|
func is_escape_dismissible() -> bool:
|
||||||
|
return _is_open
|
||||||
|
|
||||||
|
func get_escape_priority() -> int:
|
||||||
|
return 760
|
||||||
|
|
||||||
|
func request_escape_close() -> void:
|
||||||
|
hide_panel()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not _is_open or get_viewport().is_input_handled():
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
hide_panel()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
func _connect_services() -> void:
|
||||||
|
var event_system := get_node_or_null("/root/EventSystem")
|
||||||
|
if event_system != null:
|
||||||
|
event_system.call("connect_event", EventNames.HUD_TASK_BOOK_REQUESTED, _on_task_book_requested, self)
|
||||||
|
var task_manager := get_node_or_null("/root/TaskManager")
|
||||||
|
if task_manager != null:
|
||||||
|
if task_manager.has_signal("board_changed"):
|
||||||
|
task_manager.connect("board_changed", _on_board_changed)
|
||||||
|
if task_manager.has_signal("board_failed"):
|
||||||
|
task_manager.connect("board_failed", _on_board_failed)
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
_root = Control.new()
|
||||||
|
_root.name = "TaskBookRoot"
|
||||||
|
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_root.visible = false
|
||||||
|
add_child(_root)
|
||||||
|
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
dim.color = Color(0.02, 0.05, 0.09, 0.68)
|
||||||
|
dim.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
dim.gui_input.connect(func(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
||||||
|
hide_panel()
|
||||||
|
)
|
||||||
|
_root.add_child(dim)
|
||||||
|
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.custom_minimum_size = PANEL_SIZE
|
||||||
|
_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
_panel.offset_left = -PANEL_SIZE.x * 0.5
|
||||||
|
_panel.offset_top = -PANEL_SIZE.y * 0.5
|
||||||
|
_panel.offset_right = PANEL_SIZE.x * 0.5
|
||||||
|
_panel.offset_bottom = PANEL_SIZE.y * 0.5
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.96, 0.985, 1.0, 1.0), 28))
|
||||||
|
_root.add_child(_panel)
|
||||||
|
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 28)
|
||||||
|
margin.add_theme_constant_override("margin_top", 24)
|
||||||
|
margin.add_theme_constant_override("margin_right", 28)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 24)
|
||||||
|
_panel.add_child(margin)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.add_theme_constant_override("separation", 16)
|
||||||
|
margin.add_child(layout)
|
||||||
|
layout.add_child(_build_header())
|
||||||
|
_status_label = Label.new()
|
||||||
|
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_status_label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
_status_label.add_theme_font_size_override("font_size", 15)
|
||||||
|
layout.add_child(_status_label)
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
layout.add_child(scroll)
|
||||||
|
_content = VBoxContainer.new()
|
||||||
|
_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_content.add_theme_constant_override("separation", 12)
|
||||||
|
scroll.add_child(_content)
|
||||||
|
var footer := Label.new()
|
||||||
|
footer.text = "[Esc] 关闭任务书"
|
||||||
|
footer.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
footer.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
footer.add_theme_font_size_override("font_size", 15)
|
||||||
|
layout.add_child(footer)
|
||||||
|
|
||||||
|
func _build_header() -> Control:
|
||||||
|
var header := PanelContainer.new()
|
||||||
|
header.custom_minimum_size = Vector2(0, 96)
|
||||||
|
header.add_theme_stylebox_override("panel", _panel_style(ACCENT_COLOR, 20))
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 24)
|
||||||
|
margin.add_theme_constant_override("margin_top", 14)
|
||||||
|
margin.add_theme_constant_override("margin_right", 16)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 14)
|
||||||
|
header.add_child(margin)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
margin.add_child(row)
|
||||||
|
var title_box := VBoxContainer.new()
|
||||||
|
title_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(title_box)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "鲸镇任务书"
|
||||||
|
title.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
title_box.add_child(title)
|
||||||
|
var subtitle := Label.new()
|
||||||
|
subtitle.text = "完成探索、学习与社交任务,领取鲸币奖励"
|
||||||
|
subtitle.add_theme_color_override("font_color", Color(0.88, 0.95, 1.0, 1.0))
|
||||||
|
subtitle.add_theme_font_size_override("font_size", 16)
|
||||||
|
title_box.add_child(subtitle)
|
||||||
|
var guide := Button.new()
|
||||||
|
guide.text = "新人引导"
|
||||||
|
guide.focus_mode = Control.FOCUS_NONE
|
||||||
|
guide.custom_minimum_size = Vector2(104, 42)
|
||||||
|
guide.add_theme_stylebox_override("normal", _panel_style(Color(1, 1, 1, 0.18), 14))
|
||||||
|
guide.add_theme_stylebox_override("hover", _panel_style(Color(1, 1, 1, 0.30), 14))
|
||||||
|
guide.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
guide.pressed.connect(_on_guide_pressed)
|
||||||
|
row.add_child(guide)
|
||||||
|
var close := Button.new()
|
||||||
|
close.text = "关闭"
|
||||||
|
close.focus_mode = Control.FOCUS_NONE
|
||||||
|
close.custom_minimum_size = Vector2(84, 42)
|
||||||
|
close.add_theme_stylebox_override("normal", _panel_style(Color(1, 1, 1, 0.18), 14))
|
||||||
|
close.add_theme_stylebox_override("hover", _panel_style(Color(1, 1, 1, 0.30), 14))
|
||||||
|
close.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
close.pressed.connect(hide_panel)
|
||||||
|
row.add_child(close)
|
||||||
|
return header
|
||||||
|
|
||||||
|
func _on_task_book_requested(_data: Dictionary = {}) -> void:
|
||||||
|
show_panel()
|
||||||
|
|
||||||
|
func _on_guide_pressed() -> void:
|
||||||
|
hide_panel()
|
||||||
|
var root: Window = get_tree().root
|
||||||
|
var dialog: Node = root.get_node_or_null(WELCOME_DIALOG_NAME)
|
||||||
|
if dialog == null:
|
||||||
|
dialog = WELCOME_DIALOG_SCENE.instantiate()
|
||||||
|
dialog.name = WELCOME_DIALOG_NAME
|
||||||
|
root.add_child(dialog)
|
||||||
|
if dialog.has_method("_showGuide"):
|
||||||
|
dialog.call_deferred("_showGuide")
|
||||||
|
|
||||||
|
func _on_board_changed(board: Dictionary) -> void:
|
||||||
|
if _is_open:
|
||||||
|
_render_board(board)
|
||||||
|
|
||||||
|
func _on_board_failed(message: String) -> void:
|
||||||
|
if _is_open and is_instance_valid(_status_label):
|
||||||
|
_status_label.text = message
|
||||||
|
_status_label.add_theme_color_override("font_color", Color(0.86, 0.28, 0.24))
|
||||||
|
|
||||||
|
func _render_board(board: Dictionary) -> void:
|
||||||
|
_clear_content()
|
||||||
|
if board.is_empty():
|
||||||
|
_status_label.text = "正在读取任务书…"
|
||||||
|
return
|
||||||
|
_status_label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
var cycle_variant: Variant = board.get("weekly_cycle", {})
|
||||||
|
var cycle: Dictionary = cycle_variant as Dictionary if cycle_variant is Dictionary else {}
|
||||||
|
_status_label.text = "周常重置:%s" % _format_reset(str(cycle.get("ends_at", "")))
|
||||||
|
_add_section("新人主线", "可自由完成,首次购买皮肤为可跳过任务")
|
||||||
|
_add_tasks(board.get("newbie_tasks", []))
|
||||||
|
_add_section("本周任务", "四项全部完成后可领取额外 200 鲸币")
|
||||||
|
_add_tasks(board.get("weekly_tasks", []))
|
||||||
|
var bonus_variant: Variant = board.get("weekly_bonus", {})
|
||||||
|
if bonus_variant is Dictionary:
|
||||||
|
_content.add_child(_build_task_card(bonus_variant as Dictionary))
|
||||||
|
|
||||||
|
func _add_section(title_text: String, description: String) -> void:
|
||||||
|
var section := VBoxContainer.new()
|
||||||
|
section.add_theme_constant_override("separation", 2)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = title_text
|
||||||
|
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
title.add_theme_font_size_override("font_size", 23)
|
||||||
|
section.add_child(title)
|
||||||
|
var subtitle := Label.new()
|
||||||
|
subtitle.text = description
|
||||||
|
subtitle.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
subtitle.add_theme_font_size_override("font_size", 15)
|
||||||
|
section.add_child(subtitle)
|
||||||
|
_content.add_child(section)
|
||||||
|
|
||||||
|
func _add_tasks(tasks_variant: Variant) -> void:
|
||||||
|
if not (tasks_variant is Array):
|
||||||
|
return
|
||||||
|
for task_variant: Variant in tasks_variant as Array:
|
||||||
|
if task_variant is Dictionary:
|
||||||
|
_content.add_child(_build_task_card(task_variant as Dictionary))
|
||||||
|
|
||||||
|
func _build_task_card(task: Dictionary) -> Control:
|
||||||
|
var completed: bool = bool(task.get("completed", false))
|
||||||
|
var claimed: bool = bool(task.get("claimed", false))
|
||||||
|
var optional: bool = bool(task.get("optional", false))
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
card.add_theme_stylebox_override("panel", _panel_style(Color(0.90, 0.96, 1.0, 0.92) if completed else Color.WHITE, 18))
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 18)
|
||||||
|
margin.add_theme_constant_override("margin_top", 14)
|
||||||
|
margin.add_theme_constant_override("margin_right", 18)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 14)
|
||||||
|
card.add_child(margin)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 14)
|
||||||
|
margin.add_child(row)
|
||||||
|
var state := Label.new()
|
||||||
|
state.custom_minimum_size = Vector2(30, 0)
|
||||||
|
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
state.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
state.text = "✓" if claimed else ("●" if completed else "○")
|
||||||
|
state.add_theme_color_override("font_color", Color(0.17, 0.66, 0.41) if completed else MUTED_COLOR)
|
||||||
|
state.add_theme_font_size_override("font_size", 24)
|
||||||
|
row.add_child(state)
|
||||||
|
var detail := VBoxContainer.new()
|
||||||
|
detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
detail.add_theme_constant_override("separation", 4)
|
||||||
|
row.add_child(detail)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "%s%s" % [str(task.get("title", "任务")), "(可跳过)" if optional else ""]
|
||||||
|
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||||
|
title.add_theme_font_size_override("font_size", 19)
|
||||||
|
detail.add_child(title)
|
||||||
|
var description := Label.new()
|
||||||
|
description.text = str(task.get("description", ""))
|
||||||
|
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
description.add_theme_color_override("font_color", MUTED_COLOR)
|
||||||
|
description.add_theme_font_size_override("font_size", 15)
|
||||||
|
detail.add_child(description)
|
||||||
|
var progress: int = int(task.get("progress", 0))
|
||||||
|
var target: int = max(1, int(task.get("target", 1)))
|
||||||
|
var progress_label := Label.new()
|
||||||
|
progress_label.text = "进度 %d / %d" % [min(progress, target), target]
|
||||||
|
progress_label.add_theme_color_override("font_color", ACCENT_COLOR)
|
||||||
|
progress_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
detail.add_child(progress_label)
|
||||||
|
var reward := Label.new()
|
||||||
|
reward.text = "+%d 鲸币" % int(task.get("reward", 0))
|
||||||
|
reward.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
reward.add_theme_color_override("font_color", COIN_COLOR)
|
||||||
|
reward.add_theme_font_size_override("font_size", 17)
|
||||||
|
row.add_child(reward)
|
||||||
|
var action := Button.new()
|
||||||
|
action.focus_mode = Control.FOCUS_NONE
|
||||||
|
action.custom_minimum_size = Vector2(88, 42)
|
||||||
|
if claimed:
|
||||||
|
action.text = "已领取"
|
||||||
|
action.disabled = true
|
||||||
|
elif completed:
|
||||||
|
action.text = "领取"
|
||||||
|
action.pressed.connect(func() -> void:
|
||||||
|
var task_manager := get_node_or_null("/root/TaskManager")
|
||||||
|
if task_manager != null and task_manager.has_method("claim_task"):
|
||||||
|
task_manager.call("claim_task", str(task.get("id", "")))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
action.text = "进行中"
|
||||||
|
action.disabled = true
|
||||||
|
action.add_theme_stylebox_override("normal", _panel_style(ACCENT_COLOR if completed and not claimed else Color(0.80, 0.85, 0.89), 12))
|
||||||
|
action.add_theme_stylebox_override("hover", _panel_style(Color(0.06, 0.39, 0.71), 12))
|
||||||
|
action.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
row.add_child(action)
|
||||||
|
return card
|
||||||
|
|
||||||
|
func _clear_content() -> void:
|
||||||
|
for child: Node in _content.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
func _format_reset(value: String) -> String:
|
||||||
|
if value.is_empty():
|
||||||
|
return "下周一 00:00"
|
||||||
|
return value.replace("T", " ").left(16)
|
||||||
|
|
||||||
|
func _panel_style(color: Color, radius: int) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = color
|
||||||
|
style.corner_radius_top_left = radius
|
||||||
|
style.corner_radius_top_right = radius
|
||||||
|
style.corner_radius_bottom_left = radius
|
||||||
|
style.corner_radius_bottom_right = radius
|
||||||
|
style.shadow_color = Color(0.04, 0.13, 0.22, 0.24)
|
||||||
|
style.shadow_size = 16
|
||||||
|
style.shadow_offset = Vector2(0, 6)
|
||||||
|
return style
|
||||||
1
scenes/ui/TaskBookPanel.gd.uid
Normal file
1
scenes/ui/TaskBookPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d2b7h12k8nqoi
|
||||||
@@ -34,6 +34,7 @@ var _guideFooter: HBoxContainer
|
|||||||
var _guidePrevButton: Button
|
var _guidePrevButton: Button
|
||||||
var _guideNextButton: Button
|
var _guideNextButton: Button
|
||||||
var _guideDotsContainer: HBoxContainer
|
var _guideDotsContainer: HBoxContainer
|
||||||
|
var _guideTaskBookButton: Button
|
||||||
var _guideCurrentPage: int = 0
|
var _guideCurrentPage: int = 0
|
||||||
var _guideTween: Tween
|
var _guideTween: Tween
|
||||||
|
|
||||||
@@ -75,6 +76,9 @@ func _onStartPressed() -> void:
|
|||||||
_showGuide()
|
_showGuide()
|
||||||
|
|
||||||
func _showGuide() -> void:
|
func _showGuide() -> void:
|
||||||
|
var taskManager := get_node_or_null("/root/TaskManager")
|
||||||
|
if taskManager != null and taskManager.has_method("report_activity"):
|
||||||
|
taskManager.call("report_activity", "guide_opened")
|
||||||
panelContainer.custom_minimum_size = Vector2(720, 560)
|
panelContainer.custom_minimum_size = Vector2(720, 560)
|
||||||
titleLabel.text = "新人引导手册"
|
titleLabel.text = "新人引导手册"
|
||||||
logoContainer.visible = false
|
logoContainer.visible = false
|
||||||
@@ -162,6 +166,12 @@ func _buildGuideUi() -> void:
|
|||||||
_guideNextButton.pressed.connect(_onGuideNextPressed)
|
_guideNextButton.pressed.connect(_onGuideNextPressed)
|
||||||
_guideFooter.add_child(_guideNextButton)
|
_guideFooter.add_child(_guideNextButton)
|
||||||
|
|
||||||
|
_guideTaskBookButton = _createGuideButton("任务书")
|
||||||
|
_guideTaskBookButton.name = "TaskBookButton"
|
||||||
|
_guideTaskBookButton.custom_minimum_size = Vector2(96, 44)
|
||||||
|
_guideTaskBookButton.pressed.connect(_onGuideTaskBookPressed)
|
||||||
|
_guideFooter.add_child(_guideTaskBookButton)
|
||||||
|
|
||||||
func _createGuideButton(text: String) -> Button:
|
func _createGuideButton(text: String) -> Button:
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.custom_minimum_size = Vector2(56, 44)
|
button.custom_minimum_size = Vector2(56, 44)
|
||||||
@@ -285,6 +295,12 @@ func _onGuideNextPressed() -> void:
|
|||||||
_guideCurrentPage += 1
|
_guideCurrentPage += 1
|
||||||
_updateGuideUi()
|
_updateGuideUi()
|
||||||
|
|
||||||
|
func _onGuideTaskBookPressed() -> void:
|
||||||
|
var taskBook := get_node_or_null("/root/TaskBookPanel")
|
||||||
|
if taskBook != null and taskBook.has_method("show_panel"):
|
||||||
|
taskBook.call("show_panel")
|
||||||
|
queue_free()
|
||||||
|
|
||||||
func _disableChatUiMouseInput() -> void:
|
func _disableChatUiMouseInput() -> void:
|
||||||
var currentScene := get_tree().current_scene
|
var currentScene := get_tree().current_scene
|
||||||
if currentScene != null:
|
if currentScene != null:
|
||||||
|
|||||||
Reference in New Issue
Block a user