376 lines
16 KiB
GDScript
376 lines
16 KiB
GDScript
# 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")
|
||
fu.set_mobile_mode(true)
|
||
fc.friend_invite_ask.emit("MobileDave")
|
||
await process_frame
|
||
_ck(fu._invite_dialog.size == Vector2i(480, 220),
|
||
"friend: mobile confirmation uses landscape touch size")
|
||
fu._invite_dialog.canceled.emit()
|
||
|
||
# --- 商店 ---
|
||
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)
|
||
xu.set_mobile_mode(true)
|
||
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._mobile_inventory_scroll.visible and xu._mobile_inventory_list.get_child_count() == 1,
|
||
"exchange mobile mode shows one tap-to-offer inventory candidate")
|
||
if xu._mobile_inventory_list.get_child_count() == 1:
|
||
(xu._mobile_inventory_list.get_child(0) as Button).pressed.emit()
|
||
_ck(fc.calls.has(["ex_add_item", 1, 7, 1]),
|
||
"exchange mobile candidate tap sends item offer")
|
||
_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)
|
||
bu.set_mobile_mode(true)
|
||
fc.safebox_open = true
|
||
fc.safebox_size = 2
|
||
fc.safebox_gold = 99999
|
||
fc.safebox = [{"cell": 3, "vnum": 27, "count": 10}]
|
||
fc.inventory = [{"cell": 4, "vnum": 901, "count": 2}]
|
||
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")
|
||
_ck(bu._mobile_inventory_scroll.visible and bu._mobile_inventory_list.get_child_count() == 1,
|
||
"safebox mobile mode shows one tap-to-deposit inventory candidate")
|
||
if bu._mobile_inventory_list.get_child_count() == 1:
|
||
(bu._mobile_inventory_list.get_child(0) as Button).pressed.emit()
|
||
_ck(fc.calls.any(func(c): return c[0] == "sb_checkin" and c[2] == 1 and c[3] == 4),
|
||
"safebox mobile candidate tap sends checkin")
|
||
_ck(bu._mobile_deposit_pending.has(4), "safebox mobile prevents duplicate checkin taps")
|
||
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")
|