Files
mtgodot-poc/project/keyboard_motion_timeline_test.gd
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

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)