Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
176 lines
5.1 KiB
GDScript
176 lines
5.1 KiB
GDScript
# ShopUI (P8) —— NPC 商店窗。
|
|
#
|
|
# var su := preload("res://ui/shop_ui.gd").new()
|
|
# add_child(su)
|
|
# su.setup(m2client, canvas_parent, proto, item_list) # proto / item_list 可空
|
|
#
|
|
# `shop_opened(vid)` → 打开并列出货物(名字 + 价格 + [买])。
|
|
# `shop_closed` → 关闭。`shop_error(kind)` → 顶部红字提示。
|
|
# 卖:inventory_ui 在商店开着时右键道具 → 调 shop_ui.sell(cell)。
|
|
extends Node
|
|
|
|
const SHOP_SLOT_COUNT := 40 # shop.SHOP_SLOT_COUNT (== SHOP_HOST_ITEM_MAX_NUM)
|
|
|
|
var client: Node
|
|
var proto: Node
|
|
var item_list # ItemListDB (RefCounted)
|
|
var _root: Control
|
|
var _tabbar: HBoxContainer
|
|
var _list: VBoxContainer
|
|
var _err: Label
|
|
var _active_tab := 0
|
|
|
|
func setup(m2client: Node, parent: Node, proto_node: Node = null, ilist: RefCounted = null) -> void:
|
|
client = m2client
|
|
proto = proto_node
|
|
item_list = ilist
|
|
_build(parent)
|
|
if client.has_signal("shop_opened"):
|
|
client.shop_opened.connect(func(_v): open())
|
|
if client.has_signal("shop_closed"):
|
|
client.shop_closed.connect(_close)
|
|
if client.has_signal("shop_error"):
|
|
client.shop_error.connect(_on_error)
|
|
|
|
func is_open() -> bool:
|
|
return _root != null and _root.visible
|
|
|
|
func open() -> void:
|
|
_root.visible = true
|
|
_err.text = ""
|
|
_active_tab = 0
|
|
refresh()
|
|
|
|
func _close() -> void:
|
|
_root.visible = false
|
|
|
|
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 _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 not is_open() or client == null:
|
|
return
|
|
for c in _tabbar.get_children():
|
|
_tabbar.remove_child(c)
|
|
c.queue_free()
|
|
for c in _list.get_children():
|
|
_list.remove_child(c)
|
|
c.queue_free()
|
|
|
|
# START_EX 商店有多个货架(shop.GetTabCount);普通 START 商店 tabs 只 1 个。
|
|
var tabs: Array = []
|
|
if client.has_method("get_shop"):
|
|
tabs = client.get_shop().get("tabs", [])
|
|
if tabs.is_empty():
|
|
# 兼容没有 get_shop 的旧桩
|
|
tabs = [{"name": "", "items": client.get_shop_items()}]
|
|
_active_tab = clampi(_active_tab, 0, tabs.size() - 1)
|
|
|
|
if tabs.size() > 1:
|
|
_tabbar.visible = true
|
|
for i in tabs.size():
|
|
var tb := Button.new()
|
|
tb.toggle_mode = true
|
|
var tn := String(tabs[i].get("name", ""))
|
|
tb.text = tn if tn != "" else "货架 %d" % (i + 1)
|
|
tb.button_pressed = (i == _active_tab)
|
|
var idx := i
|
|
tb.pressed.connect(func() -> void:
|
|
_active_tab = idx
|
|
refresh())
|
|
_tabbar.add_child(tb)
|
|
else:
|
|
_tabbar.visible = false
|
|
|
|
var items: Array = tabs[_active_tab].get("items", [])
|
|
if items.is_empty():
|
|
var e := Label.new()
|
|
e.text = "(无货物)"
|
|
_list.add_child(e)
|
|
return
|
|
# uishop.py: 买位置 = tabIdx * SHOP_SLOT_COUNT + slotPos
|
|
var base := _active_tab * SHOP_SLOT_COUNT
|
|
for it in items:
|
|
_list.add_child(_row(it, base))
|
|
|
|
func _row(it: Dictionary, pos_base := 0) -> Control:
|
|
var row := HBoxContainer.new()
|
|
row.custom_minimum_size = Vector2(320, 0)
|
|
var nm := Label.new()
|
|
nm.text = _name_of(int(it.get("vnum", 0)))
|
|
nm.custom_minimum_size = Vector2(170, 0)
|
|
nm.add_theme_font_size_override("font_size", 12)
|
|
row.add_child(nm)
|
|
var pr := Label.new()
|
|
pr.text = "%d 金" % int(it.get("price", 0))
|
|
pr.custom_minimum_size = Vector2(90, 0)
|
|
pr.modulate = Color(0.95, 0.85, 0.5)
|
|
row.add_child(pr)
|
|
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))
|
|
row.add_child(buy)
|
|
return row
|
|
|
|
func _on_error(kind: String) -> void:
|
|
var tbl := {
|
|
"NOT_ENOUGH_MONEY": "金币不足",
|
|
"SOLDOUT": "已售罄",
|
|
"INVENTORY_FULL": "背包已满",
|
|
"INVALID_POS": "位置无效",
|
|
}
|
|
_err.text = str(tbl.get(kind, kind))
|
|
|
|
func _build(parent: Node) -> void:
|
|
_root = Control.new()
|
|
_root.set_anchors_preset(Control.PRESET_CENTER)
|
|
_root.position = Vector2(-190, -200)
|
|
_root.size = Vector2(380, 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.08, 0.07, 0.06, 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)
|
|
_err = Label.new()
|
|
_err.position = Vector2(60, 10)
|
|
_err.modulate = Color(1, 0.4, 0.4)
|
|
_err.add_theme_font_size_override("font_size", 12)
|
|
_root.add_child(_err)
|
|
_tabbar = HBoxContainer.new()
|
|
_tabbar.position = Vector2(12, 30)
|
|
_tabbar.add_theme_constant_override("separation", 4)
|
|
_tabbar.visible = false
|
|
_root.add_child(_tabbar)
|
|
_list = VBoxContainer.new()
|
|
_list.position = Vector2(12, 58)
|
|
_list.add_theme_constant_override("separation", 4)
|
|
_root.add_child(_list)
|
|
var close_btn := Button.new()
|
|
close_btn.text = "离开"
|
|
close_btn.position = Vector2(300, 360)
|
|
close_btn.pressed.connect(close_and_leave)
|
|
_root.add_child(close_btn)
|