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:
2026-07-13 13:29:59 +04:00
parent c2be8ae95e
commit f45695c26a
27 changed files with 977 additions and 0 deletions
@@ -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
+137
View File
@@ -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
+17
View File
@@ -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