Hover marker + optimal path line over the KNOWN tile store only — the character plans through what they know; fog is unpathable (info boundary at the planning layer; the follower additionally revalidates every remaining tile per step). Pure static 8-dir A*: uniform cost 1 incl. diagonals (D-248 time-optimal, no sqrt2), no corner-cutting, terrain-cost provider seam for Phase-4 terrain. Execution streams ordinary Move* steps through the existing throttle — zero protocol change, server validates every step. RMB vocabulary (live-session spec): click = walk there at current stance; double-click = sprint there (ToggleStanceUp burst — server toggle handler verified cooldown-free so bursts climb deterministically — with net-zero restore on arrival; a double upgrades the active follow in place); long-press >=400ms = go there then Crouch on arrival, no restore (input- vocabulary prototype; real cover mechanics are future combat design). Cancellation: WASD override (stance kept), invalidation, teleport, suppression. Two additive InputMapper seams (queue_move_step, queue_stance_toggle); mouse unproject shared with the facing provider (ground_hit_local). 44 new gdUnit tests across finder + follower ladders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
66 lines
3.2 KiB
GDScript
66 lines
3.2 KiB
GDScript
## PathFollower stance-ladder math (T-1088 double-RMB sprint-there): the number of
|
|
## ToggleStanceUp actions to reach Sprint from each stance, and the net-zero restore
|
|
## property. Pure static — the ladder helpers touch no autoloads, so they run
|
|
## headless (the instance follow/step behaviour is live-only, verified in the smoke).
|
|
class_name TestPathFollower
|
|
extends GdUnitTestSuite
|
|
|
|
|
|
func test_toggles_to_sprint_from_each_stance() -> void:
|
|
# Ladder: Crouch < Careful < Walk < Sprint (server MovementStance step_up).
|
|
assert_int(PathFollower.toggles_to_sprint("Sprint")).is_equal(0)
|
|
assert_int(PathFollower.toggles_to_sprint("Walk")).is_equal(1)
|
|
assert_int(PathFollower.toggles_to_sprint("Careful")).is_equal(2)
|
|
assert_int(PathFollower.toggles_to_sprint("Crouch")).is_equal(3)
|
|
|
|
|
|
func test_stance_rung_matches_ladder() -> void:
|
|
assert_int(PathFollower.stance_rung("Sprint")).is_equal(0)
|
|
assert_int(PathFollower.stance_rung("Walk")).is_equal(1)
|
|
assert_int(PathFollower.stance_rung("Careful")).is_equal(2)
|
|
assert_int(PathFollower.stance_rung("Crouch")).is_equal(3)
|
|
|
|
|
|
func test_unknown_stance_defaults_to_walk() -> void:
|
|
# Defensive: an unrecognized / empty stance is treated as Walk (GameState default).
|
|
assert_int(PathFollower.stance_rung("Bogus")).is_equal(1)
|
|
assert_int(PathFollower.toggles_to_sprint("")).is_equal(1)
|
|
|
|
|
|
func test_restore_count_equals_raise_count_net_zero() -> void:
|
|
# Restore bursts the SAME number of ToggleStanceDown as the raise bursted
|
|
# ToggleStanceUp. Since Sprint is rung 0, raising from a stance to Sprint climbs
|
|
# `rung` steps and restoring drops `rung` steps → back to the exact start rung,
|
|
# regardless of snapshot RTT. The single source of truth is toggles_to_sprint.
|
|
for stance: String in ["Sprint", "Walk", "Careful", "Crouch"]:
|
|
var raise_toggles := PathFollower.toggles_to_sprint(stance)
|
|
var restore_toggles := PathFollower.toggles_to_sprint(stance)
|
|
assert_int(restore_toggles).is_equal(raise_toggles)
|
|
# Net-zero: start rung, up to Sprint (0), then down `raise_toggles` == start.
|
|
assert_int(0 + restore_toggles).is_equal(PathFollower.stance_rung(stance))
|
|
|
|
|
|
# -- long-press crouch-on-arrival ------------------------------------------------------
|
|
|
|
|
|
func test_toggles_to_crouch_from_each_stance() -> void:
|
|
# Long-press lowers to Crouch on arrival: ToggleStanceDown from the arrival stance
|
|
# down to Crouch (rung 3). No restore.
|
|
assert_int(PathFollower.toggles_to_crouch("Sprint")).is_equal(3)
|
|
assert_int(PathFollower.toggles_to_crouch("Walk")).is_equal(2)
|
|
assert_int(PathFollower.toggles_to_crouch("Careful")).is_equal(1)
|
|
assert_int(PathFollower.toggles_to_crouch("Crouch")).is_equal(0)
|
|
|
|
|
|
func test_toggles_to_crouch_unknown_defaults_to_walk() -> void:
|
|
# Unknown/empty stance is treated as Walk (rung 1) → 2 down-toggles to Crouch.
|
|
assert_int(PathFollower.toggles_to_crouch("Bogus")).is_equal(2)
|
|
assert_int(PathFollower.toggles_to_crouch("")).is_equal(2)
|
|
|
|
|
|
func test_crouch_lands_on_bottom_rung() -> void:
|
|
# After the crouch burst the character sits exactly on Crouch's rung from any start.
|
|
for stance: String in ["Sprint", "Walk", "Careful", "Crouch"]:
|
|
var down := PathFollower.toggles_to_crouch(stance)
|
|
assert_int(PathFollower.stance_rung(stance) + down).is_equal(PathFollower.stance_rung("Crouch"))
|