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:
2026-07-14 23:10:55 +04:00
parent 5206c42cf0
commit a45d521567
14 changed files with 812 additions and 113 deletions
+27 -102
View File
@@ -10,9 +10,7 @@ const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/ad
const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd")
const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd")
const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd")
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const UNIT_SIZE := CHUNK_SIZE / 8.0
const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd")
@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
@@ -55,13 +53,24 @@ var _active_animation := ""
var _captured := false
var _yaw := 0.0
var _pitch := deg_to_rad(-18.0)
var _adt_cache: Dictionary = {}
var _player_input_source: PlayerInputSource
var _local_movement_controller: LocalPlayerMovementController
var _terrain_query: TerrainQuery
## Replaces the ground-height backend without changing player movement or presentation.
## Passing null is a composition error and leaves the current query unchanged.
func set_terrain_query(terrain_query: TerrainQuery) -> void:
if terrain_query == null:
push_error("ThirdPersonWowController: terrain query cannot be null")
return
_terrain_query = terrain_query
func _ready() -> void:
_player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new()
if _terrain_query == null:
_terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name)
_local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new(
run_speed,
backward_speed,
@@ -83,9 +92,9 @@ func _ready() -> void:
var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local)
global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units)
_load_character_visual()
var ground := _sample_ground_height(global_position)
if is_finite(ground):
global_position.y = ground + ground_offset
var spawn_ground_sample := _sample_ground_height(global_position)
if spawn_ground_sample.is_available:
global_position.y = spawn_ground_sample.height_units + ground_offset
_yaw = rotation.y
_apply_camera_transform()
@@ -124,9 +133,9 @@ func _physics_process(delta: float) -> void:
global_position += _local_movement_controller.advance(move_intent, godot_world_movement_basis, delta)
if not _local_movement_controller.is_flight_enabled:
var ground := _sample_ground_height(global_position)
if is_finite(ground):
var target_y := ground + ground_offset
var ground_sample := _sample_ground_height(global_position)
if ground_sample.is_available:
var target_y := ground_sample.height_units + ground_offset
global_position.y = lerpf(global_position.y, target_y, clampf(ground_snap_speed * delta, 0.0, 1.0))
var movement_velocity := _local_movement_controller.godot_world_velocity_units_per_second
@@ -298,95 +307,11 @@ func _apply_camera_transform() -> void:
_camera.rotation = Vector3.ZERO
func _sample_ground_height(world_pos: Vector3) -> float:
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
var data := _load_adt(tile_coordinate.tile_x, tile_coordinate.tile_y)
if data.is_empty():
return NAN
var tile_origin := _get_tile_origin(data)
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
var chunk := _find_chunk(data, chunk_x, chunk_y, tile_origin)
if chunk.is_empty():
return NAN
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
var chunk_local := world_pos - origin
var gx := clampf(chunk_local.x / UNIT_SIZE, 0.0, 8.0)
var gz := clampf(chunk_local.z / UNIT_SIZE, 0.0, 8.0)
var x0 := clampi(int(floor(gx)), 0, 8)
var z0 := clampi(int(floor(gz)), 0, 8)
var x1 := mini(x0 + 1, 8)
var z1 := mini(z0 + 1, 8)
var fx := gx - float(x0)
var fz := gz - float(z0)
var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array())
if heights.size() < 145:
return NAN
var h00 := heights[_outer(z0, x0)]
var h10 := heights[_outer(z0, x1)]
var h01 := heights[_outer(z1, x0)]
var h11 := heights[_outer(z1, x1)]
var hx0 := lerpf(h00, h10, fx)
var hx1 := lerpf(h01, h11, fx)
return origin.y + lerpf(hx0, hx1, fz)
func _load_adt(tx: int, ty: int) -> Dictionary:
var key := "%d_%d" % [tx, ty]
if _adt_cache.has(key):
return _adt_cache[key]
if not ClassDB.class_exists("ADTLoader"):
_adt_cache[key] = {}
return {}
var res_path := extracted_dir.path_join("World/Maps/%s/%s_%d_%d.adt" % [map_name, map_name, tx, ty])
var abs_path := ProjectSettings.globalize_path(res_path)
if not FileAccess.file_exists(abs_path):
_adt_cache[key] = {}
return {}
var loader = ClassDB.instantiate("ADTLoader")
var data: Dictionary = loader.call("load_adt", abs_path) if loader else {}
_adt_cache[key] = data
return data
func _find_chunk(data: Dictionary, chunk_x: int, chunk_y: int, tile_origin: Vector3) -> Dictionary:
for chunk in data.get("chunks", []) as Array:
if chunk.is_empty():
continue
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
var local := origin - tile_origin
var cx := clampi(int(round(local.x / CHUNK_SIZE)), 0, 15)
var cy := clampi(int(round(local.z / CHUNK_SIZE)), 0, 15)
if cx == chunk_x and cy == chunk_y:
return chunk
return {}
func _get_tile_origin(data: Dictionary) -> Vector3:
var found := false
var min_x := 0.0
var min_z := 0.0
for chunk in data.get("chunks", []) as Array:
if chunk.is_empty():
continue
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
if not found:
min_x = origin.x
min_z = origin.z
found = true
else:
min_x = minf(min_x, origin.x)
min_z = minf(min_z, origin.z)
return Vector3(min_x, 0.0, min_z) if found else Vector3.ZERO
func _outer(row: int, col: int) -> int:
return row * 17 + col
func _sample_ground_height(godot_world_position: Vector3) -> TerrainGroundSample:
return _terrain_query.sample_ground_height(
GODOT_WORLD_POSITION_SCRIPT.new(
godot_world_position.x,
godot_world_position.y,
godot_world_position.z
)
)