fnd(M01): add canonical coordinate mapper
Work-Package: M01-FND-COORDS-001 Agent: sindo-main-codex Tests: coordinate mapper, M00 coordinate calibration, baseline manifest, coordination and documentation gates passed Fidelity: five build 12340 camera points map within 0.002 yard; no visual parity claim
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
class_name AdtChunkCoordinate
|
||||
extends RefCounted
|
||||
|
||||
## Immutable MCNK chunk indices inside one ADT tile.
|
||||
## `chunk_x` grows south and `chunk_y` grows east, matching the canonical
|
||||
## tile conversion contract rather than Godot's X/Z member names.
|
||||
|
||||
const MINIMUM_CHUNK_INDEX: int = 0
|
||||
const MAXIMUM_CHUNK_INDEX: int = 15
|
||||
|
||||
var tile_coordinate: AdtTileCoordinate:
|
||||
get:
|
||||
return _tile_coordinate
|
||||
|
||||
var chunk_x: int:
|
||||
get:
|
||||
return _chunk_x
|
||||
|
||||
var chunk_y: int:
|
||||
get:
|
||||
return _chunk_y
|
||||
|
||||
var _tile_coordinate: AdtTileCoordinate
|
||||
var _chunk_x: int
|
||||
var _chunk_y: int
|
||||
|
||||
|
||||
## Creates chunk indices without clamping so invalid parser input is visible.
|
||||
func _init(tile_coordinate_value: AdtTileCoordinate, chunk_x_value: int, chunk_y_value: int) -> void:
|
||||
_tile_coordinate = tile_coordinate_value
|
||||
_chunk_x = chunk_x_value
|
||||
_chunk_y = chunk_y_value
|
||||
|
||||
|
||||
## Returns whether the tile and both chunk indices are inside standard bounds.
|
||||
func is_within_map_grid() -> bool:
|
||||
return (
|
||||
_tile_coordinate != null
|
||||
and _tile_coordinate.is_within_map_grid()
|
||||
and _chunk_x >= MINIMUM_CHUNK_INDEX
|
||||
and _chunk_x <= MAXIMUM_CHUNK_INDEX
|
||||
and _chunk_y >= MINIMUM_CHUNK_INDEX
|
||||
and _chunk_y <= MAXIMUM_CHUNK_INDEX
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1lf5w5p3b6ye
|
||||
@@ -0,0 +1,29 @@
|
||||
class_name AdtPlacementPosition
|
||||
extends RefCounted
|
||||
|
||||
## Immutable MDDF/MODF position in ADT file field order.
|
||||
## `x_offset_yards` and `z_offset_yards` are measured from the north-west map
|
||||
## extent; `height_yards` is the vertical field stored between them.
|
||||
|
||||
var x_offset_yards: float:
|
||||
get:
|
||||
return _x_offset_yards
|
||||
|
||||
var height_yards: float:
|
||||
get:
|
||||
return _height_yards
|
||||
|
||||
var z_offset_yards: float:
|
||||
get:
|
||||
return _z_offset_yards
|
||||
|
||||
var _x_offset_yards: float
|
||||
var _height_yards: float
|
||||
var _z_offset_yards: float
|
||||
|
||||
|
||||
## Creates an ADT placement position in on-disk X/height/Z field order.
|
||||
func _init(x_offset_yards_value: float, height_yards_value: float, z_offset_yards_value: float) -> void:
|
||||
_x_offset_yards = x_offset_yards_value
|
||||
_height_yards = height_yards_value
|
||||
_z_offset_yards = z_offset_yards_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxxlbkjmdtg7m
|
||||
@@ -0,0 +1,35 @@
|
||||
class_name AdtTileCoordinate
|
||||
extends RefCounted
|
||||
|
||||
## Immutable indices from an ADT filename `Map_tileX_tileY.adt`.
|
||||
## Both axes use the inclusive range 0..63 for a normal WotLK world map.
|
||||
|
||||
const MINIMUM_TILE_INDEX: int = 0
|
||||
const MAXIMUM_TILE_INDEX: int = 63
|
||||
|
||||
var tile_x: int:
|
||||
get:
|
||||
return _tile_x
|
||||
|
||||
var tile_y: int:
|
||||
get:
|
||||
return _tile_y
|
||||
|
||||
var _tile_x: int
|
||||
var _tile_y: int
|
||||
|
||||
|
||||
## Creates file indices without clamping so out-of-map input remains diagnosable.
|
||||
func _init(tile_x_value: int, tile_y_value: int) -> void:
|
||||
_tile_x = tile_x_value
|
||||
_tile_y = tile_y_value
|
||||
|
||||
|
||||
## Returns whether both file indices address the standard 64 by 64 ADT grid.
|
||||
func is_within_map_grid() -> bool:
|
||||
return (
|
||||
_tile_x >= MINIMUM_TILE_INDEX
|
||||
and _tile_x <= MAXIMUM_TILE_INDEX
|
||||
and _tile_y >= MINIMUM_TILE_INDEX
|
||||
and _tile_y <= MAXIMUM_TILE_INDEX
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bg0a286yty1jw
|
||||
@@ -0,0 +1,29 @@
|
||||
class_name AdtTileLocalPosition
|
||||
extends RefCounted
|
||||
|
||||
## Immutable position relative to the north-west corner of one ADT tile.
|
||||
## East and south offsets are normally in `[0, ADT_TILE_SIZE_YARDS)`; height
|
||||
## remains in the canonical vertical space.
|
||||
|
||||
var east_yards: float:
|
||||
get:
|
||||
return _east_yards
|
||||
|
||||
var height_yards: float:
|
||||
get:
|
||||
return _height_yards
|
||||
|
||||
var south_yards: float:
|
||||
get:
|
||||
return _south_yards
|
||||
|
||||
var _east_yards: float
|
||||
var _height_yards: float
|
||||
var _south_yards: float
|
||||
|
||||
|
||||
## Creates a tile-local position without clamping boundary values.
|
||||
func _init(east_yards_value: float, height_yards_value: float, south_yards_value: float) -> void:
|
||||
_east_yards = east_yards_value
|
||||
_height_yards = height_yards_value
|
||||
_south_yards = south_yards_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpit2uvrdud6h
|
||||
@@ -0,0 +1,29 @@
|
||||
class_name CanonicalWowWorldPosition
|
||||
extends RefCounted
|
||||
|
||||
## Immutable WoW 3.3.5a world position in client/server axis order and yards.
|
||||
## X and Y are the horizontal coordinates reported by the client and world
|
||||
## server; Z is height. This type deliberately does not expose a [Vector3].
|
||||
|
||||
var x_yards: float:
|
||||
get:
|
||||
return _x_yards
|
||||
|
||||
var y_yards: float:
|
||||
get:
|
||||
return _y_yards
|
||||
|
||||
var z_yards: float:
|
||||
get:
|
||||
return _z_yards
|
||||
|
||||
var _x_yards: float
|
||||
var _y_yards: float
|
||||
var _z_yards: float
|
||||
|
||||
|
||||
## Creates a canonical position without changing precision or validating map bounds.
|
||||
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
|
||||
_x_yards = x_yards_value
|
||||
_y_yards = y_yards_value
|
||||
_z_yards = z_yards_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://b75i20d4vnkyh
|
||||
@@ -0,0 +1,16 @@
|
||||
class_name CanonicalWowWorldYaw
|
||||
extends RefCounted
|
||||
|
||||
## Immutable canonical world-facing yaw in radians.
|
||||
## Zero faces increasing WoW X and positive rotation turns toward increasing Y.
|
||||
|
||||
var radians: float:
|
||||
get:
|
||||
return _radians
|
||||
|
||||
var _radians: float
|
||||
|
||||
|
||||
## Creates a yaw without normalization; [CoordinateMapper] normalizes conversions.
|
||||
func _init(radians_value: float) -> void:
|
||||
_radians = radians_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddu5eiobntlrm
|
||||
@@ -0,0 +1,137 @@
|
||||
class_name CoordinateMapper
|
||||
extends RefCounted
|
||||
|
||||
## Pure, stateless conversions between OpenWC coordinate value objects.
|
||||
## This is the only domain contract allowed to change coordinate axes. It does
|
||||
## not depend on Node, Resource, scene state, map assets or renderer globals.
|
||||
|
||||
const ADT_TILES_PER_MAP_AXIS: int = 64
|
||||
const ADT_CHUNKS_PER_TILE_AXIS: int = 16
|
||||
const ADT_TILE_SIZE_YARDS: float = 1600.0 / 3.0
|
||||
const ADT_CHUNK_SIZE_YARDS: float = ADT_TILE_SIZE_YARDS / float(ADT_CHUNKS_PER_TILE_AXIS)
|
||||
const WOW_WORLD_HALF_EXTENT_YARDS: float = ADT_TILE_SIZE_YARDS * float(ADT_TILES_PER_MAP_AXIS) * 0.5
|
||||
|
||||
|
||||
## Converts server/wire X/Y/Z to canonical WoW X/Y/Z without an axis swap.
|
||||
static func server_to_canonical(server_position: ServerWorldPosition) -> CanonicalWowWorldPosition:
|
||||
return CanonicalWowWorldPosition.new(
|
||||
server_position.x_yards,
|
||||
server_position.y_yards,
|
||||
server_position.z_yards
|
||||
)
|
||||
|
||||
|
||||
## Converts canonical WoW X/Y/Z to server/wire X/Y/Z without an axis swap.
|
||||
static func canonical_to_server(canonical_position: CanonicalWowWorldPosition) -> ServerWorldPosition:
|
||||
return ServerWorldPosition.new(
|
||||
canonical_position.x_yards,
|
||||
canonical_position.y_yards,
|
||||
canonical_position.z_yards
|
||||
)
|
||||
|
||||
|
||||
## Converts canonical WoW world axes to the Godot Y-up compatibility basis.
|
||||
static func canonical_to_godot(canonical_position: CanonicalWowWorldPosition) -> GodotWorldPosition:
|
||||
return GodotWorldPosition.new(
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards,
|
||||
canonical_position.z_yards,
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards
|
||||
)
|
||||
|
||||
|
||||
## Converts the Godot Y-up compatibility basis to canonical WoW world axes.
|
||||
static func godot_to_canonical(godot_position: GodotWorldPosition) -> CanonicalWowWorldPosition:
|
||||
return CanonicalWowWorldPosition.new(
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.z_units,
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.x_units,
|
||||
godot_position.y_units
|
||||
)
|
||||
|
||||
|
||||
## Converts canonical WoW coordinates to MDDF/MODF X/height/Z file fields.
|
||||
static func canonical_to_adt_placement(canonical_position: CanonicalWowWorldPosition) -> AdtPlacementPosition:
|
||||
return AdtPlacementPosition.new(
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards,
|
||||
canonical_position.z_yards,
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards
|
||||
)
|
||||
|
||||
|
||||
## Converts MDDF/MODF X/height/Z file fields to canonical WoW coordinates.
|
||||
static func adt_placement_to_canonical(adt_position: AdtPlacementPosition) -> CanonicalWowWorldPosition:
|
||||
return CanonicalWowWorldPosition.new(
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.x_offset_yards,
|
||||
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.z_offset_yards,
|
||||
adt_position.height_yards
|
||||
)
|
||||
|
||||
|
||||
## Returns unclamped ADT filename indices for a canonical position.
|
||||
## Exact south/east boundaries belong to the next tile by floor semantics.
|
||||
static func canonical_to_adt_tile(canonical_position: CanonicalWowWorldPosition) -> AdtTileCoordinate:
|
||||
return AdtTileCoordinate.new(
|
||||
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.x_yards / ADT_TILE_SIZE_YARDS)),
|
||||
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.y_yards / ADT_TILE_SIZE_YARDS))
|
||||
)
|
||||
|
||||
|
||||
## Converts a canonical position to east/south offsets inside its ADT tile.
|
||||
static func canonical_to_adt_tile_local(canonical_position: CanonicalWowWorldPosition) -> AdtTileLocalPosition:
|
||||
var tile_coordinate := canonical_to_adt_tile(canonical_position)
|
||||
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
|
||||
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
|
||||
return AdtTileLocalPosition.new(
|
||||
tile_west_edge_y_yards - canonical_position.y_yards,
|
||||
canonical_position.z_yards,
|
||||
tile_north_edge_x_yards - canonical_position.x_yards
|
||||
)
|
||||
|
||||
|
||||
## Reconstructs canonical coordinates from an ADT tile and tile-local offsets.
|
||||
static func adt_tile_local_to_canonical(
|
||||
tile_coordinate: AdtTileCoordinate,
|
||||
local_position: AdtTileLocalPosition
|
||||
) -> CanonicalWowWorldPosition:
|
||||
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
|
||||
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
|
||||
return CanonicalWowWorldPosition.new(
|
||||
tile_north_edge_x_yards - local_position.south_yards,
|
||||
tile_west_edge_y_yards - local_position.east_yards,
|
||||
local_position.height_yards
|
||||
)
|
||||
|
||||
|
||||
## Returns unclamped MCNK indices for a tile-local position.
|
||||
static func adt_tile_local_to_chunk(
|
||||
tile_coordinate: AdtTileCoordinate,
|
||||
local_position: AdtTileLocalPosition
|
||||
) -> AdtChunkCoordinate:
|
||||
return AdtChunkCoordinate.new(
|
||||
tile_coordinate,
|
||||
int(floor(local_position.south_yards / ADT_CHUNK_SIZE_YARDS)),
|
||||
int(floor(local_position.east_yards / ADT_CHUNK_SIZE_YARDS))
|
||||
)
|
||||
|
||||
|
||||
## Converts server movement yaw to canonical yaw and normalizes it to [-PI, PI).
|
||||
static func server_to_canonical_yaw(server_yaw: ServerWorldYaw) -> CanonicalWowWorldYaw:
|
||||
return CanonicalWowWorldYaw.new(_normalize_yaw(server_yaw.radians))
|
||||
|
||||
|
||||
## Converts canonical yaw to server movement yaw and normalizes it to [-PI, PI).
|
||||
static func canonical_to_server_yaw(canonical_yaw: CanonicalWowWorldYaw) -> ServerWorldYaw:
|
||||
return ServerWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
|
||||
|
||||
|
||||
## Converts canonical world yaw to Godot Y-axis yaw in the mapped world basis.
|
||||
static func canonical_to_godot_yaw(canonical_yaw: CanonicalWowWorldYaw) -> GodotWorldYaw:
|
||||
return GodotWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
|
||||
|
||||
|
||||
## Converts Godot Y-axis world yaw to canonical yaw in the mapped world basis.
|
||||
static func godot_to_canonical_yaw(godot_yaw: GodotWorldYaw) -> CanonicalWowWorldYaw:
|
||||
return CanonicalWowWorldYaw.new(_normalize_yaw(godot_yaw.radians))
|
||||
|
||||
|
||||
static func _normalize_yaw(radians: float) -> float:
|
||||
return fposmod(radians + PI, TAU) - PI
|
||||
@@ -0,0 +1 @@
|
||||
uid://bg8p8rrkmg84d
|
||||
@@ -0,0 +1,29 @@
|
||||
class_name GodotWorldPosition
|
||||
extends RefCounted
|
||||
|
||||
## Immutable Godot Y-up renderer position.
|
||||
## One Godot unit equals one WoW yard in the compatibility renderer. X grows
|
||||
## east, Y grows up and Z grows south from the north-west map extent.
|
||||
|
||||
var x_units: float:
|
||||
get:
|
||||
return _x_units
|
||||
|
||||
var y_units: float:
|
||||
get:
|
||||
return _y_units
|
||||
|
||||
var z_units: float:
|
||||
get:
|
||||
return _z_units
|
||||
|
||||
var _x_units: float
|
||||
var _y_units: float
|
||||
var _z_units: float
|
||||
|
||||
|
||||
## Creates a renderer position without exposing it as a cross-layer [Vector3].
|
||||
func _init(x_units_value: float, y_units_value: float, z_units_value: float) -> void:
|
||||
_x_units = x_units_value
|
||||
_y_units = y_units_value
|
||||
_z_units = z_units_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://tp7jo14w25nc
|
||||
@@ -0,0 +1,17 @@
|
||||
class_name GodotWorldYaw
|
||||
extends RefCounted
|
||||
|
||||
## Immutable Godot Y-axis yaw in radians for the canonical world basis.
|
||||
## With Godot forward `-Z`, the numeric angle equals WoW world yaw after the
|
||||
## position basis conversion. Model-local MDDF/MODF corrections are separate.
|
||||
|
||||
var radians: float:
|
||||
get:
|
||||
return _radians
|
||||
|
||||
var _radians: float
|
||||
|
||||
|
||||
## Creates a Godot world yaw without normalization.
|
||||
func _init(radians_value: float) -> void:
|
||||
_radians = radians_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://cuy2skhdv3f6g
|
||||
@@ -0,0 +1,29 @@
|
||||
class_name ServerWorldPosition
|
||||
extends RefCounted
|
||||
|
||||
## Immutable TrinityCore/AzerothCore world position in wire/database X/Y/Z order.
|
||||
## Units are WoW yards. A distinct type prevents server positions from being
|
||||
## passed directly to renderer APIs even though the scalar values are canonical.
|
||||
|
||||
var x_yards: float:
|
||||
get:
|
||||
return _x_yards
|
||||
|
||||
var y_yards: float:
|
||||
get:
|
||||
return _y_yards
|
||||
|
||||
var z_yards: float:
|
||||
get:
|
||||
return _z_yards
|
||||
|
||||
var _x_yards: float
|
||||
var _y_yards: float
|
||||
var _z_yards: float
|
||||
|
||||
|
||||
## Creates a server position without changing precision or validating map bounds.
|
||||
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
|
||||
_x_yards = x_yards_value
|
||||
_y_yards = y_yards_value
|
||||
_z_yards = z_yards_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://dilhr7ym3xr43
|
||||
@@ -0,0 +1,16 @@
|
||||
class_name ServerWorldYaw
|
||||
extends RefCounted
|
||||
|
||||
## Immutable TrinityCore/AzerothCore movement orientation in radians.
|
||||
## It uses the WoW client convention: zero faces +X and positive turns toward +Y.
|
||||
|
||||
var radians: float:
|
||||
get:
|
||||
return _radians
|
||||
|
||||
var _radians: float
|
||||
|
||||
|
||||
## Creates a server yaw without normalization.
|
||||
func _init(radians_value: float) -> void:
|
||||
_radians = radians_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://dr8k82t2qgewd
|
||||
@@ -0,0 +1,185 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless contract verification for M01 canonical coordinate value objects and
|
||||
## [CoordinateMapper]. Uses synthetic boundaries plus five positions observed in
|
||||
## the original WoW 3.3.5a build 12340 during the M00 paired renderer session.
|
||||
|
||||
const CoordinateMapperScript = preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const CanonicalWowWorldPositionScript = preload("res://src/domain/coordinates/canonical_wow_world_position.gd")
|
||||
const ServerWorldPositionScript = preload("res://src/domain/coordinates/server_world_position.gd")
|
||||
const AdtPlacementPositionScript = preload("res://src/domain/coordinates/adt_placement_position.gd")
|
||||
const AdtTileCoordinateScript = preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||
const AdtTileLocalPositionScript = preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||
const CanonicalWowWorldYawScript = preload("res://src/domain/coordinates/canonical_wow_world_yaw.gd")
|
||||
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")
|
||||
|
||||
const RENDER_BASELINE_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const GOLDEN_POSITION_TOLERANCE_YARDS := 0.002
|
||||
const ROUND_TRIP_TOLERANCE_YARDS := 0.000001
|
||||
const BOUNDARY_EPSILON_YARDS := 0.001
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_build_12340_golden_positions(failures)
|
||||
_verify_server_and_adt_round_trips(failures)
|
||||
_verify_tile_and_chunk_boundaries(failures)
|
||||
_verify_world_yaw_conversions(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("COORDINATE_MAPPER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("COORDINATE_MAPPER PASS golden_points=5 tile_boundaries=8 yaw_cases=5")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_build_12340_golden_positions(failures: Array[String]) -> void:
|
||||
var manifest := _load_json_object(RENDER_BASELINE_MANIFEST_PATH, failures)
|
||||
if manifest.is_empty():
|
||||
return
|
||||
|
||||
var golden_point_count := 0
|
||||
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var reference_wow_camera_variant = checkpoint.get("reference_wow_camera", null)
|
||||
if not (reference_wow_camera_variant is Array) or reference_wow_camera_variant.size() != 3:
|
||||
continue
|
||||
|
||||
var expected_godot_camera_variant = checkpoint.get("camera", null)
|
||||
if not (expected_godot_camera_variant is Array) or expected_godot_camera_variant.size() != 3:
|
||||
failures.append("%s has no three-component renderer camera" % checkpoint.get("name", "checkpoint"))
|
||||
continue
|
||||
|
||||
var canonical_position = CanonicalWowWorldPositionScript.new(
|
||||
float(reference_wow_camera_variant[0]),
|
||||
float(reference_wow_camera_variant[1]),
|
||||
float(reference_wow_camera_variant[2])
|
||||
)
|
||||
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
_expect_near(godot_position.x_units, float(expected_godot_camera_variant[0]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot X" % checkpoint_name, failures)
|
||||
_expect_near(godot_position.y_units, float(expected_godot_camera_variant[1]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Y" % checkpoint_name, failures)
|
||||
_expect_near(godot_position.z_units, float(expected_godot_camera_variant[2]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Z" % checkpoint_name, failures)
|
||||
|
||||
var round_trip_position = CoordinateMapperScript.godot_to_canonical(godot_position)
|
||||
_expect_canonical_position(round_trip_position, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "%s Godot round trip" % checkpoint_name, failures)
|
||||
golden_point_count += 1
|
||||
|
||||
_expect_equal(golden_point_count, 5, "build 12340 golden point count", failures)
|
||||
|
||||
|
||||
func _verify_server_and_adt_round_trips(failures: Array[String]) -> void:
|
||||
var server_position = ServerWorldPositionScript.new(-9153.334, 386.666, 180.0)
|
||||
var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
|
||||
_expect_near(canonical_position.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X remains canonical X", failures)
|
||||
_expect_near(canonical_position.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y remains canonical Y", failures)
|
||||
_expect_near(canonical_position.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z remains canonical Z", failures)
|
||||
|
||||
var round_trip_server = CoordinateMapperScript.canonical_to_server(canonical_position)
|
||||
_expect_near(round_trip_server.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X round trip", failures)
|
||||
_expect_near(round_trip_server.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y round trip", failures)
|
||||
_expect_near(round_trip_server.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z round trip", failures)
|
||||
|
||||
var adt_position = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
|
||||
_expect_near(adt_position.x_offset_yards, 26220.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement X offset", failures)
|
||||
_expect_near(adt_position.height_yards, 180.0, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement height", failures)
|
||||
_expect_near(adt_position.z_offset_yards, 16680.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement Z offset", failures)
|
||||
var adt_round_trip = CoordinateMapperScript.adt_placement_to_canonical(adt_position)
|
||||
_expect_canonical_position(adt_round_trip, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement round trip", failures)
|
||||
|
||||
var direct_adt_position = AdtPlacementPositionScript.new(26220.000666666667, 180.0, 16680.000666666667)
|
||||
var direct_canonical = CoordinateMapperScript.adt_placement_to_canonical(direct_adt_position)
|
||||
_expect_canonical_position(direct_canonical, canonical_position, GOLDEN_POSITION_TOLERANCE_YARDS, "direct ADT placement conversion", failures)
|
||||
|
||||
|
||||
func _verify_tile_and_chunk_boundaries(failures: Array[String]) -> void:
|
||||
var half_extent := CoordinateMapperScript.WOW_WORLD_HALF_EXTENT_YARDS
|
||||
var tile_size := CoordinateMapperScript.ADT_TILE_SIZE_YARDS
|
||||
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(half_extent, half_extent, 0.0)), 0, 0, true, "north-west extent", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(0.0, 0.0, 0.0)), 32, 32, true, "map center", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(BOUNDARY_EPSILON_YARDS, BOUNDARY_EPSILON_YARDS, 0.0)), 31, 31, true, "north-west of center boundary", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-BOUNDARY_EPSILON_YARDS, -BOUNDARY_EPSILON_YARDS, 0.0)), 32, 32, true, "south-east of center boundary", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent + BOUNDARY_EPSILON_YARDS, -half_extent + BOUNDARY_EPSILON_YARDS, 0.0)), 63, 63, true, "inside south-east extent", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent, -half_extent, 0.0)), 64, 64, false, "exclusive south-east extent", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(tile_size, -tile_size, 0.0)), 31, 33, true, "whole-tile offsets", failures)
|
||||
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0)), 49, 31, true, "Elwynn golden tile", failures)
|
||||
|
||||
var tile_coordinate = AdtTileCoordinateScript.new(49, 31)
|
||||
var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0))
|
||||
var local_round_trip = CoordinateMapperScript.adt_tile_local_to_canonical(tile_coordinate, local_position)
|
||||
_expect_canonical_position(local_round_trip, CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0), ROUND_TRIP_TOLERANCE_YARDS, "ADT tile-local round trip", failures)
|
||||
_expect_true(local_position.east_yards >= 0.0 and local_position.east_yards < tile_size, "tile-local east offset is half-open", failures)
|
||||
_expect_true(local_position.south_yards >= 0.0 and local_position.south_yards < tile_size, "tile-local south offset is half-open", failures)
|
||||
|
||||
var chunk_local_position = AdtTileLocalPositionScript.new(
|
||||
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 2.0,
|
||||
180.0,
|
||||
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS
|
||||
)
|
||||
var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, chunk_local_position)
|
||||
_expect_equal(chunk_coordinate.chunk_x, 1, "exact south chunk boundary", failures)
|
||||
_expect_equal(chunk_coordinate.chunk_y, 2, "exact east chunk boundary", failures)
|
||||
_expect_true(chunk_coordinate.is_within_map_grid(), "chunk coordinate is inside map grid", failures)
|
||||
|
||||
|
||||
func _verify_world_yaw_conversions(failures: Array[String]) -> void:
|
||||
var yaw_cases := [0.0, PI * 0.5, -PI * 0.5, PI, TAU + 0.25]
|
||||
for yaw_radians in yaw_cases:
|
||||
var server_yaw = ServerWorldYawScript.new(float(yaw_radians))
|
||||
var canonical_yaw = CoordinateMapperScript.server_to_canonical_yaw(server_yaw)
|
||||
var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_yaw)
|
||||
var canonical_round_trip = CoordinateMapperScript.godot_to_canonical_yaw(godot_yaw)
|
||||
var server_round_trip = CoordinateMapperScript.canonical_to_server_yaw(canonical_round_trip)
|
||||
var expected_normalized_yaw := fposmod(float(yaw_radians) + PI, TAU) - PI
|
||||
_expect_near(canonical_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "canonical yaw normalization", failures)
|
||||
_expect_near(godot_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "Godot yaw basis", failures)
|
||||
_expect_near(server_round_trip.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "server yaw round trip", failures)
|
||||
|
||||
var canonical_pi_yaw = CanonicalWowWorldYawScript.new(PI)
|
||||
var normalized_godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_pi_yaw)
|
||||
_expect_near(normalized_godot_yaw.radians, -PI, ROUND_TRIP_TOLERANCE_YARDS, "positive PI normalizes to negative PI", failures)
|
||||
|
||||
|
||||
func _expect_tile(tile_coordinate, expected_x: int, expected_y: int, expected_valid: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_equal(tile_coordinate.tile_x, expected_x, "%s tile X" % label, failures)
|
||||
_expect_equal(tile_coordinate.tile_y, expected_y, "%s tile Y" % label, failures)
|
||||
_expect_equal(tile_coordinate.is_within_map_grid(), expected_valid, "%s validity" % label, failures)
|
||||
|
||||
|
||||
func _expect_canonical_position(actual_position, expected_position, tolerance: float, label: String, failures: Array[String]) -> void:
|
||||
_expect_near(actual_position.x_yards, expected_position.x_yards, tolerance, "%s X" % label, failures)
|
||||
_expect_near(actual_position.y_yards, expected_position.y_yards, tolerance, "%s Y" % label, failures)
|
||||
_expect_near(actual_position.z_yards, expected_position.z_yards, tolerance, "%s Z" % label, failures)
|
||||
|
||||
|
||||
func _expect_near(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 %.9f, got %.9f" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_equal(actual_value, expected_value, 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)
|
||||
|
||||
|
||||
func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open JSON fixture: %s" % path)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
failures.append("JSON fixture is not an object: %s" % path)
|
||||
return {}
|
||||
return parsed
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmxjtf1fsrl2x
|
||||
Reference in New Issue
Block a user