1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
pub use wows_replays::types::WorldPos;
/// Original minimap image size in pixels (before resizing to output).
/// The game's coordinate system is based on this size.
pub const NATIVE_MINIMAP_SIZE: u32 = 760;
/// Map metadata for coordinate conversion.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct MapInfo {
pub space_size: i32,
}
/// Pixel position on the minimap image.
/// (0,0) is top-left, positive X = right, positive Y = down.
/// Does NOT include HUD offset — that's applied at draw time.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
pub struct MinimapPos {
pub x: i32,
pub y: i32,
}
impl MapInfo {
/// Convert world coordinates to minimap pixel coordinates.
///
/// Uses the native minimap size (760) for scaling to match the game's
/// coordinate system, then rescales to the output size.
pub fn world_to_minimap(&self, pos: WorldPos, output_size: u32) -> MinimapPos {
let native = NATIVE_MINIMAP_SIZE as f64;
let scale = native / self.space_size as f64;
let half = native / 2.0;
let rescale = output_size as f64 / native;
MinimapPos {
x: ((pos.x as f64 * scale + half) * rescale) as i32,
y: ((-pos.z as f64 * scale + half) * rescale) as i32,
}
}
/// Convert minimap pixel coordinates back to world coordinates.
///
/// Inverse of [`world_to_minimap`](Self::world_to_minimap).
pub fn minimap_to_world(&self, pos: MinimapPos, output_size: u32) -> WorldPos {
let native = NATIVE_MINIMAP_SIZE as f64;
let scale = native / self.space_size as f64;
let half = native / 2.0;
let rescale = output_size as f64 / native;
let x = (pos.x as f64 / rescale - half) / scale;
let z = -(pos.y as f64 / rescale - half) / scale;
WorldPos { x: x as f32, y: 0.0, z: z as f32 }
}
/// Convert minimap pixel coordinates (as f32) back to world coordinates.
///
/// Like [`minimap_to_world`](Self::minimap_to_world) but accepts fractional
/// pixel positions (useful for sub-pixel mouse positions).
pub fn minimap_to_world_f32(&self, x: f32, y: f32, output_size: u32) -> WorldPos {
let native = NATIVE_MINIMAP_SIZE as f64;
let scale = native / self.space_size as f64;
let half = native / 2.0;
let rescale = output_size as f64 / native;
let wx = (x as f64 / rescale - half) / scale;
let wz = -(y as f64 / rescale - half) / scale;
WorldPos { x: wx as f32, y: 0.0, z: wz as f32 }
}
/// Convert a distance in world (BigWorld) units to minimap pixels.
pub fn world_distance_to_minimap(&self, distance: f32, output_size: u32) -> f32 {
distance * output_size as f32 / self.space_size as f32
}
/// Convert a distance in minimap pixels to world (BigWorld) units.
pub fn minimap_distance_to_world(&self, pixels: f32, output_size: u32) -> f32 {
pixels * self.space_size as f32 / output_size as f32
}
/// Convert a NormalizedPos (from `updateMinimapVisionInfo` packets) to minimap pixels.
///
/// The decoder stores raw 11-bit values as `raw / 512.0 - 1.5`. The game's actual
/// pack format maps those 11-bit values to world coordinates in [-2500, 2500]:
/// `world = raw_11bit / 2047.0 * 5000.0 - 2500.0`
///
/// This method recovers the world coordinate and routes through `world_to_minimap`
/// so both coordinate paths produce identical pixel positions.
pub fn normalized_to_minimap(&self, pos: &wows_replays::types::NormalizedPos, output_size: u32) -> MinimapPos {
// Recover raw 11-bit value: raw = (stored + 1.5) * 512
// Convert to world: world = raw / 2047 * 5000 - 2500
let raw_x = (pos.x + 1.5) * 512.0;
let raw_y = (pos.y + 1.5) * 512.0;
let world_x = raw_x as f64 / 2047.0 * 5000.0 - 2500.0;
let world_z = raw_y as f64 / 2047.0 * 5000.0 - 2500.0;
// NormalizedPos.y maps to world Z (north-south axis), but the minimap Y axis
// is inverted relative to world Z. world_to_minimap handles -Z -> +Y, so we
// pass z directly (world_to_minimap negates it internally).
self.world_to_minimap(WorldPos { x: world_x as f32, y: 0.0, z: world_z as f32 }, output_size)
}
}