Files
mtgodot-poc/project/p8_test.gd
T

354 lines
15 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.
# 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")