Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions korangar/src/input/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions korangar/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
62 changes: 62 additions & 0 deletions korangar/src/interface/minimap/generation.rs
Original file line number Diff line number Diff line change
@@ -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<Texture> {
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 = 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 {
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
}
68 changes: 68 additions & 0 deletions korangar/src/interface/minimap/markers.rs
Original file line number Diff line number Diff line change
@@ -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<MinimapMarker> {
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),
}
}
7 changes: 7 additions & 0 deletions korangar/src/interface/minimap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod generation;
mod markers;
mod projection;
mod render;

pub use self::generation::create_minimap_state;
pub use self::render::MinimapView;
88 changes: 88 additions & 0 deletions korangar/src/interface/minimap/projection.rs
Original file line number Diff line number Diff line change
@@ -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<TilePosition>) -> 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)
}
}
129 changes: 129 additions & 0 deletions korangar/src/interface/minimap/render.rs
Original file line number Diff line number Diff line change
@@ -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<A, B> {
minimap_path: A,
entities_path: B,
status_text: String,
}

impl<A, B> MinimapView<A, B> {
pub fn new(minimap_path: A, entities_path: B) -> Self {
Self {
minimap_path,
entities_path,
status_text: String::new(),
}
}
}

impl<A, B> Element<ClientState> for MinimapView<A, B>
where
A: Path<ClientState, MinimapState>,
B: Path<ClientState, Vec<Entity>>,
{
type LayoutInfo = MinimapLayoutInfo;

fn create_layout_info(
&mut self,
state: &State<ClientState>,
_: ElementStoreMut,
resolvers: &mut dyn Resolvers<ClientState>,
) -> 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<ClientState>,
_: 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,
);
}
}
1 change: 1 addition & 0 deletions korangar/src/interface/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod components;
pub mod cursor;
pub mod minimap;
pub mod resource;
pub mod windows;
4 changes: 4 additions & 0 deletions korangar/src/interface/windows/character_overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading