major refactor/rework for better code organization
This commit is contained in:
+591
@@ -0,0 +1,591 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use crate::exporter;
|
||||
use crate::interact::{AddCorridorDrag, DragState, HoverMarker, ResizeState, draw_grid};
|
||||
use crate::layout::{
|
||||
self, DoorSettings, DungeonLayout, WindowSettings, populate_random_markers, populate_stairs,
|
||||
};
|
||||
use crate::rendering::draw_layout;
|
||||
use crate::saveandload;
|
||||
use crate::settings;
|
||||
use crate::ui::{AddTool, UiSettings, draw_legend_panel, draw_level_tabs, draw_side_panel};
|
||||
use eframe::egui;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ManualDoorKind {
|
||||
Archway,
|
||||
Regular,
|
||||
Locked,
|
||||
Secret,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AppSnapshot {
|
||||
pub settings: UiSettings,
|
||||
pub levels: Vec<DungeonLayout>,
|
||||
pub suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||
}
|
||||
|
||||
pub struct DungeonApp {
|
||||
pub settings: UiSettings,
|
||||
pub levels: Vec<DungeonLayout>,
|
||||
pub suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||
pub undo_stack: Vec<AppSnapshot>,
|
||||
pub redo_stack: Vec<AppSnapshot>,
|
||||
pub drag_state: Option<DragState>,
|
||||
pub export_rx: Option<mpsc::Receiver<exporter::ExportEvent>>,
|
||||
pub export_progress: Option<exporter::ExportProgress>,
|
||||
pub pending_delete: bool,
|
||||
pub add_corridor_drag: Option<AddCorridorDrag>,
|
||||
pub resize_state: Option<ResizeState>,
|
||||
pub hover_room_idx: Option<usize>,
|
||||
pub hover_corridor_idx: Option<usize>,
|
||||
pub hover_door_idx: Option<usize>,
|
||||
pub hover_text_idx: Option<usize>,
|
||||
pub hover_marker: Option<HoverMarker>,
|
||||
pub hover_level_idx: Option<usize>,
|
||||
pub hover_stair_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for DungeonApp {
|
||||
fn default() -> Self {
|
||||
let settings = settings::load_settings().unwrap_or_default();
|
||||
let mut app = Self {
|
||||
settings,
|
||||
levels: Vec::new(),
|
||||
suppressed_auto_door_edges: HashSet::new(),
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
drag_state: None,
|
||||
export_rx: None,
|
||||
export_progress: None,
|
||||
pending_delete: false,
|
||||
add_corridor_drag: None,
|
||||
resize_state: None,
|
||||
hover_room_idx: None,
|
||||
hover_corridor_idx: None,
|
||||
hover_door_idx: None,
|
||||
hover_text_idx: None,
|
||||
hover_marker: None,
|
||||
hover_level_idx: None,
|
||||
hover_stair_idx: None,
|
||||
};
|
||||
if app.settings.composition_mode {
|
||||
app.enter_composition_layout();
|
||||
} else {
|
||||
app.levels = generate_all_levels(&app.settings);
|
||||
app.refresh_stairs();
|
||||
}
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DungeonApp {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = settings::save_settings(&self.settings) {
|
||||
eprintln!("Failed to save settings: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for DungeonApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let panel_result = draw_side_panel(
|
||||
ctx,
|
||||
&mut self.settings,
|
||||
self.export_progress
|
||||
.map(|progress| (progress.completed, progress.total)),
|
||||
self.levels.len(),
|
||||
);
|
||||
let undo_requested = ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Z));
|
||||
let redo_requested = ctx.input(|i| {
|
||||
(i.modifiers.command && i.key_pressed(egui::Key::Y))
|
||||
|| (i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::Z))
|
||||
});
|
||||
let escape_pressed = ctx.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
|
||||
if escape_pressed || panel_result.settings_changed {
|
||||
self.settings.add_tool = AddTool::None;
|
||||
}
|
||||
|
||||
clamp_dependent_settings(&mut self.settings);
|
||||
|
||||
if undo_requested {
|
||||
self.undo();
|
||||
} else if redo_requested {
|
||||
self.redo();
|
||||
}
|
||||
|
||||
if panel_result.clear_clicked {
|
||||
self.push_undo_snapshot();
|
||||
self.clear_layout();
|
||||
} else if panel_result.reset_clicked {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.composition_mode {
|
||||
self.enter_composition_layout();
|
||||
} else {
|
||||
self.regenerate_layout();
|
||||
}
|
||||
} else if panel_result.settings_changed && !self.settings.composition_mode {
|
||||
self.push_undo_snapshot();
|
||||
self.regenerate_layout();
|
||||
} else if panel_result.save_clicked {
|
||||
let mut layouts = Vec::new();
|
||||
let mut svgs = Vec::new();
|
||||
for layout in &self.levels {
|
||||
layouts.push(layout.clone());
|
||||
svgs.push(exporter::build_svg(layout, &self.settings));
|
||||
}
|
||||
if let Err(err) = saveandload::save_dungeon(&self.settings, layouts, svgs) {
|
||||
eprintln!("{err}");
|
||||
}
|
||||
} else if panel_result.load_clicked {
|
||||
match saveandload::load_dungeon() {
|
||||
Ok(data) => {
|
||||
self.push_undo_snapshot();
|
||||
self.settings = data.settings;
|
||||
self.levels = data.layouts;
|
||||
self.settings.active_level_index = 0;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
}
|
||||
}
|
||||
} else if panel_result.new_level_clicked {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.composition_mode {
|
||||
self.levels.push(DungeonLayout::empty(
|
||||
self.settings.pack_rooms_without_corridors,
|
||||
));
|
||||
} else {
|
||||
self.levels
|
||||
.push(generate_level(&self.settings, self.levels.len()));
|
||||
self.refresh_stairs();
|
||||
}
|
||||
self.settings.active_level_index = self.levels.len() - 1;
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
|
||||
self.pending_delete = true;
|
||||
}
|
||||
if panel_result.export_clicked && self.export_rx.is_none() {
|
||||
let target = match exporter::select_export_target(&self.settings) {
|
||||
Ok(target) => target,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match &target {
|
||||
exporter::ExportTarget::File(path) => {
|
||||
if let Some(parent) = path.parent() {
|
||||
self.settings.last_export_path = Some(parent.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
exporter::ExportTarget::Folder(path) => {
|
||||
self.settings.last_export_path = Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let layouts = self.levels.clone();
|
||||
let settings = self.settings.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
let result = exporter::export_to_target(&layouts, &settings, target, &tx);
|
||||
let _ = tx.send(exporter::ExportEvent::Finished(result));
|
||||
});
|
||||
self.export_progress = Some(exporter::ExportProgress {
|
||||
completed: 0,
|
||||
total: 1,
|
||||
});
|
||||
self.export_rx = Some(rx);
|
||||
}
|
||||
|
||||
let mut export_finished = false;
|
||||
if let Some(rx) = &self.export_rx {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(exporter::ExportEvent::Progress(progress)) => {
|
||||
self.export_progress = Some(progress);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Ok(exporter::ExportEvent::Finished(Ok(_))) => {
|
||||
export_finished = true;
|
||||
break;
|
||||
}
|
||||
Ok(exporter::ExportEvent::Finished(Err(err))) => {
|
||||
eprintln!("{err}");
|
||||
export_finished = true;
|
||||
break;
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => break,
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
export_finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if export_finished {
|
||||
self.export_rx = None;
|
||||
self.export_progress = None;
|
||||
}
|
||||
|
||||
if draw_legend_panel(ctx, &mut self.settings) {
|
||||
self.push_undo_snapshot();
|
||||
if self.settings.composition_mode {
|
||||
self.enter_composition_layout();
|
||||
} else {
|
||||
self.regenerate_layout();
|
||||
}
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.hover_level_idx =
|
||||
draw_level_tabs(ui, self.levels.len(), &mut self.settings.active_level_index);
|
||||
ui.separator();
|
||||
|
||||
let available = ui.available_size_before_wrap();
|
||||
let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag());
|
||||
let canvas = response.rect.shrink(12.0);
|
||||
let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows);
|
||||
crate::interact::update_hover_targets(self, ctx, &geometry);
|
||||
|
||||
let level_delete_requested = ctx.input(|i| {
|
||||
(i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
|
||||
&& self.hover_level_idx.is_some()
|
||||
});
|
||||
if level_delete_requested {
|
||||
if let Some(level_idx) = self.hover_level_idx {
|
||||
self.delete_level(level_idx);
|
||||
}
|
||||
}
|
||||
|
||||
let resizing = crate::interact::handle_resize(self, ctx, &response, &geometry);
|
||||
if !resizing {
|
||||
crate::interact::handle_drag(self, ctx, &response, &geometry);
|
||||
}
|
||||
crate::interact::handle_add_tool(self, &response, &geometry);
|
||||
if self.pending_delete {
|
||||
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
||||
crate::interact::delete_at_pointer(self, pointer_pos, &geometry);
|
||||
}
|
||||
self.pending_delete = false;
|
||||
}
|
||||
|
||||
if self.settings.active_level_index < self.levels.len() {
|
||||
draw_layout(
|
||||
&painter,
|
||||
&geometry,
|
||||
&self.levels[self.settings.active_level_index],
|
||||
self.settings.colorblind_mode,
|
||||
self.hover_text_idx,
|
||||
self.hover_marker,
|
||||
self.hover_stair_idx,
|
||||
);
|
||||
}
|
||||
crate::interact::draw_add_overlay(self, &painter, &geometry);
|
||||
crate::interact::draw_add_tool_ghost(self, &painter, &geometry);
|
||||
crate::interact::draw_resize_overlay(self, &painter, &geometry);
|
||||
crate::interact::draw_corridor_hover_overlay(self, &painter, &geometry);
|
||||
crate::interact::draw_door_hover_overlay(self, &painter, &geometry);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_all_levels(settings: &UiSettings) -> Vec<DungeonLayout> {
|
||||
let level_count = if settings.min_levels == settings.max_levels {
|
||||
settings.min_levels
|
||||
} else {
|
||||
let range_seed = crate::seed::derive_seed(settings.seed, 0x1E_7E_1_u64);
|
||||
(range_seed as usize % (settings.max_levels - settings.min_levels + 1))
|
||||
+ settings.min_levels
|
||||
};
|
||||
|
||||
let mut levels = Vec::with_capacity(level_count);
|
||||
for i in 0..level_count {
|
||||
levels.push(generate_level(settings, i));
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
pub fn generate_level(settings: &UiSettings, level_index: usize) -> DungeonLayout {
|
||||
let level_seed = crate::seed::derive_seed(settings.seed, level_index as u64);
|
||||
let layout = layout::generate_layout(
|
||||
settings.cols,
|
||||
settings.rows,
|
||||
settings.room_count,
|
||||
level_seed,
|
||||
settings.min_room_size,
|
||||
settings.max_room_size,
|
||||
settings.square_rooms_only,
|
||||
settings.min_corridor_width,
|
||||
settings.max_corridor_width,
|
||||
settings.corridor_randomness,
|
||||
settings.dead_end_rooms_percent,
|
||||
settings.pack_rooms_without_corridors,
|
||||
door_settings_from_ui(settings),
|
||||
window_settings_from_ui(settings),
|
||||
);
|
||||
|
||||
populate_random_markers(layout, settings)
|
||||
}
|
||||
|
||||
pub fn clamp_dependent_settings(settings: &mut UiSettings) {
|
||||
if settings.min_room_size > settings.max_room_size {
|
||||
settings.max_room_size = settings.min_room_size;
|
||||
}
|
||||
if settings.min_corridor_width == 0 {
|
||||
settings.min_corridor_width = 1;
|
||||
}
|
||||
if settings.min_corridor_width > settings.max_corridor_width {
|
||||
settings.max_corridor_width = settings.min_corridor_width;
|
||||
}
|
||||
if settings.min_window_width > settings.max_window_width {
|
||||
settings.max_window_width = settings.min_window_width;
|
||||
}
|
||||
if settings.min_start_marker_size > settings.max_start_marker_size {
|
||||
settings.max_start_marker_size = settings.min_start_marker_size;
|
||||
}
|
||||
if settings.min_end_marker_size > settings.max_end_marker_size {
|
||||
settings.max_end_marker_size = settings.min_end_marker_size;
|
||||
}
|
||||
if settings.min_start_marker_count > settings.max_start_marker_count {
|
||||
settings.max_start_marker_count = settings.min_start_marker_count;
|
||||
}
|
||||
if settings.min_end_marker_count > settings.max_end_marker_count {
|
||||
settings.max_end_marker_count = settings.min_end_marker_count;
|
||||
}
|
||||
if settings.min_traps_per_area > settings.max_traps_per_area {
|
||||
settings.max_traps_per_area = settings.min_traps_per_area;
|
||||
}
|
||||
if settings.min_monsters_per_area > settings.max_monsters_per_area {
|
||||
settings.max_monsters_per_area = settings.min_monsters_per_area;
|
||||
}
|
||||
if settings.min_levels == 0 {
|
||||
settings.min_levels = 1;
|
||||
}
|
||||
if settings.min_levels > settings.max_levels {
|
||||
settings.max_levels = settings.min_levels;
|
||||
}
|
||||
}
|
||||
|
||||
impl DungeonApp {
|
||||
pub fn clear_hover_targets(&mut self) {
|
||||
self.hover_room_idx = None;
|
||||
self.hover_corridor_idx = None;
|
||||
self.hover_door_idx = None;
|
||||
self.hover_text_idx = None;
|
||||
self.hover_marker = None;
|
||||
self.hover_level_idx = None;
|
||||
self.hover_stair_idx = None;
|
||||
}
|
||||
|
||||
pub fn reset_transient_state(&mut self) {
|
||||
self.drag_state = None;
|
||||
self.add_corridor_drag = None;
|
||||
self.resize_state = None;
|
||||
self.clear_hover_targets();
|
||||
self.pending_delete = false;
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> AppSnapshot {
|
||||
AppSnapshot {
|
||||
settings: self.settings.clone(),
|
||||
levels: self.levels.clone(),
|
||||
suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_undo_snapshot(&mut self) {
|
||||
let snapshot = self.snapshot();
|
||||
if self.undo_stack.last() == Some(&snapshot) {
|
||||
return;
|
||||
}
|
||||
self.undo_stack.push(snapshot);
|
||||
if self.undo_stack.len() > 100 {
|
||||
self.undo_stack.remove(0);
|
||||
}
|
||||
self.redo_stack.clear();
|
||||
}
|
||||
|
||||
pub fn restore_snapshot(&mut self, snapshot: AppSnapshot) {
|
||||
self.settings = snapshot.settings;
|
||||
self.levels = snapshot.levels;
|
||||
self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges;
|
||||
self.reset_transient_state();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) {
|
||||
let Some(snapshot) = self.undo_stack.pop() else {
|
||||
return;
|
||||
};
|
||||
self.redo_stack.push(self.snapshot());
|
||||
self.restore_snapshot(snapshot);
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
let Some(snapshot) = self.redo_stack.pop() else {
|
||||
return;
|
||||
};
|
||||
self.undo_stack.push(self.snapshot());
|
||||
self.restore_snapshot(snapshot);
|
||||
}
|
||||
|
||||
pub fn clear_layout(&mut self) {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
self.levels[self.settings.active_level_index] =
|
||||
DungeonLayout::empty(self.settings.pack_rooms_without_corridors);
|
||||
self.suppressed_auto_door_edges.clear();
|
||||
self.reset_transient_state();
|
||||
}
|
||||
|
||||
pub fn enter_composition_layout(&mut self) {
|
||||
self.levels = vec![DungeonLayout::empty(
|
||||
self.settings.pack_rooms_without_corridors,
|
||||
)];
|
||||
self.settings.active_level_index = 0;
|
||||
self.settings.export_level_index = 0;
|
||||
self.suppressed_auto_door_edges.clear();
|
||||
self.reset_transient_state();
|
||||
}
|
||||
|
||||
pub fn regenerate_layout(&mut self) {
|
||||
self.drag_state = None;
|
||||
self.suppressed_auto_door_edges.clear();
|
||||
self.levels = generate_all_levels(&self.settings);
|
||||
self.refresh_stairs();
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
self.settings.active_level_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_level(&mut self, level_idx: usize) {
|
||||
if self.levels.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
if level_idx >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot();
|
||||
self.levels.remove(level_idx);
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
self.settings.active_level_index = self.levels.len() - 1;
|
||||
}
|
||||
self.reset_transient_state();
|
||||
}
|
||||
|
||||
pub fn refresh_level(&mut self, level_idx: usize) {
|
||||
if level_idx >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let layout = &mut self.levels[level_idx];
|
||||
if !layout.packed_rooms {
|
||||
let rooms = layout.rooms.clone();
|
||||
for corridor in &mut layout.corridors {
|
||||
let start = rooms[corridor.start_room_id].center_cell();
|
||||
let end = rooms[corridor.end_room_id].center_cell();
|
||||
let blocked = layout::blocked_room_cells(
|
||||
&rooms,
|
||||
&[corridor.start_room_id, corridor.end_room_id],
|
||||
);
|
||||
if let Some(path) = layout::shortest_path_cells(
|
||||
start,
|
||||
end,
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
&blocked,
|
||||
) {
|
||||
corridor.path = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let door_settings = door_settings_from_ui(&self.settings);
|
||||
layout::apply_doors(
|
||||
layout,
|
||||
self.settings.seed,
|
||||
door_settings,
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
layout.doors.retain(|door| {
|
||||
!crate::interact::door_edges_for(door, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
|
||||
});
|
||||
|
||||
let window_settings = window_settings_from_ui(&self.settings);
|
||||
layout::apply_windows(
|
||||
layout,
|
||||
self.settings.seed,
|
||||
window_settings,
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn refresh_stairs(&mut self) {
|
||||
let results = populate_stairs(self.levels.clone(), &self.settings);
|
||||
self.levels = results.iter().map(|(layout, _)| layout.clone()).collect();
|
||||
|
||||
for (level_idx, (_, modified)) in results.into_iter().enumerate() {
|
||||
if modified {
|
||||
self.refresh_level(level_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_doors(&mut self) {
|
||||
if self.settings.active_level_index >= self.levels.len() {
|
||||
return;
|
||||
}
|
||||
let layout = &mut self.levels[self.settings.active_level_index];
|
||||
|
||||
layout::apply_doors(
|
||||
layout,
|
||||
self.settings.seed,
|
||||
door_settings_from_ui(&self.settings),
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
layout.doors.retain(|door| {
|
||||
!crate::interact::door_edges_for(door, self.settings.cols, self.settings.rows)
|
||||
.iter()
|
||||
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
|
||||
});
|
||||
layout::apply_windows(
|
||||
layout,
|
||||
self.settings.seed,
|
||||
window_settings_from_ui(&self.settings),
|
||||
self.settings.cols,
|
||||
self.settings.rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn door_settings_from_ui(settings: &UiSettings) -> DoorSettings {
|
||||
DoorSettings {
|
||||
frequency_percent: settings.door_frequency_percent,
|
||||
room_hallway_percent: settings.room_hallway_door_percent,
|
||||
locked_percent: settings.locked_door_percent,
|
||||
secret_percent: settings.secret_door_percent,
|
||||
allow_middle_corridor_doors: settings.allow_middle_corridor_doors,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_settings_from_ui(settings: &UiSettings) -> WindowSettings {
|
||||
WindowSettings {
|
||||
enabled: settings.windows_enabled,
|
||||
min_width: settings.min_window_width,
|
||||
max_width: settings.max_window_width,
|
||||
frequency_percent: settings.window_frequency_percent,
|
||||
room_hallway_percent: settings.room_hallway_window_percent,
|
||||
allow_internal_windows: settings.allow_internal_windows,
|
||||
}
|
||||
}
|
||||
+2378
File diff suppressed because it is too large
Load Diff
@@ -1,177 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
use super::types::{
|
||||
AreaMarker, Corridor, Door, DoorSettings, DungeonLayout, Room, Staircase, Window,
|
||||
WindowSettings, WindowSide,
|
||||
};
|
||||
use super::utils::{
|
||||
SimpleRng, blocked_room_cells, corridor_cells, manhattan_distance, noisy_path,
|
||||
normalized_cell_edge, overlaps_with_padding, room_index_at_cell, rooms_overlap, rooms_touch,
|
||||
shared_boundary_edges, shared_opening_width, shortest_path_cells, shuffle_indices,
|
||||
};
|
||||
use crate::seed;
|
||||
use crate::ui::UiSettings;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Room {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
// Return the center cell of the room.
|
||||
pub fn center_cell(&self) -> (usize, usize) {
|
||||
(self.x + (self.width / 2), self.y + (self.height / 2))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Corridor {
|
||||
#[allow(dead_code)]
|
||||
pub id: u64,
|
||||
pub start_room_id: usize,
|
||||
pub end_room_id: usize,
|
||||
pub path: Vec<(usize, usize)>,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Door {
|
||||
pub from: (usize, usize),
|
||||
pub to: (usize, usize),
|
||||
pub width: usize,
|
||||
pub span_width: bool,
|
||||
pub locked: bool,
|
||||
pub archway: bool,
|
||||
pub secret: bool,
|
||||
pub manual: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Window {
|
||||
pub cell: (usize, usize),
|
||||
pub side: WindowSide,
|
||||
pub width: usize,
|
||||
pub span_width: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TextLabel {
|
||||
pub cell: (usize, usize),
|
||||
pub text: String,
|
||||
pub font_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AreaMarker {
|
||||
pub cell: (usize, usize),
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Staircase {
|
||||
pub cell: (usize, usize),
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WindowSide {
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct DoorSettings {
|
||||
pub frequency_percent: usize,
|
||||
pub room_hallway_percent: usize,
|
||||
pub locked_percent: usize,
|
||||
pub secret_percent: usize,
|
||||
pub allow_middle_corridor_doors: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct WindowSettings {
|
||||
pub enabled: bool,
|
||||
pub min_width: usize,
|
||||
pub max_width: usize,
|
||||
pub frequency_percent: usize,
|
||||
pub room_hallway_percent: usize,
|
||||
pub allow_internal_windows: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DungeonLayout {
|
||||
pub rooms: Vec<Room>,
|
||||
pub corridors: Vec<Corridor>,
|
||||
pub doors: Vec<Door>,
|
||||
pub windows: Vec<Window>,
|
||||
pub text_labels: Vec<TextLabel>,
|
||||
pub start_markers: Vec<AreaMarker>,
|
||||
pub end_markers: Vec<AreaMarker>,
|
||||
pub trap_markers: Vec<AreaMarker>,
|
||||
pub monster_markers: Vec<AreaMarker>,
|
||||
pub packed_rooms: bool,
|
||||
pub stairs: Vec<Staircase>,
|
||||
}
|
||||
|
||||
impl Default for DungeonLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rooms: Vec::new(),
|
||||
corridors: Vec::new(),
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
start_markers: Vec::new(),
|
||||
end_markers: Vec::new(),
|
||||
trap_markers: Vec::new(),
|
||||
monster_markers: Vec::new(),
|
||||
packed_rooms: false,
|
||||
stairs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DungeonLayout {
|
||||
// Build an empty layout for the current packing mode.
|
||||
pub(crate) fn empty(packed_rooms: bool) -> Self {
|
||||
Self {
|
||||
packed_rooms,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Build a layout from generated rooms and corridors.
|
||||
fn from_generated_parts(
|
||||
rooms: Vec<Room>,
|
||||
corridors: Vec<Corridor>,
|
||||
packed_rooms: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
rooms,
|
||||
corridors,
|
||||
packed_rooms,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all room cells except those belonging to excluded room ids.
|
||||
pub fn blocked_room_cells(rooms: &[Room], excluded_room_ids: &[usize]) -> HashSet<(usize, usize)> {
|
||||
let excluded: HashSet<usize> = excluded_room_ids.iter().copied().collect();
|
||||
let mut blocked = HashSet::new();
|
||||
|
||||
for (room_idx, room) in rooms.iter().enumerate() {
|
||||
if excluded.contains(&room_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
blocked.insert((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blocked
|
||||
}
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
// Build a layout based on settings and a derived deterministic seed.
|
||||
pub fn generate_layout(
|
||||
@@ -206,7 +44,7 @@ pub fn generate_layout(
|
||||
let mut corridor_rng = SimpleRng::new(seed::derive_seed(base_seed, 4));
|
||||
|
||||
let mut rooms = Vec::new();
|
||||
let mut corridors = Vec::new();
|
||||
let corridors = Vec::new();
|
||||
|
||||
if cols < 2 || rows < 2 || target_room_count == 0 {
|
||||
return DungeonLayout::empty(pack_rooms_without_corridors);
|
||||
@@ -308,6 +146,7 @@ pub fn generate_layout(
|
||||
let room_edges =
|
||||
build_room_connection_edges(¢ers, randomness, target_dead_end_rooms, &mut graph_rng);
|
||||
|
||||
let mut corridors = Vec::new();
|
||||
let mut next_corridor_id = 1u64;
|
||||
for (start_room_id, end_room_id) in room_edges {
|
||||
let start = centers[start_room_id];
|
||||
@@ -930,163 +769,6 @@ fn shared_room_boundaries(
|
||||
boundaries
|
||||
}
|
||||
|
||||
fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usize))> {
|
||||
let mut edges = Vec::new();
|
||||
|
||||
if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||
let left = if a.x < b.x { a } else { b };
|
||||
let right = if a.x < b.x { b } else { a };
|
||||
let y0 = left.y.max(right.y);
|
||||
let y1 = (left.y + left.height).min(right.y + right.height);
|
||||
for y in y0..y1 {
|
||||
edges.push(normalized_cell_edge(
|
||||
(left.x + left.width - 1, y),
|
||||
(right.x, y),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if a.y + a.height == b.y || b.y + b.height == a.y {
|
||||
let top = if a.y < b.y { a } else { b };
|
||||
let bottom = if a.y < b.y { b } else { a };
|
||||
let x0 = top.x.max(bottom.x);
|
||||
let x1 = (top.x + top.width).min(bottom.x + bottom.width);
|
||||
for x in x0..x1 {
|
||||
edges.push(normalized_cell_edge(
|
||||
(x, top.y + top.height - 1),
|
||||
(x, bottom.y),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
pub fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option<usize> {
|
||||
rooms.iter().position(|room| {
|
||||
cell.0 >= room.x
|
||||
&& cell.0 < room.x + room.width
|
||||
&& cell.1 >= room.y
|
||||
&& cell.1 < room.y + room.height
|
||||
})
|
||||
}
|
||||
|
||||
// Compute corridor cells while excluding room cells.
|
||||
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
||||
let mut cells = HashSet::new();
|
||||
if cols == 0 || rows == 0 {
|
||||
return cells;
|
||||
}
|
||||
|
||||
let mut room_cells = HashSet::new();
|
||||
for room in &layout.rooms {
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
room_cells.insert((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for corridor in &layout.corridors {
|
||||
let width = corridor.width.max(1);
|
||||
let min_offset = -((width as isize - 1) / 2);
|
||||
let max_offset = width as isize / 2;
|
||||
if corridor.path.len() == 1 {
|
||||
let (x, y) = corridor.path[0];
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = y as isize + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (x, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for pair in corridor.path.windows(2) {
|
||||
let a = pair[0];
|
||||
let b = pair[1];
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
|
||||
if a.0 != b.0 {
|
||||
let x0 = a.0.min(b.0);
|
||||
let x1 = a.0.max(b.0);
|
||||
let y = a.1 as isize;
|
||||
for x in x0..=x1 {
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = y + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (x, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let y0 = a.1.min(b.1);
|
||||
let y1 = a.1.max(b.1);
|
||||
let x = a.0 as isize;
|
||||
for y in y0..=y1 {
|
||||
for dx in min_offset..=max_offset {
|
||||
let nx = x + dx;
|
||||
if nx < 0 || nx >= cols as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (nx as usize, y);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for turn in corridor.path.windows(3) {
|
||||
let prev = turn[0];
|
||||
let corner = turn[1];
|
||||
let next = turn[2];
|
||||
let incoming = (
|
||||
corner.0 as isize - prev.0 as isize,
|
||||
corner.1 as isize - prev.1 as isize,
|
||||
);
|
||||
let outgoing = (
|
||||
next.0 as isize - corner.0 as isize,
|
||||
next.1 as isize - corner.1 as isize,
|
||||
);
|
||||
if incoming == outgoing {
|
||||
continue;
|
||||
}
|
||||
|
||||
for dx in min_offset..=max_offset {
|
||||
let nx = corner.0 as isize + dx;
|
||||
if nx < 0 || nx >= cols as isize {
|
||||
continue;
|
||||
}
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = corner.1 as isize + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = (nx as usize, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells
|
||||
}
|
||||
|
||||
const START_COUNT_STREAM: u64 = 10_001;
|
||||
const END_COUNT_STREAM: u64 = 10_002;
|
||||
const START_ROOM_STREAM_BASE: u64 = 11_000;
|
||||
@@ -1982,41 +1664,6 @@ fn shuffle_rooms(rooms: &mut [Room], rng: &mut SimpleRng) {
|
||||
}
|
||||
}
|
||||
|
||||
fn rects_overlap(
|
||||
ax: usize,
|
||||
ay: usize,
|
||||
aw: usize,
|
||||
ah: usize,
|
||||
bx: usize,
|
||||
by: usize,
|
||||
bw: usize,
|
||||
bh: usize,
|
||||
) -> bool {
|
||||
let a_right = ax + aw;
|
||||
let a_bottom = ay + ah;
|
||||
let b_right = bx + bw;
|
||||
let b_bottom = by + bh;
|
||||
|
||||
ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
|
||||
}
|
||||
|
||||
fn rooms_overlap(a: &Room, b: &Room) -> bool {
|
||||
rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
|
||||
}
|
||||
|
||||
fn rooms_touch(a: &Room, b: &Room) -> bool {
|
||||
!shared_boundary_edges(a, b).is_empty()
|
||||
}
|
||||
|
||||
fn shared_opening_width(a: &Room, b: &Room, span: usize, default_width: usize) -> usize {
|
||||
let max_width = if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||
a.height.min(b.height)
|
||||
} else {
|
||||
a.width.min(b.width)
|
||||
};
|
||||
default_width.max(1).min(span).min(max_width.max(1))
|
||||
}
|
||||
|
||||
// Create a connected graph of room-to-room edges with optional dead ends.
|
||||
fn build_room_connection_edges(
|
||||
centers: &[(usize, usize)],
|
||||
@@ -2127,253 +1774,3 @@ fn push_unique_room_edge(
|
||||
edges.push((a, b));
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle indices in place using the provided RNG.
|
||||
fn shuffle_indices(indices: &mut [usize], rng: &mut SimpleRng) {
|
||||
if indices.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
for i in (1..indices.len()).rev() {
|
||||
let j = rng.range_inclusive(0, i);
|
||||
indices.swap(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the shortest grid path between two cells using BFS.
|
||||
pub fn shortest_path_cells(
|
||||
start: (usize, usize),
|
||||
end: (usize, usize),
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
blocked: &HashSet<(usize, usize)>,
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
if start == end {
|
||||
return Some(vec![start]);
|
||||
}
|
||||
if blocked.contains(&start) || blocked.contains(&end) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let total = cols.saturating_mul(rows);
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let index = |p: (usize, usize)| -> usize { p.1 * cols + p.0 };
|
||||
let coord = |idx: usize| -> (usize, usize) { (idx % cols, idx / cols) };
|
||||
|
||||
let start_idx = index(start);
|
||||
let end_idx = index(end);
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
let mut visited = vec![false; total];
|
||||
let mut parent: Vec<Option<usize>> = vec![None; total];
|
||||
|
||||
visited[start_idx] = true;
|
||||
queue.push_back(start_idx);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
if current == end_idx {
|
||||
break;
|
||||
}
|
||||
|
||||
let (x, y) = coord(current);
|
||||
let neighbors = [
|
||||
x.checked_sub(1).map(|nx| (nx, y)),
|
||||
(x + 1 < cols).then_some((x + 1, y)),
|
||||
y.checked_sub(1).map(|ny| (x, ny)),
|
||||
(y + 1 < rows).then_some((x, y + 1)),
|
||||
];
|
||||
|
||||
for neighbor in neighbors.into_iter().flatten() {
|
||||
if blocked.contains(&neighbor) {
|
||||
continue;
|
||||
}
|
||||
let n_idx = index(neighbor);
|
||||
if !visited[n_idx] {
|
||||
visited[n_idx] = true;
|
||||
parent[n_idx] = Some(current);
|
||||
queue.push_back(n_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !visited[end_idx] {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path = Vec::new();
|
||||
let mut current = end_idx;
|
||||
path.push(coord(current));
|
||||
|
||||
while let Some(prev) = parent[current] {
|
||||
current = prev;
|
||||
path.push(coord(current));
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
Some(path)
|
||||
}
|
||||
|
||||
// Generate a noisy path biased toward the target cell.
|
||||
fn noisy_path(
|
||||
start: (usize, usize),
|
||||
end: (usize, usize),
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
randomness: f32,
|
||||
blocked: &HashSet<(usize, usize)>,
|
||||
rng: &mut SimpleRng,
|
||||
) -> Vec<(usize, usize)> {
|
||||
if start == end {
|
||||
return vec![start];
|
||||
}
|
||||
|
||||
let mut path = vec![start];
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(start);
|
||||
|
||||
let mut current = start;
|
||||
let mut prev_dir = (0isize, 0isize);
|
||||
let max_steps = cols.saturating_mul(rows).max(32);
|
||||
|
||||
for _ in 0..max_steps {
|
||||
if current == end {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut neighbors = Vec::with_capacity(4);
|
||||
let (x, y) = current;
|
||||
if x > 0 {
|
||||
neighbors.push((x - 1, y));
|
||||
}
|
||||
if x + 1 < cols {
|
||||
neighbors.push((x + 1, y));
|
||||
}
|
||||
if y > 0 {
|
||||
neighbors.push((x, y - 1));
|
||||
}
|
||||
if y + 1 < rows {
|
||||
neighbors.push((x, y + 1));
|
||||
}
|
||||
|
||||
if neighbors.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut best = neighbors[0];
|
||||
let mut best_score = f32::INFINITY;
|
||||
|
||||
for &candidate in &neighbors {
|
||||
if blocked.contains(&candidate) && candidate != end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let step_dir = (
|
||||
candidate.0 as isize - current.0 as isize,
|
||||
candidate.1 as isize - current.1 as isize,
|
||||
);
|
||||
|
||||
let dist = manhattan_distance(candidate, end) as f32;
|
||||
let progress_weight = 1.0 - (0.85 * randomness);
|
||||
let revisit_penalty = if visited.contains(&candidate) {
|
||||
2.5 + (2.0 * randomness)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let turn_penalty = if prev_dir == (0, 0) || prev_dir == step_dir {
|
||||
0.0
|
||||
} else {
|
||||
0.6 - (0.35 * randomness)
|
||||
};
|
||||
let noise = rng.next_f32() * 8.0 * randomness;
|
||||
let score = (dist * progress_weight) + revisit_penalty + turn_penalty + noise;
|
||||
|
||||
if score < best_score {
|
||||
best_score = score;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
prev_dir = (
|
||||
best.0 as isize - current.0 as isize,
|
||||
best.1 as isize - current.1 as isize,
|
||||
);
|
||||
current = best;
|
||||
path.push(current);
|
||||
visited.insert(current);
|
||||
}
|
||||
|
||||
if current != end
|
||||
&& let Some(tail) = shortest_path_cells(current, end, cols, rows, blocked)
|
||||
{
|
||||
for &cell in tail.iter().skip(1) {
|
||||
path.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
// Test whether two rooms overlap with extra padding.
|
||||
fn overlaps_with_padding(a: &Room, b: &Room, padding: usize) -> bool {
|
||||
let a_left = a.x.saturating_sub(padding);
|
||||
let a_top = a.y.saturating_sub(padding);
|
||||
let a_right = a.x + a.width + padding;
|
||||
let a_bottom = a.y + a.height + padding;
|
||||
|
||||
let b_left = b.x;
|
||||
let b_top = b.y;
|
||||
let b_right = b.x + b.width;
|
||||
let b_bottom = b.y + b.height;
|
||||
|
||||
a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top
|
||||
}
|
||||
|
||||
// Compute Manhattan distance between two grid cells.
|
||||
fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize {
|
||||
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
||||
}
|
||||
|
||||
// Normalize a cell edge ordering.
|
||||
fn normalized_cell_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
|
||||
if a <= b { (a, b) } else { (b, a) }
|
||||
}
|
||||
|
||||
struct SimpleRng {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl SimpleRng {
|
||||
// Create a small deterministic RNG with a fallback seed.
|
||||
fn new(seed: u64) -> Self {
|
||||
let state = if seed == 0 {
|
||||
0xA5A5_A5A5_1234_5678
|
||||
} else {
|
||||
seed
|
||||
};
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// Return the next random u32.
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.state ^= self.state >> 12;
|
||||
self.state ^= self.state << 25;
|
||||
self.state ^= self.state >> 27;
|
||||
(self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 32) as u32
|
||||
}
|
||||
|
||||
// Return the next random f32 in [0,1].
|
||||
fn next_f32(&mut self) -> f32 {
|
||||
self.next_u32() as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
// Generate a random usize between min and max inclusive.
|
||||
fn range_inclusive(&mut self, min: usize, max: usize) -> usize {
|
||||
if min >= max {
|
||||
return min;
|
||||
}
|
||||
let width = max - min + 1;
|
||||
min + (self.next_u32() as usize % width)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod generation;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
pub use generation::*;
|
||||
pub use types::*;
|
||||
pub use utils::*;
|
||||
@@ -0,0 +1,150 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Room {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
// Return the center cell of the room.
|
||||
pub fn center_cell(&self) -> (usize, usize) {
|
||||
(self.x + (self.width / 2), self.y + (self.height / 2))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Corridor {
|
||||
#[allow(dead_code)]
|
||||
pub id: u64,
|
||||
pub start_room_id: usize,
|
||||
pub end_room_id: usize,
|
||||
pub path: Vec<(usize, usize)>,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Door {
|
||||
pub from: (usize, usize),
|
||||
pub to: (usize, usize),
|
||||
pub width: usize,
|
||||
pub span_width: bool,
|
||||
pub locked: bool,
|
||||
pub archway: bool,
|
||||
pub secret: bool,
|
||||
pub manual: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Window {
|
||||
pub cell: (usize, usize),
|
||||
pub side: WindowSide,
|
||||
pub width: usize,
|
||||
pub span_width: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TextLabel {
|
||||
pub cell: (usize, usize),
|
||||
pub text: String,
|
||||
pub font_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AreaMarker {
|
||||
pub cell: (usize, usize),
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Staircase {
|
||||
pub cell: (usize, usize),
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WindowSide {
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct DoorSettings {
|
||||
pub frequency_percent: usize,
|
||||
pub room_hallway_percent: usize,
|
||||
pub locked_percent: usize,
|
||||
pub secret_percent: usize,
|
||||
pub allow_middle_corridor_doors: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct WindowSettings {
|
||||
pub enabled: bool,
|
||||
pub min_width: usize,
|
||||
pub max_width: usize,
|
||||
pub frequency_percent: usize,
|
||||
pub room_hallway_percent: usize,
|
||||
pub allow_internal_windows: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DungeonLayout {
|
||||
pub rooms: Vec<Room>,
|
||||
pub corridors: Vec<Corridor>,
|
||||
pub doors: Vec<Door>,
|
||||
pub windows: Vec<Window>,
|
||||
pub text_labels: Vec<TextLabel>,
|
||||
pub start_markers: Vec<AreaMarker>,
|
||||
pub end_markers: Vec<AreaMarker>,
|
||||
pub trap_markers: Vec<AreaMarker>,
|
||||
pub monster_markers: Vec<AreaMarker>,
|
||||
pub packed_rooms: bool,
|
||||
pub stairs: Vec<Staircase>,
|
||||
}
|
||||
|
||||
impl Default for DungeonLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rooms: Vec::new(),
|
||||
corridors: Vec::new(),
|
||||
doors: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
text_labels: Vec::new(),
|
||||
start_markers: Vec::new(),
|
||||
end_markers: Vec::new(),
|
||||
trap_markers: Vec::new(),
|
||||
monster_markers: Vec::new(),
|
||||
packed_rooms: false,
|
||||
stairs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DungeonLayout {
|
||||
// Build an empty layout for the current packing mode.
|
||||
pub(crate) fn empty(packed_rooms: bool) -> Self {
|
||||
Self {
|
||||
packed_rooms,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Build a layout from generated rooms and corridors.
|
||||
pub(crate) fn from_generated_parts(
|
||||
rooms: Vec<Room>,
|
||||
corridors: Vec<Corridor>,
|
||||
packed_rooms: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
rooms,
|
||||
corridors,
|
||||
packed_rooms,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
use super::types::{DungeonLayout, Room};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
pub struct SimpleRng {
|
||||
pub state: u64,
|
||||
}
|
||||
|
||||
impl SimpleRng {
|
||||
// Create a small deterministic RNG with a fallback seed.
|
||||
pub fn new(seed: u64) -> Self {
|
||||
let state = if seed == 0 {
|
||||
0xA5A5_A5A5_1234_5678
|
||||
} else {
|
||||
seed
|
||||
};
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// Return the next random u32.
|
||||
pub fn next_u32(&mut self) -> u32 {
|
||||
self.state ^= self.state >> 12;
|
||||
self.state ^= self.state << 25;
|
||||
self.state ^= self.state >> 27;
|
||||
(self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 32) as u32
|
||||
}
|
||||
|
||||
// Return the next random f32 in [0,1].
|
||||
pub fn next_f32(&mut self) -> f32 {
|
||||
self.next_u32() as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
// Generate a random usize between min and max inclusive.
|
||||
pub fn range_inclusive(&mut self, min: usize, max: usize) -> usize {
|
||||
if min >= max {
|
||||
return min;
|
||||
}
|
||||
let width = max - min + 1;
|
||||
min + (self.next_u32() as usize % width)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all room cells except those belonging to excluded room ids.
|
||||
pub fn blocked_room_cells(rooms: &[Room], excluded_room_ids: &[usize]) -> HashSet<(usize, usize)> {
|
||||
let excluded: HashSet<usize> = excluded_room_ids.iter().copied().collect();
|
||||
let mut blocked = HashSet::new();
|
||||
|
||||
for (room_idx, room) in rooms.iter().enumerate() {
|
||||
if excluded.contains(&room_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
blocked.insert((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blocked
|
||||
}
|
||||
|
||||
// Compute corridor cells while excluding room cells.
|
||||
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
||||
let mut cells = HashSet::new();
|
||||
if cols == 0 || rows == 0 {
|
||||
return cells;
|
||||
}
|
||||
|
||||
let mut room_cells = HashSet::new();
|
||||
for room in &layout.rooms {
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
room_cells.insert((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for corridor in &layout.corridors {
|
||||
let width = corridor.width.max(1);
|
||||
let min_offset = -((width as isize - 1) / 2);
|
||||
let max_offset = width as isize / 2;
|
||||
if corridor.path.len() == 1 {
|
||||
let (x, y) = corridor.path[0];
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = y as isize + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (x, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for pair in corridor.path.windows(2) {
|
||||
let a = pair[0];
|
||||
let b = pair[1];
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
|
||||
if a.0 != b.0 {
|
||||
let x0 = a.0.min(b.0);
|
||||
let x1 = a.0.max(b.0);
|
||||
let y = a.1 as isize;
|
||||
for x in x0..=x1 {
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = y + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (x, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let y0 = a.1.min(b.1);
|
||||
let y1 = a.1.max(b.1);
|
||||
let x = a.0 as isize;
|
||||
for y in y0..=y1 {
|
||||
for dx in min_offset..=max_offset {
|
||||
let nx = x + dx;
|
||||
if nx < 0 || nx >= cols as isize {
|
||||
continue;
|
||||
}
|
||||
let cell = (nx as usize, y);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for turn in corridor.path.windows(3) {
|
||||
let prev = turn[0];
|
||||
let corner = turn[1];
|
||||
let next = turn[2];
|
||||
let incoming = (
|
||||
corner.0 as isize - prev.0 as isize,
|
||||
corner.1 as isize - prev.1 as isize,
|
||||
);
|
||||
let outgoing = (
|
||||
next.0 as isize - corner.0 as isize,
|
||||
next.1 as isize - corner.1 as isize,
|
||||
);
|
||||
if incoming == outgoing {
|
||||
continue;
|
||||
}
|
||||
|
||||
for dx in min_offset..=max_offset {
|
||||
let nx = corner.0 as isize + dx;
|
||||
if nx < 0 || nx >= cols as isize {
|
||||
continue;
|
||||
}
|
||||
for dy in min_offset..=max_offset {
|
||||
let ny = corner.1 as isize + dy;
|
||||
if ny < 0 || ny >= rows as isize {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = (nx as usize, ny as usize);
|
||||
if !room_cells.contains(&cell) {
|
||||
cells.insert(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells
|
||||
}
|
||||
|
||||
pub fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option<usize> {
|
||||
rooms.iter().position(|room| {
|
||||
cell.0 >= room.x
|
||||
&& cell.0 < room.x + room.width
|
||||
&& cell.1 >= room.y
|
||||
&& cell.1 < room.y + room.height
|
||||
})
|
||||
}
|
||||
|
||||
// Normalize a cell edge ordering.
|
||||
pub fn normalized_cell_edge(
|
||||
a: (usize, usize),
|
||||
b: (usize, usize),
|
||||
) -> ((usize, usize), (usize, usize)) {
|
||||
if a <= b { (a, b) } else { (b, a) }
|
||||
}
|
||||
|
||||
// Compute Manhattan distance between two grid cells.
|
||||
pub fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize {
|
||||
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
||||
}
|
||||
|
||||
// Test whether two rooms overlap with extra padding.
|
||||
pub fn overlaps_with_padding(a: &Room, b: &Room, padding: usize) -> bool {
|
||||
let a_left = a.x.saturating_sub(padding);
|
||||
let a_top = a.y.saturating_sub(padding);
|
||||
let a_right = a.x + a.width + padding;
|
||||
let a_bottom = a.y + a.height + padding;
|
||||
|
||||
let b_left = b.x;
|
||||
let b_top = b.y;
|
||||
let b_right = b.x + b.width;
|
||||
let b_bottom = b.y + b.height;
|
||||
|
||||
a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top
|
||||
}
|
||||
|
||||
pub fn rects_overlap(
|
||||
ax: usize,
|
||||
ay: usize,
|
||||
aw: usize,
|
||||
ah: usize,
|
||||
bx: usize,
|
||||
by: usize,
|
||||
bw: usize,
|
||||
bh: usize,
|
||||
) -> bool {
|
||||
let a_right = ax + aw;
|
||||
let a_bottom = ay + ah;
|
||||
let b_right = bx + bw;
|
||||
let b_bottom = by + bh;
|
||||
|
||||
ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
|
||||
}
|
||||
|
||||
pub fn rooms_overlap(a: &Room, b: &Room) -> bool {
|
||||
rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
|
||||
}
|
||||
|
||||
pub fn rooms_touch(a: &Room, b: &Room) -> bool {
|
||||
!shared_boundary_edges(a, b).is_empty()
|
||||
}
|
||||
|
||||
pub fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usize))> {
|
||||
let mut edges = Vec::new();
|
||||
|
||||
if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||
let left = if a.x < b.x { a } else { b };
|
||||
let right = if a.x < b.x { b } else { a };
|
||||
let y0 = left.y.max(right.y);
|
||||
let y1 = (left.y + left.height).min(right.y + right.height);
|
||||
for y in y0..y1 {
|
||||
edges.push(normalized_cell_edge(
|
||||
(left.x + left.width - 1, y),
|
||||
(right.x, y),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if a.y + a.height == b.y || b.y + b.height == a.y {
|
||||
let top = if a.y < b.y { a } else { b };
|
||||
let bottom = if a.y < b.y { b } else { a };
|
||||
let x0 = top.x.max(bottom.x);
|
||||
let x1 = (top.x + top.width).min(bottom.x + bottom.width);
|
||||
for x in x0..x1 {
|
||||
edges.push(normalized_cell_edge(
|
||||
(x, top.y + top.height - 1),
|
||||
(x, bottom.y),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
pub fn shared_opening_width(a: &Room, b: &Room, span: usize, default_width: usize) -> usize {
|
||||
let max_width = if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||
a.height.min(b.height)
|
||||
} else {
|
||||
a.width.min(b.width)
|
||||
};
|
||||
default_width.max(1).min(span).min(max_width.max(1))
|
||||
}
|
||||
|
||||
// Compute the shortest grid path between two cells using BFS.
|
||||
pub fn shortest_path_cells(
|
||||
start: (usize, usize),
|
||||
end: (usize, usize),
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
blocked: &HashSet<(usize, usize)>,
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
if start == end {
|
||||
return Some(vec![start]);
|
||||
}
|
||||
if blocked.contains(&start) || blocked.contains(&end) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let total = cols.saturating_mul(rows);
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let index = |p: (usize, usize)| -> usize { p.1 * cols + p.0 };
|
||||
let coord = |idx: usize| -> (usize, usize) { (idx % cols, idx / cols) };
|
||||
|
||||
let start_idx = index(start);
|
||||
let end_idx = index(end);
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
let mut visited = vec![false; total];
|
||||
let mut parent: Vec<Option<usize>> = vec![None; total];
|
||||
|
||||
visited[start_idx] = true;
|
||||
queue.push_back(start_idx);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
if current == end_idx {
|
||||
break;
|
||||
}
|
||||
|
||||
let (x, y) = coord(current);
|
||||
let neighbors = [
|
||||
x.checked_sub(1).map(|nx| (nx, y)),
|
||||
(x + 1 < cols).then_some((x + 1, y)),
|
||||
y.checked_sub(1).map(|ny| (x, ny)),
|
||||
(y + 1 < rows).then_some((x, y + 1)),
|
||||
];
|
||||
|
||||
for neighbor in neighbors.into_iter().flatten() {
|
||||
if blocked.contains(&neighbor) {
|
||||
continue;
|
||||
}
|
||||
let n_idx = index(neighbor);
|
||||
if !visited[n_idx] {
|
||||
visited[n_idx] = true;
|
||||
parent[n_idx] = Some(current);
|
||||
queue.push_back(n_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !visited[end_idx] {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path = Vec::new();
|
||||
let mut current = end_idx;
|
||||
path.push(coord(current));
|
||||
|
||||
while let Some(prev) = parent[current] {
|
||||
current = prev;
|
||||
path.push(coord(current));
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
Some(path)
|
||||
}
|
||||
|
||||
// Generate a noisy path biased toward the target cell.
|
||||
pub fn noisy_path(
|
||||
start: (usize, usize),
|
||||
end: (usize, usize),
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
randomness: f32,
|
||||
blocked: &HashSet<(usize, usize)>,
|
||||
rng: &mut SimpleRng,
|
||||
) -> Vec<(usize, usize)> {
|
||||
if start == end {
|
||||
return vec![start];
|
||||
}
|
||||
|
||||
let mut path = vec![start];
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(start);
|
||||
|
||||
let mut current = start;
|
||||
let mut prev_dir = (0isize, 0isize);
|
||||
let max_steps = cols.saturating_mul(rows).max(32);
|
||||
|
||||
for _ in 0..max_steps {
|
||||
if current == end {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut neighbors = Vec::with_capacity(4);
|
||||
let (x, y) = current;
|
||||
if x > 0 {
|
||||
neighbors.push((x - 1, y));
|
||||
}
|
||||
if x + 1 < cols {
|
||||
neighbors.push((x + 1, y));
|
||||
}
|
||||
if y > 0 {
|
||||
neighbors.push((x, y - 1));
|
||||
}
|
||||
if y + 1 < rows {
|
||||
neighbors.push((x, y + 1));
|
||||
}
|
||||
|
||||
if neighbors.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut best = neighbors[0];
|
||||
let mut best_score = f32::INFINITY;
|
||||
|
||||
for &candidate in &neighbors {
|
||||
if blocked.contains(&candidate) && candidate != end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let step_dir = (
|
||||
candidate.0 as isize - current.0 as isize,
|
||||
candidate.1 as isize - current.1 as isize,
|
||||
);
|
||||
|
||||
let dist = manhattan_distance(candidate, end) as f32;
|
||||
let progress_weight = 1.0 - (0.85 * randomness);
|
||||
let revisit_penalty = if visited.contains(&candidate) {
|
||||
2.5 + (2.0 * randomness)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let turn_penalty = if prev_dir == (0, 0) || prev_dir == step_dir {
|
||||
0.0
|
||||
} else {
|
||||
0.6 - (0.35 * randomness)
|
||||
};
|
||||
let noise = rng.next_f32() * 8.0 * randomness;
|
||||
let score = (dist * progress_weight) + revisit_penalty + turn_penalty + noise;
|
||||
|
||||
if score < best_score {
|
||||
best_score = score;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
prev_dir = (
|
||||
best.0 as isize - current.0 as isize,
|
||||
best.1 as isize - current.1 as isize,
|
||||
);
|
||||
current = best;
|
||||
path.push(current);
|
||||
visited.insert(current);
|
||||
}
|
||||
|
||||
if current != end
|
||||
&& let Some(tail) = shortest_path_cells(current, end, cols, rows, blocked)
|
||||
{
|
||||
for &cell in tail.iter().skip(1) {
|
||||
path.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
// Shuffle indices in place using the provided RNG.
|
||||
pub fn shuffle_indices(indices: &mut [usize], rng: &mut SimpleRng) {
|
||||
if indices.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
for i in (1..indices.len()).rev() {
|
||||
let j = rng.range_inclusive(0, i);
|
||||
indices.swap(i, j);
|
||||
}
|
||||
}
|
||||
+4
-3821
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,770 @@
|
||||
use crate::interact::{
|
||||
GridGeometry, HoverMarker, MarkerKind, cell_center, cell_rect, door_edges_for,
|
||||
door_render_width, marker_rect, normalized_edge, window_render_width,
|
||||
};
|
||||
use crate::layout::{DungeonLayout, corridor_cells};
|
||||
use eframe::egui;
|
||||
use egui::{Color32, Stroke};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn draw_layout(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
layout: &DungeonLayout,
|
||||
colorblind_mode: bool,
|
||||
hover_text_idx: Option<usize>,
|
||||
hover_marker: Option<HoverMarker>,
|
||||
hover_stair_idx: Option<usize>,
|
||||
) {
|
||||
let wall_width_px = (geometry.cell_size / 2.5).max(1.0);
|
||||
let door_width_px = (geometry.cell_size / 5.0).max(1.0);
|
||||
|
||||
let room_fill = Color32::from_rgb(70, 120, 160);
|
||||
let corridor_fill = Color32::from_rgb(210, 190, 120);
|
||||
let wall_color = Color32::BLACK;
|
||||
let corridor_cells_set = corridor_cells(layout, geometry.cols, geometry.rows);
|
||||
let corridor_edges_set = corridor_edges_from_cells(&corridor_cells_set);
|
||||
let mut room_cells_set = HashSet::new();
|
||||
let room_edges_set = room_edges_from_rooms(&layout.rooms);
|
||||
let mut door_edges_set = HashSet::new();
|
||||
|
||||
for door in &layout.doors {
|
||||
for edge in door_edges_for(door, geometry.cols, geometry.rows) {
|
||||
door_edges_set.insert(edge);
|
||||
}
|
||||
}
|
||||
|
||||
for &(col, row) in &corridor_cells_set {
|
||||
painter.rect_filled(cell_rect(geometry, col, row), 0.0, corridor_fill);
|
||||
if colorblind_mode {
|
||||
painter.circle_filled(
|
||||
cell_center(geometry, col, row),
|
||||
geometry.cell_size * 0.14,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for &(col, row) in &corridor_cells_set {
|
||||
let rect = cell_rect(geometry, col, row);
|
||||
let right = (col + 1, row);
|
||||
let bottom = (col, row + 1);
|
||||
let left = col.checked_sub(1).map(|x| (x, row));
|
||||
let top = row.checked_sub(1).map(|y| (col, y));
|
||||
|
||||
if col == 0
|
||||
|| (!corridor_cells_set.contains(&(col - 1, row))
|
||||
&& left
|
||||
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.top()),
|
||||
egui::pos2(rect.left(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
if (!corridor_cells_set.contains(&right)
|
||||
|| !corridor_edges_set.contains(&normalized_edge((col, row), right)))
|
||||
&& !door_edges_set.contains(&normalized_edge((col, row), right))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.right(), rect.top()),
|
||||
egui::pos2(rect.right(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
if row == 0
|
||||
|| (!corridor_cells_set.contains(&(col, row - 1))
|
||||
&& top
|
||||
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.top()),
|
||||
egui::pos2(rect.right(), rect.top()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
if (!corridor_cells_set.contains(&bottom)
|
||||
|| !corridor_edges_set.contains(&normalized_edge((col, row), bottom)))
|
||||
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.bottom()),
|
||||
egui::pos2(rect.right(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for room in &layout.rooms {
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
room_cells_set.insert((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
|
||||
let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
|
||||
let right = left + room.width as f32 * geometry.cell_size;
|
||||
let bottom = top + room.height as f32 * geometry.cell_size;
|
||||
let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||
|
||||
painter.rect_filled(room_rect, 0.0, room_fill);
|
||||
if colorblind_mode {
|
||||
draw_room_crosshatch(painter, geometry, room, Stroke::new(1.5, Color32::BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
for &(col, row) in &room_cells_set {
|
||||
let rect = cell_rect(geometry, col, row);
|
||||
let right = (col + 1, row);
|
||||
let bottom = (col, row + 1);
|
||||
let left = col.checked_sub(1).map(|x| (x, row));
|
||||
let top = row.checked_sub(1).map(|y| (col, y));
|
||||
|
||||
let left_edge = col == 0
|
||||
|| (!room_cells_set.contains(&(col - 1, row))
|
||||
|| left
|
||||
.map(|n| !room_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true))
|
||||
&& left
|
||||
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true);
|
||||
if left_edge {
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.top()),
|
||||
egui::pos2(rect.left(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
if (!room_cells_set.contains(&right)
|
||||
|| !room_edges_set.contains(&normalized_edge((col, row), right)))
|
||||
&& !door_edges_set.contains(&normalized_edge((col, row), right))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.right(), rect.top()),
|
||||
egui::pos2(rect.right(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
let top_edge = row == 0
|
||||
|| (!room_cells_set.contains(&(col, row - 1))
|
||||
|| top
|
||||
.map(|n| !room_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true))
|
||||
&& top
|
||||
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||
.unwrap_or(true);
|
||||
if top_edge {
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.top()),
|
||||
egui::pos2(rect.right(), rect.top()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
if (!room_cells_set.contains(&bottom)
|
||||
|| !room_edges_set.contains(&normalized_edge((col, row), bottom)))
|
||||
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
|
||||
{
|
||||
draw_wall_segment(
|
||||
painter,
|
||||
egui::pos2(rect.left(), rect.bottom()),
|
||||
egui::pos2(rect.right(), rect.bottom()),
|
||||
wall_width_px,
|
||||
wall_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for door in &layout.doors {
|
||||
let color = if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else if door.secret {
|
||||
Color32::from_rgb(170, 80, 170)
|
||||
} else if door.archway {
|
||||
Color32::from_rgb(230, 140, 60)
|
||||
} else if door.locked {
|
||||
Color32::from_rgb(220, 70, 70)
|
||||
} else {
|
||||
Color32::from_rgb(80, 200, 120)
|
||||
};
|
||||
let style = if colorblind_mode {
|
||||
if door.secret {
|
||||
DoorLineStyle::DashDotDot
|
||||
} else if door.archway {
|
||||
DoorLineStyle::Dotted
|
||||
} else if door.locked {
|
||||
DoorLineStyle::ShortDash
|
||||
} else {
|
||||
DoorLineStyle::LongDash
|
||||
}
|
||||
} else {
|
||||
DoorLineStyle::Solid
|
||||
};
|
||||
draw_door_line(
|
||||
painter,
|
||||
geometry,
|
||||
door.from,
|
||||
door.to,
|
||||
door_render_width(door),
|
||||
Stroke::new(door_width_px, color),
|
||||
style,
|
||||
);
|
||||
}
|
||||
|
||||
for window in &layout.windows {
|
||||
let style = if colorblind_mode {
|
||||
DoorLineStyle::DashDot
|
||||
} else {
|
||||
DoorLineStyle::Solid
|
||||
};
|
||||
draw_window_line(
|
||||
painter,
|
||||
geometry,
|
||||
window,
|
||||
Stroke::new(door_width_px, Color32::from_rgb(70, 130, 220)),
|
||||
style,
|
||||
);
|
||||
}
|
||||
|
||||
for (idx, label) in layout.text_labels.iter().enumerate() {
|
||||
if hover_text_idx == Some(idx) {
|
||||
painter.rect_stroke(
|
||||
text_label_rect(geometry, label).expand(2.0),
|
||||
0.0,
|
||||
Stroke::new(2.0, Color32::from_rgb(255, 220, 120)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
let pos = cell_center(geometry, label.cell.0, label.cell.1);
|
||||
let color = if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
};
|
||||
painter.text(
|
||||
pos,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&label.text,
|
||||
egui::FontId::proportional(label.font_size as f32),
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
draw_area_marker_group(
|
||||
painter,
|
||||
geometry,
|
||||
&layout.start_markers,
|
||||
"S",
|
||||
Color32::from_rgb(60, 220, 200),
|
||||
colorblind_mode,
|
||||
hover_marker,
|
||||
MarkerKind::Start,
|
||||
);
|
||||
draw_area_marker_group(
|
||||
painter,
|
||||
geometry,
|
||||
&layout.end_markers,
|
||||
"E",
|
||||
Color32::from_rgb(240, 90, 90),
|
||||
colorblind_mode,
|
||||
hover_marker,
|
||||
MarkerKind::End,
|
||||
);
|
||||
draw_area_marker_group(
|
||||
painter,
|
||||
geometry,
|
||||
&layout.trap_markers,
|
||||
"T",
|
||||
Color32::from_rgb(150, 80, 230),
|
||||
colorblind_mode,
|
||||
hover_marker,
|
||||
MarkerKind::Trap,
|
||||
);
|
||||
draw_area_marker_group(
|
||||
painter,
|
||||
geometry,
|
||||
&layout.monster_markers,
|
||||
"M",
|
||||
Color32::from_rgb(200, 50, 50),
|
||||
colorblind_mode,
|
||||
hover_marker,
|
||||
MarkerKind::Monster,
|
||||
);
|
||||
|
||||
for (idx, stair) in layout.stairs.iter().enumerate() {
|
||||
draw_staircase(
|
||||
painter,
|
||||
geometry,
|
||||
stair,
|
||||
colorblind_mode,
|
||||
hover_stair_idx == Some(idx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_staircase(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
stair: &crate::layout::Staircase,
|
||||
colorblind_mode: bool,
|
||||
is_hovered: bool,
|
||||
) {
|
||||
let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size;
|
||||
let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size;
|
||||
let right = left + stair.width as f32 * geometry.cell_size;
|
||||
let bottom = top + stair.height as f32 * geometry.cell_size;
|
||||
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||
|
||||
let fill = if is_hovered {
|
||||
Color32::from_rgb(220, 180, 80).gamma_multiply(0.5)
|
||||
} else if colorblind_mode {
|
||||
Color32::from_rgb(180, 180, 180).gamma_multiply(0.4)
|
||||
} else {
|
||||
Color32::from_rgb(200, 150, 50).gamma_multiply(0.35)
|
||||
};
|
||||
painter.rect_filled(rect, 4.0, fill);
|
||||
|
||||
let stroke_color = if is_hovered {
|
||||
Color32::WHITE
|
||||
} else if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::from_rgb(200, 150, 50)
|
||||
};
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
4.0,
|
||||
Stroke::new(if is_hovered { 3.0 } else { 2.0 }, stroke_color),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
|
||||
let steps = (stair.height as f32 * 2.0).round() as usize;
|
||||
let step_height = (rect.height() / (steps as f32)).max(1.0);
|
||||
let line_color = if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::from_rgb(150, 100, 30)
|
||||
};
|
||||
for i in 0..steps {
|
||||
let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(rect.left() + 2.0, y),
|
||||
egui::pos2(rect.right() - 2.0, y),
|
||||
],
|
||||
Stroke::new(1.5, line_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_label_rect(geometry: &GridGeometry, label: &crate::layout::TextLabel) -> egui::Rect {
|
||||
let center = cell_center(geometry, label.cell.0, label.cell.1);
|
||||
let font_size = label.font_size as f32;
|
||||
let width =
|
||||
(label.text.chars().count().max(1) as f32 * font_size * 0.6).max(geometry.cell_size * 0.5);
|
||||
let height = font_size.max(geometry.cell_size * 0.35);
|
||||
egui::Rect::from_center_size(center, egui::vec2(width, height))
|
||||
}
|
||||
|
||||
pub fn draw_area_marker(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
marker: &crate::layout::AreaMarker,
|
||||
label: &str,
|
||||
base_color: Color32,
|
||||
colorblind_mode: bool,
|
||||
hovered: bool,
|
||||
) {
|
||||
let rect = marker_rect(marker, geometry);
|
||||
let fill = base_color.gamma_multiply(if colorblind_mode { 0.22 } else { 0.35 });
|
||||
let stroke_color = if hovered {
|
||||
Color32::from_rgb(255, 220, 120)
|
||||
} else if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
base_color
|
||||
};
|
||||
let text_color = if colorblind_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
};
|
||||
let font_size = (geometry.cell_size * marker.size as f32 * 0.5).clamp(14.0, 72.0);
|
||||
|
||||
painter.rect_filled(rect, 4.0, fill);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
4.0,
|
||||
Stroke::new(if hovered { 3.0 } else { 2.0 }, stroke_color),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
painter.text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
label,
|
||||
egui::FontId::proportional(font_size),
|
||||
text_color,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw_area_marker_group(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
markers: &[crate::layout::AreaMarker],
|
||||
label: &str,
|
||||
base_color: Color32,
|
||||
colorblind_mode: bool,
|
||||
hover_marker: Option<HoverMarker>,
|
||||
kind: MarkerKind,
|
||||
) {
|
||||
for (idx, marker) in markers.iter().enumerate() {
|
||||
let hovered = match (kind, hover_marker) {
|
||||
(MarkerKind::Start, Some(HoverMarker::Start(hover_idx))) => hover_idx == idx,
|
||||
(MarkerKind::End, Some(HoverMarker::End(hover_idx))) => hover_idx == idx,
|
||||
(MarkerKind::Trap, Some(HoverMarker::Trap(hover_idx))) => hover_idx == idx,
|
||||
(MarkerKind::Monster, Some(HoverMarker::Monster(hover_idx))) => hover_idx == idx,
|
||||
_ => false,
|
||||
};
|
||||
draw_area_marker(
|
||||
painter,
|
||||
geometry,
|
||||
marker,
|
||||
label,
|
||||
base_color,
|
||||
colorblind_mode,
|
||||
hovered,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_wall_segment(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
width: f32,
|
||||
color: Color32,
|
||||
) {
|
||||
let half = width * 0.5;
|
||||
if (from.x - to.x).abs() <= f32::EPSILON {
|
||||
let x = from.x.min(to.x);
|
||||
let y0 = from.y.min(to.y);
|
||||
let y1 = from.y.max(to.y);
|
||||
let rect = egui::Rect::from_min_max(
|
||||
egui::pos2(x - half, y0 - half),
|
||||
egui::pos2(x + half, y1 + half),
|
||||
);
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
} else {
|
||||
let y = from.y.min(to.y);
|
||||
let x0 = from.x.min(to.x);
|
||||
let x1 = from.x.max(to.x);
|
||||
let rect = egui::Rect::from_min_max(
|
||||
egui::pos2(x0 - half, y - half),
|
||||
egui::pos2(x1 + half, y + half),
|
||||
);
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn corridor_edges_from_cells(
|
||||
corridor_cells: &HashSet<(usize, usize)>,
|
||||
) -> HashSet<((usize, usize), (usize, usize))> {
|
||||
let mut edges = HashSet::new();
|
||||
for &(col, row) in corridor_cells {
|
||||
let right = (col + 1, row);
|
||||
let bottom = (col, row + 1);
|
||||
if corridor_cells.contains(&right) {
|
||||
edges.insert(normalized_edge((col, row), right));
|
||||
}
|
||||
if corridor_cells.contains(&bottom) {
|
||||
edges.insert(normalized_edge((col, row), bottom));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
pub fn room_edges_from_rooms(
|
||||
rooms: &[crate::layout::Room],
|
||||
) -> HashSet<((usize, usize), (usize, usize))> {
|
||||
let mut edges = HashSet::new();
|
||||
for room in rooms {
|
||||
let x_end = room.x + room.width;
|
||||
let y_end = room.y + room.height;
|
||||
for x in room.x..x_end {
|
||||
for y in room.y..y_end {
|
||||
if x + 1 < x_end {
|
||||
edges.insert(normalized_edge((x, y), (x + 1, y)));
|
||||
}
|
||||
if y + 1 < y_end {
|
||||
edges.insert(normalized_edge((x, y), (x, y + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoorLineStyle {
|
||||
Solid,
|
||||
LongDash,
|
||||
ShortDash,
|
||||
Dotted,
|
||||
DashDot,
|
||||
DashDotDot,
|
||||
}
|
||||
|
||||
pub fn draw_door_line(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
a: (usize, usize),
|
||||
b: (usize, usize),
|
||||
width_cells: usize,
|
||||
stroke: Stroke,
|
||||
style: DoorLineStyle,
|
||||
) {
|
||||
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width_cells = width_cells.max(1);
|
||||
let min_offset = -((width_cells as isize - 1) / 2);
|
||||
let max_offset = width_cells as isize / 2;
|
||||
|
||||
if a.0 != b.0 {
|
||||
let x = geometry.rect.left() + (a.0.max(b.0) as f32) * geometry.cell_size;
|
||||
let row = a.1 as isize;
|
||||
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||
} else {
|
||||
let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size;
|
||||
let col = a.0 as isize;
|
||||
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_window_line(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
window: &crate::layout::Window,
|
||||
stroke: Stroke,
|
||||
style: DoorLineStyle,
|
||||
) {
|
||||
let width_cells = window_render_width(window).max(1) as isize;
|
||||
let min_offset = -((width_cells - 1) / 2);
|
||||
let max_offset = width_cells / 2;
|
||||
|
||||
match window.side {
|
||||
crate::layout::WindowSide::Left => {
|
||||
let x = geometry.rect.left() + window.cell.0 as f32 * geometry.cell_size;
|
||||
let row = window.cell.1 as isize;
|
||||
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||
}
|
||||
crate::layout::WindowSide::Right => {
|
||||
let x = geometry.rect.left() + (window.cell.0 as f32 + 1.0) * geometry.cell_size;
|
||||
let row = window.cell.1 as isize;
|
||||
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||
}
|
||||
crate::layout::WindowSide::Top => {
|
||||
let y = geometry.rect.top() + window.cell.1 as f32 * geometry.cell_size;
|
||||
let col = window.cell.0 as isize;
|
||||
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||
}
|
||||
crate::layout::WindowSide::Bottom => {
|
||||
let y = geometry.rect.top() + (window.cell.1 as f32 + 1.0) * geometry.cell_size;
|
||||
let col = window.cell.0 as isize;
|
||||
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_styled_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
style: DoorLineStyle,
|
||||
) {
|
||||
match style {
|
||||
DoorLineStyle::Solid => {
|
||||
painter.line_segment([from, to], stroke);
|
||||
}
|
||||
DoorLineStyle::LongDash => draw_dashed_line(painter, from, to, stroke, 14.0, 7.0),
|
||||
DoorLineStyle::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0),
|
||||
DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke),
|
||||
DoorLineStyle::DashDot => draw_dash_dot_line(painter, from, to, stroke),
|
||||
DoorLineStyle::DashDotDot => draw_dash_dot_dot_line(painter, from, to, stroke),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dashed_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
dash_len: f32,
|
||||
gap_len: f32,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let seg_start = dist;
|
||||
let seg_end = (dist + dash_len).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
||||
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist += dash_len + gap_len;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let step = 5.0_f32;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist <= len {
|
||||
let p = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(p, radius, stroke.color);
|
||||
dist += step;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dash_dot_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dash_dot_dot_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot1, radius, stroke.color);
|
||||
dist += 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot2, radius, stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_room_crosshatch(
|
||||
painter: &egui::Painter,
|
||||
geometry: &GridGeometry,
|
||||
room: &crate::layout::Room,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
for x in room.x..(room.x + room.width) {
|
||||
for y in room.y..(room.y + room.height) {
|
||||
let rect = cell_rect(geometry, x, y).shrink(2.0);
|
||||
painter.line_segment([rect.left_top(), rect.right_bottom()], stroke);
|
||||
painter.line_segment([rect.right_top(), rect.left_bottom()], stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
+436
@@ -0,0 +1,436 @@
|
||||
use eframe::egui;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod tabs;
|
||||
pub mod widgets;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Tab {
|
||||
Generate,
|
||||
Layout,
|
||||
StartAndEnd,
|
||||
MonstersAndTraps,
|
||||
Add,
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Self::Generate
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExportFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
Svg,
|
||||
Folder,
|
||||
}
|
||||
|
||||
impl Default for ExportFormat {
|
||||
fn default() -> Self {
|
||||
Self::Png
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportFormat {
|
||||
pub fn extension(self) -> &'static str {
|
||||
match self {
|
||||
ExportFormat::Png => "png",
|
||||
ExportFormat::Jpeg => "jpeg",
|
||||
ExportFormat::Webp => "webp",
|
||||
ExportFormat::Svg => "svg",
|
||||
ExportFormat::Folder => "folder",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ExportFormat::Png => ".png",
|
||||
ExportFormat::Jpeg => ".jpeg",
|
||||
ExportFormat::Webp => ".webp",
|
||||
ExportFormat::Svg => ".svg",
|
||||
ExportFormat::Folder => "Folder (Composite)",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_resolution(self) -> bool {
|
||||
!matches!(self, ExportFormat::Svg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MaskFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl Default for MaskFormat {
|
||||
fn default() -> Self {
|
||||
Self::Png
|
||||
}
|
||||
}
|
||||
|
||||
impl MaskFormat {
|
||||
pub fn extension(self) -> &'static str {
|
||||
match self {
|
||||
MaskFormat::Png => "png",
|
||||
MaskFormat::Jpeg => "jpeg",
|
||||
MaskFormat::Webp => "webp",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
MaskFormat::Png => ".png",
|
||||
MaskFormat::Jpeg => ".jpeg",
|
||||
MaskFormat::Webp => ".webp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct UiSettings {
|
||||
pub seed: u64,
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
pub room_count: usize,
|
||||
pub min_room_size: usize,
|
||||
pub max_room_size: usize,
|
||||
pub square_rooms_only: bool,
|
||||
pub min_corridor_width: usize,
|
||||
pub max_corridor_width: usize,
|
||||
pub corridor_randomness: usize,
|
||||
pub dead_end_rooms_percent: usize,
|
||||
pub pack_rooms_without_corridors: bool,
|
||||
pub door_frequency_percent: usize,
|
||||
pub room_hallway_door_percent: usize,
|
||||
pub locked_door_percent: usize,
|
||||
pub secret_door_percent: usize,
|
||||
pub allow_middle_corridor_doors: bool,
|
||||
pub windows_enabled: bool,
|
||||
pub min_window_width: usize,
|
||||
pub max_window_width: usize,
|
||||
pub window_frequency_percent: usize,
|
||||
pub room_hallway_window_percent: usize,
|
||||
pub allow_internal_windows: bool,
|
||||
pub colorblind_mode: bool,
|
||||
pub composition_mode: bool,
|
||||
pub export_format: ExportFormat,
|
||||
pub mask_format: MaskFormat,
|
||||
pub export_width: u32,
|
||||
pub export_height: u32,
|
||||
pub allow_export_aspect_change: bool,
|
||||
pub export_show_grid: bool,
|
||||
pub last_export_path: Option<String>,
|
||||
pub add_tool: AddTool,
|
||||
pub add_text_value: String,
|
||||
pub add_text_font_size: u16,
|
||||
pub add_room_width: usize,
|
||||
pub add_room_height: usize,
|
||||
pub add_corridor_width: usize,
|
||||
pub add_stair_width: usize,
|
||||
pub add_stair_height: usize,
|
||||
pub add_marker_size: usize,
|
||||
pub min_start_marker_size: usize,
|
||||
pub max_start_marker_size: usize,
|
||||
pub min_end_marker_size: usize,
|
||||
pub max_end_marker_size: usize,
|
||||
pub min_start_marker_count: usize,
|
||||
pub max_start_marker_count: usize,
|
||||
pub min_end_marker_count: usize,
|
||||
pub max_end_marker_count: usize,
|
||||
pub min_levels: usize,
|
||||
pub max_levels: usize,
|
||||
pub active_level_index: usize,
|
||||
pub export_level_index: usize,
|
||||
pub trap_frequency_percent: usize,
|
||||
pub min_traps_per_area: usize,
|
||||
pub max_traps_per_area: usize,
|
||||
pub monster_frequency_percent: usize,
|
||||
pub min_monsters_per_area: usize,
|
||||
pub max_monsters_per_area: usize,
|
||||
pub min_stair_width: usize,
|
||||
pub max_stair_width: usize,
|
||||
pub min_stair_height: usize,
|
||||
pub max_stair_height: usize,
|
||||
pub min_stairs_per_level: usize,
|
||||
pub max_stairs_per_level: usize,
|
||||
pub sync_stairs_across_levels: bool,
|
||||
pub active_tab: Tab,
|
||||
}
|
||||
|
||||
impl Default for UiSettings {
|
||||
fn default() -> Self {
|
||||
UiSettings {
|
||||
seed: 0,
|
||||
cols: 32,
|
||||
rows: 32,
|
||||
room_count: 8,
|
||||
min_room_size: 3,
|
||||
max_room_size: 8,
|
||||
square_rooms_only: true,
|
||||
min_corridor_width: 1,
|
||||
max_corridor_width: 3,
|
||||
corridor_randomness: 50,
|
||||
dead_end_rooms_percent: 20,
|
||||
pack_rooms_without_corridors: false,
|
||||
door_frequency_percent: 50,
|
||||
room_hallway_door_percent: 20,
|
||||
locked_door_percent: 10,
|
||||
secret_door_percent: 5,
|
||||
allow_middle_corridor_doors: false,
|
||||
windows_enabled: false,
|
||||
min_window_width: 2,
|
||||
max_window_width: 4,
|
||||
window_frequency_percent: 30,
|
||||
room_hallway_window_percent: 10,
|
||||
allow_internal_windows: false,
|
||||
colorblind_mode: false,
|
||||
composition_mode: false,
|
||||
export_format: ExportFormat::Png,
|
||||
mask_format: MaskFormat::Png,
|
||||
export_width: 0,
|
||||
export_height: 0,
|
||||
allow_export_aspect_change: true,
|
||||
export_show_grid: false,
|
||||
last_export_path: None,
|
||||
add_tool: AddTool::None,
|
||||
add_text_value: String::from("Text"),
|
||||
add_text_font_size: 18,
|
||||
add_room_width: 5,
|
||||
add_room_height: 5,
|
||||
add_corridor_width: 2,
|
||||
add_stair_width: 2,
|
||||
add_stair_height: 2,
|
||||
add_marker_size: 1,
|
||||
min_start_marker_size: 1,
|
||||
max_start_marker_size: 3,
|
||||
min_end_marker_size: 1,
|
||||
max_end_marker_size: 3,
|
||||
min_start_marker_count: 1,
|
||||
max_start_marker_count: 3,
|
||||
min_end_marker_count: 1,
|
||||
max_end_marker_count: 3,
|
||||
min_levels: 1,
|
||||
max_levels: 1,
|
||||
active_level_index: 0,
|
||||
trap_frequency_percent: 30,
|
||||
min_traps_per_area: 1,
|
||||
max_traps_per_area: 3,
|
||||
monster_frequency_percent: 30,
|
||||
min_monsters_per_area: 1,
|
||||
max_monsters_per_area: 3,
|
||||
min_stair_width: 2,
|
||||
max_stair_width: 3,
|
||||
min_stair_height: 2,
|
||||
max_stair_height: 3,
|
||||
min_stairs_per_level: 1,
|
||||
max_stairs_per_level: 2,
|
||||
sync_stairs_across_levels: false,
|
||||
export_level_index: 0,
|
||||
active_tab: Tab::Generate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AddTool {
|
||||
None,
|
||||
Room,
|
||||
Corridor,
|
||||
Archway,
|
||||
Door,
|
||||
LockedDoor,
|
||||
SecretDoor,
|
||||
Text,
|
||||
StartMarker,
|
||||
EndMarker,
|
||||
TrapMarker,
|
||||
MonsterMarker,
|
||||
Staircase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SidePanelResult {
|
||||
pub settings_changed: bool,
|
||||
pub reset_clicked: bool,
|
||||
pub clear_clicked: bool,
|
||||
pub export_clicked: bool,
|
||||
pub save_clicked: bool,
|
||||
pub load_clicked: bool,
|
||||
pub new_level_clicked: bool,
|
||||
}
|
||||
|
||||
pub fn draw_side_panel(
|
||||
ctx: &egui::Context,
|
||||
settings: &mut UiSettings,
|
||||
export_progress: Option<(usize, usize)>,
|
||||
num_levels: usize,
|
||||
) -> SidePanelResult {
|
||||
let mut result = SidePanelResult::default();
|
||||
|
||||
egui::SidePanel::left("options_panel")
|
||||
.resizable(true)
|
||||
.min_width(180.0)
|
||||
.default_width(280.0)
|
||||
.show_separator_line(true)
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::Generate, "Generate")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::Generate;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::Layout, "Layout")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::Layout;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::StartAndEnd;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(
|
||||
settings.active_tab == Tab::MonstersAndTraps,
|
||||
"Monsters/Traps",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::MonstersAndTraps;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::Add, "Add")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::Add;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
match settings.active_tab {
|
||||
Tab::Generate => tabs::draw_generate_tab(
|
||||
ui,
|
||||
settings,
|
||||
&mut result,
|
||||
export_progress,
|
||||
num_levels,
|
||||
),
|
||||
Tab::Layout => tabs::draw_layout_tab(ui, settings, &mut result),
|
||||
Tab::StartAndEnd => tabs::draw_start_and_end_tab(ui, settings, &mut result),
|
||||
Tab::MonstersAndTraps => {
|
||||
tabs::draw_monsters_and_traps_tab(ui, settings, &mut result)
|
||||
}
|
||||
Tab::Add => tabs::draw_add_tab(ui, settings, &mut result),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn draw_legend_panel(ctx: &egui::Context, settings: &mut UiSettings) -> bool {
|
||||
let mut composition_mode_changed = false;
|
||||
use egui::Color32;
|
||||
use widgets::LegendStyle;
|
||||
|
||||
egui::SidePanel::right("legend_panel")
|
||||
.resizable(false)
|
||||
.min_width(180.0)
|
||||
.default_width(200.0)
|
||||
.show_separator_line(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Modes");
|
||||
ui.add_space(6.0);
|
||||
ui.checkbox(&mut settings.colorblind_mode, "Colorblind Mode");
|
||||
composition_mode_changed = ui
|
||||
.checkbox(&mut settings.composition_mode, "Composition Mode")
|
||||
.changed();
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.heading("Legend");
|
||||
ui.add_space(8.0);
|
||||
if settings.colorblind_mode {
|
||||
widgets::draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
||||
widgets::draw_legend_entry_colorblind(
|
||||
ui,
|
||||
"Secret Doors",
|
||||
LegendStyle::DashDotDotLine,
|
||||
);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
||||
widgets::draw_legend_entry_colorblind(
|
||||
ui,
|
||||
"Stairs",
|
||||
LegendStyle::LabelChar('\u{1F5CF}'),
|
||||
);
|
||||
widgets::draw_legend_entry_colorblind(ui, "Start Marker", LegendStyle::Label("S"));
|
||||
widgets::draw_legend_entry_colorblind(ui, "End Marker", LegendStyle::Label("E"));
|
||||
widgets::draw_legend_entry_colorblind(ui, "Trap", LegendStyle::Label("T"));
|
||||
widgets::draw_legend_entry_colorblind(ui, "Monster", LegendStyle::Label("M"));
|
||||
} else {
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
|
||||
widgets::draw_legend_entry(ui, Color32::BLACK, "Walls");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 150, 50), "Stairs");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(60, 220, 200), "Start Marker");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(240, 90, 90), "End Marker");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(150, 80, 230), "Trap");
|
||||
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 50, 50), "Monster");
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.heading("Keybinds");
|
||||
ui.add_space(6.0);
|
||||
ui.label("• Delete / Backspace: Remove hovered room, corridor, door, text, or marker");
|
||||
ui.label("• Ctrl+Z: Undo");
|
||||
ui.label("• Ctrl+Y: Redo");
|
||||
ui.label("• Left click + drag: Move hovered room");
|
||||
ui.label("• Right click: Cancel add tool");
|
||||
ui.label("• Right click + drag: Resize hovered room");
|
||||
});
|
||||
|
||||
composition_mode_changed
|
||||
}
|
||||
|
||||
pub fn draw_level_tabs(
|
||||
ui: &mut egui::Ui,
|
||||
level_count: usize,
|
||||
active_level_index: &mut usize,
|
||||
) -> Option<usize> {
|
||||
let mut hovered_level_idx = None;
|
||||
ui.horizontal(|ui| {
|
||||
for level_idx in 0..level_count {
|
||||
let label = format!("Level {}", level_idx + 1);
|
||||
let is_active = *active_level_index == level_idx;
|
||||
let response = ui.selectable_label(is_active, label);
|
||||
if response.hovered() {
|
||||
hovered_level_idx = Some(level_idx);
|
||||
}
|
||||
if response.clicked() {
|
||||
*active_level_index = level_idx;
|
||||
}
|
||||
}
|
||||
});
|
||||
hovered_level_idx
|
||||
}
|
||||
+8
-706
@@ -1,433 +1,10 @@
|
||||
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
|
||||
use crate::seed;
|
||||
use eframe::egui;
|
||||
use egui::{Color32, RichText, Stroke};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum Tab {
|
||||
Generate,
|
||||
Layout,
|
||||
StartAndEnd,
|
||||
MonstersAndTraps,
|
||||
Add,
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
// Start with the Generate tab selected.
|
||||
fn default() -> Self {
|
||||
Self::Generate
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExportFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
Svg,
|
||||
Folder,
|
||||
}
|
||||
|
||||
impl Default for ExportFormat {
|
||||
// Default export format is PNG.
|
||||
fn default() -> Self {
|
||||
Self::Png
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportFormat {
|
||||
// Return the file extension for this export format.
|
||||
pub fn extension(self) -> &'static str {
|
||||
match self {
|
||||
ExportFormat::Png => "png",
|
||||
ExportFormat::Jpeg => "jpeg",
|
||||
ExportFormat::Webp => "webp",
|
||||
ExportFormat::Svg => "svg",
|
||||
ExportFormat::Folder => "folder",
|
||||
}
|
||||
}
|
||||
|
||||
// Return the UI label for this export format.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ExportFormat::Png => ".png",
|
||||
ExportFormat::Jpeg => ".jpeg",
|
||||
ExportFormat::Webp => ".webp",
|
||||
ExportFormat::Svg => ".svg",
|
||||
ExportFormat::Folder => "Folder (Composite)",
|
||||
}
|
||||
}
|
||||
|
||||
// Report whether this export format supports a raster resolution.
|
||||
pub fn supports_resolution(self) -> bool {
|
||||
!matches!(self, ExportFormat::Svg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MaskFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl Default for MaskFormat {
|
||||
// Default mask format is PNG.
|
||||
fn default() -> Self {
|
||||
Self::Png
|
||||
}
|
||||
}
|
||||
|
||||
impl MaskFormat {
|
||||
// Return the file extension for this mask format.
|
||||
pub fn extension(self) -> &'static str {
|
||||
match self {
|
||||
MaskFormat::Png => "png",
|
||||
MaskFormat::Jpeg => "jpeg",
|
||||
MaskFormat::Webp => "webp",
|
||||
}
|
||||
}
|
||||
|
||||
// Return the UI label for this mask format.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
MaskFormat::Png => ".png",
|
||||
MaskFormat::Jpeg => ".jpeg",
|
||||
MaskFormat::Webp => ".webp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct UiSettings {
|
||||
pub seed: u64,
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
pub room_count: usize,
|
||||
pub min_room_size: usize,
|
||||
pub max_room_size: usize,
|
||||
pub square_rooms_only: bool,
|
||||
pub min_corridor_width: usize,
|
||||
pub max_corridor_width: usize,
|
||||
pub corridor_randomness: usize,
|
||||
pub dead_end_rooms_percent: usize,
|
||||
pub pack_rooms_without_corridors: bool,
|
||||
pub door_frequency_percent: usize,
|
||||
pub room_hallway_door_percent: usize,
|
||||
pub locked_door_percent: usize,
|
||||
pub secret_door_percent: usize,
|
||||
pub allow_middle_corridor_doors: bool,
|
||||
pub windows_enabled: bool,
|
||||
pub min_window_width: usize,
|
||||
pub max_window_width: usize,
|
||||
pub window_frequency_percent: usize,
|
||||
pub room_hallway_window_percent: usize,
|
||||
pub allow_internal_windows: bool,
|
||||
pub colorblind_mode: bool,
|
||||
pub composition_mode: bool,
|
||||
pub export_format: ExportFormat,
|
||||
pub mask_format: MaskFormat,
|
||||
pub export_width: u32,
|
||||
pub export_height: u32,
|
||||
pub allow_export_aspect_change: bool,
|
||||
pub export_show_grid: bool,
|
||||
pub last_export_path: Option<String>,
|
||||
pub add_tool: AddTool,
|
||||
pub add_text_value: String,
|
||||
pub add_text_font_size: u16,
|
||||
pub add_room_width: usize,
|
||||
pub add_room_height: usize,
|
||||
pub add_corridor_width: usize,
|
||||
pub add_stair_width: usize,
|
||||
pub add_stair_height: usize,
|
||||
pub add_marker_size: usize,
|
||||
pub min_start_marker_size: usize,
|
||||
pub max_start_marker_size: usize,
|
||||
pub min_end_marker_size: usize,
|
||||
pub max_end_marker_size: usize,
|
||||
pub min_start_marker_count: usize,
|
||||
pub max_start_marker_count: usize,
|
||||
pub min_end_marker_count: usize,
|
||||
pub max_end_marker_count: usize,
|
||||
pub min_levels: usize,
|
||||
pub max_levels: usize,
|
||||
pub active_level_index: usize,
|
||||
pub export_level_index: usize,
|
||||
pub trap_frequency_percent: usize,
|
||||
pub min_traps_per_area: usize,
|
||||
pub max_traps_per_area: usize,
|
||||
pub monster_frequency_percent: usize,
|
||||
pub min_monsters_per_area: usize,
|
||||
pub max_monsters_per_area: usize,
|
||||
pub min_stair_width: usize,
|
||||
pub max_stair_width: usize,
|
||||
pub min_stair_height: usize,
|
||||
pub max_stair_height: usize,
|
||||
pub min_stairs_per_level: usize,
|
||||
pub max_stairs_per_level: usize,
|
||||
pub sync_stairs_across_levels: bool,
|
||||
active_tab: Tab,
|
||||
}
|
||||
|
||||
impl Default for UiSettings {
|
||||
fn default() -> Self {
|
||||
UiSettings {
|
||||
seed: 0,
|
||||
cols: 32,
|
||||
rows: 32,
|
||||
room_count: 8,
|
||||
min_room_size: 3,
|
||||
max_room_size: 8,
|
||||
square_rooms_only: true,
|
||||
min_corridor_width: 1,
|
||||
max_corridor_width: 3,
|
||||
corridor_randomness: 50,
|
||||
dead_end_rooms_percent: 20,
|
||||
pack_rooms_without_corridors: false,
|
||||
door_frequency_percent: 50,
|
||||
room_hallway_door_percent: 20,
|
||||
locked_door_percent: 10,
|
||||
secret_door_percent: 5,
|
||||
allow_middle_corridor_doors: false,
|
||||
windows_enabled: false,
|
||||
min_window_width: 2,
|
||||
max_window_width: 4,
|
||||
window_frequency_percent: 30,
|
||||
room_hallway_window_percent: 10,
|
||||
allow_internal_windows: false,
|
||||
colorblind_mode: false,
|
||||
composition_mode: false,
|
||||
export_format: ExportFormat::Png,
|
||||
mask_format: MaskFormat::Png,
|
||||
export_width: 0,
|
||||
export_height: 0,
|
||||
allow_export_aspect_change: true,
|
||||
export_show_grid: false,
|
||||
last_export_path: None,
|
||||
add_tool: AddTool::None,
|
||||
add_text_value: String::from("Text"),
|
||||
add_text_font_size: 18,
|
||||
add_room_width: 5,
|
||||
add_room_height: 5,
|
||||
add_corridor_width: 2,
|
||||
add_stair_width: 2,
|
||||
add_stair_height: 2,
|
||||
add_marker_size: 1,
|
||||
min_start_marker_size: 1,
|
||||
max_start_marker_size: 3,
|
||||
min_end_marker_size: 1,
|
||||
max_end_marker_size: 3,
|
||||
min_start_marker_count: 1,
|
||||
max_start_marker_count: 3,
|
||||
min_end_marker_count: 1,
|
||||
max_end_marker_count: 3,
|
||||
min_levels: 1,
|
||||
max_levels: 1,
|
||||
active_level_index: 0,
|
||||
trap_frequency_percent: 30,
|
||||
min_traps_per_area: 1,
|
||||
max_traps_per_area: 3,
|
||||
monster_frequency_percent: 30,
|
||||
min_monsters_per_area: 1,
|
||||
max_monsters_per_area: 3,
|
||||
min_stair_width: 2,
|
||||
max_stair_width: 3,
|
||||
min_stair_height: 2,
|
||||
max_stair_height: 3,
|
||||
min_stairs_per_level: 1,
|
||||
max_stairs_per_level: 2,
|
||||
sync_stairs_across_levels: false,
|
||||
export_level_index: 0,
|
||||
active_tab: Tab::Generate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AddTool {
|
||||
None,
|
||||
Room,
|
||||
Corridor,
|
||||
Archway,
|
||||
Door,
|
||||
LockedDoor,
|
||||
SecretDoor,
|
||||
Text,
|
||||
StartMarker,
|
||||
EndMarker,
|
||||
TrapMarker,
|
||||
MonsterMarker,
|
||||
Staircase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SidePanelResult {
|
||||
pub settings_changed: bool,
|
||||
pub reset_clicked: bool,
|
||||
pub clear_clicked: bool,
|
||||
pub export_clicked: bool,
|
||||
pub save_clicked: bool,
|
||||
pub load_clicked: bool,
|
||||
pub new_level_clicked: bool,
|
||||
}
|
||||
|
||||
pub fn draw_side_panel(
|
||||
ctx: &egui::Context,
|
||||
settings: &mut UiSettings,
|
||||
export_progress: Option<(usize, usize)>,
|
||||
num_levels: usize,
|
||||
) -> SidePanelResult {
|
||||
// Render the left side panel and return user interaction results.
|
||||
let mut result = SidePanelResult::default();
|
||||
|
||||
egui::SidePanel::left("options_panel")
|
||||
.resizable(true)
|
||||
.min_width(180.0)
|
||||
.default_width(280.0)
|
||||
.show_separator_line(true)
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let is_generate = settings.active_tab == Tab::Generate;
|
||||
let is_layout = settings.active_tab == Tab::Layout;
|
||||
|
||||
if ui.selectable_label(is_generate, "Generate").clicked() {
|
||||
settings.active_tab = Tab::Generate;
|
||||
}
|
||||
if ui.selectable_label(is_layout, "Layout").clicked() {
|
||||
settings.active_tab = Tab::Layout;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::StartAndEnd;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(
|
||||
settings.active_tab == Tab::MonstersAndTraps,
|
||||
"Monsters/Traps",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::MonstersAndTraps;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(settings.active_tab == Tab::Add, "Add")
|
||||
.clicked()
|
||||
{
|
||||
settings.active_tab = Tab::Add;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
match settings.active_tab {
|
||||
Tab::Generate => {
|
||||
draw_generate_tab(ui, settings, &mut result, export_progress, num_levels)
|
||||
}
|
||||
Tab::Layout => draw_layout_tab(ui, settings, &mut result),
|
||||
Tab::StartAndEnd => draw_start_and_end_tab(ui, settings, &mut result),
|
||||
Tab::MonstersAndTraps => draw_monsters_and_traps_tab(ui, settings, &mut result),
|
||||
Tab::Add => draw_add_tab(ui, settings, &mut result),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// Draw mode controls and the legend panel.
|
||||
pub fn draw_legend_panel(ctx: &egui::Context, settings: &mut UiSettings) -> bool {
|
||||
let mut composition_mode_changed = false;
|
||||
|
||||
egui::SidePanel::right("legend_panel")
|
||||
.resizable(false)
|
||||
.min_width(180.0)
|
||||
.default_width(200.0)
|
||||
.show_separator_line(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Modes");
|
||||
ui.add_space(6.0);
|
||||
ui.checkbox(&mut settings.colorblind_mode, "Colorblind Mode");
|
||||
composition_mode_changed = ui
|
||||
.checkbox(&mut settings.composition_mode, "Composition Mode")
|
||||
.changed();
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.heading("Legend");
|
||||
ui.add_space(8.0);
|
||||
if settings.colorblind_mode {
|
||||
draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
|
||||
draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
|
||||
draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
|
||||
draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
|
||||
draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
||||
draw_legend_entry_colorblind(ui, "Secret Doors", LegendStyle::DashDotDotLine);
|
||||
draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
||||
draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
||||
draw_legend_entry_colorblind(ui, "Stairs", LegendStyle::LabelChar('\u{1F5CF}'));
|
||||
draw_legend_entry_colorblind(ui, "Start Marker", LegendStyle::Label("S"));
|
||||
draw_legend_entry_colorblind(ui, "End Marker", LegendStyle::Label("E"));
|
||||
draw_legend_entry_colorblind(ui, "Trap", LegendStyle::Label("T"));
|
||||
draw_legend_entry_colorblind(ui, "Monster", LegendStyle::Label("M"));
|
||||
} else {
|
||||
draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
||||
draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
|
||||
draw_legend_entry(ui, Color32::BLACK, "Walls");
|
||||
draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors");
|
||||
draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors");
|
||||
draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors");
|
||||
draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways");
|
||||
draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows");
|
||||
draw_legend_entry(ui, Color32::from_rgb(200, 150, 50), "Stairs");
|
||||
draw_legend_entry(ui, Color32::from_rgb(60, 220, 200), "Start Marker");
|
||||
draw_legend_entry(ui, Color32::from_rgb(240, 90, 90), "End Marker");
|
||||
draw_legend_entry(ui, Color32::from_rgb(150, 80, 230), "Trap");
|
||||
draw_legend_entry(ui, Color32::from_rgb(200, 50, 50), "Monster");
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.heading("Keybinds");
|
||||
ui.add_space(6.0);
|
||||
ui.label("• Delete / Backspace: Remove hovered room, corridor, door, text, or marker");
|
||||
ui.label("• Ctrl+Z: Undo");
|
||||
ui.label("• Ctrl+Y: Redo");
|
||||
ui.label("• Left click + drag: Move hovered room");
|
||||
ui.label("• Right click: Cancel add tool");
|
||||
ui.label("• Right click + drag: Resize hovered room");
|
||||
});
|
||||
|
||||
composition_mode_changed
|
||||
}
|
||||
|
||||
// Draw level tabs and return the hovered tab index.
|
||||
pub fn draw_level_tabs(
|
||||
ui: &mut egui::Ui,
|
||||
level_count: usize,
|
||||
active_level_index: &mut usize,
|
||||
) -> Option<usize> {
|
||||
let mut hovered_level_idx = None;
|
||||
ui.horizontal(|ui| {
|
||||
for level_idx in 0..level_count {
|
||||
let label = format!("Level {}", level_idx + 1);
|
||||
let is_active = *active_level_index == level_idx;
|
||||
let response = ui.selectable_label(is_active, label);
|
||||
if response.hovered() {
|
||||
hovered_level_idx = Some(level_idx);
|
||||
}
|
||||
if response.clicked() {
|
||||
*active_level_index = level_idx;
|
||||
}
|
||||
}
|
||||
});
|
||||
hovered_level_idx
|
||||
}
|
||||
use egui::RichText;
|
||||
|
||||
// Render the Generate tab controls.
|
||||
fn draw_generate_tab(
|
||||
pub fn draw_generate_tab(
|
||||
ui: &mut egui::Ui,
|
||||
settings: &mut UiSettings,
|
||||
result: &mut SidePanelResult,
|
||||
@@ -649,7 +226,7 @@ fn draw_generate_tab(
|
||||
}
|
||||
|
||||
// Render the Layout tab controls.
|
||||
fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||
pub fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||
ui.label(RichText::new("Levels").strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
@@ -1075,7 +652,7 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
|
||||
}
|
||||
|
||||
// Render the Add tab controls.
|
||||
fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||
pub fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||
ui.label(RichText::new("Add Tools").strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
@@ -1203,282 +780,7 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SideP
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a colored legend entry.
|
||||
fn draw_legend_entry(ui: &mut egui::Ui, color: Color32, label: &str) {
|
||||
ui.horizontal(|ui| {
|
||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(rect, 0.0, color);
|
||||
ui.painter().rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(1.0, Color32::WHITE),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label(label);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum LegendStyle {
|
||||
Crosshatch,
|
||||
Dots,
|
||||
SolidLine,
|
||||
LongDash,
|
||||
ShortDash,
|
||||
DottedLine,
|
||||
DashDotLine,
|
||||
DashDotDotLine,
|
||||
Label(&'static str),
|
||||
LabelChar(char),
|
||||
}
|
||||
|
||||
// Draw a patterned legend entry.
|
||||
fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendStyle) {
|
||||
ui.horizontal(|ui| {
|
||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(rect, 0.0, Color32::from_gray(220));
|
||||
ui.painter().rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(1.0, Color32::BLACK),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
|
||||
match style {
|
||||
LegendStyle::Crosshatch => {
|
||||
ui.painter().line_segment(
|
||||
[rect.left_top(), rect.right_bottom()],
|
||||
Stroke::new(1.2, Color32::BLACK),
|
||||
);
|
||||
ui.painter().line_segment(
|
||||
[rect.right_top(), rect.left_bottom()],
|
||||
Stroke::new(1.2, Color32::BLACK),
|
||||
);
|
||||
}
|
||||
LegendStyle::Dots => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 2.0, Color32::BLACK);
|
||||
ui.painter().circle_filled(
|
||||
egui::pos2(rect.center().x - 4.0, rect.center().y),
|
||||
1.4,
|
||||
Color32::BLACK,
|
||||
);
|
||||
ui.painter().circle_filled(
|
||||
egui::pos2(rect.center().x + 4.0, rect.center().y),
|
||||
1.4,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::SolidLine => {
|
||||
ui.painter().line_segment(
|
||||
[
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
],
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
);
|
||||
}
|
||||
LegendStyle::Label(prefix) => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||
ui.painter()
|
||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||
let font_size = 10.0;
|
||||
let font_id = egui::FontId::monospace(font_size);
|
||||
let text_width = prefix.len() as f32 * font_size * 0.6;
|
||||
let text_height = font_size * 0.8;
|
||||
let text_pos = egui::pos2(
|
||||
rect.center().x - text_width / 2.0,
|
||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||
);
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
prefix,
|
||||
font_id,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::LabelChar(character) => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||
ui.painter()
|
||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||
let font_size = 10.0;
|
||||
let font_id = egui::FontId::monospace(font_size);
|
||||
let text_width = font_size * 0.6;
|
||||
let text_height = font_size * 0.8;
|
||||
let text_pos = egui::pos2(
|
||||
rect.center().x - text_width / 2.0,
|
||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||
);
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
character,
|
||||
font_id,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::LongDash => draw_dashed_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
6.0,
|
||||
3.0,
|
||||
),
|
||||
LegendStyle::ShortDash => draw_dashed_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
3.0,
|
||||
2.0,
|
||||
),
|
||||
LegendStyle::DottedLine => draw_dotted_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
LegendStyle::DashDotLine => draw_dash_dot_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
LegendStyle::DashDotDotLine => draw_dash_dot_dot_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.label(label);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
// Draw a dashed line.
|
||||
fn draw_dashed_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
dash_len: f32,
|
||||
gap_len: f32,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let seg_start = dist;
|
||||
let seg_end = (dist + dash_len).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
||||
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist += dash_len + gap_len;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a dotted line.
|
||||
fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let step = 5.0_f32;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist <= len {
|
||||
let point = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(point, radius, stroke.color);
|
||||
dist += step;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_dash_dot_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_dash_dot_dot_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot1, radius, stroke.color);
|
||||
dist += 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot2, radius, stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_start_and_end_tab(
|
||||
pub fn draw_start_and_end_tab(
|
||||
ui: &mut egui::Ui,
|
||||
settings: &mut UiSettings,
|
||||
result: &mut SidePanelResult,
|
||||
@@ -1560,7 +862,7 @@ fn draw_start_and_end_tab(
|
||||
ui.label("Markers are generated inside rooms. Matching start/end indices are paired as far apart as possible.");
|
||||
}
|
||||
|
||||
fn draw_monsters_and_traps_tab(
|
||||
pub fn draw_monsters_and_traps_tab(
|
||||
ui: &mut egui::Ui,
|
||||
settings: &mut UiSettings,
|
||||
result: &mut SidePanelResult,
|
||||
@@ -1623,7 +925,7 @@ fn draw_monsters_and_traps_tab(
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_usize_slider_row(
|
||||
pub fn draw_usize_slider_row(
|
||||
ui: &mut egui::Ui,
|
||||
result: &mut SidePanelResult,
|
||||
label: &str,
|
||||
@@ -0,0 +1,282 @@
|
||||
use eframe::egui;
|
||||
use egui::{Color32, Stroke};
|
||||
|
||||
// Draw a colored legend entry.
|
||||
pub fn draw_legend_entry(ui: &mut egui::Ui, color: Color32, label: &str) {
|
||||
ui.horizontal(|ui| {
|
||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(rect, 0.0, color);
|
||||
ui.painter().rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(1.0, Color32::WHITE),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label(label);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LegendStyle {
|
||||
Crosshatch,
|
||||
Dots,
|
||||
SolidLine,
|
||||
LongDash,
|
||||
ShortDash,
|
||||
DottedLine,
|
||||
DashDotLine,
|
||||
DashDotDotLine,
|
||||
Label(&'static str),
|
||||
LabelChar(char),
|
||||
}
|
||||
|
||||
// Draw a patterned legend entry.
|
||||
pub fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendStyle) {
|
||||
ui.horizontal(|ui| {
|
||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
ui.painter().rect_filled(rect, 0.0, Color32::from_gray(220));
|
||||
ui.painter().rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(1.0, Color32::BLACK),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
|
||||
match style {
|
||||
LegendStyle::Crosshatch => {
|
||||
ui.painter().line_segment(
|
||||
[rect.left_top(), rect.right_bottom()],
|
||||
Stroke::new(1.2, Color32::BLACK),
|
||||
);
|
||||
ui.painter().line_segment(
|
||||
[rect.right_top(), rect.left_bottom()],
|
||||
Stroke::new(1.2, Color32::BLACK),
|
||||
);
|
||||
}
|
||||
LegendStyle::Dots => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 2.0, Color32::BLACK);
|
||||
ui.painter().circle_filled(
|
||||
egui::pos2(rect.center().x - 4.0, rect.center().y),
|
||||
1.4,
|
||||
Color32::BLACK,
|
||||
);
|
||||
ui.painter().circle_filled(
|
||||
egui::pos2(rect.center().x + 4.0, rect.center().y),
|
||||
1.4,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::SolidLine => {
|
||||
ui.painter().line_segment(
|
||||
[
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
],
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
);
|
||||
}
|
||||
LegendStyle::Label(prefix) => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||
ui.painter()
|
||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||
let font_size = 10.0;
|
||||
let font_id = egui::FontId::monospace(font_size);
|
||||
let text_width = prefix.len() as f32 * font_size * 0.6;
|
||||
let text_height = font_size * 0.8;
|
||||
let text_pos = egui::pos2(
|
||||
rect.center().x - text_width / 2.0,
|
||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||
);
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
prefix,
|
||||
font_id,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::LabelChar(character) => {
|
||||
ui.painter()
|
||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||
ui.painter()
|
||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||
let font_size = 10.0;
|
||||
let font_id = egui::FontId::monospace(font_size);
|
||||
let text_width = font_size * 0.6;
|
||||
let text_height = font_size * 0.8;
|
||||
let text_pos = egui::pos2(
|
||||
rect.center().x - text_width / 2.0,
|
||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||
);
|
||||
ui.painter().text(
|
||||
text_pos,
|
||||
egui::Align2::LEFT_TOP,
|
||||
character,
|
||||
font_id,
|
||||
Color32::BLACK,
|
||||
);
|
||||
}
|
||||
LegendStyle::LongDash => draw_dashed_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
6.0,
|
||||
3.0,
|
||||
),
|
||||
LegendStyle::ShortDash => draw_dashed_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
3.0,
|
||||
2.0,
|
||||
),
|
||||
LegendStyle::DottedLine => draw_dotted_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
LegendStyle::DashDotLine => draw_dash_dot_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
LegendStyle::DashDotDotLine => draw_dash_dot_dot_line(
|
||||
ui.painter(),
|
||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||
Stroke::new(2.0, Color32::BLACK),
|
||||
),
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.label(label);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
// Draw a dashed line.
|
||||
pub fn draw_dashed_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
dash_len: f32,
|
||||
gap_len: f32,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let seg_start = dist;
|
||||
let seg_end = (dist + dash_len).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
||||
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist += dash_len + gap_len;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a dotted line.
|
||||
pub fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let step = 5.0_f32;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
|
||||
let mut dist = 0.0_f32;
|
||||
while dist <= len {
|
||||
let point = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(point, radius, stroke.color);
|
||||
dist += step;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dash_dot_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dash_dot_dot_line(
|
||||
painter: &egui::Painter,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let dx = to.x - from.x;
|
||||
let dy = to.y - from.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let radius = (stroke.width * 0.35).max(1.0);
|
||||
let mut dist = 0.0_f32;
|
||||
while dist < len {
|
||||
let dash_end = (dist + 7.0).min(len);
|
||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||
painter.line_segment([p0, p1], stroke);
|
||||
dist = dash_end + 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot1, radius, stroke.color);
|
||||
dist += 3.0;
|
||||
if dist >= len {
|
||||
break;
|
||||
}
|
||||
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||
painter.circle_filled(dot2, radius, stroke.color);
|
||||
dist += 4.0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user