gmp(M02): extract player terrain query
Work-Package: M02-GMP-TERRAIN-QUERY-001 Agent: sindo-main-codex Tests: terrain query, player input and movement regressions; asset-free scene; renderer dry-run; coordinate, streaming, documentation and coordination gates Fidelity: preserves existing ADT outer-grid interpolation and player ground snap; build-12340 collision parity remains unverified
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless M02 contract, interpolation, cache and player-injection regression.
|
||||
|
||||
const TERRAIN_QUERY_PATH := "res://src/gameplay/terrain/terrain_query.gd"
|
||||
const ADT_TERRAIN_QUERY_PATH := "res://src/gameplay/terrain/adt_terrain_query.gd"
|
||||
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
|
||||
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"
|
||||
|
||||
var _synthetic_adt_tile_data: Dictionary = {}
|
||||
var _synthetic_loader_call_count := 0
|
||||
|
||||
|
||||
class FlatTerrainQuery extends TerrainQuery:
|
||||
var _height_units: float
|
||||
|
||||
func _init(height_units: float) -> void:
|
||||
_height_units = height_units
|
||||
|
||||
func sample_ground_height(_godot_world_position: GodotWorldPosition) -> TerrainGroundSample:
|
||||
return TerrainGroundSample.available(_height_units)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run_verification.call_deferred()
|
||||
|
||||
|
||||
func _run_verification() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_typed_result_contract(failures)
|
||||
_verify_synthetic_adt_interpolation_and_cache(failures)
|
||||
_verify_unavailable_tile(failures)
|
||||
_verify_player_injection(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_QUERY: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("TERRAIN_QUERY PASS contract=4 interpolation=1 cache=1 failures=2 player_injection=1")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_typed_result_contract(failures: Array[String]) -> void:
|
||||
var available_sample := TerrainGroundSample.available(123.5)
|
||||
_expect_true(available_sample.is_available, "available sample state", failures)
|
||||
_expect_near(available_sample.height_units, 123.5, "available sample height", failures)
|
||||
_expect_true(available_sample.failure_code.is_empty(), "available sample has no failure", failures)
|
||||
|
||||
var unavailable_sample := TerrainGroundSample.unavailable(&"synthetic_failure")
|
||||
_expect_true(not unavailable_sample.is_available, "unavailable sample state", failures)
|
||||
_expect_equal(unavailable_sample.failure_code, &"synthetic_failure", "unavailable failure code", failures)
|
||||
|
||||
var base_sample := TerrainQuery.new().sample_ground_height(GodotWorldPosition.new(0.0, 0.0, 0.0))
|
||||
_expect_equal(base_sample.failure_code, &"terrain_query_not_implemented", "base query failure", failures)
|
||||
|
||||
var non_finite_sample := TerrainGroundSample.available(NAN)
|
||||
_expect_true(not non_finite_sample.is_available, "non-finite sample unavailable", failures)
|
||||
_expect_equal(non_finite_sample.failure_code, &"terrain_height_not_finite", "non-finite failure code", failures)
|
||||
|
||||
|
||||
func _verify_synthetic_adt_interpolation_and_cache(failures: Array[String]) -> void:
|
||||
var query_position := GodotWorldPosition.new(17000.0, 999.0, 26525.0)
|
||||
_synthetic_adt_tile_data = _build_synthetic_adt_tile(query_position)
|
||||
_synthetic_loader_call_count = 0
|
||||
var terrain_query := AdtTerrainQuery.new("res://synthetic", "Azeroth", _load_synthetic_adt_tile)
|
||||
|
||||
var first_sample := terrain_query.sample_ground_height(query_position)
|
||||
_expect_true(first_sample.is_available, "synthetic ADT sample available", failures)
|
||||
_expect_near_with_tolerance(first_sample.height_units, 137.0, 0.002, "synthetic bilinear height", failures)
|
||||
var second_sample := terrain_query.sample_ground_height(query_position)
|
||||
_expect_near_with_tolerance(second_sample.height_units, 137.0, 0.002, "cached synthetic height", failures)
|
||||
_expect_equal(_synthetic_loader_call_count, 1, "ADT tile loaded once", failures)
|
||||
|
||||
|
||||
func _verify_unavailable_tile(failures: Array[String]) -> void:
|
||||
var query_position := GodotWorldPosition.new(17000.0, 0.0, 26525.0)
|
||||
var empty_query := AdtTerrainQuery.new("res://synthetic", "Azeroth", func(_path: String) -> Dictionary: return {})
|
||||
var empty_sample := empty_query.sample_ground_height(query_position)
|
||||
_expect_true(not empty_sample.is_available, "empty ADT unavailable", failures)
|
||||
_expect_equal(empty_sample.failure_code, &"adt_tile_unavailable", "empty ADT failure", failures)
|
||||
|
||||
var invalid_heights_data := _build_synthetic_adt_tile(query_position)
|
||||
var invalid_chunks: Array = invalid_heights_data.get("chunks", [])
|
||||
(invalid_chunks[invalid_chunks.size() - 1] as Dictionary)["heights"] = PackedFloat32Array()
|
||||
var invalid_query := AdtTerrainQuery.new(
|
||||
"res://synthetic",
|
||||
"Azeroth",
|
||||
func(_path: String) -> Dictionary: return invalid_heights_data
|
||||
)
|
||||
var invalid_sample := invalid_query.sample_ground_height(query_position)
|
||||
_expect_equal(invalid_sample.failure_code, &"adt_chunk_heights_invalid", "invalid heights failure", failures)
|
||||
|
||||
|
||||
func _verify_player_injection(failures: Array[String]) -> void:
|
||||
var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
|
||||
_expect_true(packed_scene != null, "regression scene loads", failures)
|
||||
if packed_scene == null:
|
||||
return
|
||||
var player := packed_scene.instantiate() as CharacterBody3D
|
||||
player.set_terrain_query(FlatTerrainQuery.new(20.0))
|
||||
root.add_child(player)
|
||||
player.set_physics_process(false)
|
||||
_expect_near(player.position.y, 20.05, "injected spawn ground height", failures)
|
||||
player.position.y = 30.0
|
||||
player.call("_physics_process", 1.0)
|
||||
_expect_near(player.position.y, 20.05, "injected physics ground snap", failures)
|
||||
player.free()
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var terrain_contract_source := _read_text(TERRAIN_QUERY_PATH, failures)
|
||||
_expect_true(not terrain_contract_source.contains("extends Node"), "TerrainQuery omits Node", failures)
|
||||
_expect_true(not terrain_contract_source.contains("Vector3"), "TerrainQuery omits untyped Vector3 position", failures)
|
||||
|
||||
var adt_query_source := _read_text(ADT_TERRAIN_QUERY_PATH, failures)
|
||||
_expect_true(adt_query_source.contains("ADTLoader"), "ADT adapter owns native loader", failures)
|
||||
_expect_true(adt_query_source.contains("_adt_tile_cache"), "ADT adapter owns tile cache", failures)
|
||||
|
||||
var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
|
||||
for forbidden_text in ["ADTLoader", "load_adt", "_adt_cache", "_find_chunk", "_outer_height_index"]:
|
||||
_expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures)
|
||||
_expect_true(player_source.contains("set_terrain_query"), "player exposes terrain query injection", failures)
|
||||
_expect_true(player_source.contains("_terrain_query.sample_ground_height"), "player delegates ground sampling", failures)
|
||||
|
||||
|
||||
func _build_synthetic_adt_tile(query_position: GodotWorldPosition) -> Dictionary:
|
||||
var tile_coordinate := CoordinateMapper.godot_to_adt_tile(query_position)
|
||||
var tile_local_position := CoordinateMapper.godot_to_adt_tile_local(query_position)
|
||||
var chunk_coordinate := CoordinateMapper.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
|
||||
var chunk_size := CoordinateMapper.ADT_CHUNK_SIZE_YARDS
|
||||
var outer_grid_unit_size := chunk_size / 8.0
|
||||
var target_chunk_origin := Vector3(
|
||||
query_position.x_units - outer_grid_unit_size * 4.5,
|
||||
100.0,
|
||||
query_position.z_units - outer_grid_unit_size * 3.25
|
||||
)
|
||||
var tile_origin := target_chunk_origin - Vector3(
|
||||
chunk_coordinate.chunk_x * chunk_size,
|
||||
0.0,
|
||||
chunk_coordinate.chunk_y * chunk_size
|
||||
)
|
||||
|
||||
var heights := PackedFloat32Array()
|
||||
heights.resize(145)
|
||||
for row in range(9):
|
||||
for column in range(9):
|
||||
heights[row * 17 + column] = float(row * 10 + column)
|
||||
var target_chunk := {"origin": target_chunk_origin, "heights": heights}
|
||||
if chunk_coordinate.chunk_x == 0 and chunk_coordinate.chunk_y == 0:
|
||||
return {"chunks": [target_chunk]}
|
||||
return {
|
||||
"chunks": [
|
||||
{"origin": tile_origin, "heights": PackedFloat32Array()},
|
||||
target_chunk,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
func _load_synthetic_adt_tile(_absolute_path: String) -> Dictionary:
|
||||
_synthetic_loader_call_count += 1
|
||||
return _synthetic_adt_tile_data
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||
if absf(actual_value - expected_value) > 0.000001:
|
||||
failures.append("%s expected %.6f, got %.6f" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_near_with_tolerance(
|
||||
actual_value: float,
|
||||
expected_value: float,
|
||||
tolerance: float,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if absf(actual_value - expected_value) > tolerance:
|
||||
failures.append("%s expected %.6f ± %.6f, got %.6f" % [label, expected_value, tolerance, actual_value])
|
||||
|
||||
|
||||
func _expect_equal(actual_value: Variant, expected_value: Variant, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
Reference in New Issue
Block a user