Implement 40250 classic client port
This commit is contained in:
@@ -19,6 +19,7 @@ const PlayerView = preload("res://ui/player_view.gd")
|
||||
signal select_requested(index: int)
|
||||
signal delete_requested(index: int, private_code: String)
|
||||
signal create_requested(spec: Dictionary) # {slot, name, job, shape, con, int, str, dex}
|
||||
signal change_name_requested(index: int, name: String)
|
||||
signal back_requested()
|
||||
|
||||
const CLASS_CN := ["猛将", "刺客", "术士", "巫女"]
|
||||
@@ -55,6 +56,8 @@ var _class_lbl: Label
|
||||
var _status: Label
|
||||
var _slot_lbl: Label
|
||||
var _spin := 0.0
|
||||
var _last_empire := -1
|
||||
var _rename_pending := false
|
||||
|
||||
func setup(client: Node, assets_root: String, chars: Array) -> void:
|
||||
_client = client
|
||||
@@ -66,6 +69,7 @@ func setup(client: Node, assets_root: String, chars: Array) -> void:
|
||||
_build_class_name()
|
||||
_build_panel()
|
||||
_build_status()
|
||||
_last_empire = _empire_id()
|
||||
set_process(true)
|
||||
_select(_first_occupied())
|
||||
|
||||
@@ -107,9 +111,33 @@ func on_char_created(slot: int) -> void:
|
||||
_set_status("角色已创建(槽位 %d)" % slot)
|
||||
|
||||
func on_char_create_failed(reason_type: int) -> void:
|
||||
if _rename_pending:
|
||||
_rename_pending = false
|
||||
const RENAME_R := {0: "名称不合法", 1: "该名称已被使用", 100: "角色槽位无效"}
|
||||
_set_status("改名失败:%s" % RENAME_R.get(reason_type, "错误码 %d" % reason_type))
|
||||
return
|
||||
const R := {1: "该名称已被使用", 2: "名称不合法", 3: "该槽位已有角色", 4: "权限不足"}
|
||||
_set_status("创建失败:%s" % R.get(reason_type, "错误码 %d" % reason_type))
|
||||
|
||||
func on_char_name_send_failed() -> void:
|
||||
_rename_pending = false
|
||||
_set_status("改名请求发送失败")
|
||||
|
||||
func on_char_name_changed(pid: int, name: String) -> void:
|
||||
var matched := false
|
||||
for c in _chars:
|
||||
if int(c.get("id", 0)) == pid:
|
||||
matched = true
|
||||
c["name"] = name
|
||||
c["change_name"] = false
|
||||
if not matched and _rename_pending:
|
||||
return
|
||||
_rename_pending = false
|
||||
_close_dialog()
|
||||
_refresh_class_name()
|
||||
_refresh_panel()
|
||||
_set_status("角色已改名:%s" % name)
|
||||
|
||||
func on_char_deleted(slot: int) -> void:
|
||||
_close_dialog()
|
||||
_set_status("角色已删除(槽位 %d)" % slot)
|
||||
@@ -342,6 +370,32 @@ func _refresh_panel() -> void:
|
||||
|
||||
_panel_box.add_child(_sep())
|
||||
|
||||
# A fresh account has no empire yet. 40250 keeps the connection in the
|
||||
# select phase and expects CG_EMPIRE before character creation/selection;
|
||||
# expose that real protocol step instead of leaving the page apparently
|
||||
# idle with a permanently disabled start button.
|
||||
var empire_required := _empire_id() == 0
|
||||
if empire_required:
|
||||
_panel_box.add_child(_plain("请选择阵营", 14, Color(0.95, 0.85, 0.55)))
|
||||
var erow := HBoxContainer.new()
|
||||
erow.add_theme_constant_override("separation", 6)
|
||||
_panel_box.add_child(erow)
|
||||
for eid in [1, 2, 3]:
|
||||
var empire_id: int = eid
|
||||
var eb := Button.new()
|
||||
eb.text = _empire_name_for(empire_id)
|
||||
eb.custom_minimum_size = Vector2(82, 30)
|
||||
eb.pressed.connect(func():
|
||||
if _client == null or not _client.has_method("select_empire"):
|
||||
_set_status("当前网络后端不支持选择阵营")
|
||||
return
|
||||
if bool(_client.call("select_empire", empire_id)):
|
||||
_set_status("已发送阵营选择:%s" % _empire_name_for(empire_id))
|
||||
else:
|
||||
_set_status("阵营选择发送失败")
|
||||
)
|
||||
erow.add_child(eb)
|
||||
|
||||
# 槽位切换
|
||||
if _chars.size() > 1:
|
||||
var nav := HBoxContainer.new()
|
||||
@@ -366,19 +420,19 @@ func _refresh_panel() -> void:
|
||||
var start := Button.new()
|
||||
start.text = "开始"
|
||||
start.custom_minimum_size = Vector2(122, 34)
|
||||
start.disabled = not has_char
|
||||
start.disabled = not has_char or empire_required
|
||||
start.pressed.connect(_do_start)
|
||||
brow.add_child(start)
|
||||
var create := Button.new()
|
||||
create.text = "创建"
|
||||
create.custom_minimum_size = Vector2(122, 34)
|
||||
create.disabled = has_char
|
||||
create.disabled = has_char or empire_required
|
||||
create.pressed.connect(_open_create_dialog)
|
||||
brow.add_child(create)
|
||||
var del := Button.new()
|
||||
del.text = "删除"
|
||||
del.custom_minimum_size = Vector2(122, 34)
|
||||
del.disabled = not has_char
|
||||
del.disabled = not has_char or empire_required
|
||||
del.pressed.connect(_open_delete_dialog)
|
||||
brow.add_child(del)
|
||||
var quit := Button.new()
|
||||
@@ -432,6 +486,11 @@ func _do_start() -> void:
|
||||
if d.is_empty() or String(d.get("name", "")) == "":
|
||||
_set_status("空槽位不能进入")
|
||||
return
|
||||
# 40250's original select screen forces a character marked with
|
||||
# change_name to complete the rename flow before entering the world.
|
||||
if bool(d.get("change_name", false)):
|
||||
_open_change_name_dialog()
|
||||
return
|
||||
select_requested.emit(_slot_index())
|
||||
_set_status("进入游戏:%s" % d.get("name"))
|
||||
|
||||
@@ -546,6 +605,49 @@ func _open_create_dialog() -> void:
|
||||
cancel.pressed.connect(_close_dialog)
|
||||
brow.add_child(cancel)
|
||||
|
||||
func _open_change_name_dialog() -> void:
|
||||
var d := _cur()
|
||||
if String(d.get("name", "")) == "":
|
||||
_set_status("空槽位不能改名")
|
||||
return
|
||||
var slot := _slot_index()
|
||||
var box := _modal_root("修改角色名称", 400, 250)
|
||||
box.add_child(_plain("该角色需要先修改名称才能进入游戏。", 13,
|
||||
Color(0.95, 0.75, 0.7)))
|
||||
var nrow := HBoxContainer.new()
|
||||
nrow.add_theme_constant_override("separation", 8)
|
||||
box.add_child(nrow)
|
||||
nrow.add_child(_plain("新名称", 13, Color(0.72, 0.78, 0.85)))
|
||||
var name_edit := LineEdit.new()
|
||||
name_edit.name = "Name"
|
||||
name_edit.max_length = 12
|
||||
name_edit.custom_minimum_size = Vector2(220, 0)
|
||||
name_edit.placeholder_text = "角色名(最多 12)"
|
||||
nrow.add_child(name_edit)
|
||||
box.add_child(_sep())
|
||||
var brow := HBoxContainer.new()
|
||||
brow.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
brow.add_theme_constant_override("separation", 16)
|
||||
box.add_child(brow)
|
||||
var ok := Button.new()
|
||||
ok.text = "改名"
|
||||
ok.custom_minimum_size = Vector2(120, 34)
|
||||
ok.pressed.connect(func():
|
||||
var nm := name_edit.text.strip_edges()
|
||||
if nm.length() < 2:
|
||||
_set_status("名称太短")
|
||||
return
|
||||
_rename_pending = true
|
||||
_close_dialog()
|
||||
_set_status("改名中…")
|
||||
change_name_requested.emit(slot, nm))
|
||||
brow.add_child(ok)
|
||||
var cancel := Button.new()
|
||||
cancel.text = "取消"
|
||||
cancel.custom_minimum_size = Vector2(120, 34)
|
||||
cancel.pressed.connect(_close_dialog)
|
||||
brow.add_child(cancel)
|
||||
|
||||
func _open_delete_dialog() -> void:
|
||||
var d := _cur()
|
||||
if String(d.get("name", "")) == "":
|
||||
@@ -587,7 +689,9 @@ func _empire_id() -> int:
|
||||
return 0
|
||||
|
||||
func _empire_name() -> String:
|
||||
var e := _empire_id()
|
||||
return _empire_name_for(_empire_id())
|
||||
|
||||
func _empire_name_for(e: int) -> String:
|
||||
# 优先 locale_game.txt 的 EMPIRE_A/B/C
|
||||
var key: String = EMPIRE_KEY.get(e, "")
|
||||
if key != "":
|
||||
@@ -625,6 +729,10 @@ func _empire_flag_tex() -> Texture2D:
|
||||
|
||||
func _process(dt: float) -> void:
|
||||
_spin += dt
|
||||
var empire := _empire_id()
|
||||
if empire != _last_empire:
|
||||
_last_empire = empire
|
||||
_refresh_panel()
|
||||
if _pivot:
|
||||
# 正面朝相机 + 轻微来回摆
|
||||
_pivot.rotation.y = PI + sin(_spin * 0.3) * 0.35
|
||||
|
||||
+158
-17
@@ -9,7 +9,9 @@
|
||||
# cube_changed → 刷新配方 / 材料 / 金币
|
||||
# cube_result(vnum,count,ok) → 结果提示
|
||||
# cube_closed → 隐藏
|
||||
# 选中一个配方 → [制作] → M2Client.cube_make(index)。
|
||||
# 选中一个配方、把背包物品放入材料槽 → [制作] → M2Client.cube_make(index)。
|
||||
# 40250 服务端按材料槽匹配配方,`result_index` 只保留给 m2dev 后端;classic
|
||||
# 会安全地忽略它并执行一次 `/cube make`。
|
||||
extends Node
|
||||
|
||||
var client: Node
|
||||
@@ -21,6 +23,9 @@ var _list: VBoxContainer
|
||||
var _mat: VBoxContainer
|
||||
var _make_btn: Button
|
||||
var _sel := -1
|
||||
var _cube_slots: Dictionary = {} # cube index:int -> inventory cell:int
|
||||
var _craft_in_flight := false
|
||||
var _cube_npc_vnum := 0
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client = m2client
|
||||
@@ -34,12 +39,25 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client.cube_changed.connect(refresh)
|
||||
if client.has_signal("cube_result"):
|
||||
client.cube_result.connect(_on_result)
|
||||
if client.has_signal("inventory_changed"):
|
||||
client.inventory_changed.connect(func(_window, _cell): refresh())
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func _on_open(npc_vnum: int) -> void:
|
||||
if npc_vnum <= 0:
|
||||
return
|
||||
var current: Dictionary = client.get_cube() if client.has_method("get_cube") else {}
|
||||
if current.has("open") and not bool(current.get("open", false)):
|
||||
return
|
||||
var current_npc := int(current.get("npc_vnum", 0))
|
||||
if current_npc > 0 and current_npc != npc_vnum:
|
||||
return
|
||||
_sel = -1
|
||||
_cube_slots.clear()
|
||||
_craft_in_flight = false
|
||||
_cube_npc_vnum = npc_vnum
|
||||
_root.visible = true
|
||||
_title.text = "제작 (Cube) · NPC #%d" % npc_vnum
|
||||
_status.text = ""
|
||||
@@ -48,10 +66,15 @@ func _on_open(npc_vnum: int) -> void:
|
||||
refresh()
|
||||
|
||||
func _on_result(vnum: int, count: int, ok: bool) -> void:
|
||||
_craft_in_flight = false
|
||||
if ok:
|
||||
_status.text = "제작 성공: %s ×%d" % [_name_of(vnum), count]
|
||||
# 40250 在成功后消耗材料;等待 inventory/cube_changed 的最终状态前,
|
||||
# 先清掉本地映射,避免旧背包格继续显示为可用材料。
|
||||
_cube_slots.clear()
|
||||
else:
|
||||
_status.text = "제작 실패"
|
||||
refresh()
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if proto and proto.has_method("item"):
|
||||
@@ -65,6 +88,9 @@ func refresh() -> void:
|
||||
if client == null or _root == null or not _root.visible:
|
||||
return
|
||||
var cube: Dictionary = client.get_cube()
|
||||
if not cube.get("open", true):
|
||||
_root.visible = false
|
||||
return
|
||||
var recipes: Array = cube.get("recipes", [])
|
||||
var results: Array = cube.get("results", [])
|
||||
for c in _list.get_children():
|
||||
@@ -105,21 +131,123 @@ func _refresh_materials() -> void:
|
||||
c.queue_free()
|
||||
var cube: Dictionary = client.get_cube()
|
||||
var recipes: Array = cube.get("recipes", [])
|
||||
_make_btn.disabled = _sel < 0
|
||||
var recipe: Dictionary = _selected_recipe()
|
||||
_make_btn.disabled = _sel < 0 or recipe.is_empty() or _craft_in_flight \
|
||||
or not _recipe_can_craft(recipe)
|
||||
if _sel < 0 or _sel >= recipes.size():
|
||||
_mat.add_child(_lbl("재료 정보를 불러오는 중…", 12))
|
||||
return
|
||||
var r: Dictionary = recipes[_sel]
|
||||
_mat.add_child(_lbl("필요 골드: %d" % int(r.get("gold", 0)), 12))
|
||||
var groups: Array = r.get("material_groups", [])
|
||||
if groups.is_empty():
|
||||
_mat.add_child(_lbl("(재료 없음)", 12))
|
||||
for g in groups:
|
||||
var parts: Array = []
|
||||
for s in (g as Array):
|
||||
parts.append("%s ×%d" % [_name_of(int((s as Dictionary).get("vnum", 0))),
|
||||
int((s as Dictionary).get("count", 1))])
|
||||
_mat.add_child(_lbl("• " + " / ".join(parts), 12))
|
||||
else:
|
||||
var r: Dictionary = recipe
|
||||
_mat.add_child(_lbl("필요 골드: %d" % int(r.get("gold", 0)), 12))
|
||||
var groups: Array = r.get("material_groups", [])
|
||||
if groups.is_empty():
|
||||
_mat.add_child(_lbl("(재료 없음)", 12))
|
||||
for g in groups:
|
||||
var parts: Array = []
|
||||
for s in (g as Array):
|
||||
parts.append("%s ×%d" % [_name_of(int((s as Dictionary).get("vnum", 0))),
|
||||
int((s as Dictionary).get("count", 1))])
|
||||
_mat.add_child(_lbl("• " + " / ".join(parts), 12))
|
||||
if not _recipe_can_craft(r):
|
||||
_mat.add_child(_lbl("(材料不足或未放入材料槽)", 11))
|
||||
elif client.has_method("get_points") and int(r.get("gold", 0)) > int(client.get_points().get("gold", 0)):
|
||||
_make_btn.disabled = true
|
||||
_mat.add_child(_lbl("(金币不足)", 11))
|
||||
|
||||
_mat.add_child(HSeparator.new())
|
||||
_mat.add_child(_lbl("재료 슬롯 (클릭하면 추가/삭제)", 12))
|
||||
for slot in _cube_slots.keys():
|
||||
var cell := int(_cube_slots[slot])
|
||||
var row := HBoxContainer.new()
|
||||
var label := _lbl("cube[%d] ← bag[%d]" % [int(slot), cell], 11)
|
||||
row.add_child(label)
|
||||
var remove := Button.new()
|
||||
remove.text = "삭제"
|
||||
remove.pressed.connect(func() -> void:
|
||||
if client.has_method("cube_delete_item") and client.cube_delete_item(int(slot)):
|
||||
_cube_slots.erase(slot)
|
||||
refresh())
|
||||
row.add_child(remove)
|
||||
_mat.add_child(row)
|
||||
|
||||
_mat.add_child(_lbl("背包物品:", 12))
|
||||
var used_cells := {}
|
||||
for cell in _cube_slots.values():
|
||||
used_cells[int(cell)] = true
|
||||
var inventory: Array = client.get_inventory() if client.has_method("get_inventory") else []
|
||||
var added := 0
|
||||
for item in inventory:
|
||||
var inv_cell := int(item.get("cell", -1))
|
||||
var item_vnum := int(item.get("vnum", 0))
|
||||
if used_cells.has(inv_cell):
|
||||
continue
|
||||
if not _recipe_accepts_vnum(recipe, item_vnum):
|
||||
continue
|
||||
var free_slot := _first_free_cube_slot()
|
||||
if free_slot < 0:
|
||||
break
|
||||
var add := Button.new()
|
||||
add.text = "+ bag[%d] %s ×%d" % [inv_cell, _name_of(item_vnum),
|
||||
int(item.get("count", 1))]
|
||||
add.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
add.pressed.connect(func() -> void:
|
||||
var target := _first_free_cube_slot()
|
||||
if target >= 0 and client.has_method("cube_add_item") and client.cube_add_item(target, inv_cell):
|
||||
_cube_slots[target] = inv_cell
|
||||
refresh())
|
||||
_mat.add_child(add)
|
||||
added += 1
|
||||
if added == 0:
|
||||
_mat.add_child(_lbl("(可用背包物品 없음)", 11))
|
||||
|
||||
func _selected_recipe() -> Dictionary:
|
||||
if client == null or not client.has_method("get_cube"):
|
||||
return {}
|
||||
var recipes: Array = client.get_cube().get("recipes", [])
|
||||
if _sel < 0 or _sel >= recipes.size():
|
||||
return {}
|
||||
return recipes[_sel] as Dictionary
|
||||
|
||||
func _recipe_accepts_vnum(recipe: Dictionary, vnum: int) -> bool:
|
||||
if vnum <= 0:
|
||||
return false
|
||||
for group in recipe.get("material_groups", []):
|
||||
for option in group as Array:
|
||||
if int((option as Dictionary).get("vnum", 0)) == vnum:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _recipe_can_craft(recipe: Dictionary) -> bool:
|
||||
if recipe.is_empty():
|
||||
return false
|
||||
var provided := {}
|
||||
if client.has_method("get_inventory"):
|
||||
var inventory: Array = client.get_inventory()
|
||||
for cell in _cube_slots.values():
|
||||
for item in inventory:
|
||||
if int(item.get("cell", -1)) == int(cell):
|
||||
var vnum := int(item.get("vnum", 0))
|
||||
provided[vnum] = int(provided.get(vnum, 0)) + maxi(1, int(item.get("count", 1)))
|
||||
break
|
||||
for group in recipe.get("material_groups", []):
|
||||
var matched := false
|
||||
for option in group as Array:
|
||||
var material: Dictionary = option as Dictionary
|
||||
var vnum := int(material.get("vnum", 0))
|
||||
var need := maxi(1, int(material.get("count", 1)))
|
||||
if int(provided.get(vnum, 0)) >= need:
|
||||
provided[vnum] = int(provided[vnum]) - need
|
||||
matched = true
|
||||
break
|
||||
if not matched:
|
||||
return false
|
||||
return true
|
||||
|
||||
func _first_free_cube_slot() -> int:
|
||||
for i in 24:
|
||||
if not _cube_slots.has(i):
|
||||
return i
|
||||
return -1
|
||||
|
||||
func _lbl(t: String, sz: int) -> Label:
|
||||
var l := Label.new()
|
||||
@@ -165,10 +293,23 @@ func _build(parent: Node) -> void:
|
||||
_make_btn.text = "제작"
|
||||
_make_btn.disabled = true
|
||||
_make_btn.pressed.connect(func() -> void:
|
||||
if _sel >= 0 and client.has_method("cube_make"):
|
||||
client.cube_make(_sel))
|
||||
var recipe := _selected_recipe()
|
||||
if _sel < 0 or recipe.is_empty():
|
||||
_status.text = "请先选择配方"
|
||||
return
|
||||
if not _recipe_can_craft(recipe):
|
||||
_status.text = "材料不足或未放入材料槽"
|
||||
refresh()
|
||||
return
|
||||
if client.has_method("cube_make") and client.cube_make(_sel):
|
||||
_craft_in_flight = true
|
||||
_status.text = "制作中…"
|
||||
_make_btn.disabled = true)
|
||||
brow.add_child(_make_btn)
|
||||
var close := Button.new()
|
||||
close.text = "닫기"
|
||||
close.pressed.connect(func() -> void: _root.visible = false)
|
||||
close.pressed.connect(func() -> void:
|
||||
if client.has_method("cube_close"):
|
||||
client.cube_close()
|
||||
_root.visible = false)
|
||||
brow.add_child(close)
|
||||
|
||||
@@ -16,9 +16,11 @@ var _self_box: VBoxContainer
|
||||
var _peer_box: VBoxContainer
|
||||
var _self_gold: Label
|
||||
var _peer_gold: Label
|
||||
var _status: Label
|
||||
var _accept_btn: Button
|
||||
var _gold_input: LineEdit
|
||||
var _next_display := 0
|
||||
var _offered_cells := {}
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client = m2client
|
||||
@@ -31,9 +33,29 @@ func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func offer(inv_window: int, inv_cell: int) -> void:
|
||||
if is_open():
|
||||
client.exchange_add_item(inv_window, inv_cell, _next_display)
|
||||
if not is_open() or inv_window < 0 or inv_cell < 0:
|
||||
return
|
||||
var key := "%d:%d" % [inv_window, inv_cell]
|
||||
if _offered_cells.has(key):
|
||||
_status.text = "该物品已经放入交易"
|
||||
return
|
||||
var x: Dictionary = client.get_exchange()
|
||||
var used := {}
|
||||
for item in x.get("self_items", []):
|
||||
used[int(item.get("slot", -1))] = true
|
||||
for i in 12:
|
||||
if not used.has(i):
|
||||
_next_display = i
|
||||
break
|
||||
if used.size() >= 12:
|
||||
_status.text = "交易物品栏已满"
|
||||
return
|
||||
if client.exchange_add_item(inv_window, inv_cell, _next_display):
|
||||
_offered_cells[key] = true
|
||||
_next_display = (_next_display + 1) % 12
|
||||
_status.text = ""
|
||||
else:
|
||||
_status.text = "交易请求发送失败"
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if proto and proto.has_method("item"):
|
||||
@@ -50,6 +72,9 @@ func refresh() -> void:
|
||||
if not x.get("active", false):
|
||||
_root.visible = false
|
||||
_next_display = 0
|
||||
_offered_cells.clear()
|
||||
_gold_input.text = ""
|
||||
_status.text = ""
|
||||
return
|
||||
_root.visible = true
|
||||
_fill(_self_box, x.get("self_items", []))
|
||||
@@ -61,6 +86,7 @@ func refresh() -> void:
|
||||
_accept_btn.text = "已接受 ✓" if me else "接受"
|
||||
_accept_btn.modulate = Color(0.5, 1, 0.5) if me else Color(1, 1, 1)
|
||||
_root.get_node("PeerAccept").text = "对方: 已接受" if peer else "对方: 未接受"
|
||||
_status.text = ""
|
||||
|
||||
func _fill(box: VBoxContainer, rows: Array) -> void:
|
||||
for c in box.get_children():
|
||||
@@ -72,9 +98,19 @@ func _fill(box: VBoxContainer, rows: Array) -> void:
|
||||
box.add_child(l)
|
||||
|
||||
func _on_put_gold() -> void:
|
||||
var g := int(_gold_input.text)
|
||||
if g > 0:
|
||||
client.exchange_add_gold(g)
|
||||
var raw := _gold_input.text.strip_edges()
|
||||
if raw == "" or not raw.is_valid_int():
|
||||
_status.text = "请输入有效金币数量"
|
||||
return
|
||||
var g := int(raw)
|
||||
if g <= 0 or g > 2000000000:
|
||||
_status.text = "金币数量必须在 1~2000000000 之间"
|
||||
return
|
||||
if client.exchange_add_gold(g):
|
||||
_gold_input.text = ""
|
||||
_status.text = ""
|
||||
else:
|
||||
_status.text = "金币请求发送失败"
|
||||
|
||||
func _build(parent: Node) -> void:
|
||||
_root = Control.new()
|
||||
@@ -121,6 +157,11 @@ func _build(parent: Node) -> void:
|
||||
_peer_gold.text = "金币: 0"
|
||||
_peer_gold.position = Vector2(240, 250)
|
||||
_root.add_child(_peer_gold)
|
||||
_status = Label.new()
|
||||
_status.position = Vector2(20, 276)
|
||||
_status.add_theme_font_size_override("font_size", 11)
|
||||
_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
|
||||
_root.add_child(_status)
|
||||
|
||||
var pa := Label.new()
|
||||
pa.name = "PeerAccept"
|
||||
@@ -130,7 +171,7 @@ func _build(parent: Node) -> void:
|
||||
_root.add_child(pa)
|
||||
|
||||
var bottom := HBoxContainer.new()
|
||||
bottom.position = Vector2(20, 300)
|
||||
bottom.position = Vector2(20, 310)
|
||||
_root.add_child(bottom)
|
||||
_gold_input = LineEdit.new()
|
||||
_gold_input.placeholder_text = "金币"
|
||||
|
||||
+33
-1
@@ -15,12 +15,16 @@ var client: Node
|
||||
var _root: Control
|
||||
var _list: VBoxContainer
|
||||
var _name_edit: LineEdit
|
||||
var _invite_dialog: ConfirmationDialog
|
||||
var _pending_invite := ""
|
||||
|
||||
func setup(m2client: Node, parent: Node) -> void:
|
||||
client = m2client
|
||||
_build(parent)
|
||||
if client.has_signal("friends_changed"):
|
||||
client.friends_changed.connect(refresh)
|
||||
if client.has_signal("friend_invite_ask"):
|
||||
client.friend_invite_ask.connect(_on_friend_invite)
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
@@ -55,9 +59,15 @@ func _row(f: Dictionary) -> Control:
|
||||
var nm := Button.new()
|
||||
nm.text = String(f.get("name", "?"))
|
||||
nm.flat = true
|
||||
nm.custom_minimum_size = Vector2(180, 0)
|
||||
nm.custom_minimum_size = Vector2(150, 0)
|
||||
nm.pressed.connect(func() -> void: whisper_to.emit(String(f.get("name", ""))))
|
||||
row.add_child(nm)
|
||||
if f.get("mobile", false):
|
||||
var mobile := Label.new()
|
||||
mobile.text = "手机"
|
||||
mobile.modulate = Color(0.45, 0.8, 1.0)
|
||||
mobile.add_theme_font_size_override("font_size", 10)
|
||||
row.add_child(mobile)
|
||||
var del := Button.new()
|
||||
del.text = "×"
|
||||
del.pressed.connect(func() -> void: client.remove_friend(String(f.get("name", ""))))
|
||||
@@ -70,6 +80,19 @@ func _on_add() -> void:
|
||||
client.add_friend(nm)
|
||||
_name_edit.clear()
|
||||
|
||||
func _on_friend_invite(name: String) -> void:
|
||||
if client == null or name.strip_edges() == "":
|
||||
return
|
||||
_pending_invite = name
|
||||
_invite_dialog.dialog_text = "接受“%s”的好友请求?" % name
|
||||
_invite_dialog.popup_centered()
|
||||
|
||||
func _answer_invite(accept: bool) -> void:
|
||||
var name := _pending_invite
|
||||
_pending_invite = ""
|
||||
if name != "" and client != null and client.has_method("friend_answer"):
|
||||
client.friend_answer(name, accept)
|
||||
|
||||
func _build(parent: Node) -> void:
|
||||
_root = Control.new()
|
||||
_root.set_anchors_preset(Control.PRESET_CENTER)
|
||||
@@ -105,3 +128,12 @@ func _build(parent: Node) -> void:
|
||||
add_btn.text = "添加"
|
||||
add_btn.pressed.connect(_on_add)
|
||||
bottom.add_child(add_btn)
|
||||
_invite_dialog = ConfirmationDialog.new()
|
||||
_invite_dialog.title = "好友请求"
|
||||
_invite_dialog.ok_button_text = "接受"
|
||||
_invite_dialog.cancel_button_text = "拒绝"
|
||||
_invite_dialog.confirmed.connect(func() -> void: _answer_invite(true))
|
||||
_invite_dialog.canceled.connect(func() -> void: _answer_invite(false))
|
||||
# Keep the dialog outside the toggleable friend panel: requests must still
|
||||
# be visible when the friend list window itself is closed.
|
||||
parent.add_child(_invite_dialog)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# `/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];渲染侧钩子待补。
|
||||
# —— 客户端显示开关,持久化到 user://system_option.cfg [gameopt];渲染节点按需读取这些设置。
|
||||
extends Node
|
||||
|
||||
# Packet.h EBlockAction
|
||||
@@ -67,9 +67,16 @@ var uiscript_dir := ""
|
||||
|
||||
var _win: Dictionary = {}
|
||||
var _cfg := ConfigFile.new()
|
||||
var _block_mode := 0 # blockMode(本地跟踪;服务器回包同步待补)
|
||||
var _block_mode := 0 # blockMode(本地跟踪 + 服务端回包同步)
|
||||
var _display := {} # cfg [gameopt] 快照
|
||||
|
||||
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
|
||||
@@ -79,10 +86,12 @@ func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void:
|
||||
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(CFG_PATH)
|
||||
_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 ["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:
|
||||
return not _win.is_empty() and is_instance_valid(_win.get("root"))
|
||||
@@ -166,7 +175,9 @@ func _toggle_block(bit: int) -> void:
|
||||
if client and client.has_method("say"):
|
||||
client.say(0, "/setblockmode %d" % _block_mode)
|
||||
_cfg.set_value("gameopt", "block_mode", _block_mode)
|
||||
_cfg.save(CFG_PATH)
|
||||
var err := _cfg.save(config_path())
|
||||
if err != OK:
|
||||
push_warning("GameOptionUI: save failed (%s): %s" % [err, config_path()])
|
||||
_sync_block()
|
||||
|
||||
# 开窗时把状态回填到控件
|
||||
@@ -186,8 +197,16 @@ func _sync_block() -> void:
|
||||
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)
|
||||
_cfg.save(CFG_PATH)
|
||||
var err := _cfg.save(config_path())
|
||||
if err != OK:
|
||||
push_warning("GameOptionUI: save failed (%s): %s" % [err, config_path()])
|
||||
|
||||
+135
-6
@@ -1,4 +1,4 @@
|
||||
# GuildUI (P8/M4) —— 公会窗(G 键):成员 / 技能 / 公会战 三页。
|
||||
# GuildUI (P8/M4) —— 公会窗(G 键):成员 / 技能 / 公会战 / 公告 四页。
|
||||
#
|
||||
# var gu := preload("res://ui/guild_ui.gd").new()
|
||||
# add_child(gu)
|
||||
@@ -9,12 +9,13 @@
|
||||
# 到时刷会徽图。⬜ 等级页、日志页。
|
||||
extends Node
|
||||
|
||||
const TABS := ["成员", "技能", "公会战"]
|
||||
const TABS := ["成员", "技能", "公会战", "公告"]
|
||||
# EGuildWarState (wire.h)
|
||||
const WAR_STATE_NAMES := ["无", "已宣战", "被拒", "收到宣战", "待开始", "取消", "交战中", "结束"]
|
||||
|
||||
var client: Node
|
||||
var skill_table: RefCounted # SkillTable
|
||||
var skill_table: Object # SkillTable (tests may provide a Node stub)
|
||||
var _ui_parent: Node
|
||||
# 返回一张 Image(16×12,或任意会被转换/缩放)用作会徽上传源;null = 不显示上传按钮。
|
||||
var mark_image_provider: Callable
|
||||
var _root: Control
|
||||
@@ -25,14 +26,20 @@ var _tab_btns: Array[Button] = []
|
||||
var _pages: Array[VBoxContainer] = []
|
||||
var _war_name_edit: LineEdit
|
||||
var _upload_status: Label
|
||||
var _create_dialog: ConfirmationDialog
|
||||
var _create_name_edit: LineEdit
|
||||
var _create_status: Label
|
||||
|
||||
func setup(m2client: Node, parent: Node, table: RefCounted = null) -> void:
|
||||
func setup(m2client: Node, parent: Node, table: Object = null) -> void:
|
||||
client = m2client
|
||||
skill_table = table
|
||||
_ui_parent = parent
|
||||
_build(parent)
|
||||
for sig in ["guild_changed", "guild_skill_changed", "guild_war_changed"]:
|
||||
for sig in ["guild_changed", "guild_skill_changed", "guild_war_changed", "guild_comments_changed"]:
|
||||
if client.has_signal(sig):
|
||||
client.connect(sig, refresh)
|
||||
if client.has_signal("guild_make_requested"):
|
||||
client.guild_make_requested.connect(_on_guild_make_requested)
|
||||
if client.has_signal("guild_marks_ready"):
|
||||
client.guild_marks_ready.connect(func(_n): refresh())
|
||||
if client.has_signal("guild_mark_uploaded"):
|
||||
@@ -73,6 +80,7 @@ func refresh() -> void:
|
||||
0: _fill_members()
|
||||
1: _fill_skills()
|
||||
2: _fill_war(int(g.get("id", 0)))
|
||||
3: _fill_comments()
|
||||
|
||||
func _fill_members() -> void:
|
||||
var page := _pages[0]
|
||||
@@ -109,9 +117,63 @@ func _fill_members() -> void:
|
||||
String(m.get("name", "?")), int(m.get("level", 0)),
|
||||
gname if gname != "" else str(gi),
|
||||
int(m.get("offer", 0)),
|
||||
" ★" if m.get("general", false) else ""]
|
||||
(" 在线" if m.get("online", false) else " 离线") + (" ★" if m.get("general", false) else "")]
|
||||
page.add_child(row)
|
||||
|
||||
func _fill_comments() -> void:
|
||||
var page := _pages[3]
|
||||
for c in page.get_children():
|
||||
c.queue_free()
|
||||
if not client.has_method("get_guild_comments"):
|
||||
var missing := Label.new()
|
||||
missing.text = "(当前协议没有公告接口)"
|
||||
page.add_child(missing)
|
||||
return
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 5)
|
||||
page.add_child(actions)
|
||||
var edit := LineEdit.new()
|
||||
edit.placeholder_text = "公告内容(最多 50 字)"
|
||||
edit.custom_minimum_size = Vector2(245, 26)
|
||||
actions.add_child(edit)
|
||||
var post := Button.new()
|
||||
post.text = "发布"
|
||||
post.custom_minimum_size = Vector2(52, 26)
|
||||
post.pressed.connect(func():
|
||||
var text := edit.text.strip_edges()
|
||||
if text != "" and client.has_method("guild_post_comment"):
|
||||
client.guild_post_comment(text))
|
||||
actions.add_child(post)
|
||||
var refresh_btn := Button.new()
|
||||
refresh_btn.text = "刷新"
|
||||
refresh_btn.custom_minimum_size = Vector2(52, 26)
|
||||
refresh_btn.pressed.connect(func():
|
||||
if client.has_method("guild_refresh_comments"):
|
||||
client.guild_refresh_comments())
|
||||
actions.add_child(refresh_btn)
|
||||
var comments: Array = client.get_guild_comments()
|
||||
if comments.is_empty():
|
||||
var none := Label.new()
|
||||
none.text = "(暂无公告)"
|
||||
page.add_child(none)
|
||||
return
|
||||
for item in comments:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 6)
|
||||
page.add_child(row)
|
||||
var label := Label.new()
|
||||
label.custom_minimum_size = Vector2(286, 0)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.text = "%s:%s" % [String(item.get("name", "?")), String(item.get("content", ""))]
|
||||
row.add_child(label)
|
||||
var delete := Button.new()
|
||||
delete.text = "删"
|
||||
delete.custom_minimum_size = Vector2(32, 24)
|
||||
delete.pressed.connect(func():
|
||||
if client.has_method("guild_delete_comment"):
|
||||
client.guild_delete_comment(int(item.get("id", 0))))
|
||||
row.add_child(delete)
|
||||
|
||||
func _guild_skill_rows() -> Array:
|
||||
# skill_table 的 GUILD 分类给 id(升序);按顺序配 levels[i]。返回 [{id, name}]。
|
||||
var out := []
|
||||
@@ -236,6 +298,73 @@ func _on_mark_uploaded(ok: bool) -> void:
|
||||
if _upload_status:
|
||||
_upload_status.text = "✔ 已上传(等服务器刷新)" if ok else "✘ 上传失败"
|
||||
|
||||
# 40250 原版在收到 GC_REQUEST_MAKE_GUILD 后弹 InputDialog,再由玩家回送
|
||||
# CG_ANSWER_MAKE_GUILD。这里保留同样的“服务端请求 -> 输入 -> 应答”边界,
|
||||
# 让名称校验发生在发包前,同时把最终权限 / 敏感词判断留给服务端。
|
||||
func _on_guild_make_requested() -> void:
|
||||
if is_instance_valid(_create_dialog):
|
||||
_create_dialog.popup_centered()
|
||||
_create_name_edit.grab_focus()
|
||||
return
|
||||
if _ui_parent == null or client == null:
|
||||
return
|
||||
_create_dialog = ConfirmationDialog.new()
|
||||
_create_dialog.name = "GuildCreateDialog"
|
||||
_create_dialog.title = "创建公会"
|
||||
_create_dialog.dialog_text = "请输入公会名称(最多 12 字)"
|
||||
_create_dialog.ok_button_text = "创建"
|
||||
_create_dialog.cancel_button_text = "取消"
|
||||
_create_name_edit = LineEdit.new()
|
||||
_create_name_edit.name = "GuildName"
|
||||
_create_name_edit.max_length = 12
|
||||
_create_name_edit.placeholder_text = "公会名称"
|
||||
_create_name_edit.custom_minimum_size = Vector2(250, 28)
|
||||
_create_dialog.add_child(_create_name_edit)
|
||||
_create_status = Label.new()
|
||||
_create_status.name = "Status"
|
||||
_create_status.add_theme_font_size_override("font_size", 11)
|
||||
_create_status.modulate = Color(1.0, 0.7, 0.6)
|
||||
_create_dialog.add_child(_create_status)
|
||||
_ui_parent.add_child(_create_dialog)
|
||||
_create_dialog.confirmed.connect(_on_guild_create_confirmed)
|
||||
_create_dialog.canceled.connect(_close_guild_create_dialog)
|
||||
_create_dialog.popup_centered()
|
||||
_create_name_edit.grab_focus()
|
||||
|
||||
func _on_guild_create_confirmed() -> void:
|
||||
if not is_instance_valid(_create_dialog) or _create_name_edit == null:
|
||||
return
|
||||
var name := _create_name_edit.text.strip_edges()
|
||||
var error := ""
|
||||
if name.is_empty():
|
||||
error = "公会名称不能为空"
|
||||
elif name.length() > 12:
|
||||
error = "公会名称最多 12 字"
|
||||
else:
|
||||
for ch in name:
|
||||
if ch.unicode_at(0) < 32:
|
||||
error = "公会名称包含非法字符"
|
||||
break
|
||||
if error != "":
|
||||
_create_status.text = error
|
||||
_create_dialog.call_deferred("popup_centered")
|
||||
_create_name_edit.call_deferred("grab_focus")
|
||||
return
|
||||
if not client.has_method("guild_answer_make") or not bool(client.call("guild_answer_make", name)):
|
||||
_create_status.text = "创建请求发送失败"
|
||||
_create_dialog.call_deferred("popup_centered")
|
||||
_create_name_edit.call_deferred("grab_focus")
|
||||
return
|
||||
_close_guild_create_dialog()
|
||||
|
||||
func _close_guild_create_dialog() -> void:
|
||||
var dialog := _create_dialog
|
||||
_create_dialog = null
|
||||
_create_name_edit = null
|
||||
_create_status = null
|
||||
if is_instance_valid(dialog):
|
||||
dialog.queue_free()
|
||||
|
||||
# 公会 id 变了就重新取会徽图(16x12,放大 3x 显示)。
|
||||
func _refresh_mark(guild_id: int) -> void:
|
||||
if _mark == null:
|
||||
|
||||
+42
-2
@@ -12,6 +12,8 @@ var proto: Node
|
||||
var _root: Control
|
||||
var _list: VBoxContainer
|
||||
var _title: Label
|
||||
var _status: Label
|
||||
var _password_dialog: ConfirmationDialog
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client = m2client
|
||||
@@ -21,6 +23,8 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client.mall_opened.connect(func(_s): refresh())
|
||||
if client.has_signal("mall_changed"):
|
||||
client.mall_changed.connect(refresh)
|
||||
if client.has_signal("mall_password_required"):
|
||||
client.mall_password_required.connect(_ask_password)
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
@@ -40,7 +44,7 @@ func _first_free_inv() -> int:
|
||||
for i in range(90):
|
||||
if not used.has(i):
|
||||
return i
|
||||
return 0
|
||||
return -1
|
||||
|
||||
func refresh() -> void:
|
||||
if client == null or _root == null:
|
||||
@@ -48,6 +52,7 @@ func refresh() -> void:
|
||||
_root.visible = client.is_mall_open()
|
||||
if not _root.visible:
|
||||
return
|
||||
_status.text = ""
|
||||
_title.text = "道具商城仓库(%d 格)" % client.get_mall_size()
|
||||
for c in _list.get_children():
|
||||
c.queue_free()
|
||||
@@ -60,6 +65,31 @@ func refresh() -> void:
|
||||
for it in items:
|
||||
_list.add_child(_row(it))
|
||||
|
||||
func _ask_password() -> void:
|
||||
if is_instance_valid(_password_dialog):
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = ConfirmationDialog.new()
|
||||
_password_dialog.title = "商城密码"
|
||||
_password_dialog.dialog_text = "请输入 1~6 位密码"
|
||||
_password_dialog.ok_button_text = "确认"
|
||||
_password_dialog.cancel_button_text = "取消"
|
||||
var edit := LineEdit.new()
|
||||
edit.secret = true
|
||||
edit.max_length = 6
|
||||
edit.placeholder_text = "密码"
|
||||
edit.custom_minimum_size = Vector2(220, 28)
|
||||
_password_dialog.add_child(edit)
|
||||
_password_dialog.confirmed.connect(func() -> void:
|
||||
if not client.mall_password(edit.text) and is_instance_valid(_status):
|
||||
_status.text = "密码格式无效"
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null)
|
||||
_password_dialog.canceled.connect(func() -> void:
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null)
|
||||
_root.get_parent().add_child(_password_dialog)
|
||||
_password_dialog.popup_centered()
|
||||
|
||||
func _row(it: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(300, 0)
|
||||
@@ -71,7 +101,13 @@ func _row(it: Dictionary) -> Control:
|
||||
var out := Button.new()
|
||||
out.text = "取出"
|
||||
var cell := int(it.get("cell", 0))
|
||||
out.pressed.connect(func() -> void: client.mall_checkout(cell, 1, _first_free_inv()))
|
||||
out.pressed.connect(func() -> void:
|
||||
var target := _first_free_inv()
|
||||
if target < 0:
|
||||
_status.text = "背包已满"
|
||||
return
|
||||
if not client.mall_checkout(cell, 1, target):
|
||||
_status.text = "取出请求发送失败")
|
||||
row.add_child(out)
|
||||
return row
|
||||
|
||||
@@ -92,6 +128,10 @@ func _build(parent: Node) -> void:
|
||||
_title.text = "道具商城仓库"
|
||||
_title.add_theme_font_size_override("font_size", 16)
|
||||
box.add_child(_title)
|
||||
_status = Label.new()
|
||||
_status.add_theme_font_size_override("font_size", 11)
|
||||
_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
|
||||
box.add_child(_status)
|
||||
var sc := ScrollContainer.new()
|
||||
sc.custom_minimum_size = Vector2(308, 280)
|
||||
box.add_child(sc)
|
||||
|
||||
+12
-3
@@ -119,9 +119,18 @@ func set_anim_state(s: String) -> void:
|
||||
anim.set("loop", s in ["wait", "walk", "run"])
|
||||
anim.set("anim_path", msa)
|
||||
|
||||
# 供 net_world._on_info 用(如果它调 set_display_name)
|
||||
func set_display_name(_n: String) -> void:
|
||||
pass
|
||||
# 供 net_world._on_info 用:真模型没有统一的名字节点时补一个。
|
||||
func set_display_name(n: String) -> void:
|
||||
var tag := get_node_or_null("Label3D") as Label3D
|
||||
if tag == null:
|
||||
tag = Label3D.new()
|
||||
tag.name = "Label3D"
|
||||
tag.position.y = 2.1
|
||||
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
tag.no_depth_test = true
|
||||
tag.pixel_size = 0.006
|
||||
add_child(tag)
|
||||
tag.text = n
|
||||
|
||||
# --- 内部 -------------------------------------------------------------------
|
||||
|
||||
|
||||
+51
-4
@@ -18,14 +18,14 @@ const ROLE_ATTACKER := 2
|
||||
const ROLE_TANKER := 3
|
||||
const ROLE_BUFFER := 4
|
||||
const ROLE_SKILL_MASTER := 5
|
||||
const ROLE_BERSERKER := 6
|
||||
const ROLE_HASTE := 6
|
||||
const ROLE_DEFENDER := 7
|
||||
const ROLE_LABEL := {
|
||||
0: "普", 1: "队", 2: "攻", 3: "坦", 4: "辅", 5: "宗", 6: "狂", 7: "防",
|
||||
0: "普", 1: "队", 2: "攻", 3: "坦", 4: "辅", 5: "宗", 6: "速", 7: "防",
|
||||
}
|
||||
const ROLE_MENU := [
|
||||
[ROLE_NORMAL, "普通"], [ROLE_ATTACKER, "攻击"], [ROLE_TANKER, "坦克"],
|
||||
[ROLE_BERSERKER, "狂战"], [ROLE_BUFFER, "辅助"], [ROLE_SKILL_MASTER, "宗师"],
|
||||
[ROLE_HASTE, "迅捷"], [ROLE_BUFFER, "辅助"], [ROLE_SKILL_MASTER, "宗师"],
|
||||
[ROLE_DEFENDER, "防御"],
|
||||
]
|
||||
# Packet.h EPartyExpDistributionType
|
||||
@@ -34,13 +34,15 @@ const EXP_PARITY := 1 # 均分
|
||||
# uiparty.PartyMemberInfoBoard.PARTY_SKILL_*
|
||||
const PARTY_SKILL_HEAL := 1
|
||||
# affects[7] 槽位含义(暂定,按 partymemberinfoboard.py 图标顺序)
|
||||
const AFFECT_LABEL := ["经验", "攻击", "防御", "辅助", "宗师", "时间", "回复"]
|
||||
const AFFECT_LABEL := ["经验", "攻击", "坦克", "辅助", "宗师", "迅捷", "防御"]
|
||||
|
||||
var client: Node
|
||||
var dialogs: Node
|
||||
var _root: Control
|
||||
var _list: VBoxContainer
|
||||
var _dist_btn: Button
|
||||
var _heal_btn: Button
|
||||
var _notice: Label
|
||||
var _role_popup: Control = null
|
||||
|
||||
func setup(m2client: Node, parent: Node, dlg: Node = null) -> void:
|
||||
@@ -51,6 +53,8 @@ func setup(m2client: Node, parent: Node, dlg: Node = null) -> void:
|
||||
client.party_changed.connect(refresh)
|
||||
if client.has_signal("party_invite_ask"):
|
||||
client.party_invite_ask.connect(_on_invite)
|
||||
if client.has_signal("party_request_denied"):
|
||||
client.party_request_denied.connect(_on_request_denied)
|
||||
if client.has_signal("vitals_changed"):
|
||||
client.vitals_changed.connect(func(_v): if is_open(): refresh())
|
||||
refresh()
|
||||
@@ -91,6 +95,37 @@ func _refresh_dist_btn() -> void:
|
||||
var mode := int(client.get_party_distribute_mode()) if client.has_method("get_party_distribute_mode") else 0
|
||||
_dist_btn.text = "EXP:均分" if mode == EXP_PARITY else "EXP:不均分"
|
||||
_dist_btn.disabled = not _local_is_leader()
|
||||
if is_instance_valid(_heal_btn):
|
||||
var leadership := _leadership_level()
|
||||
_heal_btn.disabled = not _local_is_leader() or (leadership >= 0 and leadership < 18)
|
||||
_heal_btn.tooltip_text = "队长 Leadership 达到 18 级后可用" if _heal_btn.disabled and leadership >= 0 and leadership < 18 else "组队治疗"
|
||||
|
||||
func _leadership_level() -> int:
|
||||
# The server's CParty::Update gates party roles/heal using the leader's
|
||||
# Leadership skill. -1 means that the snapshot has not arrived yet; in
|
||||
# that case the server remains authoritative and the controls stay usable.
|
||||
if client == null or not client.has_method("get_skills"):
|
||||
return -1
|
||||
var skills: Array = client.get_skills()
|
||||
for skill in skills:
|
||||
if int(skill.get("id", -1)) == 121: # SKILL_LEADERSHIP in 40250
|
||||
return int(skill.get("level", 0))
|
||||
return -1
|
||||
|
||||
func _role_unlock_level(role: int) -> int:
|
||||
match role:
|
||||
ROLE_ATTACKER: return 10
|
||||
ROLE_TANKER, ROLE_HASTE: return 20
|
||||
ROLE_BUFFER: return 25
|
||||
ROLE_SKILL_MASTER: return 35
|
||||
ROLE_DEFENDER: return 40
|
||||
_: return 0
|
||||
|
||||
func _role_allowed(role: int, current_role: int = -1) -> bool:
|
||||
if role == ROLE_NORMAL or role == current_role:
|
||||
return true
|
||||
var leadership := _leadership_level()
|
||||
return leadership < 0 or leadership >= _role_unlock_level(role)
|
||||
|
||||
# --- 한 명 strip ------------------------------------------------------
|
||||
|
||||
@@ -178,6 +213,9 @@ func _open_role_popup(anchor: Control, pid: int, cur_role: int) -> void:
|
||||
var b := Button.new()
|
||||
b.text = entry[1] + (" ✓" if r == cur_role else "")
|
||||
b.custom_minimum_size = Vector2(84, 22)
|
||||
b.disabled = not _role_allowed(r, cur_role)
|
||||
if b.disabled:
|
||||
b.tooltip_text = "Leadership %d 级解锁" % _role_unlock_level(r)
|
||||
b.pressed.connect(func() -> void:
|
||||
if r == ROLE_NORMAL:
|
||||
# OnSelectState(-1): 清掉当前角色
|
||||
@@ -213,6 +251,10 @@ func _on_invite(leader_pid: int) -> void:
|
||||
else:
|
||||
accept.call()
|
||||
|
||||
func _on_request_denied() -> void:
|
||||
if is_instance_valid(_notice):
|
||||
_notice.text = "组队请求被拒绝"
|
||||
|
||||
# --- EXP 분배 / 组队治疗 --------------------------------------------
|
||||
|
||||
func _toggle_distribute() -> void:
|
||||
@@ -255,7 +297,12 @@ func _build(parent: Node) -> void:
|
||||
heal.text = "组队治疗"
|
||||
heal.add_theme_font_size_override("font_size", 11)
|
||||
heal.pressed.connect(_party_heal)
|
||||
_heal_btn = heal
|
||||
header.add_child(heal)
|
||||
_notice = Label.new()
|
||||
_notice.add_theme_font_size_override("font_size", 10)
|
||||
_notice.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
|
||||
header.add_child(_notice)
|
||||
|
||||
_list = VBoxContainer.new()
|
||||
_list.position = Vector2(8, 32)
|
||||
|
||||
@@ -17,6 +17,7 @@ const FEMALE_RACES := [1, 3, 4, 6]
|
||||
var model: Node # Metin2Model
|
||||
var anim: Node # Metin2AnimPlayer
|
||||
var motion_dir := ""
|
||||
var action_dir := ""
|
||||
var _state := ""
|
||||
var _forward := ["weapon_gr2", "shield_gr2", "gr2_path", "hair_gr2", "hair_skin", "specular_power"]
|
||||
|
||||
@@ -33,6 +34,7 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
|
||||
if base == "":
|
||||
return false
|
||||
motion_dir = base.path_join("general")
|
||||
action_dir = base.path_join("action")
|
||||
|
||||
model = ClassDB.instantiate("Metin2Model")
|
||||
model.name = "Metin2Model"
|
||||
@@ -54,6 +56,8 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
|
||||
anim.set("loop", true)
|
||||
anim.set("blend_time", 0.15)
|
||||
add_child(anim)
|
||||
if anim.has_signal("playback_finished"):
|
||||
anim.playback_finished.connect(_on_playback_finished)
|
||||
if pump.is_valid(): pump.call()
|
||||
set_anim_state("wait") # 重:解 .msa
|
||||
if pump.is_valid(): pump.call()
|
||||
@@ -71,6 +75,86 @@ func set_anim_state(s: String) -> void:
|
||||
anim.set("loop", s in ["wait", "walk", "run"])
|
||||
anim.set("anim_path", msa)
|
||||
|
||||
# Play one of the 40250 CRaceMotionData one-shot motions. The protocol sends
|
||||
# the numeric motion id; paired emotions use the other entity's race to select
|
||||
# kiss/french-kiss variants, exactly like ActDualEmotion in the native client.
|
||||
func set_motion_id(motion: int, target_race: int = -1) -> bool:
|
||||
if anim == null:
|
||||
return false
|
||||
var name := ""
|
||||
if motion == 305:
|
||||
name = "clap"
|
||||
elif motion == 306:
|
||||
name = "cheers_1"
|
||||
elif motion == 307:
|
||||
name = "cheers_2"
|
||||
elif motion >= 308 and motion <= 311:
|
||||
var job := motion - 308
|
||||
if motion == 308 and target_race >= 0:
|
||||
job = target_race & 3
|
||||
name = "kiss_with_" + CLASS_OF[clampi(job, 0, 3)]
|
||||
elif motion >= 312 and motion <= 315:
|
||||
var french_job := motion - 312
|
||||
if motion == 312 and target_race >= 0:
|
||||
french_job = target_race & 3
|
||||
name = "french_kiss_with_" + CLASS_OF[clampi(french_job, 0, 3)]
|
||||
elif motion >= 316 and motion <= 319:
|
||||
name = "slap_hit"
|
||||
elif motion >= 320 and motion <= 323:
|
||||
name = "slap_hurt"
|
||||
elif motion == 324:
|
||||
name = "dig"
|
||||
elif motion >= 325 and motion <= 340:
|
||||
name = "dance_%d" % (motion - 324)
|
||||
elif motion == 341:
|
||||
name = "congratulation"
|
||||
elif motion == 342:
|
||||
name = "forgive"
|
||||
elif motion == 343:
|
||||
name = "angry"
|
||||
elif motion == 344:
|
||||
name = "attractive"
|
||||
elif motion == 345:
|
||||
name = "sad"
|
||||
elif motion == 346:
|
||||
name = "shy"
|
||||
elif motion == 347:
|
||||
name = "cheerup"
|
||||
elif motion == 348:
|
||||
name = "banter"
|
||||
elif motion == 349:
|
||||
name = "joy"
|
||||
elif motion == 5 or motion == 6 or motion == 8 or motion == 9:
|
||||
set_anim_state("damage")
|
||||
return true
|
||||
elif motion == 11 or motion == 12:
|
||||
set_anim_state("dead")
|
||||
return true
|
||||
elif motion >= 13 and motion <= 21:
|
||||
set_anim_state("attack")
|
||||
return true
|
||||
else:
|
||||
return false
|
||||
|
||||
var msa := action_dir.path_join(name + ".msa")
|
||||
if not FileAccess.file_exists(msa):
|
||||
msa = motion_dir.path_join(name + ".msa")
|
||||
if not FileAccess.file_exists(msa):
|
||||
return false
|
||||
_state = "__motion"
|
||||
anim.set("loop", false)
|
||||
# A repeated command can name the same clip; clear first because the native
|
||||
# player treats assigning the current anim_path as a no-op.
|
||||
if String(anim.get("anim_path")) == msa:
|
||||
anim.set("anim_path", "")
|
||||
anim.set("anim_path", msa)
|
||||
return true
|
||||
|
||||
func _on_playback_finished() -> void:
|
||||
if _state == "__motion":
|
||||
_state = ""
|
||||
set_anim_state("wait")
|
||||
|
||||
func _set(prop: StringName, val: Variant) -> bool:
|
||||
if String(prop) in _forward and model:
|
||||
model.set(prop, val)
|
||||
|
||||
@@ -13,9 +13,23 @@
|
||||
# (`OnSelectItemSlot` / `DelPrivateShopItemStock`)。OkButton(`BuildPrivateShop`):stock 按
|
||||
# 格号排序、`display_pos = 格号`、上限 39(`PRIVATE_SHOP_ITEM_MAX_NUM`)打包成
|
||||
# `M2Client.open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}])`
|
||||
# (= `TPacketCGMyShop` + `TShopItemTable`×N)。철수 `close_private_shop()` = SHOP_CG_END。
|
||||
# (= `TPacketCGMyShop` + `TShopItemTable`×N)。40250 收摊用空的 `CG_MYSHOP`
|
||||
# (服务端 `OpenMyShop` 会先关闭已有的个人摊位);`close_private_shop()` 已在
|
||||
# M2Client classic 分支按该语义发送。
|
||||
extends Node
|
||||
|
||||
class PrivateShopDragButton extends Button:
|
||||
var drag_entry: Dictionary = {}
|
||||
|
||||
func _get_drag_data(_at_position: Vector2):
|
||||
if drag_entry.is_empty():
|
||||
return null
|
||||
var preview := Label.new()
|
||||
preview.text = String(text)
|
||||
preview.add_theme_color_override("font_color", Color(1, 0.9, 0.55))
|
||||
set_drag_preview(preview)
|
||||
return {"private_shop_entry": drag_entry.duplicate(true)}
|
||||
|
||||
const PRIVATE_SHOP_ITEM_MAX := 39 # PRIVATE_SHOP_ITEM_MAX_NUM (TPacketCGMyShop::bCount 上限)
|
||||
const SIGN_MAX := 25 # NameLine input_limit
|
||||
|
||||
@@ -33,6 +47,7 @@ var _status: Label
|
||||
var _stock: Dictionary = {} # 格号:int -> {cell, vnum, count, price}
|
||||
var _picked = null # {cell, vnum, count} 等待落位
|
||||
var _price_dialog: Control = null
|
||||
var _server_prices: Dictionary = {} # vnum -> last price returned by MyShopPriceList
|
||||
|
||||
func setup(m2client: Node, ui_manager: CanvasLayer, proto_node: Node = null, assets := "") -> void:
|
||||
client = m2client
|
||||
@@ -46,6 +61,10 @@ func setup(m2client: Node, ui_manager: CanvasLayer, proto_node: Node = null, ass
|
||||
uiscript_dir = assets_root.path_join("uiscript")
|
||||
if client and client.has_signal("inventory_changed"):
|
||||
client.inventory_changed.connect(func(_w, _c): if is_open(): _refresh())
|
||||
if client and client.has_signal("my_shop_price_list"):
|
||||
client.my_shop_price_list.connect(_on_server_price)
|
||||
if client and client.has_signal("private_shop_open_requested"):
|
||||
client.private_shop_open_requested.connect(open)
|
||||
|
||||
# --- open / close --------------------------------------------------
|
||||
|
||||
@@ -101,6 +120,10 @@ func _index_cells(root: Node) -> void:
|
||||
n.gui_input.connect(func(e: InputEvent) -> void:
|
||||
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and e.pressed:
|
||||
_on_slot_clicked(idx))
|
||||
# 同时支持从右侧背包候选区拖到空货位;点击选取仍保留给触屏/无鼠标场景。
|
||||
n.set_drag_forwarding(Callable(self, "_no_drag_data"),
|
||||
Callable(self, "_can_drop_slot").bind(idx),
|
||||
Callable(self, "_drop_slot").bind(idx))
|
||||
|
||||
# NameLine 在 uiscript 里是 text(Label)—— 盖一个 LineEdit 上去
|
||||
func _overlay_name_edit(root: Control) -> void:
|
||||
@@ -194,12 +217,13 @@ func _refresh_inv() -> void:
|
||||
continue
|
||||
var vnum := int(it.get("vnum", 0))
|
||||
var count := int(it.get("count", 1))
|
||||
var b := Button.new()
|
||||
var b := PrivateShopDragButton.new()
|
||||
b.text = "%s ×%d" % [_name_of(vnum), count]
|
||||
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
b.custom_minimum_size = Vector2(196, 22)
|
||||
b.toggle_mode = true
|
||||
var entry := {"cell": cell, "vnum": vnum, "count": count}
|
||||
b.drag_entry = entry
|
||||
b.pressed.connect(func() -> void: _pick(entry, b))
|
||||
if _picked != null and int(_picked["cell"]) == cell:
|
||||
b.button_pressed = true
|
||||
@@ -246,6 +270,20 @@ func _pick(entry: Dictionary, _btn: Button) -> void:
|
||||
_picked = entry
|
||||
_refresh_grid()
|
||||
|
||||
func _no_drag_data(_at_position: Vector2):
|
||||
return null
|
||||
|
||||
func _can_drop_slot(_at_position: Vector2, data, slot: int) -> bool:
|
||||
return slot >= 0 and slot < 40 and not _stock.has(slot) \
|
||||
and data is Dictionary and data.has("private_shop_entry")
|
||||
|
||||
func _drop_slot(_at_position: Vector2, data, slot: int) -> void:
|
||||
if not _can_drop_slot(Vector2.ZERO, data, slot):
|
||||
return
|
||||
var entry: Dictionary = data["private_shop_entry"] as Dictionary
|
||||
_picked = entry.duplicate(true)
|
||||
_ask_price(slot)
|
||||
|
||||
func _on_slot_clicked(slot: int) -> void:
|
||||
if _stock.has(slot):
|
||||
_stock.erase(slot)
|
||||
@@ -276,7 +314,10 @@ func _ask_price(slot: int) -> void:
|
||||
spin.min_value = 1
|
||||
spin.max_value = 2000000000
|
||||
spin.step = 1
|
||||
spin.value = int(_stock.get(slot, {}).get("price", 1))
|
||||
var default_price := int(_server_prices.get(int(_picked["vnum"]), 1))
|
||||
if _stock.has(slot):
|
||||
default_price = int(_stock[slot].get("price", default_price))
|
||||
spin.value = default_price
|
||||
spin.position = Vector2(12, 40)
|
||||
spin.size = Vector2(216, 28)
|
||||
dlg.add_child(spin)
|
||||
@@ -299,7 +340,7 @@ func _dismiss_price_dialog() -> void:
|
||||
_price_dialog = null
|
||||
|
||||
func _place(slot: int, price: int) -> void:
|
||||
if price <= 0 or _picked == null:
|
||||
if slot < 0 or slot >= 40 or price <= 0 or price > 2000000000 or _picked == null:
|
||||
_dismiss_price_dialog()
|
||||
return
|
||||
_stock[slot] = {"cell": int(_picked["cell"]), "vnum": int(_picked["vnum"]),
|
||||
@@ -308,7 +349,16 @@ func _place(slot: int, price: int) -> void:
|
||||
_dismiss_price_dialog()
|
||||
_refresh()
|
||||
|
||||
func _on_server_price(vnum: int, price: int) -> void:
|
||||
if vnum > 0 and price >= 0:
|
||||
_server_prices[vnum] = price
|
||||
|
||||
func _ok() -> void:
|
||||
var sign := _sign.text.strip_edges()
|
||||
if sign.is_empty() or sign.length() > SIGN_MAX:
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "请输入 1~%d 个字符的摊位名称" % SIGN_MAX
|
||||
return
|
||||
if _stock.is_empty():
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "판매할 아이템을 올리세요"
|
||||
@@ -323,8 +373,10 @@ func _ok() -> void:
|
||||
items.append({"vnum": s["vnum"], "count": s["count"], "inv_cell": s["cell"],
|
||||
"price": s["price"], "display_pos": slot})
|
||||
if client and client.has_method("open_private_shop"):
|
||||
client.open_private_shop(_sign.text.strip_edges(), items)
|
||||
close()
|
||||
if client.open_private_shop(sign, items):
|
||||
close()
|
||||
elif is_instance_valid(_status):
|
||||
_status.text = "摆摊请求发送失败"
|
||||
|
||||
func _close_shop() -> void:
|
||||
if client and client.has_method("close_private_shop"):
|
||||
|
||||
@@ -20,6 +20,8 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
_build(parent)
|
||||
if client.has_signal("refine_ask"):
|
||||
client.refine_ask.connect(_on_ask)
|
||||
if client.has_signal("refine_result"):
|
||||
client.refine_result.connect(_on_result)
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
@@ -53,6 +55,11 @@ func _do_refine() -> void:
|
||||
client.refine(int(_cur.get("pos", 0)), int(_cur.get("type", 0)))
|
||||
_root.visible = false
|
||||
|
||||
func _on_result(ok: bool) -> void:
|
||||
_cur = {}
|
||||
_text.text = "[b]精炼成功[/b]" if ok else "[b]精炼失败[/b]"
|
||||
_root.visible = true
|
||||
|
||||
func _build(parent: Node) -> void:
|
||||
_root = Control.new()
|
||||
_root.set_anchors_preset(Control.PRESET_CENTER)
|
||||
|
||||
@@ -15,6 +15,8 @@ var _root: Control
|
||||
var _list: VBoxContainer
|
||||
var _gold: Label
|
||||
var _title: Label
|
||||
var _status: Label
|
||||
var _password_dialog: ConfirmationDialog
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client = m2client
|
||||
@@ -22,13 +24,25 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
_build(parent)
|
||||
if client.has_signal("safebox_changed"):
|
||||
client.safebox_changed.connect(refresh)
|
||||
if client.has_signal("safebox_password_required"):
|
||||
client.safebox_password_required.connect(func(): _ask_password("safebox"))
|
||||
if client.has_signal("safebox_wrong_password"):
|
||||
client.safebox_wrong_password.connect(func():
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "仓库密码错误")
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func deposit(inv_window: int, inv_cell: int) -> void:
|
||||
if is_open():
|
||||
client.safebox_checkin(_next_free_slot(), inv_window, inv_cell)
|
||||
if not is_open() or inv_window < 0 or inv_cell < 0:
|
||||
return
|
||||
var safe_pos := _next_free_slot()
|
||||
if safe_pos < 0:
|
||||
_status.text = "仓库已满"
|
||||
return
|
||||
if client.has_method("safebox_checkin") and not client.safebox_checkin(safe_pos, inv_window, inv_cell):
|
||||
_status.text = "存入请求发送失败"
|
||||
|
||||
func _next_free_slot() -> int:
|
||||
var used := {}
|
||||
@@ -38,7 +52,7 @@ func _next_free_slot() -> int:
|
||||
for i in range(cap):
|
||||
if not used.has(i):
|
||||
return i
|
||||
return 0
|
||||
return -1
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if proto and proto.has_method("item"):
|
||||
@@ -54,6 +68,7 @@ func refresh() -> void:
|
||||
_root.visible = client.is_safebox_open()
|
||||
if not _root.visible:
|
||||
return
|
||||
_status.text = ""
|
||||
_title.text = "仓库(%d 页)" % client.get_safebox_size()
|
||||
_gold.text = "仓库金币: %d" % client.get_safebox_gold()
|
||||
for c in _list.get_children():
|
||||
@@ -67,6 +82,32 @@ func refresh() -> void:
|
||||
for it in items:
|
||||
_list.add_child(_row(it))
|
||||
|
||||
func _ask_password(kind: String) -> void:
|
||||
if is_instance_valid(_password_dialog):
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = ConfirmationDialog.new()
|
||||
_password_dialog.title = "商城密码" if kind == "mall" else "仓库密码"
|
||||
_password_dialog.dialog_text = "请输入 1~6 位密码"
|
||||
_password_dialog.ok_button_text = "确认"
|
||||
_password_dialog.cancel_button_text = "取消"
|
||||
var edit := LineEdit.new()
|
||||
edit.secret = true
|
||||
edit.max_length = 6
|
||||
edit.placeholder_text = "密码"
|
||||
edit.custom_minimum_size = Vector2(220, 28)
|
||||
_password_dialog.add_child(edit)
|
||||
_password_dialog.confirmed.connect(func() -> void:
|
||||
var sent: bool = client.safebox_password(edit.text) if kind == "safebox" else client.mall_password(edit.text)
|
||||
if not sent and is_instance_valid(_status):
|
||||
_status.text = "密码格式无效"
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null)
|
||||
_password_dialog.canceled.connect(func() -> void:
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null)
|
||||
_root.get_parent().add_child(_password_dialog)
|
||||
_password_dialog.popup_centered()
|
||||
|
||||
func _row(it: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(300, 0)
|
||||
@@ -78,7 +119,13 @@ func _row(it: Dictionary) -> Control:
|
||||
var out := Button.new()
|
||||
out.text = "取出"
|
||||
var cell := int(it.get("cell", 0))
|
||||
out.pressed.connect(func() -> void: client.safebox_checkout(cell, 1, _first_free_inv()))
|
||||
out.pressed.connect(func() -> void:
|
||||
var target := _first_free_inv()
|
||||
if target < 0:
|
||||
_status.text = "背包已满"
|
||||
return
|
||||
if not client.safebox_checkout(cell, 1, target):
|
||||
_status.text = "取出请求发送失败")
|
||||
row.add_child(out)
|
||||
return row
|
||||
|
||||
@@ -89,7 +136,7 @@ func _first_free_inv() -> int:
|
||||
for i in range(90):
|
||||
if not used.has(i):
|
||||
return i
|
||||
return 0
|
||||
return -1
|
||||
|
||||
func _build(parent: Node) -> void:
|
||||
_root = Control.new()
|
||||
@@ -116,6 +163,11 @@ func _build(parent: Node) -> void:
|
||||
_gold.modulate = Color(0.95, 0.85, 0.5)
|
||||
_gold.add_theme_font_size_override("font_size", 12)
|
||||
_root.add_child(_gold)
|
||||
_status = Label.new()
|
||||
_status.position = Vector2(12, 382)
|
||||
_status.add_theme_font_size_override("font_size", 11)
|
||||
_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
|
||||
_root.add_child(_status)
|
||||
_list = VBoxContainer.new()
|
||||
_list.position = Vector2(12, 54)
|
||||
_list.add_theme_constant_override("separation", 4)
|
||||
|
||||
+44
-4
@@ -18,6 +18,7 @@ var _root: Control
|
||||
var _tabbar: HBoxContainer
|
||||
var _list: VBoxContainer
|
||||
var _err: Label
|
||||
var _sell_quantity: SpinBox
|
||||
var _active_tab := 0
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null, ilist: RefCounted = null) -> void:
|
||||
@@ -38,19 +39,27 @@ func is_open() -> bool:
|
||||
func open() -> void:
|
||||
_root.visible = true
|
||||
_err.text = ""
|
||||
_sell_quantity.value = 1
|
||||
_active_tab = 0
|
||||
refresh()
|
||||
|
||||
func _close() -> void:
|
||||
_root.visible = false
|
||||
_err.text = ""
|
||||
if _sell_quantity:
|
||||
_sell_quantity.value = 1
|
||||
|
||||
func close_and_leave() -> void:
|
||||
client.shop_close()
|
||||
_close()
|
||||
|
||||
func sell(inv_cell: int, count: int = 1) -> void:
|
||||
if is_open():
|
||||
client.shop_sell(inv_cell, count)
|
||||
func sell(inv_cell: int, count: int = -1) -> void:
|
||||
if not is_open() or inv_cell < 0:
|
||||
return
|
||||
var amount := count if count > 0 else int(_sell_quantity.value)
|
||||
amount = clampi(amount, 1, 200)
|
||||
if client.has_method("shop_sell") and not client.shop_sell(inv_cell, amount):
|
||||
_on_error("SEND_FAILED")
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if proto and proto.has_method("item"):
|
||||
@@ -122,7 +131,19 @@ func _row(it: Dictionary, pos_base := 0) -> Control:
|
||||
var buy := Button.new()
|
||||
buy.text = "买"
|
||||
var pos := pos_base + int(it.get("pos", 0))
|
||||
buy.pressed.connect(func() -> void: client.shop_buy(pos, 1))
|
||||
var stock := int(it.get("count", 0))
|
||||
var quantity := SpinBox.new()
|
||||
quantity.name = "Quantity"
|
||||
quantity.min_value = 1
|
||||
quantity.max_value = clampi(stock if stock > 0 else 200, 1, 200)
|
||||
quantity.step = 1
|
||||
quantity.value = 1
|
||||
quantity.custom_minimum_size = Vector2(64, 0)
|
||||
row.add_child(quantity)
|
||||
buy.pressed.connect(func() -> void:
|
||||
var amount := clampi(int(quantity.value), 1, int(quantity.max_value))
|
||||
if not client.shop_buy(pos, amount):
|
||||
_on_error("SEND_FAILED"))
|
||||
row.add_child(buy)
|
||||
return row
|
||||
|
||||
@@ -132,6 +153,11 @@ func _on_error(kind: String) -> void:
|
||||
"SOLDOUT": "已售罄",
|
||||
"INVENTORY_FULL": "背包已满",
|
||||
"INVALID_POS": "位置无效",
|
||||
"NOT_ENOUGH_ITEM": "物品数量不足",
|
||||
"CANNOT_BUY": "当前物品不可购买",
|
||||
"CANNOT_SELL": "当前物品不可出售",
|
||||
"SHOP_BUSY": "商店正在处理上一笔交易",
|
||||
"SEND_FAILED": "交易请求发送失败",
|
||||
}
|
||||
_err.text = str(tbl.get(kind, kind))
|
||||
|
||||
@@ -159,6 +185,20 @@ func _build(parent: Node) -> void:
|
||||
_err.modulate = Color(1, 0.4, 0.4)
|
||||
_err.add_theme_font_size_override("font_size", 12)
|
||||
_root.add_child(_err)
|
||||
var sell_hint := Label.new()
|
||||
sell_hint.text = "出售数量(背包右键):"
|
||||
sell_hint.position = Vector2(12, 360)
|
||||
sell_hint.add_theme_font_size_override("font_size", 11)
|
||||
_root.add_child(sell_hint)
|
||||
_sell_quantity = SpinBox.new()
|
||||
_sell_quantity.name = "SellQuantity"
|
||||
_sell_quantity.min_value = 1
|
||||
_sell_quantity.max_value = 200
|
||||
_sell_quantity.value = 1
|
||||
_sell_quantity.step = 1
|
||||
_sell_quantity.position = Vector2(160, 356)
|
||||
_sell_quantity.size = Vector2(70, 26)
|
||||
_root.add_child(_sell_quantity)
|
||||
_tabbar = HBoxContainer.new()
|
||||
_tabbar.position = Vector2(12, 30)
|
||||
_tabbar.add_theme_constant_override("separation", 4)
|
||||
|
||||
@@ -45,6 +45,15 @@ var camera_mode := 1 # 0 近, 1 远
|
||||
var fog_level := 1 # 0 浓, 1 中, 2 淡
|
||||
var tiling_mode := 1 # 0 CPU, 1 GPU(占位)
|
||||
|
||||
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")
|
||||
# Godot headless 在受限执行环境里可能无法写 user://;测试配置放在系统
|
||||
# 临时目录,正常图形客户端仍使用 user://。
|
||||
return OS.get_temp_dir().path_join("mtgodot_system_option.cfg") \
|
||||
if DisplayServer.get_name() == "headless" else CFG_PATH
|
||||
|
||||
func setup(ui_manager: CanvasLayer, assets: String, audio_node: Node = null,
|
||||
cam_getter := Callable(), env_getter := Callable()) -> void:
|
||||
ui = ui_manager
|
||||
@@ -228,7 +237,7 @@ func _apply_fog() -> void:
|
||||
# --- persistence (= systemSetting 配置文件) --------------------------
|
||||
|
||||
func _load_cfg() -> void:
|
||||
if _cfg.load(CFG_PATH) != OK:
|
||||
if _cfg.load(config_path()) != OK:
|
||||
return
|
||||
music_volume = float(_cfg.get_value("audio", "music_volume", music_volume))
|
||||
sound_volume = float(_cfg.get_value("audio", "sound_volume", sound_volume))
|
||||
@@ -242,4 +251,6 @@ func _save() -> void:
|
||||
_cfg.set_value("video", "camera_mode", camera_mode)
|
||||
_cfg.set_value("video", "fog_level", fog_level)
|
||||
_cfg.set_value("video", "tiling_mode", tiling_mode)
|
||||
_cfg.save(CFG_PATH)
|
||||
var err := _cfg.save(config_path())
|
||||
if err != OK:
|
||||
push_warning("SystemOptionUI: save failed (%s): %s" % [err, config_path()])
|
||||
|
||||
@@ -68,6 +68,10 @@ static func _load_uncached(assets_root: String, vpath: String) -> Texture2D:
|
||||
return _load_image_file(real)
|
||||
|
||||
static func _load_image_file(path: String) -> Texture2D:
|
||||
# .sub 文件可能引用了未随当前资源包发布的共享贴图(例如 Public.tga)。
|
||||
# 先做存在性检查,避免 headless/UI fallback 因缺失可选贴图刷错误日志。
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
var ext := path.get_extension().to_lower()
|
||||
if ext == "dds":
|
||||
var di := load_dds_image(path)
|
||||
@@ -99,6 +103,8 @@ static func _load_sub(path: String) -> Texture2D:
|
||||
var img_path := path.get_base_dir().path_join(image_name)
|
||||
if not FileAccess.file_exists(img_path):
|
||||
img_path = path.get_base_dir().path_join(image_name.get_basename() + ".tga")
|
||||
if not FileAccess.file_exists(img_path):
|
||||
return null
|
||||
var base := _load_image_file(img_path)
|
||||
if base == null:
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user