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,166 @@
|
||||
class_name AdtTerrainQuery
|
||||
extends TerrainQuery
|
||||
|
||||
## TerrainQuery adapter for existing native ADT dictionaries.
|
||||
## Parsed tiles, including unavailable results, are cached for this query's
|
||||
## lifetime. The adapter is main-thread owned and creates no scene resources.
|
||||
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
|
||||
const CHUNK_SIZE_UNITS := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
const OUTER_GRID_UNIT_SIZE := CHUNK_SIZE_UNITS / 8.0
|
||||
|
||||
var _extracted_directory: String
|
||||
var _map_directory_name: String
|
||||
var _adt_tile_loader_override: Callable
|
||||
var _adt_tile_cache: Dictionary = {}
|
||||
var _failure_code_by_tile_key: Dictionary = {}
|
||||
|
||||
|
||||
## Creates an ADT query for one map. `adt_tile_loader_override` is an optional
|
||||
## test/data-source seam receiving the absolute ADT path and returning a Dictionary.
|
||||
func _init(
|
||||
extracted_directory: String,
|
||||
map_directory_name: String,
|
||||
adt_tile_loader_override: Callable = Callable()
|
||||
) -> void:
|
||||
_extracted_directory = extracted_directory
|
||||
_map_directory_name = map_directory_name
|
||||
_adt_tile_loader_override = adt_tile_loader_override
|
||||
|
||||
|
||||
## Samples the existing 9x9 outer-height grid with bilinear interpolation.
|
||||
func sample_ground_height(godot_world_position: GodotWorldPosition) -> TerrainGroundSample:
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(godot_world_position)
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(godot_world_position)
|
||||
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(
|
||||
tile_coordinate,
|
||||
tile_local_position
|
||||
)
|
||||
var tile_key := _tile_key(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
var adt_tile_data := _load_adt_tile(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
if adt_tile_data.is_empty():
|
||||
return TerrainGroundSample.unavailable(
|
||||
_failure_code_by_tile_key.get(tile_key, &"adt_tile_unavailable")
|
||||
)
|
||||
|
||||
var tile_origin := _find_tile_origin(adt_tile_data)
|
||||
var chunk := _find_chunk(
|
||||
adt_tile_data,
|
||||
clampi(chunk_coordinate.chunk_x, 0, 15),
|
||||
clampi(chunk_coordinate.chunk_y, 0, 15),
|
||||
tile_origin
|
||||
)
|
||||
if chunk.is_empty():
|
||||
return TerrainGroundSample.unavailable(&"adt_chunk_missing")
|
||||
|
||||
var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array())
|
||||
if heights.size() < 145:
|
||||
return TerrainGroundSample.unavailable(&"adt_chunk_heights_invalid")
|
||||
|
||||
var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
||||
var chunk_local_x := godot_world_position.x_units - chunk_origin.x
|
||||
var chunk_local_z := godot_world_position.z_units - chunk_origin.z
|
||||
var grid_x := clampf(chunk_local_x / OUTER_GRID_UNIT_SIZE, 0.0, 8.0)
|
||||
var grid_z := clampf(chunk_local_z / OUTER_GRID_UNIT_SIZE, 0.0, 8.0)
|
||||
var x0 := clampi(int(floor(grid_x)), 0, 8)
|
||||
var z0 := clampi(int(floor(grid_z)), 0, 8)
|
||||
var x1 := mini(x0 + 1, 8)
|
||||
var z1 := mini(z0 + 1, 8)
|
||||
var fraction_x := grid_x - float(x0)
|
||||
var fraction_z := grid_z - float(z0)
|
||||
|
||||
var height_x0 := lerpf(
|
||||
heights[_outer_height_index(z0, x0)],
|
||||
heights[_outer_height_index(z0, x1)],
|
||||
fraction_x
|
||||
)
|
||||
var height_x1 := lerpf(
|
||||
heights[_outer_height_index(z1, x0)],
|
||||
heights[_outer_height_index(z1, x1)],
|
||||
fraction_x
|
||||
)
|
||||
return TerrainGroundSample.available(
|
||||
chunk_origin.y + lerpf(height_x0, height_x1, fraction_z)
|
||||
)
|
||||
|
||||
|
||||
func _load_adt_tile(tile_x: int, tile_y: int) -> Dictionary:
|
||||
var tile_key := _tile_key(tile_x, tile_y)
|
||||
if _adt_tile_cache.has(tile_key):
|
||||
return _adt_tile_cache[tile_key]
|
||||
|
||||
var resource_path := _extracted_directory.path_join(
|
||||
"World/Maps/%s/%s_%d_%d.adt" % [_map_directory_name, _map_directory_name, tile_x, tile_y]
|
||||
)
|
||||
var absolute_path := ProjectSettings.globalize_path(resource_path)
|
||||
var adt_tile_data: Dictionary = {}
|
||||
var failure_code: StringName = &""
|
||||
if _adt_tile_loader_override.is_valid():
|
||||
var override_result = _adt_tile_loader_override.call(absolute_path)
|
||||
if override_result is Dictionary:
|
||||
adt_tile_data = override_result
|
||||
else:
|
||||
failure_code = &"adt_loader_result_invalid"
|
||||
elif not ClassDB.class_exists("ADTLoader"):
|
||||
failure_code = &"adt_loader_unavailable"
|
||||
elif not FileAccess.file_exists(absolute_path):
|
||||
failure_code = &"adt_source_missing"
|
||||
else:
|
||||
var loader = ClassDB.instantiate("ADTLoader")
|
||||
if loader != null:
|
||||
var load_result = loader.call("load_adt", absolute_path)
|
||||
if load_result is Dictionary:
|
||||
adt_tile_data = load_result
|
||||
if adt_tile_data.is_empty():
|
||||
failure_code = &"adt_load_failed"
|
||||
|
||||
if adt_tile_data.is_empty() and failure_code.is_empty():
|
||||
failure_code = &"adt_tile_unavailable"
|
||||
_adt_tile_cache[tile_key] = adt_tile_data
|
||||
_failure_code_by_tile_key[tile_key] = failure_code
|
||||
return adt_tile_data
|
||||
|
||||
|
||||
func _find_chunk(
|
||||
adt_tile_data: Dictionary,
|
||||
chunk_x: int,
|
||||
chunk_y: int,
|
||||
tile_origin: Vector3
|
||||
) -> Dictionary:
|
||||
for chunk in adt_tile_data.get("chunks", []) as Array:
|
||||
if chunk.is_empty():
|
||||
continue
|
||||
var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
||||
var local_origin := chunk_origin - tile_origin
|
||||
var candidate_chunk_x := clampi(int(round(local_origin.x / CHUNK_SIZE_UNITS)), 0, 15)
|
||||
var candidate_chunk_y := clampi(int(round(local_origin.z / CHUNK_SIZE_UNITS)), 0, 15)
|
||||
if candidate_chunk_x == chunk_x and candidate_chunk_y == chunk_y:
|
||||
return chunk
|
||||
return {}
|
||||
|
||||
|
||||
func _find_tile_origin(adt_tile_data: Dictionary) -> Vector3:
|
||||
var has_origin := false
|
||||
var minimum_x := 0.0
|
||||
var minimum_z := 0.0
|
||||
for chunk in adt_tile_data.get("chunks", []) as Array:
|
||||
if chunk.is_empty():
|
||||
continue
|
||||
var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
||||
if not has_origin:
|
||||
minimum_x = chunk_origin.x
|
||||
minimum_z = chunk_origin.z
|
||||
has_origin = true
|
||||
else:
|
||||
minimum_x = minf(minimum_x, chunk_origin.x)
|
||||
minimum_z = minf(minimum_z, chunk_origin.z)
|
||||
return Vector3(minimum_x, 0.0, minimum_z) if has_origin else Vector3.ZERO
|
||||
|
||||
|
||||
func _tile_key(tile_x: int, tile_y: int) -> String:
|
||||
return "%d_%d" % [tile_x, tile_y]
|
||||
|
||||
|
||||
func _outer_height_index(row: int, column: int) -> int:
|
||||
return row * 17 + column
|
||||
Reference in New Issue
Block a user