forked from xiangwang25/whale-town-front-v2
feat: add in-game social test lab
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user