1use crate::hud::{Hud, HudMode};
2use crate::{
3 editor::{RUSTERIX, UNDOMANAGER},
4 prelude::*,
5};
6use MapEvent::*;
7use rusterix::EntityAction;
8use rusterix::prelude::*;
9use theframework::prelude::*;
10
11pub struct EntityTool {
12 id: TheId,
13 hud: Hud,
14
15 drag_state: Option<DragState>,
16 move_eps2: f32,
17}
18
19#[derive(Clone)]
20struct DragState {
21 target: DragTarget,
22 start_pos: Vec2<f32>,
23 changed: bool,
24 grab_offset: Vec2<f32>,
25}
26
27#[derive(Clone, Copy)]
28enum DragTarget {
29 Entity(Uuid),
30 Item(Uuid),
31}
32
33impl Tool for EntityTool {
34 fn new() -> Self
35 where
36 Self: Sized,
37 {
38 Self {
39 id: TheId::named("Entity Tool"),
40 hud: Hud::new(HudMode::Entity),
41
42 drag_state: None,
43 move_eps2: 0.01, }
45 }
46
47 fn id(&self) -> TheId {
48 self.id.clone()
49 }
50 fn info(&self) -> String {
51 fl!("tool_entity")
52 }
53 fn icon_name(&self) -> String {
54 str!("treasure-chest")
55 }
56 fn accel(&self) -> Option<char> {
57 Some('Y')
58 }
59
60 fn help_url(&self) -> Option<String> {
61 Some("docs/creator/tools/entity".to_string())
62 }
63
64 fn tool_event(
65 &mut self,
66 tool_event: ToolEvent,
67 _ui: &mut TheUI,
68 _ctx: &mut TheContext,
69 _project: &mut Project,
70 server_ctx: &mut ServerContext,
71 ) -> bool {
72 match tool_event {
73 ToolEvent::Activate => {
74 server_ctx.curr_map_tool_type = MapToolType::General;
75
76 true
77 }
78 ToolEvent::DeActivate => true,
79 _ => false,
80 }
81 }
82
83 fn map_event(
84 &mut self,
85 map_event: MapEvent,
86 ui: &mut TheUI,
87 ctx: &mut TheContext,
88 map: &mut Map,
89 server_ctx: &mut ServerContext,
90 ) -> Option<ProjectUndoAtom> {
91 match map_event {
92 MapKey(c) => {
93 let dir = match c {
94 'q' | 'Q' => Some(-1.0_f32),
95 'e' | 'E' => Some(1.0_f32),
96 _ => None,
97 };
98 if let Some(step) = dir
99 && let Some(selected_id) = map.selected_entity_item
100 && let Some(entity) = map
101 .entities
102 .iter_mut()
103 .find(|e| e.creator_id == selected_id)
104 {
105 let from = Self::snap_cardinal(entity.orientation);
106 let to = if step < 0.0 {
107 Vec2::new(-from.y, from.x)
108 } else {
109 Vec2::new(from.y, -from.x)
110 };
111 entity.orientation = to;
112 server_ctx
113 .rotated_entities
114 .entry(selected_id)
115 .and_modify(|entry| entry.1 = to)
116 .or_insert((from, to));
117 RUSTERIX.write().unwrap().set_dirty();
118 }
119 }
120 MapClicked(coord) => {
121 if self.hud.clicked(coord.x, coord.y, map, ui, ctx, server_ctx) {
122 crate::editor::RUSTERIX.write().unwrap().set_dirty();
123 return None;
124 }
125
126 if self.handle_game_click(coord, map) {
127 return None;
128 }
129
130 if server_ctx.get_map_context() == MapContext::Region
131 && let Some(hit) = self.pick_hit_for_coord(ui, server_ctx, map, coord)
132 {
133 let click_pos = self
134 .map_pos_unsnapped(ui, server_ctx, map, coord)
135 .unwrap_or(hit.pos);
136
137 map.clear_selection();
138 map.selected_entity_item = Some(hit.id());
139
140 let grab_offset = hit.pos - click_pos;
141
142 self.drag_state = Some(DragState {
143 target: hit.target,
144 start_pos: hit.pos,
145 changed: false,
146 grab_offset,
147 });
148
149 match hit.target {
150 DragTarget::Entity(id) => {
151 if let Some(entity) = map.entities.iter().find(|e| e.creator_id == id) {
152 server_ctx
153 .moved_entities
154 .entry(id)
155 .or_insert((entity.position, entity.position));
156 }
157 }
158 DragTarget::Item(id) => {
159 if let Some(item) = map.items.iter().find(|i| i.creator_id == id) {
160 server_ctx
161 .moved_items
162 .entry(id)
163 .or_insert((item.position, item.position));
164 }
165 }
166 }
167
168 self.select_in_tree(ui, server_ctx, hit.id());
169 ctx.ui.send(TheEvent::Custom(
170 TheId::named("Map Selection Changed"),
171 TheValue::Empty,
172 ));
173 RUSTERIX.write().unwrap().set_dirty();
174 }
175 }
176 MapUp(coord) => {
177 if self.handle_game_up(coord, map) {
178 return None;
179 }
180
181 if let Some(state) = self.drag_state.take() {
182 if state.changed {
183 match state.target {
184 DragTarget::Entity(id) => {
185 if let Some(entity) =
186 map.entities.iter_mut().find(|e| e.creator_id == id)
187 {
188 let snapped = Self::snap_to_grid(
190 Vec2::new(entity.position.x, entity.position.z),
191 map.subdivisions,
192 );
193 entity.position.x = snapped.x;
194 entity.position.z = snapped.y;
195 server_ctx
196 .moved_entities
197 .entry(id)
198 .and_modify(|entry| entry.1 = entity.position)
199 .or_insert((entity.position, entity.position));
200 }
201 }
202 DragTarget::Item(id) => {
203 if let Some(item) =
204 map.items.iter_mut().find(|i| i.creator_id == id)
205 {
206 let snapped = Self::snap_to_grid(
207 Vec2::new(item.position.x, item.position.z),
208 map.subdivisions,
209 );
210 item.position.x = snapped.x;
211 item.position.z = snapped.y;
212 server_ctx
213 .moved_items
214 .entry(id)
215 .and_modify(|entry| entry.1 = item.position)
216 .or_insert((item.position, item.position));
217 }
218 }
219 }
220 }
221 }
222
223 self.drag_state = None;
224 }
225 MapDragged(coord) => {
226 if let Some(_render_view) = ui.get_render_view("PolyView") {
227 if let Some(mut state) = self.drag_state.take() {
228 let pointer_pos = self
230 .map_pos_unsnapped(ui, server_ctx, map, coord)
231 .unwrap_or(Vec2::new(0.0, 0.0));
232 let mut drag_pos = pointer_pos + state.grab_offset;
233
234 let delta = drag_pos - state.start_pos;
236 let moved = delta.x * delta.x + delta.y * delta.y > self.move_eps2;
237 if !moved {
238 drag_pos = state.start_pos;
239 }
240
241 match state.target {
242 DragTarget::Entity(id) => {
243 if let Some(entity) =
244 map.entities.iter_mut().find(|e| e.creator_id == id)
245 {
246 if moved {
247 entity.position.x = drag_pos.x;
248 entity.position.z = drag_pos.y;
249 state.changed = true;
250 }
251
252 server_ctx
253 .moved_entities
254 .entry(id)
255 .and_modify(|entry| entry.1 = entity.position)
256 .or_insert((entity.position, entity.position));
257 }
258 }
259 DragTarget::Item(id) => {
260 if let Some(item) =
261 map.items.iter_mut().find(|i| i.creator_id == id)
262 {
263 if moved {
264 item.position.x = drag_pos.x;
265 item.position.z = drag_pos.y;
266 state.changed = true;
267 }
268
269 server_ctx
270 .moved_items
271 .entry(id)
272 .and_modify(|entry| entry.1 = item.position)
273 .or_insert((item.position, item.position));
274 }
275 }
276 }
277
278 self.drag_state = Some(state);
279 }
280 }
281 }
282 MapHover(coord) => {
283 if server_ctx.get_map_context() == MapContext::Region {
284 if let Some(hit) = self.pick_hit_for_coord(ui, server_ctx, map, coord) {
285 ctx.ui
286 .send(TheEvent::SetStatusText(TheId::empty(), hit.status_text()));
287 } else {
288 ctx.ui
289 .send(TheEvent::SetStatusText(TheId::empty(), "".into()));
290 }
291 }
292
293 if let Some(render_view) = ui.get_render_view("PolyView") {
294 let dim = *render_view.dim();
295 server_ctx.hover = (None, None, None);
296 let cp = server_ctx.local_to_map_cell(
297 Vec2::new(dim.width as f32, dim.height as f32),
298 Vec2::new(coord.x as f32, coord.y as f32),
299 map,
300 map.subdivisions,
301 );
302 server_ctx.hover_cursor = Some(cp);
303 }
304 }
305 _ => {}
306 }
307
308 None
309 }
310
311 fn draw_hud(
312 &mut self,
313 buffer: &mut TheRGBABuffer,
314 map: &mut Map,
315 ctx: &mut TheContext,
316 server_ctx: &mut ServerContext,
317 assets: &Assets,
318 ) {
319 let id = if !map.selected_linedefs.is_empty() {
320 Some(map.selected_linedefs[0])
321 } else {
322 None
323 };
324 self.hud.draw(buffer, map, ctx, server_ctx, id, assets);
325 }
326
327 fn handle_event(
328 &mut self,
329 event: &TheEvent,
330 ui: &mut TheUI,
331 ctx: &mut TheContext,
332 project: &mut Project,
333 server_ctx: &mut ServerContext,
334 ) -> bool {
335 #[allow(clippy::single_match)]
336 match event {
337 TheEvent::KeyCodeDown(TheValue::KeyCode(code)) => {
338 if *code == TheKeyCode::Delete {
339 if let Some(render_view) = ui.get_render_view("PolyView") {
340 if ctx.ui.has_focus(render_view.id()) {
341 return self.delete_selected(ui, ctx, project, server_ctx);
342 }
343 }
344 }
345 }
346 _ => {}
347 }
348
349 false
350 }
351}
352
353impl EntityTool {
354 fn map_pos_unsnapped(
356 &self,
357 ui: &mut TheUI,
358 server_ctx: &ServerContext,
359 map: &Map,
360 coord: Vec2<i32>,
361 ) -> Option<Vec2<f32>> {
362 if server_ctx.editor_view_mode != EditorViewMode::D2
363 && let Some(render_view) = ui.get_render_view("PolyView")
364 {
365 let dim = *render_view.dim();
366 let screen_uv = [
367 coord.x as f32 / dim.width as f32,
368 coord.y as f32 / dim.height as f32,
369 ];
370 let rusterix = RUSTERIX.write().unwrap();
371 if let Some((_, hit, _)) = rusterix.scene_handler.vm.pick_geo_id_at_uv(
372 dim.width as u32,
373 dim.height as u32,
374 screen_uv,
375 false,
376 true,
377 ) {
378 return Some(Vec2::new(hit.x, hit.z));
379 }
380 if let Some(hit) = server_ctx.hover_cursor_3d {
381 return Some(Vec2::new(hit.x, hit.z));
382 }
383 }
384
385 ui.get_render_view("PolyView").map(|render_view| {
386 let dim = *render_view.dim();
387 let grid_space_pos = Vec2::new(coord.x as f32, coord.y as f32)
388 - Vec2::new(dim.width as f32, dim.height as f32) / 2.0
389 - Vec2::new(map.offset.x, -map.offset.y);
390
391 grid_space_pos / map.grid_size
392 })
393 }
394
395 fn snap_to_grid(pos: Vec2<f32>, subdivisions: f32) -> Vec2<f32> {
397 if subdivisions > 1.0 {
398 Vec2::new(
399 (pos.x * subdivisions).round() / subdivisions,
400 (pos.y * subdivisions).round() / subdivisions,
401 )
402 } else {
403 Vec2::new(pos.x.round(), pos.y.round())
404 }
405 }
406
407 fn snap_cardinal(dir: Vec2<f32>) -> Vec2<f32> {
409 if dir.x.abs() >= dir.y.abs() {
410 if dir.x >= 0.0 {
411 Vec2::new(1.0, 0.0)
412 } else {
413 Vec2::new(-1.0, 0.0)
414 }
415 } else if dir.y >= 0.0 {
416 Vec2::new(0.0, 1.0)
417 } else {
418 Vec2::new(0.0, -1.0)
419 }
420 }
421
422 fn pick_hit(&self, map: &Map, pos: Vec2<f32>, radius2: f32) -> Option<Hit> {
423 if let Some(entity) = map.entities.iter().find(|e| {
424 let d = e.get_pos_xz() - pos;
425 d.x * d.x + d.y * d.y < radius2
426 }) {
427 return Some(Hit {
428 target: DragTarget::Entity(entity.creator_id),
429 name: entity
430 .attributes
431 .get_str("name")
432 .map(|s| s.to_string())
433 .unwrap_or_else(|| "Entity".into()),
434 pos: Vec2::new(entity.position.x, entity.position.z),
435 });
436 }
437
438 if let Some(item) = map.items.iter().find(|i| {
439 let d = i.get_pos_xz() - pos;
440 d.x * d.x + d.y * d.y < radius2
441 }) {
442 return Some(Hit {
443 target: DragTarget::Item(item.creator_id),
444 name: item
445 .attributes
446 .get_str("name")
447 .map(|s| s.to_string())
448 .unwrap_or_else(|| "Item".into()),
449 pos: Vec2::new(item.position.x, item.position.z),
450 });
451 }
452
453 None
454 }
455
456 fn pick_hit_for_coord(
457 &self,
458 ui: &mut TheUI,
459 server_ctx: &ServerContext,
460 map: &Map,
461 coord: Vec2<i32>,
462 ) -> Option<Hit> {
463 let pos = self.map_pos_unsnapped(ui, server_ctx, map, coord)?;
464 let radius2 = if server_ctx.editor_view_mode == EditorViewMode::D2 {
465 0.16
466 } else {
467 1.44
468 };
469 self.pick_hit(map, pos, radius2)
470 }
471
472 fn delete_selected(
473 &self,
474 ui: &mut TheUI,
475 ctx: &mut TheContext,
476 project: &mut Project,
477 server_ctx: &mut ServerContext,
478 ) -> bool {
479 let Some(selected) = project
480 .get_map_mut(server_ctx)
481 .and_then(|map| map.selected_entity_item)
482 else {
483 return false;
484 };
485
486 if let Some(region) = project.get_region_ctx_mut(server_ctx) {
487 if let Some(index) = region.characters.get_index_of(&selected) {
488 if let Some(character) = region.characters.get(&selected).cloned() {
489 let atom = ProjectUndoAtom::RemoveRegionCharacterInstance(
490 index,
491 server_ctx.curr_region,
492 character,
493 );
494 atom.redo(project, ui, ctx, server_ctx);
495 UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
496 return true;
497 }
498 }
499
500 if let Some(index) = region.items.get_index_of(&selected) {
501 if let Some(item) = region.items.get(&selected).cloned() {
502 let atom = ProjectUndoAtom::RemoveRegionItemInstance(
503 index,
504 server_ctx.curr_region,
505 item,
506 );
507 atom.redo(project, ui, ctx, server_ctx);
508 UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
509 return true;
510 }
511 }
512 }
513
514 false
515 }
516
517 fn select_in_tree(&self, ui: &mut TheUI, server_ctx: &ServerContext, id: Uuid) {
518 if let Some(tree_layout) = ui.get_tree_layout("Project Tree") {
519 if let Some(node) = tree_layout.get_node_by_id_mut(&server_ctx.curr_region) {
520 node.new_item_selected(&TheId::named_with_id("Region Content List Item", id));
521 }
522 }
523 }
524
525 fn handle_game_click(&self, coord: Vec2<i32>, map: &mut Map) -> bool {
526 let mut rusterix = RUSTERIX.write().unwrap();
527 let is_running = rusterix.server.state == rusterix::ServerState::Running;
528
529 if is_running {
530 if let Some(action) = rusterix.client.touch_down(coord, map) {
531 rusterix.server.local_player_action(action);
532 }
533 return true;
534 }
535 false
536 }
537
538 fn handle_game_up(&self, coord: Vec2<i32>, map: &mut Map) -> bool {
539 let mut rusterix = RUSTERIX.write().unwrap();
540 let is_running = rusterix.server.state == rusterix::ServerState::Running;
541
542 if is_running {
543 if let Some(action) = rusterix.client.touch_up(coord, map) {
544 rusterix.server.local_player_action(action);
545 }
546 rusterix.server.local_player_action(EntityAction::Off);
547 return true;
548 }
549 false
550 }
551}
552
553struct Hit {
554 target: DragTarget,
555 name: String,
556 pos: Vec2<f32>,
557}
558
559impl Hit {
560 fn id(&self) -> Uuid {
561 match self.target {
562 DragTarget::Entity(id) | DragTarget::Item(id) => id,
563 }
564 }
565
566 fn status_text(&self) -> String {
567 let prefix = match self.target {
568 DragTarget::Entity(_) => "Entity",
569 DragTarget::Item(_) => "Item",
570 };
571 format!("{prefix}: {}", self.name)
572 }
573}