Files
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
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
2026-08-31 20:02:12 +09:00

218 lines
6.9 KiB
GDScript
Raw Permalink 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.
# SelectItemUI (P11) —— 选魔石窗(1:1 迁移 `assets/root/uiselectitem.py`)。
#
# var si := preload("res://ui/select_item_ui.gd").new()
# add_child(si)
# si.setup(ui_manager, m2client, proto, assets_root, item_list)
# si.open() # 由 quest_dialog.select_item_requested 触发
#
# 布局走 `assets/uiscript/uiscript/selectitemwindow.py`UiScript → UiBuild)。
# 逐字对照 uiselectitem.py.RefreshSlot:遍历背包前 `INVENTORY_PAGE_SIZE*2` 格,
# 只保留 `item.IsMetin`proto.type == ITEM_TYPE_METIN 10)且 `GetItemGrade <= 2`
# (= 物品内部名最后一位数字,非数字按 0)的物品,最多 54 个。
# 点格子 -> `net.SendSelectItemPacket(inventoryCell)`= M2Client.script_select_item+ 关窗;
# 关闭 / ExitButton -> `net.SendSelectItemPacket(0)` + 关窗(uiselectitem.Close)。
extends Node
const UiAssets = preload("res://ui/ui_assets.gd")
const ITEM_TYPE_METIN := 10 # GameLib/ItemData.h EItemType
const INVENTORY_PAGE_SIZE := 45 # player.INVENTORY_PAGE_SIZE
const MAX_ITEMS := 54 # uiselectitem: slotPos > 54 -> break
const WINDOW_INVENTORY := 1
var ui: CanvasLayer # UiManager
var client: Node # M2Client
var proto: Node # Metin2Proto
var item_list: RefCounted # ItemListDB(可空)
var assets_root := ""
var uiscript_dir := ""
var _win: Dictionary = {} # { root, nodes }
var _cells := {} # slot_index:int -> Panel
var _slot_to_inv := {} # 选择窗格号 -> 背包格号(inventorySlotPosDict
var _sent := false # 已发过一次 select(避免关窗再补发 0)
func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets := "",
il: RefCounted = null) -> void:
ui = ui_manager
client = m2client
proto = proto_node
item_list = il
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")
if client and client.has_signal("inventory_changed"):
client.inventory_changed.connect(func(_w, _c): if is_open(): _refresh())
# --- open / close ----------------------------------------------------
func is_open() -> bool:
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func toggle() -> void:
if is_open(): close()
else: open()
# uiselectitem.Close: net.SendSelectItemPacket(0) 再隐藏
func close() -> void:
if is_open():
if not _sent and client and client.has_method("script_select_item"):
client.script_select_item(0)
ui.close(_win["root"])
_win = {}
_cells.clear()
_slot_to_inv.clear()
func open() -> void:
if is_open():
_refresh()
return
var path := uiscript_dir.path_join("selectitemwindow.py")
if not FileAccess.file_exists(path):
push_warning("SelectItemUI: no selectitemwindow.py at " + path)
return
_sent = false
_win = ui.open_script(path, assets_root)
if not is_open():
return
_index_cells(_win["root"])
_wire()
_refresh()
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 _index_cells(root: Node) -> void:
_cells.clear()
for n in root.find_children("slot_*", "Panel", true, false):
if n.has_meta("slot_index"):
_cells[int(n.get_meta("slot_index"))] = n
func _wire() -> void:
for idx in _cells:
var cell: Panel = _cells[idx]
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(int(idx), e))
var exit_btn := _node("ExitButton")
if exit_btn is BaseButton:
exit_btn.pressed.connect(close)
var tb := _node("TitleBar")
if tb:
for b in tb.find_children("*", "BaseButton", true, false):
b.pressed.connect(close)
# --- fill (uiselectitem.RefreshSlot) --------------------------------
func _item_grade(vnum: int) -> int:
# player.GetItemGrade: 物品内部名最后一位数字(非数字 -> 0)
var nm := ""
if proto:
var pd: Dictionary = proto.item(vnum)
nm = String(pd.get("name", ""))
if nm == "":
return 0
var last := nm.substr(nm.length() - 1, 1)
return int(last) if last.is_valid_int() else 0
func _is_metin(vnum: int) -> bool:
if proto == null:
return false
var pd: Dictionary = proto.item(vnum)
return int(pd.get("type", 0)) == ITEM_TYPE_METIN
func _refresh() -> void:
if not is_open() or client == null:
return
for idx in _cells:
_clear_cell(_cells[idx])
_slot_to_inv.clear()
# 背包按格号索引,只看前 INVENTORY_PAGE_SIZE*2 格(uiselectitem 的 range
var by_cell := {}
for d in client.get_inventory():
by_cell[int(d["cell"])] = d
var slot_pos := 0
for i in range(INVENTORY_PAGE_SIZE * 2):
var d = by_cell.get(i, null)
if d == null:
continue
var vnum := int(d["vnum"])
if vnum == 0 or not _is_metin(vnum):
continue
if _item_grade(vnum) > 2:
continue
_slot_to_inv[slot_pos] = i
var cell: Panel = _cells.get(slot_pos, null)
if cell:
_fill_cell(cell, vnum, int(d.get("count", 1)))
slot_pos += 1
if slot_pos > MAX_ITEMS:
break
func _fill_cell(cell: Panel, vnum: int, count: int) -> void:
_clear_cell(cell)
cell.set_meta("vnum", vnum)
var nm := "item %d" % vnum
if proto:
var pd: Dictionary = proto.item(vnum)
if not pd.is_empty():
nm = String(pd.get("locale_name", pd.get("name", nm)))
cell.tooltip_text = "%s\n#%d" % [nm, vnum]
var tex := _icon(vnum)
if tex:
var tr := TextureRect.new()
tr.name = "icon"
tr.texture = tex
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_SCALE
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(tr)
else:
var lb := Label.new()
lb.name = "icon"
lb.text = nm.substr(0, 6)
lb.add_theme_font_size_override("font_size", 9)
lb.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(lb)
if count > 1:
var cnt := Label.new()
cnt.name = "count"
cnt.text = str(count)
cnt.position = Vector2(2, 16)
cnt.add_theme_font_size_override("font_size", 9)
cnt.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(cnt)
func _clear_cell(cell: Panel) -> void:
cell.set_meta("vnum", 0)
cell.tooltip_text = ""
for c in cell.get_children():
c.queue_free()
func _icon(vnum: int) -> Texture2D:
var rel := ""
if item_list and item_list.has(vnum):
rel = item_list.icon(vnum)
if rel == "":
rel = "icon/item/%05d.tga" % ((vnum / 10) * 10)
return UiAssets.load_tex(assets_root, rel)
# --- select (uiselectitem.SelectItemSlot) --------------------------
func _on_cell_input(slot_pos: int, e: InputEvent) -> void:
if not (e is InputEventMouseButton) or e.button_index != MOUSE_BUTTON_LEFT or not e.pressed:
return
if not _slot_to_inv.has(slot_pos):
return
var inv_cell := int(_slot_to_inv[slot_pos])
if client and client.has_method("script_select_item"):
client.script_select_item(inv_cell) # = net.SendSelectItemPacket(inventorySlotPos)
_sent = true
close()