Files
mtgodot-poc/project/ui/safebox_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

182 lines
5.7 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.
# SafeboxUI (P8) —— 仓库窗。
#
# var bu := preload("res://ui/safebox_ui.gd").new()
# add_child(bu)
# bu.setup(m2client, canvas_parent, proto) # proto 可空
#
# `safebox_changed` 刷新(size>0 → 打开)。列出仓库道具 + [取出]。
# inventory_ui 在仓库开着时右键道具 → 调 bu.deposit(win, cell)。
# 取出/存入用第一个空位(简版:deposit 用 safe_pos = 下一个空格;checkout 用道具 cell)。
extends Node
var client: Node
var proto: Node
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
proto = proto_node
_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 close() -> void:
if _root:
_root.visible = false
if is_instance_valid(_password_dialog):
_password_dialog.queue_free()
_password_dialog = null
func deposit(inv_window: int, inv_cell: int) -> void:
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 := {}
for it in client.get_safebox_items():
used[int(it.get("cell", -1))] = true
var cap: int = maxi(1, client.get_safebox_size()) * 45
for i in range(cap):
if not used.has(i):
return i
return -1
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
_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():
c.queue_free()
var items: Array = client.get_safebox_items()
if items.is_empty():
var e := Label.new()
e.text = "(空)"
_list.add_child(e)
return
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 = "请输入 16 位密码"
_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)
var nm := Label.new()
nm.text = "%s ×%d" % [_name_of(int(it.get("vnum", 0))), int(it.get("count", 1))]
nm.custom_minimum_size = Vector2(220, 0)
nm.add_theme_font_size_override("font_size", 12)
row.add_child(nm)
var out := Button.new()
out.text = "取出"
var cell := int(it.get("cell", 0))
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
func _first_free_inv() -> int:
var used := {}
for it in client.get_inventory():
used[int(it.get("cell", -1))] = true
for i in range(90):
if not used.has(i):
return i
return -1
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-170, -200)
_root.size = Vector2(340, 400)
_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.06, 0.08, 0.09, 0.97)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
_title = Label.new()
_title.text = "仓库"
_title.position = Vector2(12, 8)
_root.add_child(_title)
_gold = Label.new()
_gold.text = "仓库金币: 0"
_gold.position = Vector2(12, 30)
_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)
_root.add_child(_list)