use crate::hud::{Hud, HudMode};
use crate::{
editor::{RUSTERIX, UNDOMANAGER},
prelude::*,
};
use MapEvent::*;
use rusterix::EntityAction;
use rusterix::prelude::*;
use theframework::prelude::*;
pub struct EntityTool {
id: TheId,
hud: Hud,
drag_state: Option<DragState>,
move_eps2: f32,
}
#[derive(Clone)]
struct DragState {
target: DragTarget,
start_pos: Vec2<f32>,
changed: bool,
grab_offset: Vec2<f32>,
}
#[derive(Clone, Copy)]
enum DragTarget {
Entity(Uuid),
Item(Uuid),
}
impl Tool for EntityTool {
fn new() -> Self
where
Self: Sized,
{
Self {
id: TheId::named("Entity Tool"),
hud: Hud::new(HudMode::Entity),
drag_state: None,
move_eps2: 0.01, }
}
fn id(&self) -> TheId {
self.id.clone()
}
fn info(&self) -> String {
fl!("tool_entity")
}
fn icon_name(&self) -> String {
str!("treasure-chest")
}
fn accel(&self) -> Option<char> {
Some('Y')
}
fn help_url(&self) -> Option<String> {
Some("docs/creator/tools/entity".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 {
ToolEvent::Activate => {
server_ctx.curr_map_tool_type = MapToolType::General;
true
}
ToolEvent::DeActivate => 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 {
MapKey(c) => {
let dir = match c {
'q' | 'Q' => Some(-1.0_f32),
'e' | 'E' => Some(1.0_f32),
_ => None,
};
if let Some(step) = dir
&& let Some(selected_id) = map.selected_entity_item
&& let Some(entity) = map
.entities
.iter_mut()
.find(|e| e.creator_id == selected_id)
{
let from = Self::snap_cardinal(entity.orientation);
let to = if step < 0.0 {
Vec2::new(-from.y, from.x)
} else {
Vec2::new(from.y, -from.x)
};
entity.orientation = to;
server_ctx
.rotated_entities
.entry(selected_id)
.and_modify(|entry| entry.1 = to)
.or_insert((from, to));
RUSTERIX.write().unwrap().set_dirty();
}
}
MapClicked(coord) => {
if self.hud.clicked(coord.x, coord.y, map, ui, ctx, server_ctx) {
crate::editor::RUSTERIX.write().unwrap().set_dirty();
return None;
}
if self.handle_game_click(coord, map) {
return None;
}
if server_ctx.get_map_context() == MapContext::Region
&& let Some(hit) = self.pick_hit_for_coord(ui, server_ctx, map, coord)
{
let click_pos = self
.map_pos_unsnapped(ui, server_ctx, map, coord)
.unwrap_or(hit.pos);
map.clear_selection();
map.selected_entity_item = Some(hit.id());
let grab_offset = hit.pos - click_pos;
self.drag_state = Some(DragState {
target: hit.target,
start_pos: hit.pos,
changed: false,
grab_offset,
});
match hit.target {
DragTarget::Entity(id) => {
if let Some(entity) = map.entities.iter().find(|e| e.creator_id == id) {
server_ctx
.moved_entities
.entry(id)
.or_insert((entity.position, entity.position));
}
}
DragTarget::Item(id) => {
if let Some(item) = map.items.iter().find(|i| i.creator_id == id) {
server_ctx
.moved_items
.entry(id)
.or_insert((item.position, item.position));
}
}
}
self.select_in_tree(ui, server_ctx, hit.id());
ctx.ui.send(TheEvent::Custom(
TheId::named("Map Selection Changed"),
TheValue::Empty,
));
RUSTERIX.write().unwrap().set_dirty();
}
}
MapUp(coord) => {
if self.handle_game_up(coord, map) {
return None;
}
if let Some(state) = self.drag_state.take() {
if state.changed {
match state.target {
DragTarget::Entity(id) => {
if let Some(entity) =
map.entities.iter_mut().find(|e| e.creator_id == id)
{
let snapped = Self::snap_to_grid(
Vec2::new(entity.position.x, entity.position.z),
map.subdivisions,
);
entity.position.x = snapped.x;
entity.position.z = snapped.y;
server_ctx
.moved_entities
.entry(id)
.and_modify(|entry| entry.1 = entity.position)
.or_insert((entity.position, entity.position));
}
}
DragTarget::Item(id) => {
if let Some(item) =
map.items.iter_mut().find(|i| i.creator_id == id)
{
let snapped = Self::snap_to_grid(
Vec2::new(item.position.x, item.position.z),
map.subdivisions,
);
item.position.x = snapped.x;
item.position.z = snapped.y;
server_ctx
.moved_items
.entry(id)
.and_modify(|entry| entry.1 = item.position)
.or_insert((item.position, item.position));
}
}
}
}
}
self.drag_state = None;
}
MapDragged(coord) => {
if let Some(_render_view) = ui.get_render_view("PolyView") {
if let Some(mut state) = self.drag_state.take() {
let pointer_pos = self
.map_pos_unsnapped(ui, server_ctx, map, coord)
.unwrap_or(Vec2::new(0.0, 0.0));
let mut drag_pos = pointer_pos + state.grab_offset;
let delta = drag_pos - state.start_pos;
let moved = delta.x * delta.x + delta.y * delta.y > self.move_eps2;
if !moved {
drag_pos = state.start_pos;
}
match state.target {
DragTarget::Entity(id) => {
if let Some(entity) =
map.entities.iter_mut().find(|e| e.creator_id == id)
{
if moved {
entity.position.x = drag_pos.x;
entity.position.z = drag_pos.y;
state.changed = true;
}
server_ctx
.moved_entities
.entry(id)
.and_modify(|entry| entry.1 = entity.position)
.or_insert((entity.position, entity.position));
}
}
DragTarget::Item(id) => {
if let Some(item) =
map.items.iter_mut().find(|i| i.creator_id == id)
{
if moved {
item.position.x = drag_pos.x;
item.position.z = drag_pos.y;
state.changed = true;
}
server_ctx
.moved_items
.entry(id)
.and_modify(|entry| entry.1 = item.position)
.or_insert((item.position, item.position));
}
}
}
self.drag_state = Some(state);
}
}
}
MapHover(coord) => {
if server_ctx.get_map_context() == MapContext::Region {
if let Some(hit) = self.pick_hit_for_coord(ui, server_ctx, map, coord) {
ctx.ui
.send(TheEvent::SetStatusText(TheId::empty(), hit.status_text()));
} else {
ctx.ui
.send(TheEvent::SetStatusText(TheId::empty(), "".into()));
}
}
if let Some(render_view) = ui.get_render_view("PolyView") {
let dim = *render_view.dim();
server_ctx.hover = (None, None, None);
let cp = 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,
map.subdivisions,
);
server_ctx.hover_cursor = Some(cp);
}
}
_ => {}
}
None
}
fn draw_hud(
&mut self,
buffer: &mut TheRGBABuffer,
map: &mut Map,
ctx: &mut TheContext,
server_ctx: &mut ServerContext,
assets: &Assets,
) {
let id = if !map.selected_linedefs.is_empty() {
Some(map.selected_linedefs[0])
} else {
None
};
self.hud.draw(buffer, map, ctx, server_ctx, id, assets);
}
fn handle_event(
&mut self,
event: &TheEvent,
ui: &mut TheUI,
ctx: &mut TheContext,
project: &mut Project,
server_ctx: &mut ServerContext,
) -> bool {
#[allow(clippy::single_match)]
match event {
TheEvent::KeyCodeDown(TheValue::KeyCode(code)) => {
if *code == TheKeyCode::Delete {
if let Some(render_view) = ui.get_render_view("PolyView") {
if ctx.ui.has_focus(render_view.id()) {
return self.delete_selected(ui, ctx, project, server_ctx);
}
}
}
}
_ => {}
}
false
}
}
impl EntityTool {
fn remove_selected_character_instance(
project: &mut Project,
ui: &mut TheUI,
ctx: &mut TheContext,
server_ctx: &mut ServerContext,
character_id: Uuid,
) {
if let Some(tree_layout) = ui.get_tree_layout("Project Tree") {
if let Some(region_node) = tree_layout.get_node_by_id_mut(&server_ctx.tree_regions_id) {
region_node.remove_widget_by_uuid(&character_id);
}
}
if let Some(region) = project.get_region_ctx_mut(server_ctx) {
region.characters.shift_remove(&character_id);
region.map.entities.retain(|e| e.creator_id != character_id);
region.map.selected_entity_item = None;
}
if let Some(region) = project.get_region(&server_ctx.curr_region)
&& let Some(tree_layout) = ui.get_tree_layout("Project Tree")
&& let Some(region_node) = tree_layout.get_node_by_id_mut(®ion.id)
{
region_node.set_open(true);
}
shared::rusterix_utils::insert_content_into_maps(project);
ctx.ui.send(TheEvent::Custom(
TheId::named("Map Selection Changed"),
TheValue::Empty,
));
update_region(ctx);
}
fn remove_selected_item_instance(
project: &mut Project,
ui: &mut TheUI,
ctx: &mut TheContext,
server_ctx: &mut ServerContext,
item_id: Uuid,
) {
if let Some(tree_layout) = ui.get_tree_layout("Project Tree") {
if let Some(region_node) = tree_layout.get_node_by_id_mut(&server_ctx.tree_regions_id) {
region_node.remove_widget_by_uuid(&item_id);
}
}
if let Some(region) = project.get_region_ctx_mut(server_ctx) {
region.items.shift_remove(&item_id);
region.map.items.retain(|i| i.creator_id != item_id);
region.map.selected_entity_item = None;
}
if let Some(region) = project.get_region(&server_ctx.curr_region)
&& let Some(tree_layout) = ui.get_tree_layout("Project Tree")
&& let Some(region_node) = tree_layout.get_node_by_id_mut(®ion.id)
{
region_node.set_open(true);
}
shared::rusterix_utils::insert_content_into_maps(project);
ctx.ui.send(TheEvent::Custom(
TheId::named("Map Selection Changed"),
TheValue::Empty,
));
update_region(ctx);
}
fn map_pos_unsnapped(
&self,
ui: &mut TheUI,
server_ctx: &ServerContext,
map: &Map,
coord: Vec2<i32>,
) -> Option<Vec2<f32>> {
if server_ctx.editor_view_mode != EditorViewMode::D2
&& let Some(render_view) = ui.get_render_view("PolyView")
{
let dim = *render_view.dim();
let screen_uv = [
coord.x as f32 / dim.width as f32,
coord.y as f32 / dim.height as f32,
];
let rusterix = RUSTERIX.write().unwrap();
if let Some((_, hit, _)) = rusterix.scene_handler.vm.pick_geo_id_at_uv(
dim.width as u32,
dim.height as u32,
screen_uv,
false,
true,
) {
return Some(Vec2::new(hit.x, hit.z));
}
if let Some(hit) = server_ctx.hover_cursor_3d {
return Some(Vec2::new(hit.x, hit.z));
}
}
ui.get_render_view("PolyView").map(|render_view| {
let dim = *render_view.dim();
let grid_space_pos = Vec2::new(coord.x as f32, coord.y as f32)
- Vec2::new(dim.width as f32, dim.height as f32) / 2.0
- Vec2::new(map.offset.x, -map.offset.y);
grid_space_pos / map.grid_size
})
}
fn snap_to_grid(pos: Vec2<f32>, subdivisions: f32) -> Vec2<f32> {
if subdivisions > 1.0 {
Vec2::new(
(pos.x * subdivisions).round() / subdivisions,
(pos.y * subdivisions).round() / subdivisions,
)
} else {
Vec2::new(pos.x.round(), pos.y.round())
}
}
fn snap_cardinal(dir: Vec2<f32>) -> Vec2<f32> {
if dir.x.abs() >= dir.y.abs() {
if dir.x >= 0.0 {
Vec2::new(1.0, 0.0)
} else {
Vec2::new(-1.0, 0.0)
}
} else if dir.y >= 0.0 {
Vec2::new(0.0, 1.0)
} else {
Vec2::new(0.0, -1.0)
}
}
fn pick_hit(&self, map: &Map, pos: Vec2<f32>, radius2: f32) -> Option<Hit> {
if let Some(entity) = map.entities.iter().find(|e| {
let d = e.get_pos_xz() - pos;
d.x * d.x + d.y * d.y < radius2
}) {
return Some(Hit {
target: DragTarget::Entity(entity.creator_id),
name: entity
.attributes
.get_str("name")
.map(|s| s.to_string())
.unwrap_or_else(|| "Entity".into()),
pos: Vec2::new(entity.position.x, entity.position.z),
});
}
if let Some(item) = map.items.iter().find(|i| {
let d = i.get_pos_xz() - pos;
d.x * d.x + d.y * d.y < radius2
}) {
return Some(Hit {
target: DragTarget::Item(item.creator_id),
name: item
.attributes
.get_str("name")
.map(|s| s.to_string())
.unwrap_or_else(|| "Item".into()),
pos: Vec2::new(item.position.x, item.position.z),
});
}
None
}
fn pick_hit_for_coord(
&self,
ui: &mut TheUI,
server_ctx: &ServerContext,
map: &Map,
coord: Vec2<i32>,
) -> Option<Hit> {
let pos = self.map_pos_unsnapped(ui, server_ctx, map, coord)?;
let radius2 = if server_ctx.editor_view_mode == EditorViewMode::D2 {
0.16
} else {
1.44
};
self.pick_hit(map, pos, radius2)
}
fn delete_selected(
&self,
ui: &mut TheUI,
ctx: &mut TheContext,
project: &mut Project,
server_ctx: &mut ServerContext,
) -> bool {
let Some(selected) = project
.get_map_mut(server_ctx)
.and_then(|map| map.selected_entity_item)
else {
return false;
};
let mut character_to_remove: Option<(usize, Character)> = None;
let mut item_to_remove: Option<(usize, shared::prelude::Item)> = None;
if let Some(region) = project.get_region_ctx(server_ctx) {
if let Some(index) = region.characters.get_index_of(&selected)
&& let Some(character) = region.characters.get(&selected).cloned()
{
character_to_remove = Some((index, character));
}
if let Some(index) = region.items.get_index_of(&selected)
&& let Some(item) = region.items.get(&selected).cloned()
{
item_to_remove = Some((index, item));
}
}
if let Some((index, character)) = character_to_remove {
let atom = ProjectUndoAtom::RemoveRegionCharacterInstance(
index,
server_ctx.curr_region,
character.clone(),
);
Self::remove_selected_character_instance(project, ui, ctx, server_ctx, character.id);
UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
return true;
}
if let Some((index, item)) = item_to_remove {
let atom = ProjectUndoAtom::RemoveRegionItemInstance(
index,
server_ctx.curr_region,
item.clone(),
);
Self::remove_selected_item_instance(project, ui, ctx, server_ctx, item.id);
UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
return true;
}
false
}
fn select_in_tree(&self, ui: &mut TheUI, server_ctx: &ServerContext, id: Uuid) {
if let Some(tree_layout) = ui.get_tree_layout("Project Tree") {
if let Some(node) = tree_layout.get_node_by_id_mut(&server_ctx.curr_region) {
node.new_item_selected(&TheId::named_with_id("Region Content List Item", id));
}
}
}
fn handle_game_click(&self, coord: Vec2<i32>, map: &mut Map) -> bool {
let mut rusterix = RUSTERIX.write().unwrap();
let is_running = rusterix.server.state == rusterix::ServerState::Running;
if is_running {
if let Some(action) = rusterix.client.touch_down(coord, map) {
rusterix.server.local_player_action(action);
}
return true;
}
false
}
fn handle_game_up(&self, coord: Vec2<i32>, map: &mut Map) -> bool {
let mut rusterix = RUSTERIX.write().unwrap();
let is_running = rusterix.server.state == rusterix::ServerState::Running;
if is_running {
if let Some(action) = rusterix.client.touch_up(coord, map) {
rusterix.server.local_player_action(action);
}
rusterix.server.local_player_action(EntityAction::Off);
return true;
}
false
}
}
struct Hit {
target: DragTarget,
name: String,
pos: Vec2<f32>,
}
impl Hit {
fn id(&self) -> Uuid {
match self.target {
DragTarget::Entity(id) | DragTarget::Item(id) => id,
}
}
fn status_text(&self) -> String {
let prefix = match self.target {
DragTarget::Entity(_) => "Entity",
DragTarget::Item(_) => "Item",
};
format!("{prefix}: {}", self.name)
}
}