Files
mtgodot-poc/project/ui/exchange_ui.gd
T
shenleiandClaude Sonnet 5 f32064c74a 40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.

Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:

- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
  new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
  (§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
  drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
  empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
  loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
  entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
  GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
  discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
  net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
  1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).

Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:13:01 +09:00

200 lines
5.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ExchangeUI (P8) —— 交易 / 换货窗。
#
# var xu := preload("res://ui/exchange_ui.gd").new()
# add_child(xu)
# xu.setup(m2client, canvas_parent, proto) # proto 可空
#
# 两栏:我方 / 对方,各列道具行 + 金币。底部:金币输入 + [放金币]、[接受]、[取消]。
# `exchange_changed` 刷新(active=false 时自动关闭)。
# inventory_ui 在交易开着时右键道具 → 调 xu.offer(win, cell)。
extends Node
var client: Node
var proto: Node
var _root: Control
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
proto = proto_node
_build(parent)
if client.has_signal("exchange_changed"):
client.exchange_changed.connect(refresh)
func is_open() -> bool:
return _root != null and _root.visible
func close() -> void:
if _root:
_root.visible = false
_next_display = 0
_offered_cells.clear()
if _gold_input:
_gold_input.text = ""
func offer(inv_window: int, inv_cell: int) -> void:
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"):
var d: Dictionary = proto.item(vnum)
var n := String(d.get("locale_name", d.get("name", "")))
if n != "":
return n
return "#%d" % vnum
func refresh() -> void:
if client == null:
return
var x: Dictionary = client.get_exchange()
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", []))
_fill(_peer_box, x.get("peer_items", []))
_self_gold.text = "金币: %d" % int(x.get("self_gold", 0))
_peer_gold.text = "金币: %d" % int(x.get("peer_gold", 0))
var me: bool = x.get("self_accept", false)
var peer: bool = x.get("peer_accept", false)
_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():
c.queue_free()
for r in rows:
var l := Label.new()
l.text = "%s ×%d" % [_name_of(int(r.get("vnum", 0))), int(r.get("count", 1))]
l.add_theme_font_size_override("font_size", 12)
box.add_child(l)
func _on_put_gold() -> void:
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 = "金币数量必须在 12000000000 之间"
return
if client.exchange_add_gold(g):
_gold_input.text = ""
_status.text = ""
else:
_status.text = "金币请求发送失败"
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-220, -180)
_root.size = Vector2(440, 360)
_root.visible = false
_root.set_meta("is_titlebar", true)
parent.add_child(_root)
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.07, 0.08, 0.1, 0.97)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
var title := Label.new()
title.text = "交易"
title.position = Vector2(12, 8)
_root.add_child(title)
var sl := Label.new()
sl.text = "我方"
sl.position = Vector2(20, 34)
_root.add_child(sl)
_self_box = VBoxContainer.new()
_self_box.position = Vector2(20, 56)
_self_box.custom_minimum_size = Vector2(200, 0)
_root.add_child(_self_box)
_self_gold = Label.new()
_self_gold.text = "金币: 0"
_self_gold.position = Vector2(20, 250)
_root.add_child(_self_gold)
var pl := Label.new()
pl.text = "对方"
pl.position = Vector2(240, 34)
_root.add_child(pl)
_peer_box = VBoxContainer.new()
_peer_box.position = Vector2(240, 56)
_peer_box.custom_minimum_size = Vector2(180, 0)
_root.add_child(_peer_box)
_peer_gold = Label.new()
_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"
pa.text = "对方: 未接受"
pa.position = Vector2(240, 272)
pa.add_theme_font_size_override("font_size", 12)
_root.add_child(pa)
var bottom := HBoxContainer.new()
bottom.position = Vector2(20, 310)
_root.add_child(bottom)
_gold_input = LineEdit.new()
_gold_input.placeholder_text = "金币"
_gold_input.custom_minimum_size = Vector2(90, 0)
bottom.add_child(_gold_input)
var put := Button.new()
put.text = "放金币"
put.pressed.connect(_on_put_gold)
bottom.add_child(put)
_accept_btn = Button.new()
_accept_btn.text = "接受"
_accept_btn.pressed.connect(func() -> void: client.exchange_accept())
bottom.add_child(_accept_btn)
var cancel := Button.new()
cancel.text = "取消"
cancel.pressed.connect(func() -> void: client.exchange_cancel())
bottom.add_child(cancel)