From 72f66bb71cb7f4471a0440137845d9e04103c4bc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 18:42:28 +0000 Subject: [PATCH] Draw map markers on the cartography table's preview MapRenderer.render in a screen draws the map's decorations over its image, all of them since the map isn't in an item frame. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH --- crates/horizon-client/src/game/cartography.rs | 86 ++++++++++++++++++- crates/horizon-client/src/game/maps.rs | 20 +++-- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/crates/horizon-client/src/game/cartography.rs b/crates/horizon-client/src/game/cartography.rs index 3310d7fbfa..a767dab70a 100644 --- a/crates/horizon-client/src/game/cartography.rs +++ b/crates/horizon-client/src/game/cartography.rs @@ -116,12 +116,13 @@ impl Game<'_> { ); }; let preview = |batch: &mut HudBatch, game: &mut Self, x: f32, y: f32, scale: f32| { - if let Some(rect) = id + if let Some((id, rect)) = id .filter(|_| state.is_some()) - .and_then(|id| game.map_texture(id)) + .and_then(|id| game.map_texture(id).map(|rect| (id, rect))) { let size = 128.0 * scale; batch.rect_uv(ox + x, oy + y, size, size, rect, u32::MAX); + game.draw_map_decorations(batch, id, ox + x, oy + y, scale); } }; if scaling && !locked { @@ -142,6 +143,40 @@ impl Game<'_> { } } } +impl Game<'_> { + /// `MapRenderer.render` in a screen (`GuiGraphicsExtractor.map`): map + /// `id`'s decorations, all of them (not in an item frame), over its + /// image at `(x, y)` drawn `scale` GUI pixels per map pixel. + fn draw_map_decorations(&self, batch: &mut HudBatch, id: i32, x: f32, y: f32, scale: f32) { + let Some(map) = self.maps.data.get(&id) else { + return; + }; + for d in &map.decorations { + let Some(s) = self.gui.atlas.sprites.get(d.sprite).copied() else { + continue; + }; + batch.quad( + decoration_corners(d, x, y, scale), + [[s.u0, s.v0], [s.u1, s.v0], [s.u1, s.v1], [s.u0, s.v1]], + u32::MAX, + ); + } + } +} + +/// Decoration `d`'s corners on screen, for a map drawn at `(x, y)` with +/// `scale` GUI pixels per map pixel: (-1, 1), (1, 1), (1, -1), (-1, -1) +/// under `MapRenderer.render`'s pose. +fn decoration_corners(d: &super::maps::Decoration, x: f32, y: f32, scale: f32) -> [[f32; 2]; 4] { + let m = glam::Mat4::from_translation(glam::Vec3::new(x, y, 0.0)) + * glam::Mat4::from_scale(glam::Vec3::new(scale, scale, 1.0)) + * super::maps::decoration_pose(d, 0.0); + [(-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)].map(|(cx, cy)| { + let p = m.transform_point3(glam::Vec3::new(cx, cy, 0.0)); + [p.x, p.y] + }) +} + impl MapData { /// `MapItemSavedData.scaled`: a fresh map one scale up around the same centre. pub(super) fn scaled(&self) -> MapData { @@ -172,3 +207,50 @@ impl MapData { m } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::game::maps::Decoration; + + fn close(a: [[f32; 2]; 4], b: [[f32; 2]; 4]) { + for (a, b) in a.iter().zip(&b) { + assert!( + (a[0] - b[0]).abs() < 1e-4 && (a[1] - b[1]).abs() < 1e-4, + "{a:?} {b:?}" + ); + } + } + + #[test] + fn map_markers_sit_on_the_preview_like_on_a_held_map() { + let player = |x, y, rot| Decoration { + sprite: "map/decorations/player", + x, + y, + rot, + holder: None, + }; + // The centre, facing north: 8 map pixels wide, nudged by (-0.5, 0.5). + close( + decoration_corners(&player(0, 0, 0), 0.0, 0.0, 1.0), + [[59.5, 68.5], [67.5, 68.5], [67.5, 60.5], [59.5, 60.5]], + ); + // A quarter turn (rotation 4 of 16) turns the quad about its marker. + close( + decoration_corners(&player(0, 0, 4), 0.0, 0.0, 1.0), + [[59.5, 59.5], [59.5, 67.5], [67.5, 67.5], [67.5, 59.5]], + ); + // The preview's offset and scale apply after: the top-left corner + // marker on the 0.45 preview at (71, 17). + close( + decoration_corners(&player(-128, -128, 0), 71.0, 17.0, 0.45), + [ + [71.0 - 4.5 * 0.45, 17.0 + 4.5 * 0.45], + [71.0 + 3.5 * 0.45, 17.0 + 4.5 * 0.45], + [71.0 + 3.5 * 0.45, 17.0 - 3.5 * 0.45], + [71.0 - 4.5 * 0.45, 17.0 - 3.5 * 0.45], + ], + ); + } +} diff --git a/crates/horizon-client/src/game/maps.rs b/crates/horizon-client/src/game/maps.rs index f164f9c203..987d9ff196 100644 --- a/crates/horizon-client/src/game/maps.rs +++ b/crates/horizon-client/src/game/maps.rs @@ -337,6 +337,17 @@ const DECORATION_TYPES: [(&str, &str); 24] = [ ("ocean_ruin_warm", "map/decorations/warm_ocean_ruins"), ]; /// `MapDecorationType.showOnItemFrame` is false for the player arrows and the two markers. +/// `MapRenderer.render`'s pose for decoration `d` in map pixels (0..128, +/// y down) at depth `z`: its quad is (-1, -1) to (1, 1) under it. +pub(super) fn decoration_pose(d: &Decoration, z: f32) -> glam::Mat4 { + glam::Mat4::from_translation(glam::Vec3::new( + f32::from(d.x) / 2.0 + 64.0, + f32::from(d.y) / 2.0 + 64.0, + z, + )) * glam::Mat4::from_rotation_z((f32::from(d.rot) * 360.0 / 16.0).to_radians()) + * glam::Mat4::from_scale(glam::Vec3::splat(4.0)) + * glam::Mat4::from_translation(glam::Vec3::new(-0.125, 0.125, 0.0)) +} fn shown_on_frame(sprite: &str) -> bool { !matches!( decoration_type(sprite), @@ -1277,14 +1288,7 @@ impl Game<'_> { let Some(s) = self.gui.atlas.sprites.get(d.sprite).copied() else { continue; }; - let dm = glam::Mat4::from_translation(glam::Vec3::new( - f32::from(d.x) / 2.0 + 64.0, - f32::from(d.y) / 2.0 + 64.0, - -0.02 - count as f32 * 0.001, - )) * glam::Mat4::from_rotation_z( - (f32::from(d.rot) * 360.0 / 16.0).to_radians(), - ) * glam::Mat4::from_scale(glam::Vec3::splat(4.0)) - * glam::Mat4::from_translation(glam::Vec3::new(-0.125, 0.125, 0.0)); + let dm = decoration_pose(d, -0.02 - count as f32 * 0.001); let p = |x: f32, y: f32| dm.transform_point3(glam::Vec3::new(x, y, 0.0)).to_array(); let mut q = |corners: [[f32; 3]; 4], uv: [[f32; 2]; 4]| { for (c, uv) in corners.into_iter().zip(uv) {