@@ -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 )