# p8_test —— P8:组队 / 好友 / 商店 / 交易 / 仓库 UI + 胶水 headless 自检。 # godot --headless --path project --script p8_test.gd extends SceneTree const PartyUI = preload("res://ui/party_ui.gd") const FriendUI = preload("res://ui/friend_ui.gd") const ShopUI = preload("res://ui/shop_ui.gd") const ExchangeUI = preload("res://ui/exchange_ui.gd") const SafeboxUI = preload("res://ui/safebox_ui.gd") class FakeClient extends Node: signal party_changed() signal party_invite_ask(leader_pid: int) signal friends_changed() signal friend_invite_ask(name: String) signal shop_opened(vid: int) signal shop_closed() signal shop_error(kind: String) signal exchange_changed() signal safebox_changed() var calls := [] var party := [] var friends := [] var shop := [] var shop_tabs := [] # 非空 -> get_shop() 走多货架 (SHOP_GC_START_EX) var shop_open := false var exchange := {"active": false} var safebox := [] var safebox_open := false var safebox_size := 0 var safebox_gold := 0 var inventory := [] var main_pos := Vector3.ZERO var main_vid := 20 var distribute_mode := 0 var leadership := 40 func is_in_game() -> bool: return true func get_main_vid() -> int: return main_vid func get_skills() -> Array: return [{"id": 121, "level": leadership}] func get_party() -> Array: return party func get_party_distribute_mode() -> int: return distribute_mode func party_answer(pid, accept) -> bool: calls.append(["party_answer", pid, accept]); return true func party_invite(vid) -> bool: calls.append(["party_invite", vid]); return true func party_leave(pid) -> bool: calls.append(["party_leave", pid]); return true func party_set_state(pid, role, on) -> bool: calls.append(["party_set_state", pid, role, on]); return true func party_set_distribute(mode) -> bool: calls.append(["party_set_distribute", mode]); return true func party_use_skill(idx, vid) -> bool: calls.append(["party_use_skill", idx, vid]); return true func set_target(vid) -> void: calls.append(["set_target", vid]) func get_friends() -> Array: return friends func add_friend(nm) -> bool: calls.append(["add_friend", nm]); return true func remove_friend(nm) -> bool: calls.append(["remove_friend", nm]); return true func friend_answer(nm, accept) -> bool: calls.append(["friend_answer", nm, accept]); return true func get_shop_items() -> Array: return shop func get_shop() -> Dictionary: var tabs := shop_tabs if not shop_tabs.is_empty() else [{"name": "", "coin_type": 0, "items": shop}] return {"vid": 8080, "open": shop_open, "tabs": tabs} func is_shop_open() -> bool: return shop_open func shop_buy(pos, cnt) -> bool: calls.append(["shop_buy", pos, cnt]); return true func shop_sell(cell, cnt) -> bool: calls.append(["shop_sell", cell, cnt]); return true func shop_close() -> bool: calls.append(["shop_close"]); return true func get_exchange() -> Dictionary: return exchange func exchange_add_item(w, c, d) -> bool: calls.append(["ex_add_item", w, c, d]); return true func exchange_add_gold(g) -> bool: calls.append(["ex_gold", g]); return true func exchange_accept() -> bool: calls.append(["ex_accept"]); return true func exchange_cancel() -> bool: calls.append(["ex_cancel"]); return true func get_safebox_items() -> Array: return safebox func is_safebox_open() -> bool: return safebox_open func get_safebox_size() -> int: return safebox_size func get_safebox_gold() -> int: return safebox_gold func get_inventory() -> Array: return inventory func get_item(window, cell) -> Dictionary: if window != 1: return {} for item in inventory: if int(item.get("cell", -1)) == int(cell): return item return {} func get_entity(_vid) -> Dictionary: return {"pos_cm": main_pos} func safebox_checkin(sp, w, c) -> bool: calls.append(["sb_checkin", sp, w, c]); return true func safebox_checkout(sp, w, c) -> bool: calls.append(["sb_checkout", sp, w, c]); return true var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: await _run() if _fail == 0: print("PASS: p8_test (party + friends + shop + exchange + safebox)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var canvas := CanvasLayer.new() get_root().add_child(canvas) var fc := FakeClient.new() get_root().add_child(fc) # --- 组队 --- var pu: Node = PartyUI.new() get_root().add_child(pu) pu.setup(fc, canvas) # 无 dialogs -> 邀请自动接受 _ck(not pu.is_open(), "party panel hidden when empty") # 本地玩家 vid=20 是队长 -> 角色分配按钮可用 fc.party = [ {"pid": 2, "vid": 20, "name": "Me", "leader": true, "hp_pct": 90, "state": 1, "affects": [0, 0, 0, 0, 0, 0, 0]}, {"pid": 1, "vid": 10, "name": "Bob", "leader": false, "hp_pct": 50, "state": 0, "affects": [12, 0, 0, 0, 0, 0, 0]}, ] fc.party_changed.emit() _ck(pu.is_open(), "party panel shows with members") _ck(pu._list.get_child_count() == 2, "party: 2 member strips") fc.party_invite_ask.emit(77) _ck(fc.calls.has(["party_answer", 77, true]), "party: auto-accept invite (no dialogs)") # 附加효과 칩:Bob 的 affects[0]=12 -> 一个「经验」chip var bob_strip: Control = pu._list.get_child(1) var chip_found := false for l in bob_strip.find_children("*", "Label", true, false): if String(l.text) == "经验": chip_found = true _ck(chip_found, "party: non-zero affect renders a chip") # 队长点 Me 的 StateButton -> 角色弹窗 -> 「攻击」-> party_set_state(pid, ROLE_ATTACKER, true) var me_strip: Control = pu._list.get_child(0) var state_btn: Button = null for b in me_strip.find_children("*", "Button", true, false): if String(b.text) in ["队", "普", "攻", "坦", "辅", "宗", "速", "防"]: state_btn = b break _ck(state_btn != null and not state_btn.disabled, "party: leader's StateButton enabled") state_btn.pressed.emit() _ck(is_instance_valid(pu._role_popup), "party: role popup opens for leader") var atk_btn: Button = null var expel_btn: Button = null for b in pu._role_popup.find_children("*", "Button", true, false): if String(b.text).begins_with("攻击"): atk_btn = b if String(b.text) == "踢出队伍": expel_btn = b atk_btn.pressed.emit() _ck(fc.calls.has(["party_set_state", 2, 2, true]), "party: 攻击 -> party_set_state(pid, ATTACKER=2, true)") # 다시 열어 踢出 state_btn.pressed.emit() for b in pu._role_popup.find_children("*", "Button", true, false): if String(b.text) == "踢出队伍": expel_btn = b expel_btn.pressed.emit() _ck(fc.calls.has(["party_leave", 2]), "party: 踢出队伍 -> party_leave(pid)") # EXP 分配切换(当前 mode 0 -> 发 1) pu._toggle_distribute() _ck(fc.calls.has(["party_set_distribute", 1]), "party: 分配切换 -> party_set_distribute(PARITY)") # 组队治疗 pu._party_heal() _ck(fc.calls.has(["party_use_skill", 1, 0]), "party: 组队治疗 -> party_use_skill(HEAL=1, 0)") # 点名字 -> 选中该员 vid var name_btn: Button = null for b in me_strip.find_children("*", "Button", true, false): if String(b.text).ends_with("Me"): name_btn = b if name_btn: name_btn.pressed.emit() _ck(fc.calls.has(["set_target", 20]), "party: 点名字 -> set_target(vid)") # 40250 CParty::Update 的 Leadership 门控:攻击至少 10,治疗至少 18。 fc.party[0]["leader"] = true fc.leadership = 9 fc.party_changed.emit() _ck(pu._heal_btn.disabled, "party: Leadership<18 disables heal") var gated_state: Button = null for b in pu._list.get_child(0).find_children("*", "Button", true, false): if String(b.text) == "队": gated_state = b break if gated_state: gated_state.pressed.emit() var gated_attack: Button = null for b in pu._role_popup.find_children("*", "Button", true, false): if String(b.text).begins_with("攻击"): gated_attack = b break _ck(gated_attack != null and gated_attack.disabled, "party: Leadership<10 disables attacker role") pu._dismiss_role_popup() # 非队长 -> StateButton disabled fc.party[0]["leader"] = false fc.party[0]["state"] = 0 fc.party_changed.emit() var me2: Control = pu._list.get_child(0) for b in me2.find_children("*", "Button", true, false): if String(b.text) in ["普", "队"]: _ck(b.disabled, "party: non-leader StateButton disabled") break # --- 好友 --- var fu: Node = FriendUI.new() get_root().add_child(fu) fu.setup(fc, canvas) fu.toggle() _ck(fu.is_open(), "friend window opens") fc.friends = [{"name": "Alice", "online": true, "mobile": true}, {"name": "Bob", "online": false}] fc.friends_changed.emit() await process_frame var alice_mobile := false for l in fu._list.find_children("*", "Label", true, false): if String(l.text) == "手机": alice_mobile = true _ck(alice_mobile, "friend: mobile state renders") var wt := [] fu.whisper_to.connect(func(n): wt.append(n)) # 找 Alice 行的名字按钮 var alice_btn: Button = null for b in fu._list.find_children("*", "Button", true, false): if (b as Button).text == "Alice": alice_btn = b _ck(alice_btn != null, "friend: Alice row present") if alice_btn: alice_btn.pressed.emit() _ck(wt == ["Alice"], "friend: click name -> whisper_to(Alice)") fu._name_edit.text = "Carol" fu._on_add() _ck(fc.calls.has(["add_friend", "Carol"]), "friend: add via input") fc.friend_invite_ask.emit("Dave") await process_frame _ck(fu._invite_dialog.visible and fu._invite_dialog.dialog_text.contains("Dave"), "friend: incoming request opens confirmation") fu._invite_dialog.confirmed.emit() _ck(fc.calls.has(["friend_answer", "Dave", true]), "friend: accept incoming request") fc.friend_invite_ask.emit("Eve") fu._invite_dialog.canceled.emit() _ck(fc.calls.has(["friend_answer", "Eve", false]), "friend: reject incoming request") # --- 商店 --- var su: Node = ShopUI.new() get_root().add_child(su) su.setup(fc, canvas) fc.shop = [{"pos": 0, "vnum": 27, "price": 1500, "count": 1}, {"pos": 1, "vnum": 19, "price": 800, "count": 1}] fc.shop_opened.emit(8080) _ck(su.is_open(), "shop opens on shop_opened") _ck(su._grid.get_child_count() == 40, "shop: 5x8 item grid") # 第一格点击进入购买确认,再确认后固定以 count=1 发包。 (su._grid.get_child(0) as Button).pressed.emit() _ck(su._buy_confirm != null, "shop: slot click opens buy confirmation") if su._buy_confirm: su._buy_confirm.confirmed.emit() _ck(fc.calls.has(["shop_buy", 0, 1]), "shop: buy row 0") su.sell(5) _ck(fc.calls.has(["shop_sell", 5, 1]), "shop: sell inv cell 5") fc.shop_error.emit("NOT_ENOUGH_MONEY") _ck(su._err.text == "金币不足", "shop: error text localised") fc.shop_closed.emit() _ck(not su.is_open(), "shop closes on shop_closed") # --- 商店 SHOP_GC_START_EX(多货架 + tabIdx*40+slot 买位置) --- fc.calls.clear() fc.shop_tabs = [ {"name": "武器", "coin_type": 0, "items": [{"pos": 0, "vnum": 101, "price": 500, "count": 1}, {"pos": 5, "vnum": 202, "price": 900, "count": 3}]}, {"name": "防具", "coin_type": 1, "items": [{"pos": 3, "vnum": 303, "price": 12000, "count": 1}]}, ] fc.shop_opened.emit(8080) _ck(su.is_open(), "shop_ex: opens") _ck(su._tabbar.visible and su._tabbar.get_child_count() == 2, "shop_ex: 2 tab buttons") _ck(su._grid.get_child_count() == 40, "shop_ex: tab 0 has 5x8 grid") # tab 0 第二个货位(slot 5)买 -> pos = 0*40 + 5 (su._grid.get_child(5) as Button).pressed.emit() if su._buy_confirm: su._buy_confirm.confirmed.emit() _ck(fc.calls.has(["shop_buy", 5, 1]), "shop_ex: tab0 slot5 -> buy pos 5") # 切到货架 2 su._tabbar.get_child(1).pressed.emit() _ck(su._active_tab == 1, "shop_ex: switched to tab 1") _ck(su._grid.get_child_count() == 40, "shop_ex: tab 1 has 5x8 grid") (su._grid.get_child(3) as Button).pressed.emit() if su._buy_confirm: su._buy_confirm.confirmed.emit() _ck(fc.calls.has(["shop_buy", 43, 1]), "shop_ex: tab1 slot3 -> buy pos 1*40+3=43") fc.shop_closed.emit() fc.shop_tabs = [] # --- 交易 --- var xu: Node = ExchangeUI.new() get_root().add_child(xu) xu.setup(fc, canvas) fc.exchange = { "active": true, "partner_vid": 4321, "self_items": [{"slot": 0, "vnum": 19, "count": 1}], "peer_items": [], "self_gold": 100, "peer_gold": 0, "self_accept": false, "peer_accept": true, } fc.inventory = [{"cell": 7, "vnum": 901, "count": 1, "anti_flags": 0}] fc.exchange_changed.emit() _ck(xu.is_open(), "exchange window opens when active") _ck(xu._self_box.get_child_count() == 1, "exchange: 1 self item row") _ck(xu._root.get_node("PeerAccept").text == "对方: 已接受", "exchange: peer accept shown") _ck(not xu._accept_btn.disabled, "exchange: accept enabled before server accept") xu._on_accept() _ck(fc.calls.has(["ex_accept"]) and xu._accept_btn.disabled, "exchange: accept sends once and disables immediately") fc.exchange["self_accept"] = false fc.exchange_changed.emit() _ck(not xu._accept_btn.disabled, "exchange: server-cleared accept re-enables button") xu.offer(1, 7) _ck(fc.calls.has(["ex_add_item", 1, 7, 1]), "exchange: offer item uses first free display slot (1)") fc.exchange["self_gold"] = 0 xu._gold_input.text = "500" xu._on_put_gold() _ck(fc.calls.has(["ex_gold", 500]), "exchange: put gold") fc.exchange["self_gold"] = 500 xu._gold_input.text = "100" xu._on_put_gold() _ck(not fc.calls.has(["ex_gold", 100]), "exchange: existing gold cannot be edited") fc.exchange["self_gold"] = 0 xu._gold_input.text = "10000000" xu._on_put_gold() _ck(not fc.calls.has(["ex_gold", 10000000]), "exchange: gold is limited to 7 digits") fc.inventory = [{"cell": 8, "vnum": 902, "count": 1, "anti_flags": 1 << 13}] _ck(not xu._drop_to_slot({"window": 1, "cell": 8, "vnum": 902, "count": 1}, 3), "exchange: ANTIFLAG_GIVE item rejected") fc.main_pos = Vector3(1001, 0, 0) xu._process(0.1) _ck(not xu.is_open() and fc.calls.has(["ex_cancel"]), "exchange: moving beyond 1000 exits trade") fc.exchange = {"active": false} fc.exchange_changed.emit() _ck(not xu.is_open(), "exchange closes when inactive") # --- 仓库 --- var bu: Node = SafeboxUI.new() get_root().add_child(bu) bu.setup(fc, canvas) fc.safebox_open = true fc.safebox_size = 2 fc.safebox_gold = 99999 fc.safebox = [{"cell": 3, "vnum": 27, "count": 10}] fc.safebox_changed.emit() _ck(bu.is_open(), "safebox opens") _ck(bu._gold.text.contains("99999"), "safebox: gold shown") _ck(bu._list.get_child_count() == 1, "safebox: 1 item row") var out_btn: Button = null for b in bu._list.get_child(0).find_children("*", "Button", true, false): out_btn = b if out_btn: out_btn.pressed.emit() _ck(fc.calls.any(func(c): return c[0] == "sb_checkout" and c[1] == 3), "safebox: checkout cell 3") fc.inventory = [{"cell": 0}, {"cell": 1}] bu.deposit(1, 4) # 下一个空仓位应是 0(cell 3 已用) _ck(fc.calls.any(func(c): return c[0] == "sb_checkin" and c[2] == 1 and c[3] == 4), "safebox: deposit inv cell 4")