use crate::editor::RUSTERIX;
use crate::hud::{Hud, HudMode};
use crate::prelude::*;
use MapEvent::*;
use ToolEvent::*;
use rusterix::Assets;
use rusterix::rebuild_generated_geometry;
pub struct DungeonTool {
id: TheId,
hud: Hud,
stroke_active: bool,
stroke_changed: bool,
stroke_prev_map: Option<Map>,
stroke_anchor: Option<Vec2<i32>>,
line_axis_horizontal: Option<bool>,
last_cell: Option<Vec2<i32>>,
}
impl Tool for DungeonTool {
fn new() -> Self
where
Self: Sized,
{
Self {
id: TheId::named("Dungeon Tool"),
hud: Hud::new(HudMode::Dungeon),
stroke_active: false,
stroke_changed: false,
stroke_prev_map: None,
stroke_anchor: None,
line_axis_horizontal: None,
last_cell: None,
}
}
fn id(&self) -> TheId {
self.id.clone()
}
fn info(&self) -> String {
fl!("tool_dungeon")
}
fn icon_name(&self) -> String {
"skull".to_string()
}
fn accel(&self) -> Option<char> {
Some('U')
}
fn help_url(&self) -> Option<String> {
Some("docs/creator/tools/dungeon".to_string())
}
fn tool_event(
&mut self,
tool_event: ToolEvent,
ui: &mut TheUI,
ctx: &mut TheContext,
project: &mut Project,
server_ctx: &mut ServerContext,
) -> bool {
match tool_event {
Activate => {
ctx.ui
.send(TheEvent::SetStackIndex(TheId::named("Main Stack"), 0));
if let Some(layout) = ui.get_sharedhlayout("Shared Panel Layout") {
layout.set_mode(TheSharedHLayoutMode::Right);
ctx.ui.relayout = true;
}
server_ctx.editing_surface = None;
server_ctx.prev_dungeon_view_mode = Some(server_ctx.editor_view_mode);
ui.set_widget_value(
"Editor View Switch",
ctx,
TheValue::Int(EditorViewMode::D2.to_index()),
);
ctx.ui.send(TheEvent::IndexChanged(
TheId::named("Editor View Switch"),
EditorViewMode::D2.to_index() as usize,
));
server_ctx.curr_map_tool_type = MapToolType::Dungeon;
server_ctx.hover_cursor = None;
server_ctx.hover_cursor_3d = None;
if let Some(map) = project.get_map_mut(server_ctx) {
server_ctx.prev_dungeon_subdivisions = Some(map.subdivisions);
map.subdivisions = 1.0;
map.selected_vertices.clear();
map.selected_linedefs.clear();
map.selected_sectors.clear();
}
ctx.ui.send(TheEvent::Custom(
TheId::named("Map Selection Changed"),
TheValue::Empty,
));
ctx.ui.send(TheEvent::Custom(
TheId::named("Open Dungeon Dock"),
TheValue::Empty,
));
true
}
DeActivate => {
server_ctx.curr_map_tool_type = MapToolType::General;
server_ctx.hover_cursor = None;
server_ctx.hover_cursor_3d = None;
if let Some(map) = project.get_map_mut(server_ctx)
&& let Some(prev_subdivisions) = server_ctx.prev_dungeon_subdivisions.take()
{
map.subdivisions = prev_subdivisions;
}
if let Some(prev_view_mode) = server_ctx.prev_dungeon_view_mode.take() {
ui.set_widget_value(
"Editor View Switch",
ctx,
TheValue::Int(prev_view_mode.to_index()),
);
ctx.ui.send(TheEvent::IndexChanged(
TheId::named("Editor View Switch"),
prev_view_mode.to_index() as usize,
));
}
if let Some(prev) = server_ctx.prev_dungeon_dock.take() {
ctx.ui.send(TheEvent::Custom(
TheId::named("Restore Previous Dock"),
TheValue::Text(prev),
));
}
ctx.ui.send(TheEvent::Custom(
TheId::named("Hide Dungeon Settings"),
TheValue::Empty,
));
self.reset_stroke();
true
}
_ => false,
}
}
fn map_event(
&mut self,
map_event: MapEvent,
ui: &mut TheUI,
ctx: &mut TheContext,
map: &mut Map,
server_ctx: &mut ServerContext,
) -> Option<ProjectUndoAtom> {
match map_event {
MapClicked(coord) => {
self.update_hover_cell(coord, ui, ctx, map, server_ctx);
self.begin_stroke(map);
self.apply_at(coord, ui, map, server_ctx);
}
MapDragged(coord) => {
if !self.stroke_active {
self.begin_stroke(map);
}
self.update_hover_cell(coord, ui, ctx, map, server_ctx);
self.apply_at(coord, ui, map, server_ctx);
}
MapUp(_) => {
if self.stroke_active {
self.last_cell = None;
self.stroke_anchor = None;
self.line_axis_horizontal = None;
self.stroke_active = false;
if self.stroke_changed
&& let Some(prev) = self.stroke_prev_map.take()
{
self.stroke_changed = false;
return Some(ProjectUndoAtom::MapEdit(
server_ctx.pc,
Box::new(prev),
Box::new(map.clone()),
));
}
self.stroke_changed = false;
self.stroke_prev_map = None;
}
}
MapHover(coord) => {
if self.hud.hovered(coord.x, coord.y, map, ui, ctx, server_ctx) {
RUSTERIX.write().unwrap().set_dirty();
return None;
}
if server_ctx.editor_view_mode == EditorViewMode::D2
&& let Some(render_view) = ui.get_render_view("PolyView")
{
let dim = *render_view.dim();
let cp = server_ctx.local_to_map_grid(
Vec2::new(dim.width as f32, dim.height as f32),
Vec2::new(coord.x as f32, coord.y as f32),
map,
map.subdivisions,
);
ctx.ui.send(TheEvent::Custom(
TheId::named("Cursor Pos Changed"),
TheValue::Float2(cp),
));
let cell_pos = server_ctx.local_to_map_cell(
Vec2::new(dim.width as f32, dim.height as f32),
Vec2::new(coord.x as f32, coord.y as f32),
map,
1.0,
);
server_ctx.hover_cursor = Some(cell_pos);
let hovered_cell =
Vec2::new(cell_pos.x.floor() as i32, cell_pos.y.floor() as i32);
self.update_status_text(ctx, map, server_ctx, hovered_cell);
RUSTERIX.write().unwrap().set_dirty();
}
}
MapEscape => {
if self.stroke_active {
if let Some(prev) = self.stroke_prev_map.take() {
*map = prev;
}
self.reset_stroke();
}
server_ctx.hover_cursor = None;
RUSTERIX.write().unwrap().set_dirty();
}
_ => {}
}
None
}
fn draw_hud(
&mut self,
buffer: &mut TheRGBABuffer,
map: &mut Map,
ctx: &mut TheContext,
server_ctx: &mut ServerContext,
assets: &Assets,
) {
self.hud.draw(buffer, map, ctx, server_ctx, None, assets);
}
}
impl DungeonTool {
fn update_status_text(
&self,
ctx: &mut TheContext,
map: &Map,
server_ctx: &ServerContext,
cell: Vec2<i32>,
) {
let status = if let Some(layer) = map.dungeon.active_layer() {
if let Some(dcell) = layer
.cells
.iter()
.find(|dcell| dcell.x == cell.x && dcell.y == cell.y)
{
let mut text = format!(
"{} ({}, {}), floor_base = {:.2}, height = {:.2}",
dcell.kind.label(),
dcell.x,
dcell.y,
dcell.floor_base,
dcell.height
);
text.push_str(&format!(
", floors = {}, ceilings = {}",
server_ctx.curr_dungeon_create_floor, server_ctx.curr_dungeon_create_ceiling
));
if dcell.standalone {
text.push_str(", standalone = true");
} else {
text.push_str(", standalone = false");
}
if dcell.kind.is_door() {
text.push_str(&format!(
", door_width = {}, door_depth = {:.2}, door_height = {:.2}",
dcell.door_width, dcell.door_depth, dcell.door_height
));
}
if dcell.kind.is_stair() {
text.push_str(&format!(
", floor_delta = {:.2}, steps = {}",
dcell.stair_target_floor_base - dcell.floor_base,
dcell.stair_steps
));
if !dcell.stair_tile_id.trim().is_empty() {
text.push_str(&format!(", tile_id = {}", dcell.stair_tile_id));
}
}
text
} else {
let mut text = format!(
"{} ({}, {}), floor_base = {:.2}, height = {:.2}",
server_ctx.curr_dungeon_tile.label(),
cell.x,
cell.y,
server_ctx.curr_dungeon_floor_base,
server_ctx.curr_dungeon_height
);
text.push_str(&format!(
", floors = {}, ceilings = {}, standalone = {}",
server_ctx.curr_dungeon_create_floor,
server_ctx.curr_dungeon_create_ceiling,
server_ctx.curr_dungeon_standalone
));
if server_ctx.curr_dungeon_tile.is_door() {
text.push_str(&format!(
", door_width = {}, door_depth = {:.2}, door_height = {:.2}",
server_ctx.curr_dungeon_tile_span.max(1),
server_ctx.curr_dungeon_tile_depth.max(0.05),
server_ctx.curr_dungeon_tile_height.max(0.5)
));
}
if server_ctx.curr_dungeon_tile.is_stair() {
text.push_str(&format!(
", floor_delta = {:.2}, steps = {}",
server_ctx.curr_dungeon_stair_target_floor_base,
server_ctx.curr_dungeon_stair_steps.max(1)
));
if !server_ctx.curr_dungeon_stair_tile_id.trim().is_empty() {
text.push_str(&format!(
", tile_id = {}",
server_ctx.curr_dungeon_stair_tile_id
));
}
}
text
}
} else {
format!(
"{} ({}, {}), floor_base = {:.2}, height = {:.2}, floors = {}, ceilings = {}, standalone = {}",
server_ctx.curr_dungeon_tile.label(),
cell.x,
cell.y,
server_ctx.curr_dungeon_floor_base,
server_ctx.curr_dungeon_height,
server_ctx.curr_dungeon_create_floor,
server_ctx.curr_dungeon_create_ceiling,
server_ctx.curr_dungeon_standalone
)
};
ctx.ui.send(TheEvent::SetStatusText(TheId::empty(), status));
}
fn begin_stroke(&mut self, map: &Map) {
if !self.stroke_active {
self.stroke_active = true;
self.stroke_changed = false;
self.stroke_prev_map = Some(map.clone());
self.stroke_anchor = None;
self.line_axis_horizontal = None;
self.last_cell = None;
}
}
fn reset_stroke(&mut self) {
self.stroke_active = false;
self.stroke_changed = false;
self.stroke_prev_map = None;
self.stroke_anchor = None;
self.line_axis_horizontal = None;
self.last_cell = None;
}
fn apply_at(
&mut self,
coord: Vec2<i32>,
ui: &mut TheUI,
map: &mut Map,
server_ctx: &mut ServerContext,
) {
if server_ctx.editor_view_mode != EditorViewMode::D2 {
return;
}
let hovered = if let Some(cp) = server_ctx.hover_cursor {
cp
} else if let Some(render_view) = ui.get_render_view("PolyView") {
let dim = *render_view.dim();
server_ctx.local_to_map_cell(
Vec2::new(dim.width as f32, dim.height as f32),
Vec2::new(coord.x as f32, coord.y as f32),
map,
1.0,
)
} else {
return;
};
let mut cell = Vec2::new(hovered.x.floor() as i32, hovered.y.floor() as i32);
let anchor = *self.stroke_anchor.get_or_insert(cell);
if ui.ctrl {
if self.line_axis_horizontal.is_none() {
let dx = (cell.x - anchor.x).abs();
let dy = (cell.y - anchor.y).abs();
if dx > 0 || dy > 0 {
self.line_axis_horizontal = Some(dx >= dy);
}
}
if let Some(horizontal) = self.line_axis_horizontal {
if horizontal {
cell.y = anchor.y;
} else {
cell.x = anchor.x;
}
}
} else {
self.line_axis_horizontal = None;
}
let stroke_cells = if let Some(last) = self.last_cell {
Self::cells_between(last, cell)
} else {
vec![cell]
};
self.last_cell = Some(cell);
let mut cells = Vec::new();
for cell in stroke_cells {
cells.extend(Self::stamp_cells_for_tile(
cell,
server_ctx.curr_dungeon_tile,
server_ctx.curr_dungeon_tile_span,
));
}
let mut any_changed = false;
for cell in cells {
let changed = if ui.shift {
map.dungeon.remove_active_cell(cell.x, cell.y)
} else {
let effective_stair_target = if server_ctx.curr_dungeon_tile.is_stair() {
server_ctx.curr_dungeon_floor_base
+ server_ctx.curr_dungeon_stair_target_floor_base
} else {
server_ctx.curr_dungeon_stair_target_floor_base
};
map.dungeon.upsert_active_cell(
cell.x,
cell.y,
server_ctx.curr_dungeon_tile,
server_ctx.curr_dungeon_floor_base,
server_ctx.curr_dungeon_height,
server_ctx.curr_dungeon_standalone,
server_ctx.curr_dungeon_tile_span.max(1),
server_ctx.curr_dungeon_tile_depth.max(0.05),
server_ctx.curr_dungeon_tile_height.max(0.5),
server_ctx.curr_dungeon_tile_open_mode,
server_ctx.curr_dungeon_tile_item.clone(),
effective_stair_target,
server_ctx.curr_dungeon_stair_steps.max(1),
server_ctx.curr_dungeon_stair_tile_id.clone(),
server_ctx.curr_dungeon_stair_tile_mode,
)
};
any_changed |= changed;
}
if any_changed {
rebuild_generated_geometry(
map,
server_ctx.curr_dungeon_create_floor,
server_ctx.curr_dungeon_create_ceiling,
);
map.changed += 1;
self.stroke_changed = true;
RUSTERIX.write().unwrap().set_dirty();
}
}
fn update_hover_cell(
&mut self,
coord: Vec2<i32>,
ui: &mut TheUI,
ctx: &mut TheContext,
map: &Map,
server_ctx: &mut ServerContext,
) {
if server_ctx.editor_view_mode != EditorViewMode::D2 {
return;
}
if let Some(render_view) = ui.get_render_view("PolyView") {
let dim = *render_view.dim();
let screen_size = Vec2::new(dim.width as f32, dim.height as f32);
let local = Vec2::new(coord.x as f32, coord.y as f32);
let cp = server_ctx.local_to_map_grid(screen_size, local, map, map.subdivisions);
ctx.ui.send(TheEvent::Custom(
TheId::named("Cursor Pos Changed"),
TheValue::Float2(cp),
));
let cell_pos = server_ctx.local_to_map_cell(screen_size, local, map, 1.0);
server_ctx.hover_cursor = Some(cell_pos);
let hovered_cell = Vec2::new(cell_pos.x.floor() as i32, cell_pos.y.floor() as i32);
self.update_status_text(ctx, map, server_ctx, hovered_cell);
RUSTERIX.write().unwrap().set_dirty();
}
}
fn cells_between(from: Vec2<i32>, to: Vec2<i32>) -> Vec<Vec2<i32>> {
let dx = to.x - from.x;
let dy = to.y - from.y;
let steps = dx.abs().max(dy.abs());
if steps == 0 {
return vec![to];
}
let mut cells = Vec::with_capacity(steps as usize + 1);
for step in 1..=steps {
let t = step as f32 / steps as f32;
let x = from.x as f32 + dx as f32 * t;
let y = from.y as f32 + dy as f32 * t;
let cell = Vec2::new(x.round() as i32, y.round() as i32);
if cells.last().copied() != Some(cell) {
cells.push(cell);
}
}
cells
}
fn stamp_cells_for_tile(
origin: Vec2<i32>,
tile: rusterix::DungeonTileKind,
span: i32,
) -> Vec<Vec2<i32>> {
let span = span.max(1);
if !tile.is_door() || span == 1 {
return vec![origin];
}
let mut cells = Vec::with_capacity(span as usize);
if tile.has_door_north() || tile.has_door_south() {
let start_x = origin.x - (span - 1) / 2;
for dx in 0..span {
cells.push(Vec2::new(start_x + dx, origin.y));
}
} else {
let start_y = origin.y - (span - 1) / 2;
for dy in 0..span {
cells.push(Vec2::new(origin.x, start_y + dy));
}
}
cells
}
}