added logic for doors
This commit is contained in:
@@ -18,6 +18,11 @@ A Rust desktop app for generating simple tabletop dungeon layouts.
|
|||||||
- Corridor controls:
|
- Corridor controls:
|
||||||
- `Corridor Randomness (%)` from `0` to `100`
|
- `Corridor Randomness (%)` from `0` to `100`
|
||||||
- `Dead-End Rooms (%)` from `0` to `50`
|
- `Dead-End Rooms (%)` from `0` to `50`
|
||||||
|
- Door controls:
|
||||||
|
- `Door Frequency (%)`
|
||||||
|
- `Room/Hallway Door Chance (%)`
|
||||||
|
- `Locked Door Chance (%)`
|
||||||
|
- `Allow Middle Corridor Doors` toggle
|
||||||
- Generate control:
|
- Generate control:
|
||||||
- Master seed input (`u64`)
|
- Master seed input (`u64`)
|
||||||
- `Random` seed button
|
- `Random` seed button
|
||||||
@@ -28,6 +33,10 @@ A Rust desktop app for generating simple tabletop dungeon layouts.
|
|||||||
- Corridors are stored separately as vector line segments between grid cells.
|
- Corridors are stored separately as vector line segments between grid cells.
|
||||||
- Every generated room is connected into one navigable network when possible.
|
- Every generated room is connected into one navigable network when possible.
|
||||||
- A master seed drives deterministic generation, and internal subsystems derive their own sub-seeds from it.
|
- A master seed drives deterministic generation, and internal subsystems derive their own sub-seeds from it.
|
||||||
|
- Doors are generated from corridor adjacency:
|
||||||
|
- Open doors are rendered in red
|
||||||
|
- Locked doors are rendered in green
|
||||||
|
- Doors are rendered as line segments on grid boundaries
|
||||||
|
|
||||||
### Corridor Randomness
|
### Corridor Randomness
|
||||||
|
|
||||||
|
|||||||
+104
-4
@@ -25,10 +25,27 @@ pub struct Corridor {
|
|||||||
pub path: Vec<(usize, usize)>,
|
pub path: Vec<(usize, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Door {
|
||||||
|
pub from: (usize, usize),
|
||||||
|
pub to: (usize, usize),
|
||||||
|
pub locked: bool,
|
||||||
|
pub archway: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct DoorSettings {
|
||||||
|
pub frequency_percent: usize,
|
||||||
|
pub room_hallway_percent: usize,
|
||||||
|
pub locked_percent: usize,
|
||||||
|
pub allow_middle_corridor_doors: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct DungeonLayout {
|
pub struct DungeonLayout {
|
||||||
pub rooms: Vec<Room>,
|
pub rooms: Vec<Room>,
|
||||||
pub corridors: Vec<Corridor>,
|
pub corridors: Vec<Corridor>,
|
||||||
|
pub doors: Vec<Door>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_layout(
|
pub fn generate_layout(
|
||||||
@@ -41,6 +58,7 @@ pub fn generate_layout(
|
|||||||
square_rooms_only: bool,
|
square_rooms_only: bool,
|
||||||
corridor_randomness_percent: usize,
|
corridor_randomness_percent: usize,
|
||||||
dead_end_room_percent: usize,
|
dead_end_room_percent: usize,
|
||||||
|
door_settings: DoorSettings,
|
||||||
) -> DungeonLayout {
|
) -> DungeonLayout {
|
||||||
let layout_salt = ((cols as u64) << 48)
|
let layout_salt = ((cols as u64) << 48)
|
||||||
^ ((rows as u64) << 32)
|
^ ((rows as u64) << 32)
|
||||||
@@ -58,7 +76,11 @@ pub fn generate_layout(
|
|||||||
let mut corridors = Vec::new();
|
let mut corridors = Vec::new();
|
||||||
|
|
||||||
if cols < 2 || rows < 2 || target_room_count == 0 {
|
if cols < 2 || rows < 2 || target_room_count == 0 {
|
||||||
return DungeonLayout { rooms, corridors };
|
return DungeonLayout {
|
||||||
|
rooms,
|
||||||
|
corridors,
|
||||||
|
doors: Vec::new(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut min_size = min_room_size.max(2);
|
let mut min_size = min_room_size.max(2);
|
||||||
@@ -74,7 +96,11 @@ pub fn generate_layout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if min_size == 0 || max_size < min_size {
|
if min_size == 0 || max_size < min_size {
|
||||||
return DungeonLayout { rooms, corridors };
|
return DungeonLayout {
|
||||||
|
rooms,
|
||||||
|
corridors,
|
||||||
|
doors: Vec::new(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_attempts = target_room_count.saturating_mul(40).max(50);
|
let max_attempts = target_room_count.saturating_mul(40).max(50);
|
||||||
@@ -121,7 +147,11 @@ pub fn generate_layout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rooms.len() < 2 {
|
if rooms.len() < 2 {
|
||||||
return DungeonLayout { rooms, corridors };
|
return DungeonLayout {
|
||||||
|
rooms,
|
||||||
|
corridors,
|
||||||
|
doors: Vec::new(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let randomness = (corridor_randomness_percent.min(100) as f32) / 100.0;
|
let randomness = (corridor_randomness_percent.min(100) as f32) / 100.0;
|
||||||
@@ -162,7 +192,64 @@ pub fn generate_layout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DungeonLayout { rooms, corridors }
|
let mut layout = DungeonLayout {
|
||||||
|
rooms,
|
||||||
|
corridors,
|
||||||
|
doors: Vec::new(),
|
||||||
|
};
|
||||||
|
apply_doors(&mut layout, seed, door_settings);
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) {
|
||||||
|
layout.doors.clear();
|
||||||
|
|
||||||
|
let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64));
|
||||||
|
let base = (settings.frequency_percent.min(100) as f32) / 100.0;
|
||||||
|
let room_hall = (settings.room_hallway_percent.min(100) as f32) / 100.0;
|
||||||
|
let locked = (settings.locked_percent.min(100) as f32) / 100.0;
|
||||||
|
|
||||||
|
let mut seen_edges = HashSet::new();
|
||||||
|
for corridor in &layout.corridors {
|
||||||
|
for pair in corridor.path.windows(2) {
|
||||||
|
if pair[0] == pair[1] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let edge = normalized_cell_edge(pair[0], pair[1]);
|
||||||
|
if !seen_edges.insert(edge) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let a_in_room = room_index_at_cell(&layout.rooms, edge.0).is_some();
|
||||||
|
let b_in_room = room_index_at_cell(&layout.rooms, edge.1).is_some();
|
||||||
|
let is_room_hallway = a_in_room ^ b_in_room;
|
||||||
|
let is_middle = !a_in_room && !b_in_room;
|
||||||
|
|
||||||
|
if is_room_hallway {
|
||||||
|
let door_chance = base * room_hall;
|
||||||
|
let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance;
|
||||||
|
|
||||||
|
layout.doors.push(Door {
|
||||||
|
from: edge.0,
|
||||||
|
to: edge.1,
|
||||||
|
locked: place_door && rng.next_f32() <= locked,
|
||||||
|
archway: !place_door,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_middle && settings.allow_middle_corridor_doors {
|
||||||
|
if base > 0.0 && rng.next_f32() <= base {
|
||||||
|
layout.doors.push(Door {
|
||||||
|
from: edge.0,
|
||||||
|
to: edge.1,
|
||||||
|
locked: rng.next_f32() <= locked,
|
||||||
|
archway: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_room_connection_edges(
|
fn build_room_connection_edges(
|
||||||
@@ -470,6 +557,19 @@ fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize {
|
|||||||
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_cell_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
|
||||||
|
if a <= b { (a, b) } else { (b, a) }
|
||||||
|
}
|
||||||
|
|
||||||
struct SimpleRng {
|
struct SimpleRng {
|
||||||
state: u64,
|
state: u64,
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-1
@@ -7,7 +7,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{Color32, Stroke};
|
use egui::{Color32, Stroke};
|
||||||
use layout::{DungeonLayout, shortest_path_cells};
|
use layout::{DoorSettings, DungeonLayout, shortest_path_cells};
|
||||||
use ui::{UiSettings, draw_side_panel};
|
use ui::{UiSettings, draw_side_panel};
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
@@ -39,6 +39,7 @@ impl Default for DungeonApp {
|
|||||||
settings.square_rooms_only,
|
settings.square_rooms_only,
|
||||||
settings.corridor_randomness,
|
settings.corridor_randomness,
|
||||||
settings.dead_end_rooms_percent,
|
settings.dead_end_rooms_percent,
|
||||||
|
door_settings_from_ui(&settings),
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
settings,
|
settings,
|
||||||
@@ -92,6 +93,7 @@ impl DungeonApp {
|
|||||||
self.settings.square_rooms_only,
|
self.settings.square_rooms_only,
|
||||||
self.settings.corridor_randomness,
|
self.settings.corridor_randomness,
|
||||||
self.settings.dead_end_rooms_percent,
|
self.settings.dead_end_rooms_percent,
|
||||||
|
door_settings_from_ui(&self.settings),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +187,7 @@ impl DungeonApp {
|
|||||||
self.layout.rooms[drag.room_idx].x = new_x;
|
self.layout.rooms[drag.room_idx].x = new_x;
|
||||||
self.layout.rooms[drag.room_idx].y = new_y;
|
self.layout.rooms[drag.room_idx].y = new_y;
|
||||||
self.reroute_corridors_for_room(drag.room_idx);
|
self.reroute_corridors_for_room(drag.room_idx);
|
||||||
|
self.refresh_doors();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reroute_corridors_for_room(&mut self, room_idx: usize) {
|
fn reroute_corridors_for_room(&mut self, room_idx: usize) {
|
||||||
@@ -253,6 +256,15 @@ impl DungeonApp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.layout.corridors[drag.corridor_index].path = new_path;
|
self.layout.corridors[drag.corridor_index].path = new_path;
|
||||||
|
self.refresh_doors();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_doors(&mut self) {
|
||||||
|
layout::apply_doors(
|
||||||
|
&mut self.layout,
|
||||||
|
self.settings.seed,
|
||||||
|
door_settings_from_ui(&self.settings),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +417,23 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
|
|||||||
painter.rect_filled(room_rect, 0.0, room_fill);
|
painter.rect_filled(room_rect, 0.0, room_fill);
|
||||||
painter.rect_stroke(room_rect, 0.0, outline_stroke, egui::StrokeKind::Middle);
|
painter.rect_stroke(room_rect, 0.0, outline_stroke, egui::StrokeKind::Middle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for door in &layout.doors {
|
||||||
|
let color = if door.archway {
|
||||||
|
Color32::from_rgb(70, 130, 220)
|
||||||
|
} else if door.locked {
|
||||||
|
Color32::from_rgb(80, 200, 120)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(220, 70, 70)
|
||||||
|
};
|
||||||
|
draw_door_line(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
door.from,
|
||||||
|
door.to,
|
||||||
|
Stroke::new(3.0, color),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cell_rect(geometry: &GridGeometry, col: usize, row: usize) -> egui::Rect {
|
fn cell_rect(geometry: &GridGeometry, col: usize, row: usize) -> egui::Rect {
|
||||||
@@ -495,3 +524,38 @@ fn simplify_path_loops(path: &mut Vec<(usize, usize)>) {
|
|||||||
}
|
}
|
||||||
*path = out;
|
*path = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
allow_middle_corridor_doors: settings.allow_middle_corridor_doors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_door_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
a: (usize, usize),
|
||||||
|
b: (usize, usize),
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.0 != b.0 {
|
||||||
|
let x = geometry.rect.left() + (a.0.max(b.0) as f32) * geometry.cell_size;
|
||||||
|
let row = a.1;
|
||||||
|
let y0 = geometry.rect.top() + row as f32 * geometry.cell_size;
|
||||||
|
let y1 = y0 + geometry.cell_size;
|
||||||
|
painter.line_segment([egui::pos2(x, y0), egui::pos2(x, y1)], stroke);
|
||||||
|
} else {
|
||||||
|
let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size;
|
||||||
|
let col = a.0;
|
||||||
|
let x0 = geometry.rect.left() + col as f32 * geometry.cell_size;
|
||||||
|
let x1 = x0 + geometry.cell_size;
|
||||||
|
painter.line_segment([egui::pos2(x0, y), egui::pos2(x1, y)], stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ pub struct UiSettings {
|
|||||||
pub square_rooms_only: bool,
|
pub square_rooms_only: bool,
|
||||||
pub corridor_randomness: usize,
|
pub corridor_randomness: usize,
|
||||||
pub dead_end_rooms_percent: usize,
|
pub dead_end_rooms_percent: usize,
|
||||||
|
pub door_frequency_percent: usize,
|
||||||
|
pub room_hallway_door_percent: usize,
|
||||||
|
pub locked_door_percent: usize,
|
||||||
|
pub allow_middle_corridor_doors: bool,
|
||||||
active_tab: Tab,
|
active_tab: Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +46,10 @@ impl Default for UiSettings {
|
|||||||
square_rooms_only: false,
|
square_rooms_only: false,
|
||||||
corridor_randomness: 0,
|
corridor_randomness: 0,
|
||||||
dead_end_rooms_percent: 0,
|
dead_end_rooms_percent: 0,
|
||||||
|
door_frequency_percent: 30,
|
||||||
|
room_hallway_door_percent: 70,
|
||||||
|
locked_door_percent: 25,
|
||||||
|
allow_middle_corridor_doors: false,
|
||||||
active_tab: Tab::Generate,
|
active_tab: Tab::Generate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,4 +235,65 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
|
|||||||
)
|
)
|
||||||
.changed();
|
.changed();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label(RichText::new("Door Settings").strong());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
ui.label("Door Frequency (%)");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(egui::Slider::new(&mut settings.door_frequency_percent, 0..=100).show_value(false))
|
||||||
|
.changed();
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::DragValue::new(&mut settings.door_frequency_percent)
|
||||||
|
.speed(1.0)
|
||||||
|
.range(0..=100),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label("Room/Hallway Door Chance (%)");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::Slider::new(&mut settings.room_hallway_door_percent, 0..=100)
|
||||||
|
.show_value(false),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::DragValue::new(&mut settings.room_hallway_door_percent)
|
||||||
|
.speed(1.0)
|
||||||
|
.range(0..=100),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label("Locked Door Chance (%)");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(egui::Slider::new(&mut settings.locked_door_percent, 0..=100).show_value(false))
|
||||||
|
.changed();
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::DragValue::new(&mut settings.locked_door_percent)
|
||||||
|
.speed(1.0)
|
||||||
|
.range(0..=100),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
result.settings_changed |= ui
|
||||||
|
.checkbox(
|
||||||
|
&mut settings.allow_middle_corridor_doors,
|
||||||
|
"Allow Middle Corridor Doors",
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user