From 64cbf45114d7aee7d05f4b2d8a3836dd1c4fd150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 03:07:03 +0800 Subject: [PATCH 01/13] Add a basic minimap subsystem --- korangar/src/input/event.rs | 2 + korangar/src/input/mod.rs | 4 + korangar/src/interface/minimap/generation.rs | 62 +++++++++ korangar/src/interface/minimap/markers.rs | 68 +++++++++ korangar/src/interface/minimap/mod.rs | 7 + korangar/src/interface/minimap/projection.rs | 88 ++++++++++++ korangar/src/interface/minimap/render.rs | 129 ++++++++++++++++++ korangar/src/interface/mod.rs | 1 + .../interface/windows/character_overview.rs | 4 + korangar/src/interface/windows/minimap.rs | 88 ++++++++++++ korangar/src/interface/windows/mod.rs | 3 + korangar/src/main.rs | 40 +++++- korangar/src/state/mod.rs | 25 ++++ korangar/src/world/entity/mod.rs | 4 + korangar/src/world/map/mod.rs | 12 ++ 15 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 korangar/src/interface/minimap/generation.rs create mode 100644 korangar/src/interface/minimap/markers.rs create mode 100644 korangar/src/interface/minimap/mod.rs create mode 100644 korangar/src/interface/minimap/projection.rs create mode 100644 korangar/src/interface/minimap/render.rs create mode 100644 korangar/src/interface/windows/minimap.rs diff --git a/korangar/src/input/event.rs b/korangar/src/input/event.rs index 930653962..afc62343c 100644 --- a/korangar/src/input/event.rs +++ b/korangar/src/input/event.rs @@ -75,6 +75,8 @@ pub enum InputEvent { ToggleAudioSettingsWindow, /// Open or close the friend list window. Only works while playing. ToggleFriendListWindow, + /// Open or close the minimap window. Only works while playing. + ToggleMinimapWindow, /// Close the most recently opened or clicked closable window. CloseTopWindow, /// Toggle if the user interface should be rendered or not. diff --git a/korangar/src/input/mod.rs b/korangar/src/input/mod.rs index b916abfe0..63c2dab6f 100644 --- a/korangar/src/input/mod.rs +++ b/korangar/src/input/mod.rs @@ -226,6 +226,10 @@ impl InputSystem { events.push(InputEvent::ToggleFriendListWindow); } + if alt_down && self.get_key(KeyCode::KeyM).pressed() { + events.push(InputEvent::ToggleMinimapWindow); + } + if alt_down && self.get_key(KeyCode::KeyQ).pressed() { events.push(InputEvent::ToggleEquipmentWindow); } diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs new file mode 100644 index 000000000..e261cc614 --- /dev/null +++ b/korangar/src/interface/minimap/generation.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use image::{Rgba, RgbaImage}; +use ragnarok_formats::map::{Tile, TileFlags}; + +use crate::graphics::Texture; +use crate::loaders::TextureLoader; +use crate::state::MinimapState; +use crate::world::Map; + +pub fn create_minimap_state(texture_loader: &TextureLoader, previous: &MinimapState, map_name: &str, map: &Map) -> MinimapState { + let texture = create_generated_minimap_texture(texture_loader, map_name, map); + + MinimapState { + map_name: map_name.strip_suffix(".gat").unwrap_or(map_name).to_owned(), + width: map.get_width(), + height: map.get_height(), + zoom: previous.zoom.max(1.0), + texture: Some(texture), + } +} + +fn create_generated_minimap_texture(texture_loader: &TextureLoader, map_name: &str, map: &Map) -> Arc { + let width = u32::from(map.get_width().max(1)); + let height = u32::from(map.get_height().max(1)); + let mut image = RgbaImage::new(width, height); + let (lowest_height, highest_height) = map + .get_tiles() + .iter() + .fold((f32::INFINITY, f32::NEG_INFINITY), |(lowest_height, highest_height), tile| { + let height = average_tile_height(tile); + (lowest_height.min(height), highest_height.max(height)) + }); + let height_range = (highest_height - lowest_height).max(0.001); + let map_width = map.get_width().max(1) as usize; + + map.get_tiles().iter().enumerate().for_each(|(index, tile)| { + let x = (index % map_width) as u32; + let y = (index / map_width) as u32; + let normalized_height = ((average_tile_height(tile) - lowest_height) / height_range).clamp(0.0, 1.0); + let walkable = tile.flags.contains(TileFlags::WALKABLE); + let base = if walkable { + 72.0 + normalized_height * 118.0 + } else { + 26.0 + normalized_height * 52.0 + }; + + let pixel = if walkable { + Rgba([(base * 0.62) as u8, base as u8, (base * 0.66) as u8, 255]) + } else { + Rgba([(base * 0.45) as u8, (base * 0.50) as u8, (base * 0.58) as u8, 255]) + }; + + image.put_pixel(x, y, pixel); + }); + + texture_loader.create_color(&format!("minimap {}", map_name.strip_suffix(".gat").unwrap_or(map_name)), image, false) +} + +fn average_tile_height(tile: &Tile) -> f32 { + (tile.southwest_corner_height + tile.southeast_corner_height + tile.northwest_corner_height + tile.northeast_corner_height) / 4.0 +} diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs new file mode 100644 index 000000000..2df5624c7 --- /dev/null +++ b/korangar/src/interface/minimap/markers.rs @@ -0,0 +1,68 @@ +use ragnarok_packets::{Direction, TilePosition}; + +use crate::graphics::Color; +use crate::world::{Entity, EntityType}; + +pub struct MinimapMarker { + pub tile_position: TilePosition, + pub symbol: &'static str, + pub color: Color, + pub size: f32, + pub font_size: f32, +} + +pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entity>, base_size: f32) -> Vec { + let player_entity_id = player_entity.map(Entity::get_entity_id); + let player_direction = player_entity.map(Entity::get_direction).unwrap_or(Direction::South); + + entities + .iter() + .filter_map(|entity| { + let is_player = player_entity_id.is_some_and(|player_entity_id| player_entity_id == entity.get_entity_id()); + let entity_type = entity.get_entity_type(); + + if entity_type == EntityType::Hidden { + return None; + } + + let size = if is_player { base_size + 2.0 } else { base_size }; + let symbol = if is_player { player_marker(player_direction) } else { "●" }; + let font_size = size + if is_player { 8.0 } else { 6.0 }; + + Some(MinimapMarker { + tile_position: entity.get_tile_position(), + symbol, + color: marker_color(entity_type, is_player), + size, + font_size, + }) + }) + .collect() +} + +fn player_marker(direction: Direction) -> &'static str { + match direction { + Direction::North => "↑", + Direction::NorthEast => "↗", + Direction::East => "→", + Direction::SouthEast => "↘", + Direction::South => "↓", + Direction::SouthWest => "↙", + Direction::West => "←", + Direction::NorthWest => "↖", + } +} + +fn marker_color(entity_type: EntityType, is_player: bool) -> Color { + if is_player { + return Color::rgb_u8(80, 255, 160); + } + + match entity_type { + EntityType::Player => Color::rgb_u8(100, 180, 255), + EntityType::Npc => Color::rgb_u8(255, 200, 80), + EntityType::Monster => Color::rgb_u8(255, 110, 120), + EntityType::Warp => Color::rgb_u8(180, 255, 255), + EntityType::Hidden => Color::rgba_u8(0, 0, 0, 0), + } +} diff --git a/korangar/src/interface/minimap/mod.rs b/korangar/src/interface/minimap/mod.rs new file mode 100644 index 000000000..2b9e7b53d --- /dev/null +++ b/korangar/src/interface/minimap/mod.rs @@ -0,0 +1,7 @@ +mod generation; +mod markers; +mod projection; +mod render; + +pub use self::generation::create_minimap_state; +pub use self::render::MinimapView; diff --git a/korangar/src/interface/minimap/projection.rs b/korangar/src/interface/minimap/projection.rs new file mode 100644 index 000000000..551ff0883 --- /dev/null +++ b/korangar/src/interface/minimap/projection.rs @@ -0,0 +1,88 @@ +use korangar_interface::layout::area::Area; +use ragnarok_packets::TilePosition; + +pub struct MinimapProjection { + texture_area: Area, + map_width: f32, + map_height: f32, +} + +impl MinimapProjection { + pub fn new(view_area: Area, map_width: u16, map_height: u16, zoom: f32, focus_position: Option) -> Self { + if map_width == 0 || map_height == 0 { + return Self { + texture_area: view_area, + map_width: 1.0, + map_height: 1.0, + }; + } + + let map_width = map_width as f32; + let map_height = map_height as f32; + let scale = (view_area.width / map_width).min(view_area.height / map_height) * zoom.max(1.0); + let texture_width = map_width * scale; + let texture_height = map_height * scale; + let centered_area = view_area.interior( + texture_width, + texture_height, + korangar_interface::prelude::HorizontalAlignment::Center { + offset: 0.0, + border: 0.0, + }, + korangar_interface::prelude::VerticalAlignment::Center { offset: 0.0 }, + ); + + let texture_area = focus_position.map_or(centered_area, |focus_position| { + let (normalized_x, normalized_y) = Self::normalized_tile_position(map_width, map_height, focus_position); + let desired_left = view_area.left + view_area.width / 2.0 - normalized_x * texture_width; + let desired_top = view_area.top + view_area.height / 2.0 - normalized_y * texture_height; + let left = if texture_width > view_area.width { + desired_left.clamp(view_area.left + view_area.width - texture_width, view_area.left) + } else { + centered_area.left + }; + let top = if texture_height > view_area.height { + desired_top.clamp(view_area.top + view_area.height - texture_height, view_area.top) + } else { + centered_area.top + }; + + Area { + left, + top, + width: texture_width, + height: texture_height, + } + }); + + Self { + texture_area, + map_width, + map_height, + } + } + + pub fn texture_area(&self) -> Area { + self.texture_area + } + + pub fn marker_area(&self, position: TilePosition, size: f32) -> Area { + let (normalized_x, normalized_y) = Self::normalized_tile_position(self.map_width, self.map_height, position); + let left = self.texture_area.left + normalized_x * self.texture_area.width - size / 2.0; + let top = self.texture_area.top + normalized_y * self.texture_area.height - size / 2.0; + + Area { + left, + top, + width: size, + height: size, + } + } + + fn normalized_tile_position(map_width: f32, map_height: f32, position: TilePosition) -> (f32, f32) { + let x = (position.x as f32 + 0.5) / map_width.max(1.0); + let y = ((map_height - position.y as f32) - 0.5) / map_height.max(1.0); + + (x, y) + } +} diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs new file mode 100644 index 000000000..8a9529560 --- /dev/null +++ b/korangar/src/interface/minimap/render.rs @@ -0,0 +1,129 @@ +use korangar_interface::element::Element; +use korangar_interface::element::store::{ElementStore, ElementStoreMut}; +use korangar_interface::layout::area::Area; +use korangar_interface::layout::{Resolvers, WindowLayout, with_single_resolver}; +use korangar_interface::prelude::{HorizontalAlignment, VerticalAlignment}; +use rust_state::{Path, State}; + +use super::markers::collect_minimap_markers; +use super::projection::MinimapProjection; +use crate::graphics::{Color, CornerDiameter, ShadowPadding}; +use crate::loaders::{FontSize, OverflowBehavior}; +use crate::renderer::LayoutExt; +use crate::state::{ClientState, MinimapState, this_entity}; +use crate::world::Entity; + +pub struct MinimapLayoutInfo { + map_area: Area, + status_area: Area, +} + +pub struct MinimapView { + minimap_path: A, + entities_path: B, + status_text: String, +} + +impl MinimapView { + pub fn new(minimap_path: A, entities_path: B) -> Self { + Self { + minimap_path, + entities_path, + status_text: String::new(), + } + } +} + +impl Element for MinimapView +where + A: Path, + B: Path>, +{ + type LayoutInfo = MinimapLayoutInfo; + + fn create_layout_info( + &mut self, + state: &State, + _: ElementStoreMut, + resolvers: &mut dyn Resolvers, + ) -> Self::LayoutInfo { + with_single_resolver(resolvers, |resolver| { + let map_area = resolver.with_height(192.0); + let status_area = resolver.with_height(22.0); + let minimap = state.get(&self.minimap_path); + let player_position = state.try_get(&this_entity()).map(Entity::get_tile_position); + + self.status_text = if minimap.map_name.is_empty() { + "Loading map".to_owned() + } else if let Some(position) = player_position { + format!("{} ({}, {})", minimap.map_name, position.x, position.y) + } else { + minimap.map_name.clone() + }; + + MinimapLayoutInfo { map_area, status_area } + }) + } + + fn lay_out<'a>( + &'a self, + state: &'a State, + _: ElementStore<'a>, + layout_info: &'a Self::LayoutInfo, + layout: &mut WindowLayout<'a, ClientState>, + ) { + let minimap = state.get(&self.minimap_path); + let player_position = state.try_get(&this_entity()).map(Entity::get_tile_position); + + layout.add_rectangle( + layout_info.map_area, + CornerDiameter::uniform(6.0), + Color::rgb_u8(24, 26, 32), + Color::rgba_u8(0, 0, 0, 110), + ShadowPadding::diagonal(2.0, 5.0), + ); + + if let Some(texture) = &minimap.texture { + let projection = MinimapProjection::new(layout_info.map_area, minimap.width, minimap.height, minimap.zoom, player_position); + + layout.add_texture(projection.texture_area(), texture.clone(), Color::WHITE, false); + + let entities = state.get(&self.entities_path); + let player_path = this_entity(); + let player_entity = state.try_get(&player_path); + let marker_size = (projection.texture_area().width.min(projection.texture_area().height) / 40.0).clamp(4.0, 8.0); + + collect_minimap_markers(entities, player_entity, marker_size) + .into_iter() + .for_each(|marker| { + layout.add_text( + projection.marker_area(marker.tile_position, marker.size), + marker.symbol, + FontSize(marker.font_size), + marker.color, + Color::BLACK, + HorizontalAlignment::Center { + offset: 0.0, + border: 0.0, + }, + VerticalAlignment::Center { offset: 0.0 }, + OverflowBehavior::Shrink, + ); + }); + } + + layout.add_text( + layout_info.status_area, + &self.status_text, + FontSize(14.0), + Color::monochrome_u8(225), + Color::rgb_u8(255, 180, 80), + HorizontalAlignment::Center { + offset: 0.0, + border: 4.0, + }, + VerticalAlignment::Center { offset: 0.0 }, + OverflowBehavior::Shrink, + ); + } +} diff --git a/korangar/src/interface/mod.rs b/korangar/src/interface/mod.rs index a30fa8898..a2c3e4969 100644 --- a/korangar/src/interface/mod.rs +++ b/korangar/src/interface/mod.rs @@ -1,4 +1,5 @@ pub mod components; pub mod cursor; +pub mod minimap; pub mod resource; pub mod windows; diff --git a/korangar/src/interface/windows/character_overview.rs b/korangar/src/interface/windows/character_overview.rs index a9ef3c75d..dc6b6f88b 100644 --- a/korangar/src/interface/windows/character_overview.rs +++ b/korangar/src/interface/windows/character_overview.rs @@ -112,6 +112,10 @@ where text: client_state().localization().friend_list_button_text(), event: InputEvent::ToggleFriendListWindow, }, + button! { + text: "Minimap", + event: InputEvent::ToggleMinimapWindow, + }, button! { text: client_state().localization().menu_button_text(), event: InputEvent::ToggleMenuWindow, diff --git a/korangar/src/interface/windows/minimap.rs b/korangar/src/interface/windows/minimap.rs new file mode 100644 index 000000000..501c8e45b --- /dev/null +++ b/korangar/src/interface/windows/minimap.rs @@ -0,0 +1,88 @@ +use korangar_interface::window::{CustomWindow, Window}; +use rust_state::{Path, PathExt, State}; + +use crate::interface::minimap::MinimapView; +use crate::loaders::OverflowBehavior; +use crate::state::theme::InterfaceThemeType; +use crate::state::{ClientState, MinimapState, MinimapStatePathExt}; +use crate::world::Entity; + +use super::WindowClass; + +pub struct MinimapWindow { + minimap_path: A, + entities_path: B, +} + +impl MinimapWindow { + pub fn new(minimap_path: A, entities_path: B) -> Self { + Self { + minimap_path, + entities_path, + } + } +} + +impl CustomWindow for MinimapWindow +where + A: Path + Clone + 'static, + B: Path> + Clone + 'static, +{ + fn window_class() -> Option { + Some(WindowClass::Minimap) + } + + fn to_window<'a>(self) -> impl Window + 'a { + use korangar_interface::prelude::*; + + const MIN_ZOOM: f32 = 1.0; + const MAX_ZOOM: f32 = 4.0; + const ZOOM_STEP: f32 = 0.5; + + let zoom_out_path = self.minimap_path; + let zoom_text_path = self.minimap_path; + let zoom_in_path = self.minimap_path; + + window! { + title: "Minimap", + class: Self::window_class(), + theme: InterfaceThemeType::InGame, + minimum_width: 240.0, + maximum_width: 360.0, + minimum_height: 290.0, + maximum_height: 290.0, + closable: true, + elements: ( + split! { + children: ( + button! { + text: "-", + event: move |state: &State, _: &mut EventQueue| { + state.update_value_with(zoom_out_path.zoom(), |zoom| { + *zoom = (*zoom - ZOOM_STEP).clamp(MIN_ZOOM, MAX_ZOOM); + }); + }, + }, + text! { + text: ComputedSelector::new_default(move |state: &ClientState| { + format!("Zoom {:.1}x", zoom_text_path.follow_safe(state).zoom) + }), + horizontal_alignment: HorizontalAlignment::Center { offset: 0.0, border: 4.0 }, + vertical_alignment: VerticalAlignment::Center { offset: 0.0 }, + overflow_behavior: OverflowBehavior::Shrink, + }, + button! { + text: "+", + event: move |state: &State, _: &mut EventQueue| { + state.update_value_with(zoom_in_path.zoom(), |zoom| { + *zoom = (*zoom + ZOOM_STEP).clamp(MIN_ZOOM, MAX_ZOOM); + }); + }, + }, + ), + }, + MinimapView::new(self.minimap_path, self.entities_path), + ), + } + } +} diff --git a/korangar/src/interface/windows/mod.rs b/korangar/src/interface/windows/mod.rs index a847845c7..8e8534c90 100644 --- a/korangar/src/interface/windows/mod.rs +++ b/korangar/src/interface/windows/mod.rs @@ -22,6 +22,7 @@ mod hotbar; mod interface_settings; mod inventory; mod login; +mod minimap; #[cfg(feature = "debug")] mod maps; mod menu; @@ -66,6 +67,7 @@ pub use self::hotbar::HotbarWindow; pub use self::interface_settings::InterfaceSettingsWindow; pub use self::inventory::InventoryWindow; pub use self::login::{LoginWindow, LoginWindowState, LoginWindowStatePathExt}; +pub use self::minimap::MinimapWindow; #[cfg(feature = "debug")] pub use self::maps::MapsWindow; pub use self::menu::MenuWindow; @@ -107,6 +109,7 @@ pub enum WindowClass { FriendRequest, Login, Menu, + Minimap, Respawn, SelectServer, Sell, diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 7f8889f21..f3c8d6afd 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -85,7 +85,7 @@ use state::inventory::InventoryPathExt; use state::localization::Localization; use state::skills::SkillTreePathExt; use state::theme::{CursorThemePathExt, IndicatorThemePathExt, InterfaceThemePathExt, WorldThemePathExt}; -use state::{ChatMessage, ClientState, ClientStatePathExt, client_state, this_entity, this_player}; +use state::{ChatMessage, ClientState, ClientStatePathExt, MinimapState, client_state, this_entity, this_player}; #[cfg(feature = "debug")] use wgpu::Device; use wgpu::util::initialize_adapter_from_env_or_default; @@ -105,6 +105,7 @@ use winit::window::{Icon, Window, WindowId}; use crate::graphics::*; use crate::input::{InputEvent, InputSystem}; use crate::interface::cursor::{MouseCursor, MouseCursorState}; +use crate::interface::minimap::create_minimap_state; use crate::interface::resource::{ItemSource, SkillSource}; use crate::interface::windows::*; use crate::loaders::*; @@ -745,6 +746,17 @@ impl Client { }) } + fn clear_minimap_state(&mut self) { + *self.client_state.follow_mut(client_state().minimap()) = MinimapState::default(); + } + + fn update_minimap_state(&mut self, map_name: &str, map: &Map) { + let current_state = self.client_state.follow(client_state().minimap()); + let next_state = create_minimap_state(&self.texture_loader, current_state, map_name, map); + + *self.client_state.follow_mut(client_state().minimap()) = next_state; + } + fn render_frame(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { return; @@ -1106,6 +1118,11 @@ impl Client { client_state().hotbar().skills(), client_state().skill_tree().skills(), )); + *self.client_state.follow_mut(client_state().minimap()) = MinimapState::default(); + if !self.interface.is_window_with_class_open(WindowClass::Minimap) { + self.interface + .open_window(MinimapWindow::new(client_state().minimap(), client_state().entities())); + } // Put the dialog system in a well-defined state. self.client_state.follow_mut(client_state().dialog_window()).end(); @@ -2051,6 +2068,16 @@ impl Client { } } } + InputEvent::ToggleMinimapWindow => { + if self.client_state.try_follow(this_entity()).is_some() { + match self.interface.is_window_with_class_open(WindowClass::Minimap) { + true => self.interface.close_window_with_class(WindowClass::Minimap), + false => self + .interface + .open_window(MinimapWindow::new(client_state().minimap(), client_state().entities())), + } + } + } InputEvent::CloseTopWindow => self.interface.close_top_window(&self.client_state), InputEvent::ToggleShowInterface => self.show_interface = !self.show_interface, InputEvent::SelectCharacter { slot } => { @@ -2557,7 +2584,9 @@ impl Client { #[cfg(feature = "debug")] let loads_measurement = Profiler::start_measurement("complete async loads"); - for completed in self.async_loader.take_completed() { + let completed_loads: Vec<_> = self.async_loader.take_completed().collect(); + + for completed in completed_loads { match completed { (LoaderId::AnimationData(entity_id), LoadableResource::AnimationData(animation_data)) => { if let Some(entity) = self @@ -2594,9 +2623,12 @@ impl Client { .follow_mut(client_state().inventory()) .update_item_sprite(item_id, texture); } - (LoaderId::Map(..), LoadableResource::Map { map, position }) => { + (LoaderId::Map(map_name), LoadableResource::Map { map, position }) => { match self.client_state.try_follow(this_player()).is_none() { true => { + self.clear_minimap_state(); + self.interface.close_window_with_class(WindowClass::Minimap); + // Load of main menu map let map = self.map.insert(map); @@ -2612,6 +2644,8 @@ impl Client { self.directional_shadow_camera.set_level_bound(map.get_level_bound()); } false => { + self.update_minimap_state(&map_name, &map); + // Normal map switch let map = self.map.insert(map); diff --git a/korangar/src/state/mod.rs b/korangar/src/state/mod.rs index 878ac543c..e8e5d752e 100644 --- a/korangar/src/state/mod.rs +++ b/korangar/src/state/mod.rs @@ -79,6 +79,28 @@ impl ChatMessage { } } +#[derive(RustState, StateElement)] +pub struct MinimapState { + pub map_name: String, + pub width: u16, + pub height: u16, + pub zoom: f32, + #[hidden_element] + pub texture: Option>, +} + +impl Default for MinimapState { + fn default() -> Self { + Self { + map_name: String::new(), + width: 0, + height: 0, + zoom: 1.0, + texture: None, + } + } +} + #[derive(Debug, Clone, Copy, RustState, StateElement)] pub enum BufferedAction { AttackEntity { entity_id: EntityId }, @@ -172,6 +194,7 @@ pub struct ClientState { /// The name of the active character. This information is not available /// while playing if we don't save it here. player_name: String, + minimap: MinimapState, /// Player configured hotbar. hotbar: Hotbar, /// Player inventory. @@ -328,6 +351,7 @@ impl ClientState { let sell_items = Vec::default(); let sell_cart = Vec::default(); let player_name = String::new(); + let minimap = MinimapState::default(); let hotbar = Hotbar::default(); let inventory = Inventory::default(); let skill_tree = SkillTree::default(); @@ -397,6 +421,7 @@ impl ClientState { sell_items, sell_cart, player_name, + minimap, hotbar, inventory, skill_tree, diff --git a/korangar/src/world/entity/mod.rs b/korangar/src/world/entity/mod.rs index 1dc10aa75..370ef35f9 100644 --- a/korangar/src/world/entity/mod.rs +++ b/korangar/src/world/entity/mod.rs @@ -1324,6 +1324,10 @@ impl Entity { self.get_common().world_position } + pub fn get_direction(&self) -> Direction { + self.get_common().direction + } + pub fn set_position(&mut self, map: &Map, position: TilePosition, client_tick: ClientTick) { self.get_common_mut().set_position(map, position, client_tick); } diff --git a/korangar/src/world/map/mod.rs b/korangar/src/world/map/mod.rs index d0ae322d3..32ab2cd34 100644 --- a/korangar/src/world/map/mod.rs +++ b/korangar/src/world/map/mod.rs @@ -270,6 +270,18 @@ impl Map { self.tiles.get(position.x as usize + position.y as usize * self.width as usize) } + pub fn get_width(&self) -> u16 { + self.width + } + + pub fn get_height(&self) -> u16 { + self.height + } + + pub fn get_tiles(&self) -> &[Tile] { + &self.tiles + } + pub fn background_music_track_name(&self) -> Option<&str> { self.background_music_track_name.as_deref() } From 56bcce6123f2d86e1ea11d7bdf0441e010134a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 20:45:25 +0800 Subject: [PATCH 02/13] Fix minimap generated texture Y-axis flip --- korangar/src/interface/minimap/generation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index e261cc614..42c250e7a 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -36,7 +36,7 @@ fn create_generated_minimap_texture(texture_loader: &TextureLoader, map_name: &s map.get_tiles().iter().enumerate().for_each(|(index, tile)| { let x = (index % map_width) as u32; - let y = (index / map_width) as u32; + let y = height.saturating_sub(1) - (index / map_width) as u32; let normalized_height = ((average_tile_height(tile) - lowest_height) / height_range).clamp(0.0, 1.0); let walkable = tile.flags.contains(TileFlags::WALKABLE); let base = if walkable { From 0993c50a73eb9c3d5e1b3156db4dc3ca540c6b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 21:22:15 +0800 Subject: [PATCH 03/13] Minimap: avoid missing arrow glyphs --- korangar/src/interface/minimap/markers.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs index 2df5624c7..e4dc4225f 100644 --- a/korangar/src/interface/minimap/markers.rs +++ b/korangar/src/interface/minimap/markers.rs @@ -42,14 +42,14 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit fn player_marker(direction: Direction) -> &'static str { match direction { - Direction::North => "↑", - Direction::NorthEast => "↗", - Direction::East => "→", - Direction::SouthEast => "↘", - Direction::South => "↓", - Direction::SouthWest => "↙", - Direction::West => "←", - Direction::NorthWest => "↖", + Direction::North => "N", + Direction::NorthEast => "NE", + Direction::East => "E", + Direction::SouthEast => "SE", + Direction::South => "S", + Direction::SouthWest => "SW", + Direction::West => "W", + Direction::NorthWest => "NW", } } From 33bec1f6fcd44923753d26ea40d372fcff228e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 21:46:44 +0800 Subject: [PATCH 04/13] Engine: add rotation to sprite rendering; Minimap: use rotated arrow texture for player marker --- korangar/shaders/modules/interface.slang | 16 +++++- korangar/shaders/modules/postprocessing.slang | 2 + .../passes/postprocessing/rectangle.slang | 12 ++++- .../postprocessing/rectangle_bindless.slang | 12 ++++- korangar/src/graphics/instruction.rs | 2 + .../graphics/passes/interface/rectangle.rs | 13 ++++- .../passes/postprocessing/rectangle.rs | 13 ++++- korangar/src/interface/minimap/generation.rs | 3 ++ korangar/src/interface/minimap/markers.rs | 19 ++----- korangar/src/interface/minimap/render.rs | 30 ++++++++++- korangar/src/renderer/game_interface.rs | 1 + korangar/src/renderer/interface.rs | 53 ++++++++++++++++++- korangar/src/state/mod.rs | 3 ++ 13 files changed, 157 insertions(+), 22 deletions(-) diff --git a/korangar/shaders/modules/interface.slang b/korangar/shaders/modules/interface.slang index a50e3f1f7..125cf3d87 100644 --- a/korangar/shaders/modules/interface.slang +++ b/korangar/shaders/modules/interface.slang @@ -21,6 +21,8 @@ public struct RectangleInstanceData { public var texture_size: float2; public var rectangle_type: uint; public var texture_index: int; + public var rotation: float; + public var padding: uint; }; public struct RectangleVertexInput { @@ -258,7 +260,19 @@ public func rectangle_vertex_shader( adjusted_size += shadow_expansion; let clip_size = adjusted_size * 2.0; - let position = coordinate_space::screen_to_clip_space(instance.screen_position + position_offset) + vertex.xy * clip_size; + + var rotated_vertex = vertex.xy * clip_size; + if (instance.rotation != 0.0) { + let center = float2(0.5, -0.5) * clip_size; + let c = cos(instance.rotation); + let s = sin(instance.rotation); + let p = rotated_vertex - center; + let rx = p.x * c - p.y * s; + let ry = p.x * s + p.y * c; + rotated_vertex = float2(rx, ry) + center; + } + + let position = coordinate_space::screen_to_clip_space(instance.screen_position + position_offset) + rotated_vertex; var output: RectangleVertexOutput; output.position = float4(position, 0.0, 1.0); diff --git a/korangar/shaders/modules/postprocessing.slang b/korangar/shaders/modules/postprocessing.slang index e0a6176a3..ecfc93110 100644 --- a/korangar/shaders/modules/postprocessing.slang +++ b/korangar/shaders/modules/postprocessing.slang @@ -46,6 +46,8 @@ public struct RectangleInstanceData { public var texture_size: float2; public var rectangle_type: uint; public var texture_index: int; + public var rotation: float; + public var padding: uint; } public struct DebugUniforms { diff --git a/korangar/shaders/passes/postprocessing/rectangle.slang b/korangar/shaders/passes/postprocessing/rectangle.slang index 9e67ca917..45f456459 100644 --- a/korangar/shaders/passes/postprocessing/rectangle.slang +++ b/korangar/shaders/passes/postprocessing/rectangle.slang @@ -15,7 +15,17 @@ func vs_main(input: PostprocessingVertexInput) -> PostprocessingVertexOutput { let vertex = rectangle_vertex_data(input.vertex_index); let clip_size = instance.screen_size * 2.0; - let position = screen_to_clip_space(instance.screen_position) + vertex.xy * clip_size; + var rotated_vertex = vertex.xy * clip_size; + if (instance.rotation != 0.0) { + let center = float2(0.5, -0.5) * clip_size; + let c = cos(instance.rotation); + let s = sin(instance.rotation); + let p = rotated_vertex - center; + let rx = p.x * c - p.y * s; + let ry = p.x * s + p.y * c; + rotated_vertex = float2(rx, ry) + center; + } + let position = screen_to_clip_space(instance.screen_position) + rotated_vertex; var output: PostprocessingVertexOutput; output.position = float4(position, 0.0, 1.0); diff --git a/korangar/shaders/passes/postprocessing/rectangle_bindless.slang b/korangar/shaders/passes/postprocessing/rectangle_bindless.slang index 0f1c79a5d..2726b4f0b 100644 --- a/korangar/shaders/passes/postprocessing/rectangle_bindless.slang +++ b/korangar/shaders/passes/postprocessing/rectangle_bindless.slang @@ -15,7 +15,17 @@ func vs_main(input: PostprocessingVertexInput) -> PostprocessingVertexOutput { let vertex = rectangle_vertex_data(input.vertex_index); let clip_size = instance.screen_size * 2.0; - let position = screen_to_clip_space(instance.screen_position) + vertex.xy * clip_size; + var rotated_vertex = vertex.xy * clip_size; + if (instance.rotation != 0.0) { + let center = float2(0.5, -0.5) * clip_size; + let c = cos(instance.rotation); + let s = sin(instance.rotation); + let p = rotated_vertex - center; + let rx = p.x * c - p.y * s; + let ry = p.x * s + p.y * c; + rotated_vertex = float2(rx, ry) + center; + } + let position = screen_to_clip_space(instance.screen_position) + rotated_vertex; var output: PostprocessingVertexOutput; output.position = float4(position, 0.0, 1.0); diff --git a/korangar/src/graphics/instruction.rs b/korangar/src/graphics/instruction.rs index 85ce561f8..d3e8ec9b3 100644 --- a/korangar/src/graphics/instruction.rs +++ b/korangar/src/graphics/instruction.rs @@ -184,6 +184,7 @@ pub enum RectangleInstruction { texture_size: Vector2, linear_filtering: bool, texture: Arc, + rotation: f32, }, Sdf { screen_position: ScreenPosition, @@ -221,6 +222,7 @@ pub enum InterfaceRectangleInstruction { corner_diameter: CornerDiameter, texture: Arc, smooth: bool, + rotation: f32, }, Sdf { screen_position: ScreenPosition, diff --git a/korangar/src/graphics/passes/interface/rectangle.rs b/korangar/src/graphics/passes/interface/rectangle.rs index 16c049984..84e398a4e 100644 --- a/korangar/src/graphics/passes/interface/rectangle.rs +++ b/korangar/src/graphics/passes/interface/rectangle.rs @@ -37,7 +37,8 @@ struct InstanceData { texture_size: [f32; 2], rectangle_type: u32, texture_index: i32, - padding: [f32; 2], + rotation: f32, + padding: f32, } pub(crate) struct InterfaceRectangleDrawer { @@ -293,6 +294,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 0, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } @@ -304,6 +306,7 @@ impl Prepare for InterfaceRectangleDrawer { corner_diameter, texture, smooth, + rotation, } => { let rectangle_type = if *smooth { 1 } else { 2 }; @@ -330,6 +333,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type, texture_index, + rotation: *rotation, padding: Default::default(), }); } @@ -364,6 +368,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 3, texture_index, + rotation: 0.0, padding: Default::default(), }); } @@ -387,6 +392,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 4, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } @@ -429,6 +435,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 0, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } @@ -441,6 +448,7 @@ impl Prepare for InterfaceRectangleDrawer { corner_diameter, texture: _, smooth, + rotation, } => { let rectangle_type = if *smooth { 1 } else { 2 }; @@ -456,6 +464,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type, texture_index: 0, + rotation: *rotation, padding: Default::default(), }); } @@ -479,6 +488,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 3, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } @@ -502,6 +512,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 4, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } diff --git a/korangar/src/graphics/passes/postprocessing/rectangle.rs b/korangar/src/graphics/passes/postprocessing/rectangle.rs index 846f42d31..9e73f07cd 100644 --- a/korangar/src/graphics/passes/postprocessing/rectangle.rs +++ b/korangar/src/graphics/passes/postprocessing/rectangle.rs @@ -43,7 +43,8 @@ pub(crate) struct InstanceData { texture_size: [f32; 2], rectangle_type: u32, texture_index: i32, - padding: [u32; 2], + rotation: f32, + padding: u32, } #[derive(Default, Copy, Clone)] @@ -318,6 +319,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: [0.0; 2], rectangle_type: 0, texture_index: -1, + rotation: 0.0, padding: Default::default(), }); } @@ -329,6 +331,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size, linear_filtering, texture, + rotation, } => { let rectangle_type = if *linear_filtering { 1 } else { 2 }; @@ -351,6 +354,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type, texture_index, + rotation: *rotation, padding: Default::default(), }); } @@ -381,6 +385,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 3, texture_index, + rotation: 0.0, padding: Default::default(), }); } @@ -399,6 +404,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 4, texture_index: -1, + rotation: 0.0, padding: Default::default(), }); } @@ -448,6 +454,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: [0.0; 2], rectangle_type: 0, texture_index: -1, + rotation: 0.0, padding: Default::default(), }); } @@ -459,6 +466,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size, linear_filtering, texture: _, + rotation, } => { let rectangle_type = if *linear_filtering { 1 } else { 2 }; @@ -470,6 +478,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type, texture_index: 0, + rotation: *rotation, padding: Default::default(), }); } @@ -489,6 +498,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 3, texture_index: 0, + rotation: 0.0, padding: Default::default(), }); } @@ -507,6 +517,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 4, texture_index: -1, + rotation: 0.0, padding: Default::default(), }); } diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index 42c250e7a..f389c9fe7 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -11,12 +11,15 @@ use crate::world::Map; pub fn create_minimap_state(texture_loader: &TextureLoader, previous: &MinimapState, map_name: &str, map: &Map) -> MinimapState { let texture = create_generated_minimap_texture(texture_loader, map_name, map); + let arrow_texture = texture_loader.get_or_load("arrow_left.png", ImageType::Sdf).ok(); + MinimapState { map_name: map_name.strip_suffix(".gat").unwrap_or(map_name).to_owned(), width: map.get_width(), height: map.get_height(), zoom: previous.zoom.max(1.0), texture: Some(texture), + arrow_texture, } } diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs index e4dc4225f..539a7754d 100644 --- a/korangar/src/interface/minimap/markers.rs +++ b/korangar/src/interface/minimap/markers.rs @@ -9,6 +9,8 @@ pub struct MinimapMarker { pub color: Color, pub size: f32, pub font_size: f32, + pub is_player: bool, + pub player_direction: Option, } pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entity>, base_size: f32) -> Vec { @@ -26,7 +28,7 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit } let size = if is_player { base_size + 2.0 } else { base_size }; - let symbol = if is_player { player_marker(player_direction) } else { "●" }; + let symbol = if is_player { "" } else { "●" }; let font_size = size + if is_player { 8.0 } else { 6.0 }; Some(MinimapMarker { @@ -35,24 +37,13 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit color: marker_color(entity_type, is_player), size, font_size, + is_player, + player_direction: if is_player { Some(player_direction) } else { None }, }) }) .collect() } -fn player_marker(direction: Direction) -> &'static str { - match direction { - Direction::North => "N", - Direction::NorthEast => "NE", - Direction::East => "E", - Direction::SouthEast => "SE", - Direction::South => "S", - Direction::SouthWest => "SW", - Direction::West => "W", - Direction::NorthWest => "NW", - } -} - fn marker_color(entity_type: EntityType, is_player: bool) -> Color { if is_player { return Color::rgb_u8(80, 255, 160); diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 8a9529560..ac925b6e2 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -11,7 +11,7 @@ use crate::graphics::{Color, CornerDiameter, ShadowPadding}; use crate::loaders::{FontSize, OverflowBehavior}; use crate::renderer::LayoutExt; use crate::state::{ClientState, MinimapState, this_entity}; -use crate::world::Entity; +use crate::world::{Direction, Entity}; pub struct MinimapLayoutInfo { map_area: Area, @@ -96,8 +96,34 @@ where collect_minimap_markers(entities, player_entity, marker_size) .into_iter() .for_each(|marker| { + if marker.is_player { + if let Some(arrow_texture) = &minimap.arrow_texture { + let rotation = match marker.player_direction { + Some(Direction::West) => 0.0, + Some(Direction::NorthWest) => std::f32::consts::PI / 4.0, + Some(Direction::North) => std::f32::consts::PI / 2.0, + Some(Direction::NorthEast) => 3.0 * std::f32::consts::PI / 4.0, + Some(Direction::East) => std::f32::consts::PI, + Some(Direction::SouthEast) => 5.0 * std::f32::consts::PI / 4.0, + Some(Direction::South) => 3.0 * std::f32::consts::PI / 2.0, + Some(Direction::SouthWest) => 7.0 * std::f32::consts::PI / 4.0, + None => 0.0, + }; + + let mut area = projection.marker_area(marker.position, marker.size); + // Make the arrow slightly larger to be visible + area.width *= 2.0; + area.height *= 2.0; + area.left -= marker.size / 2.0; + area.top -= marker.size / 2.0; + + layout.add_rotated_texture(area, arrow_texture.clone(), marker.color, true, rotation); + return; + } + } + layout.add_text( - projection.marker_area(marker.tile_position, marker.size), + projection.marker_area(marker.position, marker.size), marker.symbol, FontSize(marker.font_size), marker.color, diff --git a/korangar/src/renderer/game_interface.rs b/korangar/src/renderer/game_interface.rs index 4f4ffa140..e14adea7e 100644 --- a/korangar/src/renderer/game_interface.rs +++ b/korangar/src/renderer/game_interface.rs @@ -303,6 +303,7 @@ impl GameInterfaceRenderer { texture_size, linear_filtering: smooth, texture, + rotation: 0.0, }); } } diff --git a/korangar/src/renderer/interface.rs b/korangar/src/renderer/interface.rs index 30646fab2..383cc3c4f 100644 --- a/korangar/src/renderer/interface.rs +++ b/korangar/src/renderer/interface.rs @@ -424,6 +424,46 @@ impl InterfaceRenderer { pub fn render_trash_can(&self, position: ScreenPosition, size: ScreenSize, clip: ScreenClip, color: Color) { self.render_sdf(self.trash_can_texture.clone(), position, size, clip, color); } + + /// Render a sprite with an optional rotation (in radians). + pub fn render_rotated_texture( + &self, + texture: Arc, + position: ScreenPosition, + size: ScreenSize, + mut screen_clip: ScreenClip, + color: Color, + smooth: bool, + rotation: f32, + ) { + if position.left > screen_clip.right + || position.top > screen_clip.bottom + || position.left + size.width < screen_clip.left + || position.top + size.height < screen_clip.top + { + return; + } + + if self.high_quality_interface { + screen_clip = screen_clip * 2.0; + } + + let screen_position = position / self.window_size; + let screen_size = size / self.window_size; + + let corner_diameter = CornerDiameter::default(); + + self.instructions.borrow_mut().push(InterfaceRectangleInstruction::Sprite { + screen_position, + screen_size, + screen_clip, + color, + corner_diameter, + texture, + smooth, + rotation, + }); + } } impl SpriteRenderer for InterfaceRenderer { @@ -495,6 +535,7 @@ impl SpriteRenderer for InterfaceRenderer { corner_diameter, texture, smooth, + rotation: 0.0, }); } @@ -572,6 +613,7 @@ struct TextureInstruction { area: Area, color: Color, smooth: bool, + rotation: f32, } /// An instruction to render a sprite. @@ -667,6 +709,7 @@ impl RenderLayer for InterfaceRenderer { area, color, smooth, + rotation, }) => { let position = ScreenPosition { left: area.left, @@ -678,7 +721,7 @@ impl RenderLayer for InterfaceRenderer { }; let screen_clip = clips[clip_id.as_index()]; - self.render_sprite(texture, position, size, screen_clip, color, smooth); + self.render_rotated_texture(texture, position, size, screen_clip, color, smooth, rotation); } } } @@ -690,6 +733,9 @@ pub trait LayoutExt<'a> { /// Add an instruction to render a texture. fn add_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool); + /// Add an instruction to render a texture with a given rotation. + fn add_rotated_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool, rotation: f32); + /// Add an instruction to render a sprite. fn add_sprite( &mut self, @@ -704,6 +750,10 @@ pub trait LayoutExt<'a> { impl<'a> LayoutExt<'a> for WindowLayout<'a, ClientState> { fn add_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool) { + self.add_rotated_texture(area, texture, color, smooth, 0.0); + } + + fn add_rotated_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool, rotation: f32) { let clip_id = self.get_active_clip_id(); let area = self.scale_area(area); @@ -713,6 +763,7 @@ impl<'a> LayoutExt<'a> for WindowLayout<'a, ClientState> { area, color, smooth, + rotation, })); } diff --git a/korangar/src/state/mod.rs b/korangar/src/state/mod.rs index e8e5d752e..7c4820a43 100644 --- a/korangar/src/state/mod.rs +++ b/korangar/src/state/mod.rs @@ -87,6 +87,8 @@ pub struct MinimapState { pub zoom: f32, #[hidden_element] pub texture: Option>, + #[hidden_element] + pub arrow_texture: Option>, } impl Default for MinimapState { @@ -97,6 +99,7 @@ impl Default for MinimapState { height: 0, zoom: 1.0, texture: None, + arrow_texture: None, } } } From 7480dee67e40d920517c32afad790dce77c691f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 21:50:17 +0800 Subject: [PATCH 05/13] Minimap: fix compilation errors (imports and field names) --- korangar/src/interface/minimap/generation.rs | 2 +- korangar/src/interface/minimap/render.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index f389c9fe7..a6a31b977 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -4,7 +4,7 @@ use image::{Rgba, RgbaImage}; use ragnarok_formats::map::{Tile, TileFlags}; use crate::graphics::Texture; -use crate::loaders::TextureLoader; +use crate::loaders::{ImageType, TextureLoader}; use crate::state::MinimapState; use crate::world::Map; diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index ac925b6e2..d664fcb4f 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -10,8 +10,9 @@ use super::projection::MinimapProjection; use crate::graphics::{Color, CornerDiameter, ShadowPadding}; use crate::loaders::{FontSize, OverflowBehavior}; use crate::renderer::LayoutExt; -use crate::state::{ClientState, MinimapState, this_entity}; -use crate::world::{Direction, Entity}; +use crate::state::{this_entity, ClientState, MinimapState}; +use crate::world::Entity; +use ragnarok_packets::Direction; pub struct MinimapLayoutInfo { map_area: Area, @@ -110,7 +111,7 @@ where None => 0.0, }; - let mut area = projection.marker_area(marker.position, marker.size); + let mut area = projection.marker_area(marker.tile_position, marker.size); // Make the arrow slightly larger to be visible area.width *= 2.0; area.height *= 2.0; @@ -123,7 +124,7 @@ where } layout.add_text( - projection.marker_area(marker.position, marker.size), + projection.marker_area(marker.tile_position, marker.size), marker.symbol, FontSize(marker.font_size), marker.color, From 812491daba61d3f234d78fd212d88c9771f4e98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 21:58:13 +0800 Subject: [PATCH 06/13] Engine: extend rotation support to SDF rendering for minimap markers --- korangar/src/graphics/instruction.rs | 2 + .../graphics/passes/interface/rectangle.rs | 6 ++- .../passes/postprocessing/rectangle.rs | 6 ++- korangar/src/interface/minimap/render.rs | 2 +- korangar/src/renderer/game_interface.rs | 15 +++++- korangar/src/renderer/interface.rs | 49 ++++++++++++++++--- korangar/src/renderer/mod.rs | 3 ++ 7 files changed, 71 insertions(+), 12 deletions(-) diff --git a/korangar/src/graphics/instruction.rs b/korangar/src/graphics/instruction.rs index d3e8ec9b3..d8fba7ddd 100644 --- a/korangar/src/graphics/instruction.rs +++ b/korangar/src/graphics/instruction.rs @@ -193,6 +193,7 @@ pub enum RectangleInstruction { texture_position: Vector2, texture_size: Vector2, texture: Arc, + rotation: f32, }, Text { screen_position: ScreenPosition, @@ -231,6 +232,7 @@ pub enum InterfaceRectangleInstruction { color: Color, corner_diameter: CornerDiameter, texture: Arc, + rotation: f32, }, Text { screen_position: ScreenPosition, diff --git a/korangar/src/graphics/passes/interface/rectangle.rs b/korangar/src/graphics/passes/interface/rectangle.rs index 84e398a4e..eb2e5dac8 100644 --- a/korangar/src/graphics/passes/interface/rectangle.rs +++ b/korangar/src/graphics/passes/interface/rectangle.rs @@ -344,6 +344,7 @@ impl Prepare for InterfaceRectangleDrawer { color, corner_diameter, texture, + rotation, } => { let mut texture_index = texture_views.len() as i32; let id = texture.get_id(); @@ -368,7 +369,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 3, texture_index, - rotation: 0.0, + rotation: *rotation, padding: Default::default(), }); } @@ -475,6 +476,7 @@ impl Prepare for InterfaceRectangleDrawer { color, corner_diameter, texture: _, + rotation, } => { self.instance_data.push(InstanceData { color: color.components_linear(), @@ -488,7 +490,7 @@ impl Prepare for InterfaceRectangleDrawer { texture_size: [1.0, 1.0], rectangle_type: 3, texture_index: 0, - rotation: 0.0, + rotation: *rotation, padding: Default::default(), }); } diff --git a/korangar/src/graphics/passes/postprocessing/rectangle.rs b/korangar/src/graphics/passes/postprocessing/rectangle.rs index 9e73f07cd..6fb27a3d2 100644 --- a/korangar/src/graphics/passes/postprocessing/rectangle.rs +++ b/korangar/src/graphics/passes/postprocessing/rectangle.rs @@ -365,6 +365,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_position, texture_size, texture, + rotation, } => { let mut texture_index = texture_views.len() as i32; let id = texture.get_id(); @@ -385,7 +386,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 3, texture_index, - rotation: 0.0, + rotation: *rotation, padding: Default::default(), }); } @@ -489,6 +490,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_position, texture_size, texture: _, + rotation, } => { self.instance_data.push(InstanceData { color: color.components_linear(), @@ -498,7 +500,7 @@ impl Prepare for PostProcessingRectangleDrawer { texture_size: (*texture_size).into(), rectangle_type: 3, texture_index: 0, - rotation: 0.0, + rotation: *rotation, padding: Default::default(), }); } diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index d664fcb4f..403b934bb 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -118,7 +118,7 @@ where area.left -= marker.size / 2.0; area.top -= marker.size / 2.0; - layout.add_rotated_texture(area, arrow_texture.clone(), marker.color, true, rotation); + layout.add_rotated_sdf(area, arrow_texture.clone(), marker.color, rotation); return; } } diff --git a/korangar/src/renderer/game_interface.rs b/korangar/src/renderer/game_interface.rs index e14adea7e..89c239d6b 100644 --- a/korangar/src/renderer/game_interface.rs +++ b/korangar/src/renderer/game_interface.rs @@ -60,13 +60,14 @@ impl SpriteRenderer for GameInterfaceRenderer { self.render_indexed(texture, position, size, color, 1, 0, smooth); } - fn render_sdf( + fn render_rotated_sdf( &self, texture: Arc, screen_position: ScreenPosition, screen_size: ScreenSize, _screen_clip: ScreenClip, color: Color, + rotation: f32, ) { let screen_position = ScreenPosition { left: screen_position.left / self.window_size.width, @@ -88,8 +89,20 @@ impl SpriteRenderer for GameInterfaceRenderer { texture_position, texture_size, texture, + rotation, }); } + + fn render_sdf( + &self, + texture: Arc, + screen_position: ScreenPosition, + screen_size: ScreenSize, + screen_clip: ScreenClip, + color: Color, + ) { + self.render_rotated_sdf(texture, screen_position, screen_size, screen_clip, color, 0.0); + } } impl GameInterfaceRenderer { diff --git a/korangar/src/renderer/interface.rs b/korangar/src/renderer/interface.rs index 383cc3c4f..1a98756c4 100644 --- a/korangar/src/renderer/interface.rs +++ b/korangar/src/renderer/interface.rs @@ -539,7 +539,7 @@ impl SpriteRenderer for InterfaceRenderer { }); } - fn render_sdf(&self, texture: Arc, position: ScreenPosition, size: ScreenSize, mut screen_clip: ScreenClip, color: Color) { + fn render_rotated_sdf(&self, texture: Arc, position: ScreenPosition, size: ScreenSize, mut screen_clip: ScreenClip, color: Color, rotation: f32) { // If the SDF is not even within the bounds of the clip, discard it early // saving GPU resources. if position.left > screen_clip.right @@ -598,8 +598,13 @@ impl SpriteRenderer for InterfaceRenderer { color, corner_diameter, texture, + rotation, }); } + + fn render_sdf(&self, texture: Arc, position: ScreenPosition, size: ScreenSize, screen_clip: ScreenClip, color: Color) { + self.render_rotated_sdf(texture, position, size, screen_clip, color, 0.0); + } } /// An instruction to render a texture. @@ -638,6 +643,8 @@ struct SpriteInstruction<'a> { pub enum CustomInstruction<'a> { /// An instruction to render a texture. Texture(TextureInstruction), + /// An instruction to render a rotated SDF. + RotatedSdf(TextureInstruction), /// An instruction to render a sprite. Sprite(SpriteInstruction<'a>), } @@ -723,6 +730,26 @@ impl RenderLayer for InterfaceRenderer { self.render_rotated_texture(texture, position, size, screen_clip, color, smooth, rotation); } + CustomInstruction::RotatedSdf(TextureInstruction { + texture, + clip_id, + area, + color, + smooth: _, + rotation, + }) => { + let position = ScreenPosition { + left: area.left, + top: area.top, + }; + let size = ScreenSize { + width: area.width, + height: area.height, + }; + let screen_clip = clips[clip_id.as_index()]; + + self.render_rotated_sdf(texture, position, size, screen_clip, color, rotation); + } } } } @@ -734,7 +761,7 @@ pub trait LayoutExt<'a> { fn add_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool); /// Add an instruction to render a texture with a given rotation. - fn add_rotated_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool, rotation: f32); + fn add_rotated_sdf(&mut self, area: Area, texture: Arc, color: Color, rotation: f32); /// Add an instruction to render a sprite. fn add_sprite( @@ -750,10 +777,6 @@ pub trait LayoutExt<'a> { impl<'a> LayoutExt<'a> for WindowLayout<'a, ClientState> { fn add_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool) { - self.add_rotated_texture(area, texture, color, smooth, 0.0); - } - - fn add_rotated_texture(&mut self, area: Area, texture: Arc, color: Color, smooth: bool, rotation: f32) { let clip_id = self.get_active_clip_id(); let area = self.scale_area(area); @@ -763,6 +786,20 @@ impl<'a> LayoutExt<'a> for WindowLayout<'a, ClientState> { area, color, smooth, + rotation: 0.0, + })); + } + + fn add_rotated_sdf(&mut self, area: Area, texture: Arc, color: Color, rotation: f32) { + let clip_id = self.get_active_clip_id(); + let area = self.scale_area(area); + + self.add_custom_instruction(CustomInstruction::RotatedSdf(TextureInstruction { + texture, + clip_id, + area, + color, + smooth: false, rotation, })); } diff --git a/korangar/src/renderer/mod.rs b/korangar/src/renderer/mod.rs index 19b9cd64d..d82d1b2d6 100644 --- a/korangar/src/renderer/mod.rs +++ b/korangar/src/renderer/mod.rs @@ -37,6 +37,9 @@ pub trait SpriteRenderer { ); fn render_sdf(&self, texture: Arc, position: ScreenPosition, size: ScreenSize, screen_clip: ScreenClip, color: Color); + + /// Render a rotated Signed Distance Field (SDF) based image. + fn render_rotated_sdf(&self, texture: Arc, position: ScreenPosition, size: ScreenSize, screen_clip: ScreenClip, color: Color, rotation: f32); } /// Trait to render markers. From 093d57fe83e450ca80c58f193548b465a288f33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 22:15:39 +0800 Subject: [PATCH 07/13] Minimap: dynamically rotate player marker arrow with camera view angle --- korangar/src/interface/minimap/markers.rs | 5 +---- korangar/src/interface/minimap/render.rs | 22 ++++++++----------- korangar/src/main.rs | 3 +++ korangar/src/state/mod.rs | 6 +++++ korangar/src/world/cameras/debug.rs | 4 ++++ .../src/world/cameras/directional_shadow.rs | 4 ++++ korangar/src/world/cameras/mod.rs | 1 + korangar/src/world/cameras/player.rs | 11 ++++++++++ korangar/src/world/cameras/point_shadow.rs | 4 ++++ korangar/src/world/cameras/start.rs | 4 ++++ 10 files changed, 47 insertions(+), 17 deletions(-) diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs index 539a7754d..273cf45c6 100644 --- a/korangar/src/interface/minimap/markers.rs +++ b/korangar/src/interface/minimap/markers.rs @@ -1,4 +1,4 @@ -use ragnarok_packets::{Direction, TilePosition}; +use ragnarok_packets::TilePosition; use crate::graphics::Color; use crate::world::{Entity, EntityType}; @@ -10,12 +10,10 @@ pub struct MinimapMarker { pub size: f32, pub font_size: f32, pub is_player: bool, - pub player_direction: Option, } pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entity>, base_size: f32) -> Vec { let player_entity_id = player_entity.map(Entity::get_entity_id); - let player_direction = player_entity.map(Entity::get_direction).unwrap_or(Direction::South); entities .iter() @@ -38,7 +36,6 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit size, font_size, is_player, - player_direction: if is_player { Some(player_direction) } else { None }, }) }) .collect() diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 403b934bb..c4d6237ef 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -10,9 +10,8 @@ use super::projection::MinimapProjection; use crate::graphics::{Color, CornerDiameter, ShadowPadding}; use crate::loaders::{FontSize, OverflowBehavior}; use crate::renderer::LayoutExt; -use crate::state::{this_entity, ClientState, MinimapState}; +use crate::state::{this_entity, ClientState, MinimapState, ClientStatePathExt}; use crate::world::Entity; -use ragnarok_packets::Direction; pub struct MinimapLayoutInfo { map_area: Area, @@ -93,23 +92,20 @@ where let player_path = this_entity(); let player_entity = state.try_get(&player_path); let marker_size = (projection.texture_area().width.min(projection.texture_area().height) / 40.0).clamp(4.0, 8.0); + + // To get camera info, we need a way to pass it here or just fallback to 0.0 rotation. + // Ideally, the camera should be accessible via state, but if not we can add it to ClientState + // For now, let's use the camera view angle from the layout or client state + let view_angle = *state.get(&crate::state::client_state().camera_view_angle()); collect_minimap_markers(entities, player_entity, marker_size) .into_iter() .for_each(|marker| { if marker.is_player { if let Some(arrow_texture) = &minimap.arrow_texture { - let rotation = match marker.player_direction { - Some(Direction::West) => 0.0, - Some(Direction::NorthWest) => std::f32::consts::PI / 4.0, - Some(Direction::North) => std::f32::consts::PI / 2.0, - Some(Direction::NorthEast) => 3.0 * std::f32::consts::PI / 4.0, - Some(Direction::East) => std::f32::consts::PI, - Some(Direction::SouthEast) => 5.0 * std::f32::consts::PI / 4.0, - Some(Direction::South) => 3.0 * std::f32::consts::PI / 2.0, - Some(Direction::SouthWest) => 7.0 * std::f32::consts::PI / 4.0, - None => 0.0, - }; + // view_angle is positive counter-clockwise, but in the SDF shader we + // need negative rotation to turn clockwise on screen. + let rotation = -view_angle; let mut area = projection.marker_area(marker.tile_position, marker.size); // Make the arrow slightly larger to be visible diff --git a/korangar/src/main.rs b/korangar/src/main.rs index f3c8d6afd..dffa01ebd 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -2876,6 +2876,9 @@ impl Client { true => &self.player_camera, false => &self.start_camera, }; + + // Push the camera view angle to the state for UI components like the minimap to use + *self.client_state.follow_mut(client_state().camera_view_angle()) = current_camera.view_angle(); let (view_matrix, projection_matrix) = current_camera.view_projection_matrices(); let camera_position = current_camera.camera_position().to_homogeneous(); diff --git a/korangar/src/state/mod.rs b/korangar/src/state/mod.rs index 7c4820a43..34b18bd60 100644 --- a/korangar/src/state/mod.rs +++ b/korangar/src/state/mod.rs @@ -146,6 +146,9 @@ pub struct ClientState { /// Graphics capabilities used in the graphics settings window. graphics_settings_capabilities: GraphicsSettingsCapabilities, + /// View angle of the current camera. + camera_view_angle: f32, + /// The interface theme for the menu windows. menu_theme: InterfaceTheme, /// The interface theme for in-game windows. @@ -405,6 +408,9 @@ impl ClientState { interface_settings_capabilities, graphics_settings, graphics_settings_capabilities, + + camera_view_angle: 0.0, + menu_theme, in_game_theme, world_theme, diff --git a/korangar/src/world/cameras/debug.rs b/korangar/src/world/cameras/debug.rs index cb8195071..2ade64841 100644 --- a/korangar/src/world/cameras/debug.rs +++ b/korangar/src/world/cameras/debug.rs @@ -98,4 +98,8 @@ impl Camera for DebugCamera { fn view_direction(&self) -> Vector3 { self.orientation.rotate_vector(Vector3::unit_z()) } + + fn view_angle(&self) -> f32 { + 0.0 // Debug camera doesn't strictly have a tracked yaw + } } diff --git a/korangar/src/world/cameras/directional_shadow.rs b/korangar/src/world/cameras/directional_shadow.rs index 268b04f10..4ef60727d 100644 --- a/korangar/src/world/cameras/directional_shadow.rs +++ b/korangar/src/world/cameras/directional_shadow.rs @@ -403,4 +403,8 @@ impl Camera for PartitionCamera { fn view_direction(&self) -> Vector3 { self.view_direction } + + fn view_angle(&self) -> f32 { + 0.0 // Shadow cameras don't have a tracked yaw + } } diff --git a/korangar/src/world/cameras/mod.rs b/korangar/src/world/cameras/mod.rs index 478164066..263c5d8a9 100644 --- a/korangar/src/world/cameras/mod.rs +++ b/korangar/src/world/cameras/mod.rs @@ -40,6 +40,7 @@ pub trait Camera { fn view_direction(&self) -> Vector3; + fn view_angle(&self) -> f32; fn billboard_matrix(&self, position: Point3, origin: Point3, size: Vector2) -> Matrix4 { let view_direction = self.view_direction(); let right_vector = self.look_up_vector().cross(view_direction).normalize(); diff --git a/korangar/src/world/cameras/player.rs b/korangar/src/world/cameras/player.rs index 88fbd01ae..d11585c6d 100644 --- a/korangar/src/world/cameras/player.rs +++ b/korangar/src/world/cameras/player.rs @@ -72,6 +72,10 @@ impl PlayerCamera { rotation_velocity > ROTATION_SPEED_THRESHOLD || zoom_velocity > ZOOM_SPEED_THRESHOLD } + pub fn get_view_angle(&self) -> f32 { + self.view_angle.get_current() + } + pub fn update(&mut self, delta_time: f64) { self.focus_point.x.update(delta_time); self.focus_point.y.update(delta_time); @@ -80,6 +84,9 @@ impl PlayerCamera { self.view_angle.update(delta_time); let view_distance = self.camera_distance.get_current(); + // Since in Ragnarok / korangar math, map X is East-West and map Z is North-South, + // and we want North to be up on the minimap, we negate the yaw here so that + // a positive rotation turns the arrow clockwise to match the screen's visual rotation. let view_angle = self.view_angle.get_current(); let pitch_rotation = Quaternion::from_angle_x(CAMERA_PITCH); @@ -124,4 +131,8 @@ impl Camera for PlayerCamera { fn view_direction(&self) -> Vector3 { self.view_direction } + + fn view_angle(&self) -> f32 { + self.view_angle.get_current() + } } diff --git a/korangar/src/world/cameras/point_shadow.rs b/korangar/src/world/cameras/point_shadow.rs index 894244468..f32553ae1 100644 --- a/korangar/src/world/cameras/point_shadow.rs +++ b/korangar/src/world/cameras/point_shadow.rs @@ -73,4 +73,8 @@ impl Camera for PointShadowCamera { fn view_direction(&self) -> Vector3 { self.view_direction } + + fn view_angle(&self) -> f32 { + 0.0 // Shadow cameras don't have a tracked yaw + } } diff --git a/korangar/src/world/cameras/start.rs b/korangar/src/world/cameras/start.rs index 91b696639..1dc24cb75 100644 --- a/korangar/src/world/cameras/start.rs +++ b/korangar/src/world/cameras/start.rs @@ -82,4 +82,8 @@ impl Camera for StartCamera { fn view_direction(&self) -> Vector3 { self.view_direction } + + fn view_angle(&self) -> f32 { + self.view_angle + } } From cfb31849e582e2844018f6e7eb25d56b90e9c83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 22:26:57 +0800 Subject: [PATCH 08/13] Minimap: dynamically rotate player marker arrow with both 8-way walking direction and camera view angle --- korangar/src/interface/minimap/markers.rs | 3 +++ korangar/src/interface/minimap/render.rs | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs index 273cf45c6..0d4ae84c0 100644 --- a/korangar/src/interface/minimap/markers.rs +++ b/korangar/src/interface/minimap/markers.rs @@ -10,10 +10,12 @@ pub struct MinimapMarker { pub size: f32, pub font_size: f32, pub is_player: bool, + pub player_direction: Option, } pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entity>, base_size: f32) -> Vec { let player_entity_id = player_entity.map(Entity::get_entity_id); + let player_direction = player_entity.map(Entity::get_direction).unwrap_or(ragnarok_packets::Direction::South); entities .iter() @@ -36,6 +38,7 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit size, font_size, is_player, + player_direction: if is_player { Some(player_direction) } else { None }, }) }) .collect() diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index c4d6237ef..d4b235b81 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -103,9 +103,26 @@ where .for_each(|marker| { if marker.is_player { if let Some(arrow_texture) = &minimap.arrow_texture { - // view_angle is positive counter-clockwise, but in the SDF shader we - // need negative rotation to turn clockwise on screen. - let rotation = -view_angle; + // Map the 8-way walking direction to an angle in radians + let direction_angle = match marker.player_direction { + Some(ragnarok_packets::Direction::West) => 0.0, + Some(ragnarok_packets::Direction::NorthWest) => std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::North) => std::f32::consts::PI / 2.0, + Some(ragnarok_packets::Direction::NorthEast) => 3.0 * std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::East) => std::f32::consts::PI, + Some(ragnarok_packets::Direction::SouthEast) => 5.0 * std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::South) => 3.0 * std::f32::consts::PI / 2.0, + Some(ragnarok_packets::Direction::SouthWest) => 7.0 * std::f32::consts::PI / 4.0, + None => 0.0, + }; + + // Since the minimap renders top-down, we add the camera's view angle + // to the character's walking direction to ensure the arrow points relative to what you see. + let total_rotation = direction_angle - view_angle; + + // We negate the angle because the SDF shader needs negative rotation to turn clockwise on screen, + // matching the minimap's coordinate system. + let rotation = -total_rotation; let mut area = projection.marker_area(marker.tile_position, marker.size); // Make the arrow slightly larger to be visible From 66c9054a245d1f30fe4b880a708c8cb34e2376ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 22:35:30 +0800 Subject: [PATCH 09/13] Minimap: increase player arrow size and switch color to bright yellow for clarity --- korangar/src/interface/minimap/markers.rs | 4 ++-- korangar/src/interface/minimap/render.rs | 15 +++++++++------ korangar/src/world/cameras/player.rs | 4 ---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/korangar/src/interface/minimap/markers.rs b/korangar/src/interface/minimap/markers.rs index 0d4ae84c0..e1b6f4515 100644 --- a/korangar/src/interface/minimap/markers.rs +++ b/korangar/src/interface/minimap/markers.rs @@ -27,7 +27,7 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit return None; } - let size = if is_player { base_size + 2.0 } else { base_size }; + let size = if is_player { base_size * 1.5 } else { base_size }; // Make the player marker significantly larger let symbol = if is_player { "" } else { "●" }; let font_size = size + if is_player { 8.0 } else { 6.0 }; @@ -46,7 +46,7 @@ pub fn collect_minimap_markers(entities: &[Entity], player_entity: Option<&Entit fn marker_color(entity_type: EntityType, is_player: bool) -> Color { if is_player { - return Color::rgb_u8(80, 255, 160); + return Color::rgb_u8(255, 215, 0); // Base color (can be overridden in render) } match entity_type { diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index d4b235b81..1b0f06fc5 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -125,13 +125,16 @@ where let rotation = -total_rotation; let mut area = projection.marker_area(marker.tile_position, marker.size); - // Make the arrow slightly larger to be visible - area.width *= 2.0; - area.height *= 2.0; - area.left -= marker.size / 2.0; - area.top -= marker.size / 2.0; + // Make the arrow significantly larger and sharper to be clearly visible + area.width *= 2.5; + area.height *= 2.5; + area.left -= marker.size * 0.75; + area.top -= marker.size * 0.75; - layout.add_rotated_sdf(area, arrow_texture.clone(), marker.color, rotation); + // Use an explicit sharp color like bright yellow or cyan for the player marker instead of the generic green/white + let player_marker_color = Color::rgb_u8(255, 215, 0); // Bright Yellow + + layout.add_rotated_sdf(area, arrow_texture.clone(), player_marker_color, rotation); return; } } diff --git a/korangar/src/world/cameras/player.rs b/korangar/src/world/cameras/player.rs index d11585c6d..1cd6d31c6 100644 --- a/korangar/src/world/cameras/player.rs +++ b/korangar/src/world/cameras/player.rs @@ -72,10 +72,6 @@ impl PlayerCamera { rotation_velocity > ROTATION_SPEED_THRESHOLD || zoom_velocity > ZOOM_SPEED_THRESHOLD } - pub fn get_view_angle(&self) -> f32 { - self.view_angle.get_current() - } - pub fn update(&mut self, delta_time: f64) { self.focus_point.x.update(delta_time); self.focus_point.y.update(delta_time); From ed8e0a5ed99686c6008422ce61c33584064dd078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 22:39:21 +0800 Subject: [PATCH 10/13] Minimap: switch to arrow_right.png for clearer, sharper forward-pointing player indicator --- korangar/src/interface/minimap/generation.rs | 2 +- korangar/src/interface/minimap/render.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index a6a31b977..fee2c1db5 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -11,7 +11,7 @@ use crate::world::Map; pub fn create_minimap_state(texture_loader: &TextureLoader, previous: &MinimapState, map_name: &str, map: &Map) -> MinimapState { let texture = create_generated_minimap_texture(texture_loader, map_name, map); - let arrow_texture = texture_loader.get_or_load("arrow_left.png", ImageType::Sdf).ok(); + let arrow_texture = texture_loader.get_or_load("arrow_right.png", ImageType::Sdf).ok(); MinimapState { map_name: map_name.strip_suffix(".gat").unwrap_or(map_name).to_owned(), diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 1b0f06fc5..0101ca984 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -126,14 +126,16 @@ where let mut area = projection.marker_area(marker.tile_position, marker.size); // Make the arrow significantly larger and sharper to be clearly visible - area.width *= 2.5; - area.height *= 2.5; - area.left -= marker.size * 0.75; - area.top -= marker.size * 0.75; + area.width *= 2.0; + area.height *= 2.0; + area.left -= marker.size / 2.0; + area.top -= marker.size / 2.0; // Use an explicit sharp color like bright yellow or cyan for the player marker instead of the generic green/white let player_marker_color = Color::rgb_u8(255, 215, 0); // Bright Yellow + // The arrow_right.png points exactly East (0 radians), so we don't need + // an additional rotation offset like we might have needed with arrow_left.png. layout.add_rotated_sdf(area, arrow_texture.clone(), player_marker_color, rotation); return; } From fc698bb1e54f67f8e4fcc2d8b7cb2a8176053ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 22:42:09 +0800 Subject: [PATCH 11/13] Minimap: switch to marker_player.png for native, sharper player marker and adjust base rotation --- korangar/src/interface/minimap/generation.rs | 2 +- korangar/src/interface/minimap/render.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index fee2c1db5..8d45d03e3 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -11,7 +11,7 @@ use crate::world::Map; pub fn create_minimap_state(texture_loader: &TextureLoader, previous: &MinimapState, map_name: &str, map: &Map) -> MinimapState { let texture = create_generated_minimap_texture(texture_loader, map_name, map); - let arrow_texture = texture_loader.get_or_load("arrow_right.png", ImageType::Sdf).ok(); + let arrow_texture = texture_loader.get_or_load("marker_player.png", ImageType::Sdf).ok(); MinimapState { map_name: map_name.strip_suffix(".gat").unwrap_or(map_name).to_owned(), diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 0101ca984..090a7e6ef 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -122,7 +122,11 @@ where // We negate the angle because the SDF shader needs negative rotation to turn clockwise on screen, // matching the minimap's coordinate system. - let rotation = -total_rotation; + // The marker_player.png likely points North (up) by default, + // which corresponds to pi/2 radians offset compared to arrow_right.png (East, 0 rads). + // If it points North natively, we adjust the base angle so that walking East (0 rads) rotates it properly. + let base_texture_rotation = std::f32::consts::PI / 2.0; + let rotation = -(total_rotation - base_texture_rotation); let mut area = projection.marker_area(marker.tile_position, marker.size); // Make the arrow significantly larger and sharper to be clearly visible @@ -134,8 +138,6 @@ where // Use an explicit sharp color like bright yellow or cyan for the player marker instead of the generic green/white let player_marker_color = Color::rgb_u8(255, 215, 0); // Bright Yellow - // The arrow_right.png points exactly East (0 radians), so we don't need - // an additional rotation offset like we might have needed with arrow_left.png. layout.add_rotated_sdf(area, arrow_texture.clone(), player_marker_color, rotation); return; } From ff7a4b99cfa7e6f316bde07bc0a760fa3ba513a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Tue, 7 Apr 2026 23:48:56 +0800 Subject: [PATCH 12/13] Minimap: fix player arrow texture fallback to arrow_right.png, align rotation to point North, and sharpen shape --- korangar/src/interface/minimap/generation.rs | 2 +- korangar/src/interface/minimap/render.rs | 24 ++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/korangar/src/interface/minimap/generation.rs b/korangar/src/interface/minimap/generation.rs index 8d45d03e3..fee2c1db5 100644 --- a/korangar/src/interface/minimap/generation.rs +++ b/korangar/src/interface/minimap/generation.rs @@ -11,7 +11,7 @@ use crate::world::Map; pub fn create_minimap_state(texture_loader: &TextureLoader, previous: &MinimapState, map_name: &str, map: &Map) -> MinimapState { let texture = create_generated_minimap_texture(texture_loader, map_name, map); - let arrow_texture = texture_loader.get_or_load("marker_player.png", ImageType::Sdf).ok(); + let arrow_texture = texture_loader.get_or_load("arrow_right.png", ImageType::Sdf).ok(); MinimapState { map_name: map_name.strip_suffix(".gat").unwrap_or(map_name).to_owned(), diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 090a7e6ef..2266494ac 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -120,20 +120,24 @@ where // to the character's walking direction to ensure the arrow points relative to what you see. let total_rotation = direction_angle - view_angle; + // `arrow_right.png` points exactly East (positive X) on the screen. + // In our 2D screen coordinate system (X is right, Y is down), + // we want North (0 rads in the minimap logic) to point UP (negative Y). + // So we rotate by an additional -PI/2 to align "North" to "Up". + let base_texture_rotation = -std::f32::consts::PI / 2.0; + // We negate the angle because the SDF shader needs negative rotation to turn clockwise on screen, // matching the minimap's coordinate system. - // The marker_player.png likely points North (up) by default, - // which corresponds to pi/2 radians offset compared to arrow_right.png (East, 0 rads). - // If it points North natively, we adjust the base angle so that walking East (0 rads) rotates it properly. - let base_texture_rotation = std::f32::consts::PI / 2.0; - let rotation = -(total_rotation - base_texture_rotation); + // We also add PI (180 degrees) because the in-game direction enum maps "North" to looking DOWN + // from the camera's default perspective, but on the minimap we want North to point UP. + let rotation = -total_rotation + base_texture_rotation + std::f32::consts::PI; let mut area = projection.marker_area(marker.tile_position, marker.size); - // Make the arrow significantly larger and sharper to be clearly visible - area.width *= 2.0; - area.height *= 2.0; - area.left -= marker.size / 2.0; - area.top -= marker.size / 2.0; + // Make the arrow significantly larger but keep the base narrow so it forms a sharp pointer + area.width *= 2.5; // Length of the arrow + area.height *= 1.2; // Narrow base + area.left -= marker.size * 0.75; + area.top -= marker.size * 0.1; // Use an explicit sharp color like bright yellow or cyan for the player marker instead of the generic green/white let player_marker_color = Color::rgb_u8(255, 215, 0); // Bright Yellow From f75669cb812523d18f1094c588524c1358097c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cs3ptavios=E2=80=9D?= <“seksun.sheu@gmail.com”> Date: Thu, 9 Apr 2026 00:34:15 +0800 Subject: [PATCH 13/13] Minimap: fix player arrow rotation and SDF rendering --- korangar/src/interface/minimap/render.rs | 84 +++++++++++------------- korangar/src/renderer/game_interface.rs | 1 + 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/korangar/src/interface/minimap/render.rs b/korangar/src/interface/minimap/render.rs index 2266494ac..86411a2bc 100644 --- a/korangar/src/interface/minimap/render.rs +++ b/korangar/src/interface/minimap/render.rs @@ -10,7 +10,7 @@ use super::projection::MinimapProjection; use crate::graphics::{Color, CornerDiameter, ShadowPadding}; use crate::loaders::{FontSize, OverflowBehavior}; use crate::renderer::LayoutExt; -use crate::state::{this_entity, ClientState, MinimapState, ClientStatePathExt}; +use crate::state::{ClientState, MinimapState, this_entity}; use crate::world::Entity; pub struct MinimapLayoutInfo { @@ -84,7 +84,13 @@ where ); if let Some(texture) = &minimap.texture { - let projection = MinimapProjection::new(layout_info.map_area, minimap.width, minimap.height, minimap.zoom, player_position); + let projection = MinimapProjection::new( + layout_info.map_area, + minimap.width, + minimap.height, + minimap.zoom, + player_position, + ); layout.add_texture(projection.texture_area(), texture.clone(), Color::WHITE, false); @@ -92,57 +98,47 @@ where let player_path = this_entity(); let player_entity = state.try_get(&player_path); let marker_size = (projection.texture_area().width.min(projection.texture_area().height) / 40.0).clamp(4.0, 8.0); - - // To get camera info, we need a way to pass it here or just fallback to 0.0 rotation. - // Ideally, the camera should be accessible via state, but if not we can add it to ClientState - // For now, let's use the camera view angle from the layout or client state - let view_angle = *state.get(&crate::state::client_state().camera_view_angle()); collect_minimap_markers(entities, player_entity, marker_size) .into_iter() .for_each(|marker| { if marker.is_player { if let Some(arrow_texture) = &minimap.arrow_texture { - // Map the 8-way walking direction to an angle in radians let direction_angle = match marker.player_direction { - Some(ragnarok_packets::Direction::West) => 0.0, - Some(ragnarok_packets::Direction::NorthWest) => std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::East) => 0.0, + Some(ragnarok_packets::Direction::NorthEast) => std::f32::consts::PI / 4.0, Some(ragnarok_packets::Direction::North) => std::f32::consts::PI / 2.0, - Some(ragnarok_packets::Direction::NorthEast) => 3.0 * std::f32::consts::PI / 4.0, - Some(ragnarok_packets::Direction::East) => std::f32::consts::PI, - Some(ragnarok_packets::Direction::SouthEast) => 5.0 * std::f32::consts::PI / 4.0, - Some(ragnarok_packets::Direction::South) => 3.0 * std::f32::consts::PI / 2.0, - Some(ragnarok_packets::Direction::SouthWest) => 7.0 * std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::NorthWest) => 3.0 * std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::West) => std::f32::consts::PI, + Some(ragnarok_packets::Direction::SouthWest) => -3.0 * std::f32::consts::PI / 4.0, + Some(ragnarok_packets::Direction::South) => -std::f32::consts::PI / 2.0, + Some(ragnarok_packets::Direction::SouthEast) => -std::f32::consts::PI / 4.0, None => 0.0, }; + let rotation = direction_angle + std::f32::consts::PI; - // Since the minimap renders top-down, we add the camera's view angle - // to the character's walking direction to ensure the arrow points relative to what you see. - let total_rotation = direction_angle - view_angle; - - // `arrow_right.png` points exactly East (positive X) on the screen. - // In our 2D screen coordinate system (X is right, Y is down), - // we want North (0 rads in the minimap logic) to point UP (negative Y). - // So we rotate by an additional -PI/2 to align "North" to "Up". - let base_texture_rotation = -std::f32::consts::PI / 2.0; - - // We negate the angle because the SDF shader needs negative rotation to turn clockwise on screen, - // matching the minimap's coordinate system. - // We also add PI (180 degrees) because the in-game direction enum maps "North" to looking DOWN - // from the camera's default perspective, but on the minimap we want North to point UP. - let rotation = -total_rotation + base_texture_rotation + std::f32::consts::PI; - let mut area = projection.marker_area(marker.tile_position, marker.size); - // Make the arrow significantly larger but keep the base narrow so it forms a sharp pointer - area.width *= 2.5; // Length of the arrow - area.height *= 1.2; // Narrow base - area.left -= marker.size * 0.75; - area.top -= marker.size * 0.1; - - // Use an explicit sharp color like bright yellow or cyan for the player marker instead of the generic green/white - let player_marker_color = Color::rgb_u8(255, 215, 0); // Bright Yellow + area.width *= 1.6; + area.height *= 0.9; + area.left -= marker.size * 0.3; + area.top += marker.size * 0.05; + let player_marker_color = Color::rgb_u8(255, 215, 0); layout.add_rotated_sdf(area, arrow_texture.clone(), player_marker_color, rotation); + + let tip_offset = marker.size * 0.28; + let tip_width = area.width * 0.48; + let tip_height = area.height * 0.52; + let tip_center_x = area.left + area.width / 2.0 + rotation.cos() * tip_offset; + let tip_center_y = area.top + area.height / 2.0 - rotation.sin() * tip_offset; + let tip_area = Area { + left: tip_center_x - tip_width / 2.0, + top: tip_center_y - tip_height / 2.0, + width: tip_width, + height: tip_height, + }; + + layout.add_rotated_sdf(tip_area, arrow_texture.clone(), Color::rgba_u8(20, 20, 20, 235), rotation); return; } } @@ -153,10 +149,7 @@ where FontSize(marker.font_size), marker.color, Color::BLACK, - HorizontalAlignment::Center { - offset: 0.0, - border: 0.0, - }, + HorizontalAlignment::Center { offset: 0.0, border: 0.0 }, VerticalAlignment::Center { offset: 0.0 }, OverflowBehavior::Shrink, ); @@ -169,10 +162,7 @@ where FontSize(14.0), Color::monochrome_u8(225), Color::rgb_u8(255, 180, 80), - HorizontalAlignment::Center { - offset: 0.0, - border: 4.0, - }, + HorizontalAlignment::Center { offset: 0.0, border: 4.0 }, VerticalAlignment::Center { offset: 0.0 }, OverflowBehavior::Shrink, ); diff --git a/korangar/src/renderer/game_interface.rs b/korangar/src/renderer/game_interface.rs index 89c239d6b..4eb5e781c 100644 --- a/korangar/src/renderer/game_interface.rs +++ b/korangar/src/renderer/game_interface.rs @@ -353,6 +353,7 @@ impl MarkerRenderer for GameInterfaceRenderer { texture_position: Vector2::new(0.0, 0.0), texture_size: Vector2::new(1.0, 1.0), texture: texture.clone(), + rotation: 0.0, }); } }