215 lines
6.5 KiB
GDScript
215 lines
6.5 KiB
GDScript
# Tests for res://scripts/player.gd (minimal tree: CharacterBody3D + NavigationAgent3D).
|
|
extends GdUnitTestSuite
|
|
|
|
const PLAYER_SCRIPT: Script = preload("res://scripts/player.gd")
|
|
|
|
|
|
func _make_player() -> CharacterBody3D:
|
|
var body := CharacterBody3D.new()
|
|
var nav := NavigationAgent3D.new()
|
|
nav.name = "NavigationAgent3D"
|
|
body.add_child(nav)
|
|
body.set_script(PLAYER_SCRIPT)
|
|
auto_free(body)
|
|
add_child(body)
|
|
return body
|
|
|
|
|
|
func test_snap_to_server_resets_goal_position_and_velocity() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
var target := Vector3(1.0, 0.9, -2.0)
|
|
# Act
|
|
p.snap_to_server(target)
|
|
# Assert
|
|
assert_that(p.global_position).is_equal(target)
|
|
assert_that(p.velocity).is_equal(Vector3.ZERO)
|
|
assert_that(p.get("_has_walk_goal")).is_false()
|
|
|
|
|
|
func test_set_authoritative_nav_goal_sets_goal_and_target() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
p.set("_idle_anchor_active", true)
|
|
var goal := Vector3(10.0, 0.0, 3.0)
|
|
# Act
|
|
p.set_authoritative_nav_goal(goal)
|
|
# Assert
|
|
assert_that(p.get("_has_walk_goal")).is_true()
|
|
assert_that(p.get("_idle_anchor_active")).is_false()
|
|
assert_that(p.get("_auth_walk_goal")).is_equal(goal)
|
|
var nav: NavigationAgent3D = p.get_node("NavigationAgent3D") as NavigationAgent3D
|
|
assert_that(nav.target_position).is_equal(goal)
|
|
|
|
|
|
func test_set_locomotion_wish_world_xz_normalizes_horizontal() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
# Act
|
|
p.call("set_locomotion_wish_world_xz", Vector3(3.0, 9.0, 4.0))
|
|
# Assert
|
|
var w: Vector3 = p.get("_locomotion_wish_world_xz") as Vector3
|
|
assert_that(w.y).is_equal(0.0)
|
|
assert_that(absf(w.length_squared() - 1.0)).is_less(0.0001)
|
|
|
|
|
|
func test_set_locomotion_wish_world_xz_zero_for_near_zero() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
# Act
|
|
p.call("set_locomotion_wish_world_xz", Vector3(0.0, 1.0, 0.0))
|
|
# Assert
|
|
assert_that(p.get("_locomotion_wish_world_xz") as Vector3).is_equal(Vector3.ZERO)
|
|
|
|
|
|
func test_clear_nav_goal_clears_velocity_and_nav_target() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
p.velocity = Vector3(1.0, 0.0, 0.0)
|
|
p.set_authoritative_nav_goal(Vector3(5.0, 0.0, 5.0))
|
|
# Act
|
|
p.clear_nav_goal()
|
|
# Assert
|
|
assert_that(p.velocity).is_equal(Vector3.ZERO)
|
|
assert_that(p.get("_has_walk_goal")).is_false()
|
|
assert_that(p.get("_idle_anchor_active")).is_false()
|
|
var nav: NavigationAgent3D = p.get_node("NavigationAgent3D") as NavigationAgent3D
|
|
assert_that(nav.target_position).is_equal(p.global_position)
|
|
|
|
|
|
func test_nav_goal_lifecycle_resets_vert_route_latch() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
# Act
|
|
p.set_authoritative_nav_goal(Vector3(1.0, 0.0, 2.0))
|
|
p.set("_walk_vert_route_latched", true)
|
|
p.clear_nav_goal()
|
|
# Assert
|
|
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
|
# Act
|
|
p.set("_walk_vert_route_latched", true)
|
|
p.set_authoritative_nav_goal(Vector3(3.0, 0.0, 4.0))
|
|
# Assert
|
|
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
|
# Act
|
|
p.set("_walk_vert_route_latched", true)
|
|
p.snap_to_server(Vector3(0.0, 0.9, 0.0))
|
|
# Assert
|
|
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
|
|
|
|
|
func test_hold_idle_anchor_sets_anchor_first_then_clamps_xz() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
p.global_position = Vector3(1.0, 0.5, 2.0)
|
|
# Act
|
|
p.call("_hold_idle_anchor")
|
|
# Assert
|
|
assert_that(p.get("_idle_anchor_active")).is_true()
|
|
assert_that(p.get("_idle_anchor_xz")).is_equal(Vector2(1.0, 2.0))
|
|
# Act
|
|
p.global_position = Vector3(1.2, 0.7, 2.3)
|
|
p.call("_hold_idle_anchor")
|
|
# Assert
|
|
assert_that(p.global_position).is_equal(Vector3(1.0, 0.5, 2.0))
|
|
|
|
|
|
func test_snap_to_server_clears_idle_anchor() -> void:
|
|
# Arrange
|
|
var p := _make_player()
|
|
p.set("_idle_anchor_active", true)
|
|
p.set("_idle_anchor_xz", Vector2(9.0, 9.0))
|
|
# Act
|
|
p.snap_to_server(Vector3(3.0, 0.9, 4.0))
|
|
# Assert
|
|
assert_that(p.get("_idle_anchor_active")).is_false()
|
|
|
|
|
|
func test_capsule_feet_y_uses_body_origin_minus_half_height() -> void:
|
|
# Arrange
|
|
# Act
|
|
var feet_y: float = PLAYER_SCRIPT.capsule_feet_y(0.545678, 0.5)
|
|
# Assert
|
|
assert_that(absf(feet_y - 0.045678)).is_less(0.000001)
|
|
|
|
|
|
func test_vertical_arrival_error_uses_capsule_feet_height() -> void:
|
|
# Arrange
|
|
var goal_y := 0.045678
|
|
var body_origin_y := 0.545678
|
|
# Act
|
|
var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_origin_y, 0.5)
|
|
# Assert
|
|
assert_that(absf(err)).is_less(0.000001)
|
|
|
|
|
|
func test_vertical_arrival_error_stays_large_for_origin_height_only_match() -> void:
|
|
# Arrange
|
|
var goal_y := 0.545678
|
|
var body_origin_y := 0.545678
|
|
# Act
|
|
var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_origin_y, 0.5)
|
|
# Assert
|
|
assert_that(absf(err - 0.5)).is_less(0.000001)
|
|
|
|
|
|
func test_arrival_vert_err_large_for_step_goal_from_floor() -> void:
|
|
# Arrange
|
|
# Player at floor height (body Y=0.9). Goal at step surface (Y=0.3).
|
|
# Using actual capsule bottom (CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS) the error
|
|
# must exceed VERT_ARRIVE_EPS so the arrival check does NOT fire prematurely.
|
|
var goal_y := 0.3
|
|
var body_y := 0.9
|
|
var full_half: float = (
|
|
(
|
|
PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float
|
|
+ PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS")
|
|
)
|
|
as float
|
|
)
|
|
# Act
|
|
var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_y, full_half)
|
|
# Assert
|
|
assert_that(err).is_greater(PLAYER_SCRIPT.get("VERT_ARRIVE_EPS") as float)
|
|
|
|
|
|
func test_arrival_vert_err_zero_when_standing_on_step() -> void:
|
|
# Arrange
|
|
# Player body centred at goal_y + total_half_height (i.e. actually standing on the step).
|
|
# Error must be within VERT_ARRIVE_EPS so arrival fires correctly once the player is up.
|
|
var goal_y := 0.3
|
|
var full_half: float = (
|
|
(
|
|
PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float
|
|
+ PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS")
|
|
)
|
|
as float
|
|
)
|
|
var body_y: float = goal_y + full_half
|
|
# Act
|
|
var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_y, full_half)
|
|
# Assert
|
|
assert_that(err).is_less_equal(PLAYER_SCRIPT.get("VERT_ARRIVE_EPS") as float)
|
|
|
|
|
|
func test_flat_floor_goal_is_not_below_feet_height_margin() -> void:
|
|
# Arrange
|
|
var goal_y := 0.0
|
|
var body_origin_y := 0.4
|
|
var descend_margin := PLAYER_SCRIPT.get("DESCEND_GOAL_Y_MARGIN") as float
|
|
# Act
|
|
var below_feet_margin: bool = (
|
|
goal_y < PLAYER_SCRIPT.capsule_feet_y(body_origin_y, 0.5) - descend_margin
|
|
)
|
|
# Assert
|
|
assert_that(below_feet_margin).is_false()
|
|
|
|
|
|
func test_step_assist_max_surface_delta_matches_navigation_mesh_agent_max_climb() -> void:
|
|
# Arrange
|
|
# Keep in sync with NavigationMesh `agent_max_climb` in `scenes/main.tscn`.
|
|
# Act
|
|
var delta: float = PLAYER_SCRIPT.get("WALK_STEP_ASSIST_MAX_SURFACE_DELTA") as float
|
|
# Assert
|
|
assert_that(delta).is_equal(0.35)
|