365 lines
14 KiB
GDScript
365 lines
14 KiB
GDScript
# GameOptionUI (P11) —— 游戏设置窗(1:1 迁移 `assets/root/uigameoption.py` `OptionDialog`)。
|
||
#
|
||
# var go := preload("res://ui/game_option_ui.gd").new()
|
||
# add_child(go)
|
||
# go.setup(ui_manager, m2client, assets_root)
|
||
# go.toggle() # 由 system_menu_ui 的 game_option_button 打开
|
||
#
|
||
# 布局走真 `assets/uiscript/uiscript/gameoptiondialog.py`。绑定逐字对照 uigameoption.py:
|
||
# block_{exchange,party,guild,whisper,friend,party_request}_button (toggle) →
|
||
# `/setblockmode <mask ^ bit>`(EBlockAction 位:1<<0..1<<5)—— 真聊天命令
|
||
# pvp_{peace,revenge,guild,free} (radio) → `/pkmode {0,1,4,2}` —— 真聊天命令
|
||
# name_color / target_board / view_chat / always_show_name / show_damage / salestext (radio)
|
||
# —— 客户端显示开关,持久化到 user://system_option.cfg [gameopt];渲染节点按需读取这些设置。
|
||
extends Node
|
||
|
||
const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd")
|
||
# Packet.h EBlockAction
|
||
const BLOCK_BITS := {
|
||
"block_exchange_button": 1 << 0,
|
||
"block_party_button": 1 << 1,
|
||
"block_guild_button": 1 << 2,
|
||
"block_whisper_button": 1 << 3,
|
||
"block_friend_button": 1 << 4,
|
||
"block_party_request_button": 1 << 5,
|
||
}
|
||
# uigameoption: peace /pkmode 0, revenge /pkmode 1, free /pkmode 2, guild /pkmode 4
|
||
const PK_CMD := {"pvp_peace": 0, "pvp_revenge": 1, "pvp_free": 2, "pvp_guild": 4}
|
||
|
||
# 客户端显示开关:{radio 名 -> [cfg key, 该按钮代表的值]}
|
||
const DISPLAY_RADIOS := {
|
||
"name_color_normal": ["name_color", 0], "name_color_empire": ["name_color", 1],
|
||
"target_board_no_view": ["target_board", 0], "target_board_view": ["target_board", 1],
|
||
"view_chat_on_button": ["view_chat", 1], "view_chat_off_button": ["view_chat", 0],
|
||
"always_show_name_on_button": ["always_show_name", 1], "always_show_name_off_button": ["always_show_name", 0],
|
||
"show_damage_on_button": ["show_damage", 1], "show_damage_off_button": ["show_damage", 0],
|
||
"salestext_on_button": ["salestext", 1], "salestext_off_button": ["salestext", 0],
|
||
}
|
||
const RADIO_GROUPS := [
|
||
["name_color_normal", "name_color_empire"],
|
||
["target_board_no_view", "target_board_view"],
|
||
["view_chat_on_button", "view_chat_off_button"],
|
||
["always_show_name_on_button", "always_show_name_off_button"],
|
||
["show_damage_on_button", "show_damage_off_button"],
|
||
["salestext_on_button", "salestext_off_button"],
|
||
]
|
||
|
||
const LABELS := {
|
||
"titlename": "游戏设置", "name_color": "名字颜色", "target_board": "目标框",
|
||
"pvp_mode": "PK 模式", "block": "屏蔽", "chat": "聊天显示",
|
||
"always_show_name": "总显示名字", "effect_on_off": "伤害数字", "salestext_on_off": "叫卖文字",
|
||
"name_color_normal": "普通", "name_color_empire": "阵营",
|
||
"target_board_no_view": "不看他国", "target_board_view": "看他国",
|
||
"pvp_peace": "和平", "pvp_revenge": "反击", "pvp_guild": "帮会", "pvp_free": "自由",
|
||
"block_exchange_button": "交易", "block_party_button": "组队", "block_guild_button": "帮会",
|
||
"block_whisper_button": "密语", "block_friend_button": "好友", "block_party_request_button": "组队申请",
|
||
"view_chat_on_button": "开", "view_chat_off_button": "关",
|
||
"always_show_name_on_button": "开", "always_show_name_off_button": "关",
|
||
"show_damage_on_button": "开", "show_damage_off_button": "关",
|
||
"salestext_on_button": "开", "salestext_off_button": "关",
|
||
}
|
||
|
||
const CFG_PATH := "user://system_option.cfg"
|
||
|
||
# uigameoption RefreshAlwaysShowName / 名字色 / 伤害数字等:radio 选中后广播给渲染层。
|
||
# key ∈ DISPLAY_RADIOS 的 cfg key(name_color / always_show_name / show_damage / ...)。
|
||
signal display_option_changed(key: String, value: int)
|
||
|
||
var ui: CanvasLayer
|
||
var client: Node
|
||
var assets_root := ""
|
||
var uiscript_dir := ""
|
||
|
||
var _win: Dictionary = {}
|
||
var _cfg := ConfigFile.new()
|
||
var _block_mode := 0 # blockMode(本地跟踪 + 服务端回包同步)
|
||
var _display := {} # cfg [gameopt] 快照
|
||
var _mobile_mode := false
|
||
var _mobile_root: Control
|
||
|
||
static func config_path() -> String:
|
||
var override_dir := OS.get_environment("MT_CONFIG_DIR")
|
||
if override_dir != "":
|
||
return override_dir.path_join("system_option.cfg")
|
||
return OS.get_temp_dir().path_join("mtgodot_system_option.cfg") \
|
||
if DisplayServer.get_name() == "headless" else CFG_PATH
|
||
|
||
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void:
|
||
ui = ui_manager
|
||
client = m2client
|
||
assets_root = assets
|
||
if assets_root == "" and ui and "assets_root" in ui:
|
||
assets_root = ui.assets_root
|
||
uiscript_dir = assets_root.path_join("uiscript/uiscript")
|
||
if not DirAccess.dir_exists_absolute(uiscript_dir):
|
||
uiscript_dir = assets_root.path_join("uiscript")
|
||
_cfg.load(config_path())
|
||
for k in ["name_color", "target_board", "view_chat", "always_show_name", "show_damage", "salestext"]:
|
||
_display[k] = int(_cfg.get_value("gameopt", k, 1 if k in ["target_board", "view_chat", "always_show_name", "show_damage"] else 0))
|
||
_block_mode = int(_cfg.get_value("gameopt", "block_mode", 0))
|
||
if client and client.has_signal("block_mode_changed"):
|
||
client.block_mode_changed.connect(_on_server_block_mode)
|
||
|
||
func is_open() -> bool:
|
||
if _mobile_mode:
|
||
return _mobile_root != null and is_instance_valid(_mobile_root)
|
||
return not _win.is_empty() and is_instance_valid(_win.get("root"))
|
||
|
||
func set_mobile_mode(enabled: bool) -> void:
|
||
if not enabled and _mobile_root != null:
|
||
var old_mode := _mobile_mode
|
||
_mobile_mode = true
|
||
close()
|
||
_mobile_mode = old_mode
|
||
_mobile_mode = enabled
|
||
|
||
# 渲染层开局回填用:cfg [gameopt] 里某显示开关的当前值(0/1),缺省同 setup() 的默认。
|
||
func display_value(key: String) -> int:
|
||
return int(_display.get(key, 0))
|
||
|
||
func toggle() -> void:
|
||
if is_open(): close()
|
||
else: open()
|
||
|
||
func close() -> void:
|
||
if _mobile_mode:
|
||
if _mobile_root and is_instance_valid(_mobile_root) and ui:
|
||
ui.close(_mobile_root)
|
||
_mobile_root = null
|
||
return
|
||
if is_open():
|
||
ui.close(_win["root"])
|
||
_win = {}
|
||
|
||
func open() -> void:
|
||
if _mobile_mode:
|
||
_open_mobile()
|
||
return
|
||
if is_open():
|
||
return
|
||
var path := uiscript_dir.path_join("gameoptiondialog.py")
|
||
if not FileAccess.file_exists(path):
|
||
push_warning("GameOptionUI: no gameoptiondialog.py at " + path)
|
||
return
|
||
_win = ui.open_script(path, assets_root)
|
||
if not is_open():
|
||
return
|
||
_relabel()
|
||
_wire()
|
||
_sync()
|
||
|
||
func _open_mobile() -> void:
|
||
if is_open():
|
||
return
|
||
_mobile_root = Control.new()
|
||
_mobile_root.name = "MobileGameOptionWindow"
|
||
_mobile_root.size = Vector2(700, 360)
|
||
_mobile_root.set_meta("mobile_title", "游戏设置")
|
||
_mobile_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
var panel := Panel.new()
|
||
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
panel.add_theme_stylebox_override("panel", _mobile_panel_style())
|
||
_mobile_root.add_child(panel)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.name = "MobileGameOptionScroll"
|
||
scroll.position = Vector2(14, 48)
|
||
scroll.size = Vector2(672, 298)
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
scroll.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_mobile_root.add_child(scroll)
|
||
var content := VBoxContainer.new()
|
||
content.custom_minimum_size = Vector2(650, 0)
|
||
content.add_theme_constant_override("separation", 8)
|
||
scroll.add_child(content)
|
||
_add_mobile_section_title(content, "屏蔽设置")
|
||
_add_mobile_block_buttons(content)
|
||
_add_mobile_section_title(content, "PK 模式")
|
||
_add_mobile_pk_radios(content)
|
||
_add_mobile_section_title(content, "显示设置")
|
||
_add_mobile_display_radios(content)
|
||
ui.open(_mobile_root)
|
||
|
||
func _add_mobile_section_title(parent: VBoxContainer, text: String) -> void:
|
||
var title := Label.new()
|
||
title.text = text
|
||
title.custom_minimum_size = Vector2(640, 28)
|
||
title.add_theme_font_size_override("font_size", 14)
|
||
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
|
||
parent.add_child(title)
|
||
|
||
func _add_mobile_block_buttons(parent: VBoxContainer) -> void:
|
||
var grid := GridContainer.new()
|
||
grid.columns = 3
|
||
grid.custom_minimum_size = Vector2(640, 94)
|
||
grid.add_theme_constant_override("h_separation", 6)
|
||
grid.add_theme_constant_override("v_separation", 6)
|
||
parent.add_child(grid)
|
||
for name in BLOCK_BITS:
|
||
var button := Button.new()
|
||
button.text = String(LABELS.get(name, name))
|
||
button.toggle_mode = true
|
||
button.button_pressed = (_block_mode & int(BLOCK_BITS[name])) != 0
|
||
button.custom_minimum_size = Vector2(206, 42)
|
||
var bit := int(BLOCK_BITS[name])
|
||
button.pressed.connect(func(): _toggle_block(bit))
|
||
grid.add_child(button)
|
||
|
||
func _add_mobile_pk_radios(parent: VBoxContainer) -> void:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(640, 44)
|
||
row.add_theme_constant_override("separation", 6)
|
||
var buttons: Array[Button] = []
|
||
for name in ["pvp_peace", "pvp_revenge", "pvp_guild", "pvp_free"]:
|
||
var button := Button.new()
|
||
button.text = String(LABELS.get(name, name))
|
||
button.toggle_mode = true
|
||
button.custom_minimum_size = Vector2(145, 42)
|
||
button.button_pressed = false
|
||
var picked: String = name
|
||
button.pressed.connect(func():
|
||
for other in buttons.size():
|
||
buttons[other].set_pressed_no_signal(buttons[other] == button)
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/pkmode %d" % PK_CMD[picked]))
|
||
buttons.append(button)
|
||
row.add_child(button)
|
||
var current: Variant = _cfg.get_value("gameopt", "pk_mode", -1)
|
||
if int(current) in PK_CMD.values():
|
||
for i in PK_CMD.keys().size():
|
||
if int(PK_CMD[PK_CMD.keys()[i]]) == int(current):
|
||
buttons[i].button_pressed = true
|
||
parent.add_child(row)
|
||
|
||
func _add_mobile_display_radios(parent: VBoxContainer) -> void:
|
||
for group in RADIO_GROUPS:
|
||
var row := HBoxContainer.new()
|
||
row.custom_minimum_size = Vector2(640, 44)
|
||
row.add_theme_constant_override("separation", 6)
|
||
var key: String = String(DISPLAY_RADIOS[group[0]][0])
|
||
var caption := Label.new()
|
||
caption.text = String(LABELS.get(key, key))
|
||
caption.custom_minimum_size = Vector2(126, 40)
|
||
caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
caption.add_theme_font_size_override("font_size", 12)
|
||
row.add_child(caption)
|
||
var buttons: Array[Button] = []
|
||
var current := int(_display.get(key, 0))
|
||
for name in group:
|
||
var button := Button.new()
|
||
button.text = String(LABELS.get(name, name))
|
||
button.toggle_mode = true
|
||
button.custom_minimum_size = Vector2(112, 42)
|
||
button.button_pressed = int(DISPLAY_RADIOS[name][1]) == current
|
||
var selected: String = name
|
||
button.pressed.connect(func():
|
||
for other in buttons.size():
|
||
buttons[other].set_pressed_no_signal(buttons[other] == button)
|
||
var spec: Array = DISPLAY_RADIOS[selected]
|
||
_display[String(spec[0])] = int(spec[1])
|
||
_save()
|
||
display_option_changed.emit(String(spec[0]), int(spec[1])))
|
||
buttons.append(button)
|
||
row.add_child(button)
|
||
parent.add_child(row)
|
||
|
||
func _mobile_panel_style() -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color(0.04, 0.065, 0.1, 0.98)
|
||
style.border_color = Color(0.52, 0.68, 0.9, 0.86)
|
||
style.set_border_width_all(1)
|
||
style.set_corner_radius_all(8)
|
||
return style
|
||
|
||
func _node(nm: String) -> Control:
|
||
if _win.is_empty():
|
||
return null
|
||
var n = _win.get("nodes", {}).get(nm, null)
|
||
return n if n is Control else null
|
||
|
||
func _relabel() -> void:
|
||
for nm in LABELS:
|
||
var n := _node(nm)
|
||
if n and n.has_method("set_text"):
|
||
n.set_text(LABELS[nm])
|
||
|
||
func _wire() -> void:
|
||
# 屏蔽 toggle → /setblockmode
|
||
for nm in BLOCK_BITS:
|
||
var b := _node(nm)
|
||
if b is BaseButton:
|
||
b.toggle_mode = true
|
||
var bit: int = BLOCK_BITS[nm]
|
||
b.pressed.connect(func(): _toggle_block(bit))
|
||
# PK radio → /pkmode
|
||
_bind_radio(PK_CMD.keys(), func(nm):
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/pkmode %d" % PK_CMD[nm])) # CHAT_TYPE_TALKING
|
||
# 显示开关 radio → 持久化
|
||
for group in RADIO_GROUPS:
|
||
_bind_radio(group, func(nm):
|
||
var spec: Array = DISPLAY_RADIOS[nm]
|
||
_display[spec[0]] = int(spec[1])
|
||
_save()
|
||
display_option_changed.emit(String(spec[0]), int(spec[1])))
|
||
var tb := _node("titlebar")
|
||
if tb:
|
||
for x in tb.find_children("*", "BaseButton", true, false):
|
||
x.pressed.connect(close)
|
||
|
||
func _bind_radio(names: Array, on_pick: Callable) -> void:
|
||
var boxes := {}
|
||
for nm: String in names:
|
||
boxes[nm] = _node(nm)
|
||
for nm: String in names:
|
||
var b = boxes[nm]
|
||
if not (b is BaseButton):
|
||
continue
|
||
b.toggle_mode = true
|
||
var picked: String = nm
|
||
b.pressed.connect(func():
|
||
for other: String in names:
|
||
if boxes[other] is BaseButton:
|
||
boxes[other].set_pressed_no_signal(other == picked)
|
||
on_pick.call(picked))
|
||
|
||
func _toggle_block(bit: int) -> void:
|
||
_block_mode ^= bit
|
||
if client and client.has_method("say"):
|
||
client.say(0, "/setblockmode %d" % _block_mode)
|
||
_cfg.set_value("gameopt", "block_mode", _block_mode)
|
||
var err := _cfg.save(config_path())
|
||
if err != OK:
|
||
push_warning("GameOptionUI: save failed (%s): %s" % [err, config_path()])
|
||
_sync_block()
|
||
|
||
# 开窗时把状态回填到控件
|
||
func _sync() -> void:
|
||
_sync_block()
|
||
for group in RADIO_GROUPS:
|
||
var key: String = DISPLAY_RADIOS[group[0]][0]
|
||
var cur := int(_display.get(key, 0))
|
||
for nm in group:
|
||
var b := _node(nm)
|
||
if b is BaseButton:
|
||
b.set_pressed_no_signal(int(DISPLAY_RADIOS[nm][1]) == cur)
|
||
|
||
func _sync_block() -> void:
|
||
for nm in BLOCK_BITS:
|
||
var b := _node(nm)
|
||
if b is BaseButton:
|
||
b.set_pressed_no_signal((_block_mode & int(BLOCK_BITS[nm])) != 0)
|
||
|
||
func _on_server_block_mode(mask: int) -> void:
|
||
_block_mode = maxi(0, mask)
|
||
_cfg.set_value("gameopt", "block_mode", _block_mode)
|
||
_cfg.save(config_path())
|
||
_sync_block()
|
||
|
||
func _save() -> void:
|
||
for k in _display:
|
||
_cfg.set_value("gameopt", k, int(_display[k]))
|
||
_cfg.set_value("gameopt", "block_mode", _block_mode)
|
||
var err := _cfg.save(config_path())
|
||
if err != OK:
|
||
push_warning("GameOptionUI: save failed (%s): %s" % [err, config_path()])
|