fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
+102 -5
View File
@@ -16,6 +16,7 @@ class FakeClient extends Node:
signal entity_main_set(vid: int)
signal entity_dead(vid: int)
signal vitals_changed(vid: int)
signal point_changed(vid: int, point_type: int, value: int, amount: int)
signal points_changed(points: Dictionary)
signal target_info(vid: int, hp_percent: int)
signal observer_mode_changed(enabled: bool)
@@ -24,6 +25,7 @@ class FakeClient extends Node:
var ents := {}
var main := 0
var in_game := true
var guild_war := {"state": 0}
var ground_items := []
var skills := []
var calls := {"move": [], "attack": [], "set_target": [], "click_npc": [],
@@ -41,6 +43,7 @@ class FakeClient extends Node:
func fishing(rot_deg) -> bool: calls.fishing.append(rot_deg); return true
func get_ground_items() -> Array: return ground_items
func get_skills() -> Array: return skills
func get_guild_war() -> Dictionary: return guild_war
class FakePC extends Node:
signal target_selected(node: Node3D)
@@ -51,10 +54,15 @@ class FakePC extends Node:
var walk_calls := []
var frozen := false
var going := false
var dead := false
func set_server_speed(moving_speed: int) -> void: server_speed = float(moving_speed) / 100.0
func walk_to(p: Vector3) -> void: walk_calls.append(p); going = true
func is_going() -> bool: return going
func stop() -> void: going = false
func set_dead(value: bool) -> void:
dead = value
if dead:
stop()
class FakeHud extends Node:
var vitals := []
@@ -62,11 +70,13 @@ class FakeHud extends Node:
var level := 0
var target := []
var energy := []
var picked_money := []
var cleared := 0
func set_vitals(hp, mhp, sp, msp): vitals = [hp, mhp, sp, msp]
func set_exp(xp, nxp): exp_v = [xp, nxp]
func set_level(lv): level = lv
func set_energy(value, maximum): energy = [value, maximum]
func on_pick_money(amount): picked_money.append(amount)
func set_target(nm, pct): target = [nm, pct]
func clear_target(): cleared += 1
@@ -107,7 +117,15 @@ func _run() -> void:
# §3.3:attack_speed 只喂攻速系数(缩放播放速率),不再在收包时把 period 直接写死
_ck(is_equal_approx(np._atk_speed_factor, 1.5), "entity attack_speed -> attack-speed factor")
_ck(is_equal_approx(np._current_attack_period(), np.DEFAULT_ATTACK_PERIOD / 1.5),
"no motion data -> cadence = DEFAULT_ATTACK_PERIOD / factor")
"no motion data -> cadence = DEFAULT_ATTACK_PERIOD / factor")
_ck(not bool(np.skill_context().get("guild_war_active", false)),
"GUILD_WAR_NONE -> guild skill gate closed")
fc.guild_war["state"] = 6
_ck(bool(np.skill_context().get("guild_war_active", false)),
"GUILD_WAR_ON_WAR -> guild skill gate open")
fc.guild_war["state"] = 4
_ck(not bool(np.skill_context().get("guild_war_active", false)),
"GUILD_WAR_WAIT_START -> guild skill gate remains closed")
# §5.3:SHORSE::CanAttack / CPythonPlayer::__CanAttack 的坐骑等级门。
# 普通坐骑 level 1 不能攻击;新坐骑还需要骑乘术 level 11 或 master。
@@ -213,9 +231,8 @@ func _run() -> void:
# 5) 只有按住 SPACE(参考端 m_isAtkKey)才在攻击距离内 attack
np.set_attack_key(true)
for i in 4: await process_frame
_ck(fc.calls.attack.size() >= 1, "space-held attack fired, got %d" % fc.calls.attack.size())
if fc.calls.attack.size() >= 1:
_ck(fc.calls.attack[0][1] == 2000, "attack victim == 2000")
_ck(np._is_attacking, "space-held input starts the local attack motion state")
_ck(fc.calls.attack.is_empty(), "missing .msa AttackingData -> no synthetic CG_ATTACK")
var n1: int = fc.calls.attack.size()
await process_frame
_ck(fc.calls.attack.size() == n1, "attack throttled within period")
@@ -237,7 +254,7 @@ func _run() -> void:
np._attack_cd = 0.0
fc.calls.attack.clear()
await process_frame
_ck(fc.calls.attack.size() >= 1, "in-range reserved target -> attack fired")
_ck(fc.calls.attack.is_empty(), "in-range reserved target without .msa -> no CG_ATTACK")
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "reservation cleared after swing")
# 步骤 5:自己在安全区 -> OnCannotAttack('IN_SAFE') + 清预约,不攻击
@@ -262,6 +279,8 @@ func _run() -> void:
# 5c) §3.4 增量 91 —— CLICK_ITEM / CLICK_POSITION / USE_SKILL 预约分派
# + __ChangeTargetToPickedInstance + 扇形/圆形补 fly-targeting
_ck(NetPlay.CLICK_ITEM_PICKUP_CM == 150.0,
"40250 __OnPressItem 的直接点击拾取阈值固定为 150cm")
np._target_vid = 0
np._auto_attack_vid = 0
np.set_attack_key(false)
@@ -351,6 +370,50 @@ func _run() -> void:
_ck(ok_mob and np._target_vid == 4100 and fc.calls.set_target == [4100],
"_change_target_to_picked_instance(monster) -> target set")
# 40250 SetTarget 的 CanChangeTarget / CanPickInstance 门:技能动作中不
# 允许改目标;普通隐身、复活隐身、EUNHYEONG 和视锥外实体也不能选中。
np._target_vid = 4100
fc.calls.set_target.clear()
np._using_skill = true
_ck(not np._change_target_to_picked_instance(2000) and np._target_vid == 4100
and fc.calls.set_target.is_empty(),
"CanChangeTarget blocks target switch during skill motion")
np._using_skill = false
np._target_end_time = 0.0
np._target_vid = 0
fc.ents[6101] = {"vid": 6101, "ch_type": 0, "name": "Hidden", "dead": false,
"affect_flags": 1 << EntityRules.AFFECT_EUNHYEONG}
fc.calls.set_target.clear()
_ck(not np._change_target_to_picked_instance(6101) and fc.calls.set_target == [0],
"CanPickInstance rejects EUNHYEONG PC")
fc.ents[6101]["affect_flags"] = 1 << EntityRules.AFFECT_INVISIBILITY
fc.calls.set_target.clear()
_ck(not np._change_target_to_picked_instance(6101) and fc.calls.set_target == [0],
"CanPickInstance rejects ordinary invisibility PC")
fc.ents[6101]["affect_flags"] = 0
fc.ents[6101]["in_view_frustum"] = false
fc.calls.set_target.clear()
_ck(not np._change_target_to_picked_instance(6101) and fc.calls.set_target == [0],
"CanPickInstance rejects out-of-frustum entity")
fc.ents[6101].erase("in_view_frustum")
fc.ents[6101]["dead"] = true
fc.calls.set_target.clear()
_ck(not np._change_target_to_picked_instance(6101) and fc.calls.set_target == [0],
"CanPickInstance rejects dead entity")
# OnHit uses SetTarget(victim, FALSE): a live target-window lock suppresses
# only the automatic target packet, while the attack packet is still sent by
# the hit handler.
np._target_end_time = np._now() + 1.0
np._target_vid = 4100
fc.calls.set_target.clear()
fc.calls.attack.clear()
np._on_hit(2000, true)
_ck(fc.calls.attack.size() == 1 and fc.calls.set_target.is_empty() and np._target_vid == 4100,
"OnHit non-forced target change respects target window")
np._target_end_time = 0.0
np._target_vid = 0
# send_fly_targeting —— 把早前实体挪远(不销毁,后续用例还引用),铺一组已知几何
np._target_vid = 0
np._clear_reserved()
@@ -411,6 +474,23 @@ func _run() -> void:
_ck(hud.level == 7, "hud.set_level")
_ck(hud.energy == [75, 100], "hud.set_energy from POINT_ENERGY")
# 6b) 40250 RecvPointChange: typed event branches remain distinct from the
# aggregate snapshot. Positive GOLD amount calls OnPickMoney immediately;
# level/four-stat refresh both status and skills; skill points refresh skills.
var picked: Array = []
var status_refreshes := [0]
var skill_refreshes := [0]
np.money_picked.connect(func(amount: int): picked.append(amount))
np.status_refresh_requested.connect(func(): status_refreshes[0] += 1)
np.skill_refresh_requested.connect(func(): skill_refreshes[0] += 1)
fc.point_changed.emit(1000, 11, 1250, 250)
fc.point_changed.emit(1000, 12, 8, 0)
fc.point_changed.emit(1000, 28, 1, 0)
_ck(picked == [250] and hud.picked_money == [250],
"POINT_GOLD.amount -> OnPickMoney / money_picked")
_ck(status_refreshes[0] == 2 and skill_refreshes[0] == 2,
"POINT_GOLD/POINT_ST refresh status; POINT_ST/POINT_SKILL refresh skills")
# 7) 自己的 GC_MOVE 位置校正(远 -> 瞬移)
fc.ents[1000]["pos"] = Vector3(50, 0, -5)
fc.entity_moved.emit(1000)
@@ -496,6 +576,23 @@ func _run() -> void:
fc.entity_dead.emit(2000)
_ck(hud.cleared >= 1, "target cleared on death")
# 40250 main-character death transition: movement, auto actions and
# network-state handling are blocked until a positive HP vitals update.
fc.ents[1000]["dead"] = true
fc.ents[1000]["hp"] = 0
pc.going = true
np._auto_attack_vid = 2000
np._reserved_mode = 1
fc.entity_dead.emit(1000)
_ck(np._main_dead and pc.dead, "main death enters the controller death gate")
_ck(not pc.going and np._auto_attack_vid == 0, "main death clears movement and auto attack")
_ck(not np._can_process_network_state(), "main death blocks network state processing")
fc.ents[1000]["dead"] = false
fc.ents[1000]["hp"] = 100
fc.vitals_changed.emit(1000)
_ck(not np._main_dead and not pc.dead, "positive HP vitals update restores main control")
_ck(np._can_process_network_state(), "revived main can process network state")
# 10) 观战模式锁住本地移动/选目标,并清掉当前目标
fc.ents[2000]["dead"] = false
fc.observer_mode_changed.emit(true)