fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+102
-5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user