- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
141 lines
5.1 KiB
GDScript
141 lines
5.1 KiB
GDScript
# keyboard_motion_timeline_test —— 40250 键盘事件 -> 本地移动/停止时序回归。
|
|
# godot --headless --path project --script keyboard_motion_timeline_test.gd
|
|
extends SceneTree
|
|
|
|
const PlayerController = preload("res://player_controller.gd")
|
|
|
|
var _fail := 0
|
|
|
|
func _ck(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
_fail += 1
|
|
printerr("FAIL: " + message)
|
|
|
|
func _key(code: Key, pressed: bool) -> InputEventKey:
|
|
var event := InputEventKey.new()
|
|
event.keycode = code
|
|
event.pressed = pressed
|
|
return event
|
|
|
|
func _init() -> void:
|
|
var player := Node3D.new()
|
|
var pc := PlayerController.new()
|
|
get_root().add_child(player)
|
|
get_root().add_child(pc)
|
|
pc.player = player
|
|
pc.default_run = false
|
|
pc.force_walk = true
|
|
var moved := []
|
|
var states := []
|
|
pc.moved.connect(func(pos: Vector3): moved.append(pos))
|
|
pc.anim_state.connect(func(state: String): states.append(state))
|
|
|
|
# A key pressed while the game phase is inactive must not be inherited.
|
|
pc.active = false
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
pc.active = true
|
|
pc._process(0.1)
|
|
_ck(is_zero_approx(player.position.length()), "inactive key-down does not move on game entry")
|
|
|
|
# 40250 updates direction immediately on each key event. W wins while W+S
|
|
# overlap; releasing W exposes the still-held S direction.
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
pc._process(0.1)
|
|
var after_w := player.position.z
|
|
_ck(after_w < -0.01 and not moved.is_empty(), "W key-down starts motion and emits moved")
|
|
pc._unhandled_input(_key(KEY_S, true))
|
|
pc._process(0.1)
|
|
_ck(player.position.z < after_w, "W keeps priority while W and S overlap")
|
|
pc._input(_key(KEY_W, false))
|
|
pc._process(0.1)
|
|
var after_release_w := player.position.z
|
|
_ck(after_release_w > after_w - 0.01, "releasing W switches to held S")
|
|
pc._input(_key(KEY_S, false))
|
|
var before_stop := player.position
|
|
pc._process(0.1)
|
|
_ck(player.position == before_stop, "releasing the last direction stops translation")
|
|
_ck(not states.is_empty() and states[states.size() - 1] == "wait",
|
|
"releasing the last direction emits wait state")
|
|
|
|
# Holding one direction must consume the reference's fixed 300-pixel target;
|
|
# it must not keep extending the destination every frame.
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
var fixed_target_z := pc._direction_dst.z
|
|
_ck(is_equal_approx(fixed_target_z, player.position.z - 3.0),
|
|
"W creates a fixed 300-pixel destination from the event position")
|
|
for _i in range(40):
|
|
pc._process(0.1)
|
|
_ck(absf(player.position.z - fixed_target_z) <= 0.06,
|
|
"held W stops at the fixed 300-pixel destination")
|
|
_ck(not pc._direction_active, "fixed keyboard destination is consumed once")
|
|
pc._input(_key(KEY_W, false))
|
|
|
|
# Reference CPythonPlayer keeps the direction bit while movement is locked
|
|
# and retries it from Update after the lock is released.
|
|
pc.locked = true
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
var locked_pos := player.position
|
|
pc._process(0.2)
|
|
_ck(player.position == locked_pos and pc._direction_resume_pending,
|
|
"locked held direction is deferred, not discarded")
|
|
pc.locked = false
|
|
pc._process(0.1)
|
|
_ck(player.position.z < locked_pos.z and not pc._direction_resume_pending,
|
|
"held direction resumes on the first unlocked update")
|
|
pc._input(_key(KEY_W, false))
|
|
|
|
# A moving skill owns the same motion edge: it may turn, but its old walking
|
|
# destination must not resume until the held direction is retried afterward.
|
|
pc.stop()
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
pc._process(0.1)
|
|
pc.moving_skill = true
|
|
var skill_pos := player.position
|
|
pc._process(0.1)
|
|
_ck(player.position == skill_pos and pc._direction_resume_pending,
|
|
"moving skill suppresses translation and defers held direction")
|
|
pc.moving_skill = false
|
|
pc._process(0.1)
|
|
_ck(player.position.z < skill_pos.z and not pc._direction_resume_pending,
|
|
"held direction resumes after moving skill ends")
|
|
pc._input(_key(KEY_W, false))
|
|
|
|
# 40250 __CanMove also rejects a main actor that owns a private shop or is
|
|
# processing an emotion. The direction bits remain latched and Update()
|
|
# retries after the server/entity state releases either gate.
|
|
pc.stop()
|
|
pc.set_private_shop_open(true)
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
var shop_pos := player.position
|
|
pc._process(0.1)
|
|
_ck(player.position == shop_pos and pc._direction_resume_pending,
|
|
"private shop blocks translation without losing held direction")
|
|
pc.set_private_shop_open(false)
|
|
pc._process(0.1)
|
|
_ck(player.position.z < shop_pos.z and not pc._direction_resume_pending,
|
|
"held direction resumes after private shop closes")
|
|
pc._input(_key(KEY_W, false))
|
|
|
|
pc.stop()
|
|
pc.set_processing_emotion(true)
|
|
pc._unhandled_input(_key(KEY_W, true))
|
|
var emotion_pos := player.position
|
|
pc._process(0.1)
|
|
_ck(player.position == emotion_pos and pc._direction_resume_pending,
|
|
"emotion processing blocks translation without losing held direction")
|
|
pc.set_processing_emotion(false)
|
|
pc._process(0.1)
|
|
_ck(player.position.z < emotion_pos.z and not pc._direction_resume_pending,
|
|
"held direction resumes after emotion ends")
|
|
pc._input(_key(KEY_W, false))
|
|
|
|
pc.stop()
|
|
player.free()
|
|
pc.free()
|
|
if _fail == 0:
|
|
print("PASS: keyboard_motion_timeline_test")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed" % _fail)
|
|
quit(1)
|