yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_interaction.cc
Go to the documentation of this file.
1// Related header
3#include "absl/strings/str_format.h"
4
5// C++ standard library headers
6#include <algorithm>
7
8// Third-party library headers
9#include "imgui/imgui.h"
10
11// Project headers
15#include "app/gui/core/icons.h"
16
17namespace yaze::editor {
18
20 const ImGuiIO& io = ImGui::GetIO();
21
22 // Check if mouse is over the canvas
23 if (!canvas_->IsMouseHovering()) {
24 return;
25 }
26
27 // Handle Escape key to cancel any active placement mode
28 if (ImGui::IsKeyPressed(ImGuiKey_Escape) &&
31 return;
32 }
33
34 // Handle scroll wheel for resizing selected objects
36
37 // Handle layer assignment keyboard shortcuts (1, 2, 3 keys)
39
40 // Get mouse position relative to canvas
41 ImVec2 mouse_pos = io.MousePos;
42 ImVec2 canvas_pos = canvas_->zero_point();
43
44 // Convert to canvas coordinates
45 ImVec2 canvas_mouse_pos =
46 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
47
48 // Handle left mouse click based on current mode
49 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
50 switch (mode_manager_.GetMode()) {
52 PlaceDoorAtPosition(static_cast<int>(canvas_mouse_pos.x),
53 static_cast<int>(canvas_mouse_pos.y));
54 break;
55
57 PlaceSpriteAtPosition(static_cast<int>(canvas_mouse_pos.x),
58 static_cast<int>(canvas_mouse_pos.y));
59 break;
60
62 PlaceItemAtPosition(static_cast<int>(canvas_mouse_pos.x),
63 static_cast<int>(canvas_mouse_pos.y));
64 break;
65
67 auto [room_x, room_y] =
68 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
69 static_cast<int>(canvas_mouse_pos.y));
70 PlaceObjectAtPosition(room_x, room_y);
71 break;
72 }
73
75 default:
76 // Selection mode: try to select entity (door/sprite/item) first, then objects
78 // No entity - try to select object at cursor
80 // Clicked empty space - start rectangle selection
81 if (!io.KeyShift && !io.KeyCtrl) {
82 // Clear selection unless modifier held
85 }
86 // Begin rectangle selection for multi-select
88 auto& state = mode_manager_.GetModeState();
89 state.rect_start_x = static_cast<int>(canvas_mouse_pos.x);
90 state.rect_start_y = static_cast<int>(canvas_mouse_pos.y);
91 state.rect_end_x = state.rect_start_x;
92 state.rect_end_y = state.rect_start_y;
93 selection_.BeginRectangleSelection(state.rect_start_x,
94 state.rect_start_y);
95 } else {
96 // Clicked on an object - start drag if we have selected objects
97 ClearEntitySelection(); // Clear entity selection when selecting object
100 auto& state = mode_manager_.GetModeState();
101 state.drag_start = canvas_mouse_pos;
102 state.drag_current = canvas_mouse_pos;
103 }
104 }
105 }
106 break;
107 }
108 }
109
110 // Handle entity drag if active
113 }
114
115 // Handle drag in progress
117 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
118 mode_manager_.GetModeState().drag_current = canvas_mouse_pos;
120 }
121
122 // Handle mouse release - complete drag operation
123 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
125 auto& state = mode_manager_.GetModeState();
126
127 // Apply drag transformation to selected objects
128 auto selected_indices = selection_.GetSelectedIndices();
129 if (!selected_indices.empty() && rooms_ && current_room_id_ >= 0 &&
130 current_room_id_ < 296) {
132
133 auto& room = (*rooms_)[current_room_id_];
134 ImVec2 drag_delta = ImVec2(state.drag_current.x - state.drag_start.x,
135 state.drag_current.y - state.drag_start.y);
136
137 // Convert pixel delta to tile delta
138 int tile_delta_x = static_cast<int>(drag_delta.x) / 8;
139 int tile_delta_y = static_cast<int>(drag_delta.y) / 8;
140
141 // Only apply if there's meaningful movement
142 if (tile_delta_x != 0 || tile_delta_y != 0) {
143 auto& objects = room.GetTileObjects();
144 for (size_t index : selected_indices) {
145 if (index < objects.size()) {
146 objects[index].x_ += tile_delta_x;
147 objects[index].y_ += tile_delta_y;
148
149 // Clamp to room bounds (64x64 tiles)
150 objects[index].x_ =
151 std::clamp(static_cast<int>(objects[index].x_), 0, 63);
152 objects[index].y_ =
153 std::clamp(static_cast<int>(objects[index].y_), 0, 63);
154 }
155 }
156
157 // Ensure renderers refresh after positional change
158 room.MarkObjectsDirty();
159
160 // Trigger cache invalidation and re-render
162 }
163 }
164
165 // Return to select mode
167 }
168}
169
171 // Draw and handle object selection rectangle
173}
174
176 if (!canvas_->IsMouseHovering())
177 return;
178 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
179 return;
180
181 const ImGuiIO& io = ImGui::GetIO();
182 const ImVec2 canvas_pos = canvas_->zero_point();
183 const ImVec2 mouse_pos =
184 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
185
186 // Rectangle selection is started in HandleCanvasMouseInput on left-click
187 // Here we just update and draw during drag
188
189 // Update rectangle during left-click drag
191 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
192 selection_.UpdateRectangleSelection(static_cast<int>(mouse_pos.x),
193 static_cast<int>(mouse_pos.y));
194 // Use ObjectSelection's drawing (themed, consistent)
196 }
197
198 // Complete selection on left mouse release
200 !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
201 auto& room = (*rooms_)[current_room_id_];
202
203 // Determine selection mode based on modifiers
206 if (io.KeyShift) {
208 } else if (io.KeyCtrl) {
210 }
211
212 selection_.EndRectangleSelection(room.GetTileObjects(), mode);
213 }
214}
215
217 // Legacy method - rectangle selection is now handled by ObjectSelection
218 // in DrawObjectSelectRect() / EndRectangleSelection()
219 // This method is kept for API compatibility but does nothing
220}
221
223 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
224 return;
225
226 auto& room = (*rooms_)[current_room_id_];
227 const auto& objects = room.GetTileObjects();
228
229 // Use ObjectSelection's rendering (handles pulsing border, corner handles)
231 canvas_, objects, [this](const zelda3::RoomObject& obj) {
232 // Use GetSelectionDimensions for accurate visual bounds
233 // (doesn't inflate size=0 to 32 like the game's GetSize_1to15or32)
234 auto& dim_table = zelda3::ObjectDimensionTable::Get();
235 if (dim_table.IsLoaded()) {
236 auto [w_tiles, h_tiles] =
237 dim_table.GetSelectionDimensions(obj.id_, obj.size_);
238 return std::make_pair(w_tiles * 8, h_tiles * 8);
239 }
240 // Fallback to drawer (aligns with render) if table not loaded
241 if (object_drawer_) {
242 return object_drawer_->CalculateObjectDimensions(obj);
243 }
244 return std::make_pair(16, 16); // Safe fallback
245 });
246
247 // Enhanced hover tooltip showing object info (always visible on hover)
248 // Skip completely in exclusive entity mode (door/sprite/item selected)
249 if (is_entity_mode_) {
250 return; // Entity mode active - no object tooltips or hover
251 }
252
253 if (canvas_->IsMouseHovering()) {
254 // Also skip tooltip if cursor is over a door/sprite/item entity (not selected yet)
255 ImGuiIO& io = ImGui::GetIO();
256 ImVec2 canvas_pos = canvas_->zero_point();
257 int cursor_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
258 int cursor_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
259 auto entity_at_cursor = GetEntityAtPosition(cursor_x, cursor_y);
260 if (entity_at_cursor.has_value()) {
261 // Entity has priority - skip object tooltip, DrawHoverHighlight will also skip
262 DrawHoverHighlight(objects);
263 return;
264 }
265
266 size_t hovered_index = GetHoveredObjectIndex();
267 if (hovered_index != static_cast<size_t>(-1) &&
268 hovered_index < objects.size()) {
269 const auto& object = objects[hovered_index];
270 std::string object_name = zelda3::GetObjectName(object.id_);
271 int subtype = zelda3::GetObjectSubtype(object.id_);
272 int layer = object.GetLayerValue();
273
274 // Get subtype name
275 const char* subtype_names[] = {"Unknown", "Type 1", "Type 2", "Type 3"};
276 const char* subtype_name =
277 (subtype >= 0 && subtype <= 3) ? subtype_names[subtype] : "Unknown";
278
279 // Build informative tooltip
280 std::string tooltip;
281 tooltip += object_name;
282 tooltip += " (" + std::string(subtype_name) + ")";
283 tooltip += "\n";
284 tooltip += "ID: 0x" + absl::StrFormat("%03X", object.id_);
285 tooltip += " | Layer: " + std::to_string(layer + 1);
286 tooltip += " | Pos: (" + std::to_string(object.x_) + ", " +
287 std::to_string(object.y_) + ")";
288 tooltip += "\nSize: " + std::to_string(object.size_) + " (0x" +
289 absl::StrFormat("%02X", object.size_) + ")";
290
291 if (selection_.IsObjectSelected(hovered_index)) {
292 tooltip += "\n" ICON_MD_MOUSE " Scroll wheel to resize";
293 tooltip += "\n" ICON_MD_DRAG_INDICATOR " Drag to move";
294 } else {
295 tooltip += "\n" ICON_MD_TOUCH_APP " Click to select";
296 }
297
298 ImGui::SetTooltip("%s", tooltip.c_str());
299 }
300 }
301
302 // Draw hover highlight for non-selected objects
303 DrawHoverHighlight(objects);
304}
305
307 const std::vector<zelda3::RoomObject>& objects) {
308 if (!canvas_->IsMouseHovering())
309 return;
310
311 // Skip all object hover in exclusive entity mode (door/sprite/item selected)
312 if (is_entity_mode_)
313 return;
314
315 // Don't show object hover highlight if cursor is over a door/sprite/item entity
316 // Entities take priority over objects for interaction
317 ImGuiIO& io = ImGui::GetIO();
318 ImVec2 canvas_pos = canvas_->zero_point();
319 int cursor_canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
320 int cursor_canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
321 auto entity_at_cursor = GetEntityAtPosition(cursor_canvas_x, cursor_canvas_y);
322 if (entity_at_cursor.has_value()) {
323 return; // Entity has priority - skip object hover highlight
324 }
325
326 size_t hovered_index = GetHoveredObjectIndex();
327 if (hovered_index == static_cast<size_t>(-1) ||
328 hovered_index >= objects.size()) {
329 return;
330 }
331
332 // Don't draw hover highlight if object is already selected
333 if (selection_.IsObjectSelected(hovered_index)) {
334 return;
335 }
336
337 const auto& object = objects[hovered_index];
338 const auto& theme = AgentUI::GetTheme();
339 ImDrawList* draw_list = ImGui::GetWindowDrawList();
340 // canvas_pos already defined above for entity check
341 float scale = canvas_->global_scale();
342
343 // Calculate object position and dimensions
344 auto [obj_x, obj_y] =
345 selection_.RoomToCanvasCoordinates(object.x_, object.y_);
346
347 int pixel_width, pixel_height;
348 auto& dim_table = zelda3::ObjectDimensionTable::Get();
349 if (dim_table.IsLoaded()) {
350 auto [w_tiles, h_tiles] =
351 dim_table.GetSelectionDimensions(object.id_, object.size_);
352 pixel_width = w_tiles * 8;
353 pixel_height = h_tiles * 8;
354 } else if (object_drawer_) {
355 auto dims = object_drawer_->CalculateObjectDimensions(object);
356 pixel_width = dims.first;
357 pixel_height = dims.second;
358 } else {
359 pixel_width = 16;
360 pixel_height = 16;
361 }
362
363 // Apply scale and canvas offset
364 ImVec2 obj_start(canvas_pos.x + obj_x * scale, canvas_pos.y + obj_y * scale);
365 ImVec2 obj_end(obj_start.x + pixel_width * scale,
366 obj_start.y + pixel_height * scale);
367
368 // Expand slightly for visibility
369 constexpr float margin = 2.0f;
370 obj_start.x -= margin;
371 obj_start.y -= margin;
372 obj_end.x += margin;
373 obj_end.y += margin;
374
375 // Get layer-based color for consistent highlighting
376 ImVec4 layer_color = selection_.GetLayerTypeColor(object);
377
378 // Draw subtle hover highlight with layer-based color
379 ImVec4 hover_fill = ImVec4(layer_color.x, layer_color.y, layer_color.z,
380 0.15f // Very subtle fill
381 );
382 ImVec4 hover_border =
383 ImVec4(layer_color.x, layer_color.y, layer_color.z,
384 0.6f // Visible but not as prominent as selection
385 );
386
387 // Draw filled background for better visibility
388 draw_list->AddRectFilled(obj_start, obj_end, ImGui::GetColorU32(hover_fill));
389
390 // Draw dashed-style border (simulated with thinner line)
391 draw_list->AddRect(obj_start, obj_end, ImGui::GetColorU32(hover_border), 0.0f,
392 0, 1.5f);
393}
394
397 !rooms_)
398 return;
399
400 if (current_room_id_ < 0 || current_room_id_ >= 296)
401 return;
402
404
405 // Create new object at the specified position
406 auto new_object = preview_object_;
407 new_object.x_ = room_x;
408 new_object.y_ = room_y;
409
410 // Add object to room
411 auto& room = (*rooms_)[current_room_id_];
412 room.AddTileObject(new_object);
413
414 // Notify callback if set
417 }
418
419 // Trigger cache invalidation
421
422 // Exit placement mode after placing a single object
424}
425
427 // Legacy method - rectangle selection now handled by ObjectSelection
428 // Delegates to ObjectSelection's DrawRectangleSelectionBox if active
431 }
432}
433
435 const auto& theme = AgentUI::GetTheme();
436 auto selected_indices = selection_.GetSelectedIndices();
438 selected_indices.empty() || !rooms_)
439 return;
440 if (current_room_id_ < 0 || current_room_id_ >= 296)
441 return;
442
443 // Draw drag preview for selected objects
444 ImDrawList* draw_list = ImGui::GetWindowDrawList();
445 ImVec2 canvas_pos = canvas_->zero_point();
446 const auto& state = mode_manager_.GetModeState();
447 ImVec2 drag_delta = ImVec2(state.drag_current.x - state.drag_start.x,
448 state.drag_current.y - state.drag_start.y);
449
450 auto& room = (*rooms_)[current_room_id_];
451 const auto& objects = room.GetTileObjects();
452
453 // Draw preview of where objects would be moved
454 for (size_t index : selected_indices) {
455 if (index < objects.size()) {
456 const auto& object = objects[index];
457 auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
458
459 // Calculate object size using shared dimension logic
460 auto [obj_width, obj_height] = CalculateObjectBounds(object);
461
462 // Draw semi-transparent preview at new position
463 ImVec2 preview_start(canvas_pos.x + canvas_x + drag_delta.x,
464 canvas_pos.y + canvas_y + drag_delta.y);
465 ImVec2 preview_end(preview_start.x + obj_width,
466 preview_start.y + obj_height);
467
468 // Draw ghosted object
469 draw_list->AddRectFilled(preview_start, preview_end,
470 ImGui::GetColorU32(theme.dungeon_drag_preview));
471 draw_list->AddRect(preview_start, preview_end,
472 ImGui::GetColorU32(theme.dungeon_selection_secondary),
473 0.0f, 0, 1.5f);
474 }
475 }
476}
477
479 // Legacy method - selection now handled by ObjectSelection class
480 // Kept for API compatibility
481}
482
484 const zelda3::RoomObject& object) const {
485 // Legacy method - selection now handled by ObjectSelection class
486 // Kept for API compatibility
487 return false;
488}
489
491 int room_x, int room_y) const {
492 // Dungeon tiles are 8x8 pixels, convert room coordinates (tiles) to pixels
493 return {room_x * 8, room_y * 8};
494}
495
497 int canvas_x, int canvas_y) const {
498 // Convert canvas pixels back to room coordinates (tiles)
499 return {canvas_x / 8, canvas_y / 8};
500}
501
503 int margin) const {
504 auto canvas_size = canvas_->canvas_size();
505 auto global_scale = canvas_->global_scale();
506 int scaled_width = static_cast<int>(canvas_size.x * global_scale);
507 int scaled_height = static_cast<int>(canvas_size.y * global_scale);
508
509 return (canvas_x >= -margin && canvas_y >= -margin &&
510 canvas_x <= scaled_width + margin &&
511 canvas_y <= scaled_height + margin);
512}
513
515 std::array<zelda3::Room, dungeon_coords::kRoomCount>* rooms, int room_id) {
516 rooms_ = rooms;
517 current_room_id_ = room_id;
521}
522
524 const zelda3::RoomObject& object, bool loaded) {
525 preview_object_ = object;
526
527 if (loaded && object.id_ >= 0) {
528 // Enter object placement mode
532 } else {
533 // Exit placement mode if not loaded
536 }
537 ghost_preview_buffer_.reset();
538 }
539}
540
542 if (!rom_ || !rom_->is_loaded()) {
543 ghost_preview_buffer_.reset();
544 return;
545 }
546
547 // Need room graphics to render the object
548 if (!rooms_ || current_room_id_ < 0 ||
549 current_room_id_ >= static_cast<int>(rooms_->size())) {
550 ghost_preview_buffer_.reset();
551 return;
552 }
553
554 auto& room = (*rooms_)[current_room_id_];
555 if (!room.IsLoaded()) {
556 ghost_preview_buffer_.reset();
557 return;
558 }
559
560 // Calculate object dimensions
561 auto [width, height] = CalculateObjectBounds(preview_object_);
562 width = std::max(width, 16);
563 height = std::max(height, 16);
564
565 // Create or resize the buffer
567 std::make_unique<gfx::BackgroundBuffer>(width, height);
568
569 // Get graphics data from the room
570 const uint8_t* gfx_data = room.get_gfx_buffer().data();
571
572 // Render the preview object
573 zelda3::ObjectDrawer drawer(rom_, current_room_id_, gfx_data);
574 drawer.InitializeDrawRoutines();
575
576 auto status =
579 if (!status.ok()) {
580 ghost_preview_buffer_.reset();
581 return;
582 }
583
584 // Create texture for the preview
585 auto& bitmap = ghost_preview_buffer_->bitmap();
586 if (bitmap.size() > 0) {
590 }
591}
592
599
601 // Don't attempt object selection in exclusive entity mode
602 if (is_entity_mode_)
603 return false;
604
605 size_t hovered = GetHoveredObjectIndex();
606 if (hovered == static_cast<size_t>(-1)) {
607 return false;
608 }
609
610 const ImGuiIO& io = ImGui::GetIO();
612
613 if (io.KeyShift) {
615 } else if (io.KeyCtrl) {
617 }
618
619 selection_.SelectObject(hovered, mode);
620 return true;
621}
622
624 auto indices = selection_.GetSelectedIndices();
625 if (indices.empty() || !rooms_)
626 return;
627 if (current_room_id_ < 0 || current_room_id_ >= 296)
628 return;
629
631
632 auto& room = (*rooms_)[current_room_id_];
633
634 // Sort indices in descending order to avoid index shifts during deletion
635 std::sort(indices.rbegin(), indices.rend());
636
637 // Delete selected objects using Room's RemoveTileObject method
638 for (size_t index : indices) {
639 room.RemoveTileObject(index);
640 }
641
642 // Clear selection
644
645 // Trigger cache invalidation and re-render
647}
648
650 auto indices = selection_.GetSelectedIndices();
651 if (indices.empty() || !rooms_)
652 return;
653 if (current_room_id_ < 0 || current_room_id_ >= 296)
654 return;
655
656 auto& room = (*rooms_)[current_room_id_];
657 const auto& objects = room.GetTileObjects();
658
659 // Copy selected objects to clipboard
660 clipboard_.clear();
661 for (size_t index : indices) {
662 if (index < objects.size()) {
663 clipboard_.push_back(objects[index]);
664 }
665 }
666
668}
669
672 return;
673 if (current_room_id_ < 0 || current_room_id_ >= 296)
674 return;
675
677
678 auto& room = (*rooms_)[current_room_id_];
679
680 // Get mouse position for paste location
681 const ImGuiIO& io = ImGui::GetIO();
682 ImVec2 mouse_pos = io.MousePos;
683 ImVec2 canvas_pos = canvas_->zero_point();
684 ImVec2 canvas_mouse_pos =
685 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
686 auto [paste_x, paste_y] =
687 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
688 static_cast<int>(canvas_mouse_pos.y));
689
690 // Calculate offset from first object in clipboard
691 if (!clipboard_.empty()) {
692 int offset_x = paste_x - clipboard_[0].x_;
693 int offset_y = paste_y - clipboard_[0].y_;
694
695 // Paste all objects with offset
696 for (const auto& obj : clipboard_) {
697 auto new_obj = obj;
698 new_obj.x_ = obj.x_ + offset_x;
699 new_obj.y_ = obj.y_ + offset_y;
700
701 // Clamp to room bounds
702 new_obj.x_ = std::clamp(static_cast<int>(new_obj.x_), 0, 63);
703 new_obj.y_ = std::clamp(static_cast<int>(new_obj.y_), 0, 63);
704
705 room.AddTileObject(new_obj);
706 }
707
708 // Trigger cache invalidation and re-render
710 }
711}
712
714 // Draw entity-specific ghost previews based on current mode
715 switch (mode_manager_.GetMode()) {
718 return;
721 return;
724 return;
726 // Continue below to draw object ghost preview
727 break;
728 default:
729 return; // No ghost preview in other modes
730 }
731
732 // Only draw object ghost preview when in object placement mode
733 if (preview_object_.id_ < 0)
734 return;
735
736 // Check if mouse is over the canvas
737 if (!canvas_->IsMouseHovering())
738 return;
739
740 const ImGuiIO& io = ImGui::GetIO();
741 ImVec2 canvas_pos = canvas_->zero_point();
742 ImVec2 mouse_pos = io.MousePos;
743
744 // Convert mouse position to canvas coordinates
745 ImVec2 canvas_mouse_pos =
746 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
747
748 // Convert to room tile coordinates
749 auto [room_x, room_y] =
750 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
751 static_cast<int>(canvas_mouse_pos.y));
752
753 // Validate position is within room bounds (64x64 tiles)
754 if (room_x < 0 || room_x >= 64 || room_y < 0 || room_y >= 64)
755 return;
756
757 // Convert back to canvas pixel coordinates (for snapped position)
758 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvasCoordinates(room_x, room_y);
759
760 // Calculate object dimensions
761 // Size is a single 4-bit value (0-15), NOT two separate nibbles
762 int size = preview_object_.size_ & 0x0F;
763 int obj_width, obj_height;
764 if (preview_object_.id_ >= 0x60 && preview_object_.id_ <= 0x7F) {
765 // Vertical objects
766 obj_width = 16;
767 obj_height = 16 + size * 16;
768 } else {
769 // Horizontal objects (default)
770 obj_width = 16 + size * 16;
771 obj_height = 16;
772 }
773 obj_width = std::min(obj_width, 256);
774 obj_height = std::min(obj_height, 256);
775
776 // Draw ghost preview at snapped position
777 ImDrawList* draw_list = ImGui::GetWindowDrawList();
778 float scale = canvas_->global_scale();
779
780 // Apply canvas scale and offset
781 ImVec2 preview_start(canvas_pos.x + snap_canvas_x * scale,
782 canvas_pos.y + snap_canvas_y * scale);
783 ImVec2 preview_end(preview_start.x + obj_width * scale,
784 preview_start.y + obj_height * scale);
785
786 const auto& theme = AgentUI::GetTheme();
787 bool drew_bitmap = false;
788
789 // Try to draw the rendered object preview bitmap
791 auto& bitmap = ghost_preview_buffer_->bitmap();
792 if (bitmap.texture()) {
793 // Draw the actual object graphics with transparency
794 ImVec2 bitmap_end(preview_start.x + bitmap.width() * scale,
795 preview_start.y + bitmap.height() * scale);
796
797 // Draw with semi-transparency (ghost effect)
798 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(),
799 preview_start, bitmap_end, ImVec2(0, 0), ImVec2(1, 1),
800 IM_COL32(255, 255, 255, 180)); // Semi-transparent
801
802 // Draw outline around the bitmap
803 ImVec4 preview_outline = ImVec4(theme.dungeon_selection_primary.x,
804 theme.dungeon_selection_primary.y,
805 theme.dungeon_selection_primary.z,
806 0.78f); // More visible
807 draw_list->AddRect(preview_start, bitmap_end,
808 ImGui::GetColorU32(preview_outline), 0.0f, 0, 2.0f);
809 drew_bitmap = true;
810 }
811 }
812
813 // Fallback: draw colored rectangle if no bitmap available
814 if (!drew_bitmap) {
815 // Draw semi-transparent filled rectangle (ghost effect)
816 ImVec4 preview_fill = ImVec4(theme.dungeon_selection_primary.x,
817 theme.dungeon_selection_primary.y,
818 theme.dungeon_selection_primary.z,
819 0.25f); // Semi-transparent
820 draw_list->AddRectFilled(preview_start, preview_end,
821 ImGui::GetColorU32(preview_fill));
822
823 // Draw solid outline for visibility
824 ImVec4 preview_outline = ImVec4(theme.dungeon_selection_primary.x,
825 theme.dungeon_selection_primary.y,
826 theme.dungeon_selection_primary.z,
827 0.78f); // More visible
828 draw_list->AddRect(preview_start, preview_end,
829 ImGui::GetColorU32(preview_outline), 0.0f, 0, 2.0f);
830 }
831
832 // Draw object ID text at corner
833 std::string id_text = absl::StrFormat("0x%02X", preview_object_.id_);
834 ImVec2 text_pos(preview_start.x + 2, preview_start.y + 2);
835
836 // Draw text background for readability
837 ImVec2 text_size = ImGui::CalcTextSize(id_text.c_str());
838 draw_list->AddRectFilled(
839 text_pos,
840 ImVec2(text_pos.x + text_size.x + 4, text_pos.y + text_size.y + 2),
841 IM_COL32(0, 0, 0, 180));
842 draw_list->AddText(ImVec2(text_pos.x + 2, text_pos.y + 1),
843 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
844
845 // Draw crosshair at placement position
846 constexpr float crosshair_size = 8.0f;
847 ImVec2 center(preview_start.x + (obj_width * scale) / 2,
848 preview_start.y + (obj_height * scale) / 2);
849 ImVec4 crosshair_color =
850 ImVec4(theme.text_primary.x, theme.text_primary.y, theme.text_primary.z,
851 0.78f); // Slightly transparent
852 ImU32 crosshair = ImGui::GetColorU32(crosshair_color);
853 draw_list->AddLine(ImVec2(center.x - crosshair_size, center.y),
854 ImVec2(center.x + crosshair_size, center.y), crosshair,
855 1.5f);
856 draw_list->AddLine(ImVec2(center.x, center.y - crosshair_size),
857 ImVec2(center.x, center.y + crosshair_size), crosshair,
858 1.5f);
859}
860
862 const ImGuiIO& io = ImGui::GetIO();
863
864 // Only resize if mouse wheel is being used
865 if (io.MouseWheel == 0.0f)
866 return;
867
868 // Don't resize if in any placement mode
870 return;
871
872 // Check if cursor is over a selected object
874 return;
875
876 size_t hovered = GetHoveredObjectIndex();
877 if (hovered == static_cast<size_t>(-1))
878 return;
879
880 // Only resize if hovering over a selected object
881 if (!selection_.IsObjectSelected(hovered))
882 return;
883
884 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
885 return;
886
887 // Call mutation hook before changes
889
890 auto& room = (*rooms_)[current_room_id_];
891 auto& objects = room.GetTileObjects();
892
893 // Determine resize delta (1 for scroll up, -1 for scroll down)
894 int resize_delta = (io.MouseWheel > 0.0f) ? 1 : -1;
895
896 // Resize all selected objects uniformly
897 auto selected_indices = selection_.GetSelectedIndices();
898 for (size_t index : selected_indices) {
899 if (index >= objects.size())
900 continue;
901
902 auto& object = objects[index];
903
904 // Current size value (0-15)
905 int current_size = static_cast<int>(object.size_);
906 int new_size = current_size + resize_delta;
907
908 // Clamp to valid range (0-15)
909 new_size = std::clamp(new_size, 0, 15);
910
911 // Update object size
912 object.size_ = static_cast<uint8_t>(new_size);
913 }
914
915 room.MarkObjectsDirty();
916
917 // Trigger cache invalidation and re-render
919}
920
922 const zelda3::RoomObject& object) {
923 // Try dimension table first for consistency with selection/highlights
924 auto& dim_table = zelda3::ObjectDimensionTable::Get();
925 if (dim_table.IsLoaded()) {
926 auto [w_tiles, h_tiles] = dim_table.GetDimensions(object.id_, object.size_);
927 return {w_tiles * 8, h_tiles * 8};
928 }
929
930 // If we have a ROM, use ObjectDrawer to calculate accurate dimensions
931 if (rom_) {
932 if (!object_drawer_) {
934 std::make_unique<zelda3::ObjectDrawer>(rom_, current_room_id_);
935 }
936 return object_drawer_->CalculateObjectDimensions(object);
937 }
938
939 // Fallback to simplified calculation if no ROM available
940 // Size is a single 4-bit value (0-15), NOT two separate nibbles
941 int size = object.size_ & 0x0F;
942 int width, height;
943 if (object.id_ >= 0x60 && object.id_ <= 0x7F) {
944 // Vertical objects
945 width = 16;
946 height = 16 + size * 16;
947 } else {
948 // Horizontal objects (default)
949 width = 16 + size * 16;
950 height = 16;
951 }
952 return {width, height};
953}
954
956 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
957 return static_cast<size_t>(-1);
958
959 // Get mouse position
960 const ImGuiIO& io = ImGui::GetIO();
961 ImVec2 canvas_pos = canvas_->zero_point();
962 ImVec2 mouse_pos = io.MousePos;
963 ImVec2 canvas_mouse_pos =
964 ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
965
966 // Convert to room coordinates
967 auto [room_x, room_y] =
968 CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
969 static_cast<int>(canvas_mouse_pos.y));
970
971 // Check all objects in reverse order (top to bottom, prioritize recent)
972 auto& room = (*rooms_)[current_room_id_];
973 const auto& objects = room.GetTileObjects();
974
975 // We need to cast away constness to call CalculateObjectBounds which might
976 // initialize the drawer. This is safe as it doesn't modify logical state.
977 auto* mutable_this = const_cast<DungeonObjectInteraction*>(this);
978
979 for (size_t i = objects.size(); i > 0; --i) {
980 size_t index = i - 1;
981 const auto& object = objects[index];
982
983 // Calculate object bounds using accurate logic
984 auto [width, height] = mutable_this->CalculateObjectBounds(object);
985
986 // Convert width/height (pixels) to tiles for comparison with room_x/room_y
987 // room_x/room_y are in tiles (8x8 pixels)
988 // object.x_/y_ are in tiles
989
990 int obj_x = object.x_;
991 int obj_y = object.y_;
992
993 // Check if mouse is within object bounds
994 // Note: room_x/y are tile coordinates. width/height are pixels.
995 // We need to check pixel coordinates or convert width/height to tiles.
996 // Let's check pixel coordinates for better precision if needed,
997 // but room_x/y are integers (tiles).
998
999 // Convert mouse to pixels relative to room origin
1000 int mouse_pixel_x = static_cast<int>(canvas_mouse_pos.x);
1001 int mouse_pixel_y = static_cast<int>(canvas_mouse_pos.y);
1002
1003 int obj_pixel_x = obj_x * 8;
1004 int obj_pixel_y = obj_y * 8;
1005
1006 if (mouse_pixel_x >= obj_pixel_x && mouse_pixel_x < obj_pixel_x + width &&
1007 mouse_pixel_y >= obj_pixel_y && mouse_pixel_y < obj_pixel_y + height) {
1008 return index;
1009 }
1010 }
1011
1012 return static_cast<size_t>(-1);
1013}
1014
1016 auto indices = selection_.GetSelectedIndices();
1017 if (indices.empty() || !rooms_)
1018 return;
1019 if (current_room_id_ < 0 || current_room_id_ >= 296)
1020 return;
1021
1022 // Validate target layer
1023 if (target_layer < 0 || target_layer > 2) {
1024 return;
1025 }
1026
1028
1029 auto& room = (*rooms_)[current_room_id_];
1030 auto& objects = room.GetTileObjects();
1031
1032 // Update layer for all selected objects
1033 for (size_t index : indices) {
1034 if (index < objects.size()) {
1035 objects[index].layer_ =
1036 static_cast<zelda3::RoomObject::LayerType>(target_layer);
1037 }
1038 }
1039
1040 room.MarkObjectsDirty();
1041
1042 // Trigger cache invalidation and re-render
1044}
1045
1047 auto indices = selection_.GetSelectedIndices();
1048 if (indices.empty() || !rooms_)
1049 return;
1050 if (current_room_id_ < 0 || current_room_id_ >= 296)
1051 return;
1052
1054
1055 auto& room = (*rooms_)[current_room_id_];
1056 auto& objects = room.GetTileObjects();
1057
1058 // Move selected objects to the end of the list (drawn last = appears on top)
1059 // Process in reverse order to maintain relative order of selected objects
1060 std::vector<zelda3::RoomObject> selected_objects;
1061 std::vector<zelda3::RoomObject> other_objects;
1062
1063 for (size_t i = 0; i < objects.size(); ++i) {
1064 if (std::find(indices.begin(), indices.end(), i) != indices.end()) {
1065 selected_objects.push_back(objects[i]);
1066 } else {
1067 other_objects.push_back(objects[i]);
1068 }
1069 }
1070
1071 // Rebuild: other objects first, then selected objects at end
1072 objects.clear();
1073 objects.insert(objects.end(), other_objects.begin(), other_objects.end());
1074 objects.insert(objects.end(), selected_objects.begin(),
1075 selected_objects.end());
1076
1077 // Update selection to new indices (at end of list)
1079 for (size_t i = 0; i < selected_objects.size(); ++i) {
1080 selection_.SelectObject(other_objects.size() + i,
1082 }
1083
1084 room.MarkObjectsDirty();
1085
1087}
1088
1090 auto indices = selection_.GetSelectedIndices();
1091 if (indices.empty() || !rooms_)
1092 return;
1093 if (current_room_id_ < 0 || current_room_id_ >= 296)
1094 return;
1095
1097
1098 auto& room = (*rooms_)[current_room_id_];
1099 auto& objects = room.GetTileObjects();
1100
1101 // Move selected objects to the beginning of the list (drawn first = appears behind)
1102 std::vector<zelda3::RoomObject> selected_objects;
1103 std::vector<zelda3::RoomObject> other_objects;
1104
1105 for (size_t i = 0; i < objects.size(); ++i) {
1106 if (std::find(indices.begin(), indices.end(), i) != indices.end()) {
1107 selected_objects.push_back(objects[i]);
1108 } else {
1109 other_objects.push_back(objects[i]);
1110 }
1111 }
1112
1113 // Rebuild: selected objects first, then other objects
1114 objects.clear();
1115 objects.insert(objects.end(), selected_objects.begin(),
1116 selected_objects.end());
1117 objects.insert(objects.end(), other_objects.begin(), other_objects.end());
1118
1119 // Update selection to new indices (at start of list)
1121 for (size_t i = 0; i < selected_objects.size(); ++i) {
1123 }
1124
1125 room.MarkObjectsDirty();
1126
1128}
1129
1131 auto indices = selection_.GetSelectedIndices();
1132 if (indices.empty() || !rooms_)
1133 return;
1134 if (current_room_id_ < 0 || current_room_id_ >= 296)
1135 return;
1136
1137 auto& room = (*rooms_)[current_room_id_];
1138 auto& objects = room.GetTileObjects();
1139
1140 // Move each selected object up one position (towards end of list)
1141 // Process from end to start to avoid shifting issues
1142 std::sort(indices.begin(), indices.end());
1143
1144 // Check if any selected object is already at the end
1145 bool all_at_end = true;
1146 for (size_t idx : indices) {
1147 if (idx < objects.size() - 1) {
1148 all_at_end = false;
1149 break;
1150 }
1151 }
1152 if (all_at_end)
1153 return;
1154
1156
1157 // Track new indices after moves
1158 std::vector<size_t> new_indices;
1159
1160 // Process from end to avoid index shifting issues
1161 for (auto it = indices.rbegin(); it != indices.rend(); ++it) {
1162 size_t idx = *it;
1163 if (idx < objects.size() - 1) {
1164 // Swap with next object
1165 std::swap(objects[idx], objects[idx + 1]);
1166 new_indices.push_back(idx + 1);
1167 } else {
1168 new_indices.push_back(idx);
1169 }
1170 }
1171
1172 // Update selection
1174 for (size_t idx : new_indices) {
1176 }
1177
1178 room.MarkObjectsDirty();
1179
1181}
1182
1184 auto indices = selection_.GetSelectedIndices();
1185 if (indices.empty() || !rooms_)
1186 return;
1187 if (current_room_id_ < 0 || current_room_id_ >= 296)
1188 return;
1189
1190 auto& room = (*rooms_)[current_room_id_];
1191 auto& objects = room.GetTileObjects();
1192
1193 // Move each selected object down one position (towards start of list)
1194 // Process from start to end to avoid shifting issues
1195 std::sort(indices.begin(), indices.end());
1196
1197 // Check if any selected object is already at the start
1198 bool all_at_start = true;
1199 for (size_t idx : indices) {
1200 if (idx > 0) {
1201 all_at_start = false;
1202 break;
1203 }
1204 }
1205 if (all_at_start)
1206 return;
1207
1209
1210 // Track new indices after moves
1211 std::vector<size_t> new_indices;
1212
1213 // Process from start to avoid index shifting issues
1214 for (size_t idx : indices) {
1215 if (idx > 0) {
1216 // Swap with previous object
1217 std::swap(objects[idx], objects[idx - 1]);
1218 new_indices.push_back(idx - 1);
1219 } else {
1220 new_indices.push_back(idx);
1221 }
1222 }
1223
1224 // Update selection
1226 for (size_t idx : new_indices) {
1228 }
1229
1230 room.MarkObjectsDirty();
1231
1233}
1234
1236 // Only process if we have selected objects
1237 if (!selection_.HasSelection())
1238 return;
1239
1240 // Only when not typing in a text field
1241 if (ImGui::IsAnyItemActive())
1242 return;
1243
1244 // Check for layer assignment shortcuts (1, 2, 3 keys)
1245 if (ImGui::IsKeyPressed(ImGuiKey_1)) {
1246 SendSelectedToLayer(0); // Layer 1 (BG1)
1247 } else if (ImGui::IsKeyPressed(ImGuiKey_2)) {
1248 SendSelectedToLayer(1); // Layer 2 (BG2)
1249 } else if (ImGui::IsKeyPressed(ImGuiKey_3)) {
1250 SendSelectedToLayer(2); // Layer 3 (BG3)
1251 }
1252
1253 // Object ordering shortcuts
1254 // Ctrl+Shift+] = Bring to Front, Ctrl+Shift+[ = Send to Back
1255 // Ctrl+] = Bring Forward, Ctrl+[ = Send Backward
1256 auto& io = ImGui::GetIO();
1257 if (io.KeyCtrl && io.KeyShift) {
1258 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
1260 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
1262 }
1263 } else if (io.KeyCtrl) {
1264 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
1266 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
1268 }
1269 }
1270}
1271
1272// ============================================================================
1273// Door Placement Methods
1274// ============================================================================
1275
1277 zelda3::DoorType type) {
1278 if (enabled) {
1281 ghost_preview_buffer_.reset(); // Clear object ghost preview
1282 } else {
1285 }
1286 }
1287}
1288
1290 // Only draw if door placement mode is active
1292 return;
1293
1294 // Check if mouse is over the canvas
1295 if (!canvas_->IsMouseHovering())
1296 return;
1297
1298 const ImGuiIO& io = ImGui::GetIO();
1299 ImVec2 canvas_pos = canvas_->zero_point();
1300 ImVec2 mouse_pos = io.MousePos;
1301
1302 // Convert mouse position to canvas coordinates (in pixels)
1303 int canvas_x = static_cast<int>(mouse_pos.x - canvas_pos.x);
1304 int canvas_y = static_cast<int>(mouse_pos.y - canvas_pos.y);
1305
1306 // Detect which wall the cursor is near
1307 zelda3::DoorDirection direction;
1309 direction)) {
1310 // Not near a wall - don't show preview
1311 return;
1312 }
1313
1314 // Snap to nearest valid door position
1316 canvas_x, canvas_y, direction);
1317
1318 // Store detected values for placement
1319 auto& state = mode_manager_.GetModeState();
1320 state.detected_door_direction = direction;
1321 state.snapped_door_position = position;
1322
1323 // Get door position in tile coordinates
1324 auto [tile_x, tile_y] =
1326
1327 // Get door dimensions
1328 auto dims = zelda3::GetDoorDimensions(direction);
1329 int door_width_px = dims.width_tiles * 8;
1330 int door_height_px = dims.height_tiles * 8;
1331
1332 // Convert to canvas pixel coordinates
1333 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvasCoordinates(tile_x, tile_y);
1334
1335 // Draw ghost preview
1336 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1337 float scale = canvas_->global_scale();
1338
1339 ImVec2 preview_start(canvas_pos.x + snap_canvas_x * scale,
1340 canvas_pos.y + snap_canvas_y * scale);
1341 ImVec2 preview_end(preview_start.x + door_width_px * scale,
1342 preview_start.y + door_height_px * scale);
1343
1344 const auto& theme = AgentUI::GetTheme();
1345
1346 // Draw semi-transparent filled rectangle
1347 ImU32 fill_color = IM_COL32(theme.dungeon_selection_primary.x * 255,
1348 theme.dungeon_selection_primary.y * 255,
1349 theme.dungeon_selection_primary.z * 255,
1350 80); // Semi-transparent
1351 draw_list->AddRectFilled(preview_start, preview_end, fill_color);
1352
1353 // Draw outline
1354 ImVec4 outline_color = ImVec4(theme.dungeon_selection_primary.x,
1355 theme.dungeon_selection_primary.y,
1356 theme.dungeon_selection_primary.z, 0.9f);
1357 draw_list->AddRect(preview_start, preview_end,
1358 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
1359
1360 // Draw door type label
1361 const char* type_name =
1362 std::string(zelda3::GetDoorTypeName(GetPreviewDoorType())).c_str();
1363 const char* dir_name =
1364 std::string(zelda3::GetDoorDirectionName(direction)).c_str();
1365 char label[64];
1366 snprintf(label, sizeof(label), "%s (%s)", type_name, dir_name);
1367
1368 ImVec2 text_pos(preview_start.x, preview_start.y - 16 * scale);
1369 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 200), label);
1370}
1371
1372void DungeonObjectInteraction::PlaceDoorAtPosition(int canvas_x, int canvas_y) {
1374 return;
1375
1376 if (current_room_id_ < 0 || current_room_id_ >= 296)
1377 return;
1378
1379 // Detect wall from position
1380 zelda3::DoorDirection direction;
1382 direction)) {
1383 // Not near a wall - can't place door
1384 return;
1385 }
1386
1387 // Snap to nearest valid door position
1389 canvas_x, canvas_y, direction);
1390
1391 // Validate position
1392 if (!zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
1393 return;
1394 }
1395
1397
1398 // Create the door
1399 zelda3::Room::Door new_door;
1400 new_door.position = position;
1401 new_door.type = GetPreviewDoorType();
1402 new_door.direction = direction;
1403 // Encode bytes for ROM storage
1404 auto [byte1, byte2] = new_door.EncodeBytes();
1405 new_door.byte1 = byte1;
1406 new_door.byte2 = byte2;
1407
1408 // Add door to room
1409 auto& room = (*rooms_)[current_room_id_];
1410 room.AddDoor(new_door);
1411
1412 // Trigger cache invalidation
1414}
1415
1416// ============================================================================
1417// Sprite Placement Methods
1418// ============================================================================
1419
1421 uint8_t sprite_id) {
1422 if (enabled) {
1425 ghost_preview_buffer_.reset(); // Clear object ghost preview
1426 } else {
1429 }
1430 }
1431}
1432
1435 return;
1436
1437 if (!canvas_->IsMouseHovering())
1438 return;
1439
1440 const ImGuiIO& io = ImGui::GetIO();
1441 ImVec2 canvas_pos = canvas_->zero_point();
1442 ImVec2 mouse_pos = io.MousePos;
1443 float scale = canvas_->global_scale();
1444
1445 // Convert to room coordinates (sprites use 16-pixel grid)
1446 int canvas_x = static_cast<int>((mouse_pos.x - canvas_pos.x) / scale);
1447 int canvas_y = static_cast<int>((mouse_pos.y - canvas_pos.y) / scale);
1448
1449 // Snap to 16-pixel grid (sprite coordinate system)
1450 int snapped_x = (canvas_x / 16) * 16;
1451 int snapped_y = (canvas_y / 16) * 16;
1452
1453 // Draw ghost rectangle for sprite preview
1454 ImVec2 rect_min(canvas_pos.x + snapped_x * scale,
1455 canvas_pos.y + snapped_y * scale);
1456 ImVec2 rect_max(rect_min.x + 16 * scale, rect_min.y + 16 * scale);
1457
1458 // Semi-transparent green for sprites
1459 ImU32 fill_color = IM_COL32(50, 200, 50, 100);
1460 ImU32 outline_color = IM_COL32(50, 255, 50, 200);
1461
1462 canvas_->draw_list()->AddRectFilled(rect_min, rect_max, fill_color);
1463 canvas_->draw_list()->AddRect(rect_min, rect_max, outline_color, 0.0f, 0,
1464 2.0f);
1465
1466 // Draw sprite ID label
1467 std::string label = absl::StrFormat("%02X", GetPreviewSpriteId());
1468 canvas_->draw_list()->AddText(rect_min, IM_COL32(255, 255, 255, 255),
1469 label.c_str());
1470}
1471
1473 int canvas_y) {
1475 return;
1476
1477 if (current_room_id_ < 0 || current_room_id_ >= 296)
1478 return;
1479
1480 float scale = canvas_->global_scale();
1481 if (scale <= 0.0f)
1482 scale = 1.0f;
1483
1484 // Convert to sprite coordinates (16-pixel units)
1485 int sprite_x = canvas_x / static_cast<int>(16 * scale);
1486 int sprite_y = canvas_y / static_cast<int>(16 * scale);
1487
1488 // Clamp to valid range (0-31 for each axis in a 512x512 room)
1489 sprite_x = std::clamp(sprite_x, 0, 31);
1490 sprite_y = std::clamp(sprite_y, 0, 31);
1491
1493
1494 // Create the sprite
1496 static_cast<uint8_t>(sprite_x),
1497 static_cast<uint8_t>(sprite_y), 0, 0);
1498
1499 // Add sprite to room
1500 auto& room = (*rooms_)[current_room_id_];
1501 room.GetSprites().push_back(new_sprite);
1502
1503 // Trigger cache invalidation
1505}
1506
1507// ============================================================================
1508// Item Placement Methods
1509// ============================================================================
1510
1512 uint8_t item_id) {
1513 if (enabled) {
1516 ghost_preview_buffer_.reset(); // Clear object ghost preview
1517 } else {
1520 }
1521 }
1522}
1523
1526 return;
1527
1528 if (!canvas_->IsMouseHovering())
1529 return;
1530
1531 const ImGuiIO& io = ImGui::GetIO();
1532 ImVec2 canvas_pos = canvas_->zero_point();
1533 ImVec2 mouse_pos = io.MousePos;
1534 float scale = canvas_->global_scale();
1535
1536 // Convert to room coordinates (items use 8-pixel grid for fine positioning)
1537 int canvas_x = static_cast<int>((mouse_pos.x - canvas_pos.x) / scale);
1538 int canvas_y = static_cast<int>((mouse_pos.y - canvas_pos.y) / scale);
1539
1540 // Snap to 8-pixel grid
1541 int snapped_x = (canvas_x / 8) * 8;
1542 int snapped_y = (canvas_y / 8) * 8;
1543
1544 // Draw ghost rectangle for item preview
1545 ImVec2 rect_min(canvas_pos.x + snapped_x * scale,
1546 canvas_pos.y + snapped_y * scale);
1547 ImVec2 rect_max(rect_min.x + 16 * scale, rect_min.y + 16 * scale);
1548
1549 // Semi-transparent yellow for items
1550 ImU32 fill_color = IM_COL32(200, 200, 50, 100);
1551 ImU32 outline_color = IM_COL32(255, 255, 50, 200);
1552
1553 canvas_->draw_list()->AddRectFilled(rect_min, rect_max, fill_color);
1554 canvas_->draw_list()->AddRect(rect_min, rect_max, outline_color, 0.0f, 0,
1555 2.0f);
1556
1557 // Draw item ID label
1558 std::string label = absl::StrFormat("%02X", GetPreviewItemId());
1559 canvas_->draw_list()->AddText(rect_min, IM_COL32(255, 255, 255, 255),
1560 label.c_str());
1561}
1562
1563void DungeonObjectInteraction::PlaceItemAtPosition(int canvas_x, int canvas_y) {
1565 return;
1566
1567 if (current_room_id_ < 0 || current_room_id_ >= 296)
1568 return;
1569
1570 float scale = canvas_->global_scale();
1571 if (scale <= 0.0f)
1572 scale = 1.0f;
1573
1574 // Convert to pixel coordinates
1575 int pixel_x = canvas_x / static_cast<int>(scale);
1576 int pixel_y = canvas_y / static_cast<int>(scale);
1577
1578 // PotItem position encoding:
1579 // high byte * 16 = Y, low byte * 4 = X
1580 // So: X = pixel_x / 4, Y = pixel_y / 16
1581 int encoded_x = pixel_x / 4;
1582 int encoded_y = pixel_y / 16;
1583
1584 // Clamp to valid range
1585 encoded_x = std::clamp(encoded_x, 0, 255);
1586 encoded_y = std::clamp(encoded_y, 0, 255);
1587
1589
1590 // Create the pot item
1591 zelda3::PotItem new_item;
1592 new_item.position = static_cast<uint16_t>((encoded_y << 8) | encoded_x);
1593 new_item.item = GetPreviewItemId();
1594
1595 // Add item to room
1596 auto& room = (*rooms_)[current_room_id_];
1597 room.GetPotItems().push_back(new_item);
1598
1599 // Trigger cache invalidation
1601}
1602
1603// ============================================================================
1604// Entity Selection Methods (Doors, Sprites, Items)
1605// ============================================================================
1606
1608 // Clear object selection when selecting an entity
1609 if (type != EntityType::Object) {
1611 }
1612
1613 selected_entity_.type = type;
1614 selected_entity_.index = index;
1615
1616 // Enter exclusive entity mode - suppresses all object interactions
1618
1620}
1621
1630
1632 int canvas_x, int canvas_y) const {
1633 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
1634 return std::nullopt;
1635
1636 const auto& room = (*rooms_)[current_room_id_];
1637
1638 // Convert screen coordinates to room coordinates by accounting for canvas scale
1639 float scale = canvas_->global_scale();
1640 if (scale <= 0.0f)
1641 scale = 1.0f;
1642 int room_x = static_cast<int>(canvas_x / scale);
1643 int room_y = static_cast<int>(canvas_y / scale);
1644
1645 // Check doors first (they have higher priority for selection)
1646 const auto& doors = room.GetDoors();
1647 for (size_t i = 0; i < doors.size(); ++i) {
1648 const auto& door = doors[i];
1649
1650 // Get door position in tile coordinates
1651 auto [tile_x, tile_y] = door.GetTileCoords();
1652
1653 // Get door dimensions
1654 auto dims = zelda3::GetDoorDimensions(door.direction);
1655
1656 // Convert to pixel coordinates
1657 int door_x = tile_x * 8;
1658 int door_y = tile_y * 8;
1659 int door_w = dims.width_tiles * 8;
1660 int door_h = dims.height_tiles * 8;
1661
1662 // Check if point is inside door bounds (using room coordinates)
1663 if (room_x >= door_x && room_x < door_x + door_w && room_y >= door_y &&
1664 room_y < door_y + door_h) {
1666 }
1667 }
1668
1669 // Check sprites (16x16 hitbox)
1670 // NOTE: Sprite coordinates are in 16-pixel units (0-31 range = 512 pixels)
1671 const auto& sprites = room.GetSprites();
1672 for (size_t i = 0; i < sprites.size(); ++i) {
1673 const auto& sprite = sprites[i];
1674
1675 // Sprites use 16-pixel coordinate system
1676 int sprite_x = sprite.x() * 16;
1677 int sprite_y = sprite.y() * 16;
1678
1679 // 16x16 hitbox (using room coordinates)
1680 if (room_x >= sprite_x && room_x < sprite_x + 16 && room_y >= sprite_y &&
1681 room_y < sprite_y + 16) {
1683 }
1684 }
1685
1686 // Check pot items - they have their own position data from ROM
1687 const auto& pot_items = room.GetPotItems();
1688
1689 for (size_t i = 0; i < pot_items.size(); ++i) {
1690 const auto& pot_item = pot_items[i];
1691
1692 // Get pixel coordinates from PotItem
1693 int item_x = pot_item.GetPixelX();
1694 int item_y = pot_item.GetPixelY();
1695
1696 // 16x16 hitbox (using room coordinates)
1697 if (room_x >= item_x && room_x < item_x + 16 && room_y >= item_y &&
1698 room_y < item_y + 16) {
1700 }
1701 }
1702
1703 return std::nullopt;
1704}
1705
1707 if (!canvas_->IsMouseHovering())
1708 return false;
1709
1710 const ImGuiIO& io = ImGui::GetIO();
1711 ImVec2 canvas_pos = canvas_->zero_point();
1712 int canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
1713 int canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
1714
1715 auto entity = GetEntityAtPosition(canvas_x, canvas_y);
1716 if (entity.has_value()) {
1717 // Clear previous object selection
1719
1720 SelectEntity(entity->type, entity->index);
1721
1722 // Start drag
1724 auto& state = mode_manager_.GetModeState();
1725 state.entity_drag_start =
1726 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
1727 state.entity_drag_current = state.entity_drag_start;
1728
1729 return true;
1730 }
1731
1732 // No entity at cursor - clear entity selection
1734 return false;
1735}
1736
1740 return;
1741
1742 const ImGuiIO& io = ImGui::GetIO();
1743 auto& state = mode_manager_.GetModeState();
1744
1745 if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1746 // Mouse released - complete the drag
1748 // For doors, snap to valid wall position
1749 ImVec2 canvas_pos = canvas_->zero_point();
1750 int canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
1751 int canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
1752
1753 // Detect wall
1754 zelda3::DoorDirection direction;
1756 canvas_x, canvas_y, direction)) {
1757 // Snap to nearest valid position
1759 canvas_x, canvas_y, direction);
1760
1761 if (zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
1762 // Update door position
1763 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
1764 auto& room = (*rooms_)[current_room_id_];
1765 auto& doors = room.GetDoors();
1766 if (selected_entity_.index < doors.size()) {
1768
1769 doors[selected_entity_.index].position = position;
1770 doors[selected_entity_.index].direction = direction;
1771
1772 // Re-encode bytes
1773 auto [b1, b2] = doors[selected_entity_.index].EncodeBytes();
1774 doors[selected_entity_.index].byte1 = b1;
1775 doors[selected_entity_.index].byte2 = b2;
1776
1777 // Mark room dirty
1778 room.MarkObjectsDirty();
1779
1781 }
1782 }
1783 }
1784 }
1786 // Move sprite to new position
1787 ImVec2 canvas_pos = canvas_->zero_point();
1788 int canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
1789 int canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
1790
1791 // Convert to sprite coordinates (16-pixel units)
1792 int tile_x = canvas_x / 16;
1793 int tile_y = canvas_y / 16;
1794
1795 // Clamp to room bounds (sprites use 0-31 range)
1796 tile_x = std::clamp(tile_x, 0, 31);
1797 tile_y = std::clamp(tile_y, 0, 31);
1798
1799 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
1800 auto& room = (*rooms_)[current_room_id_];
1801 auto& sprites = room.GetSprites();
1802 if (selected_entity_.index < sprites.size()) {
1804
1805 sprites[selected_entity_.index].set_x(tile_x);
1806 sprites[selected_entity_.index].set_y(tile_y);
1807
1809 }
1810 }
1811 }
1812
1813 // Return to select mode
1815 return;
1816 }
1817
1818 // Update drag position
1819 ImVec2 canvas_pos = canvas_->zero_point();
1820 state.entity_drag_current =
1821 ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
1822}
1823
1826 return;
1827
1828 if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296)
1829 return;
1830
1831 const auto& room = (*rooms_)[current_room_id_];
1832 const auto& theme = AgentUI::GetTheme();
1833 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1834 ImVec2 canvas_pos = canvas_->zero_point();
1835 float scale = canvas_->global_scale();
1836
1837 ImVec2 pos, size;
1838 ImU32 color;
1839 const char* label = "";
1840
1841 switch (selected_entity_.type) {
1842 case EntityType::Door: {
1843 const auto& doors = room.GetDoors();
1844 if (selected_entity_.index >= doors.size())
1845 return;
1846
1847 const auto& door = doors[selected_entity_.index];
1848 auto [tile_x, tile_y] = door.GetTileCoords();
1849 auto dims = zelda3::GetDoorDimensions(door.direction);
1850
1851 // If dragging, use current drag position for door preview
1853 const auto& state = mode_manager_.GetModeState();
1854 int drag_x = static_cast<int>(state.entity_drag_current.x);
1855 int drag_y = static_cast<int>(state.entity_drag_current.y);
1856
1858 bool is_inner = false;
1859 if (zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, dir,
1860 is_inner)) {
1862 drag_x, drag_y, dir);
1863 auto [snap_x, snap_y] =
1865 tile_x = snap_x;
1866 tile_y = snap_y;
1867 dims = zelda3::GetDoorDimensions(dir);
1868 }
1869 }
1870
1871 pos = ImVec2(canvas_pos.x + tile_x * 8 * scale,
1872 canvas_pos.y + tile_y * 8 * scale);
1873 size =
1874 ImVec2(dims.width_tiles * 8 * scale, dims.height_tiles * 8 * scale);
1875 color = IM_COL32(255, 165, 0, 180); // Orange
1876 label = "Door";
1877 break;
1878 }
1879
1880 case EntityType::Sprite: {
1881 const auto& sprites = room.GetSprites();
1882 if (selected_entity_.index >= sprites.size())
1883 return;
1884
1885 const auto& sprite = sprites[selected_entity_.index];
1886 // Sprites use 16-pixel coordinate system
1887 int pixel_x = sprite.x() * 16;
1888 int pixel_y = sprite.y() * 16;
1889
1890 // If dragging, use current drag position (snapped to 16-pixel grid)
1892 const auto& state = mode_manager_.GetModeState();
1893 int tile_x = static_cast<int>(state.entity_drag_current.x) / 16;
1894 int tile_y = static_cast<int>(state.entity_drag_current.y) / 16;
1895 tile_x = std::clamp(tile_x, 0, 31);
1896 tile_y = std::clamp(tile_y, 0, 31);
1897 pixel_x = tile_x * 16;
1898 pixel_y = tile_y * 16;
1899 }
1900
1901 pos = ImVec2(canvas_pos.x + pixel_x * scale,
1902 canvas_pos.y + pixel_y * scale);
1903 size = ImVec2(16 * scale, 16 * scale);
1904 color = IM_COL32(0, 255, 0, 180); // Green
1905 label = "Sprite";
1906 break;
1907 }
1908
1909 case EntityType::Item: {
1910 // Pot items have their own position data from ROM
1911 const auto& pot_items = room.GetPotItems();
1912
1913 if (selected_entity_.index >= pot_items.size())
1914 return;
1915
1916 const auto& pot_item = pot_items[selected_entity_.index];
1917 int pixel_x = pot_item.GetPixelX();
1918 int pixel_y = pot_item.GetPixelY();
1919
1920 pos = ImVec2(canvas_pos.x + pixel_x * scale,
1921 canvas_pos.y + pixel_y * scale);
1922 size = ImVec2(16 * scale, 16 * scale);
1923 color = IM_COL32(255, 255, 0, 180); // Yellow
1924 label = "Item";
1925 break;
1926 }
1927
1928 default:
1929 return;
1930 }
1931
1932 // Draw selection rectangle with animated border
1933 static float pulse = 0.0f;
1934 pulse += ImGui::GetIO().DeltaTime * 3.0f;
1935 float alpha = 0.5f + 0.3f * sinf(pulse);
1936
1937 ImU32 fill_color =
1938 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
1939 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
1940 fill_color);
1941 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
1942 0, 2.0f);
1943
1944 // Draw label
1945 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
1946 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), label);
1947
1948 // Draw snap position indicators when dragging a door
1950}
1951
1953 // Only show snap indicators when dragging a door entity
1956 return;
1957
1958 // Detect wall direction and section (outer wall vs inner seam) from drag position
1959 const auto& state = mode_manager_.GetModeState();
1960 zelda3::DoorDirection direction;
1961 bool is_inner = false;
1962 int drag_x = static_cast<int>(state.entity_drag_current.x);
1963 int drag_y = static_cast<int>(state.entity_drag_current.y);
1964 if (!zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, direction,
1965 is_inner))
1966 return;
1967
1968 // Get the starting position index for this section
1969 uint8_t start_pos =
1971
1972 // Get the nearest snap position
1974 drag_x, drag_y, direction);
1975
1976 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1977 ImVec2 canvas_pos = canvas_->zero_point();
1978 float scale = canvas_->global_scale();
1979 const auto& theme = AgentUI::GetTheme();
1980 auto dims = zelda3::GetDoorDimensions(direction);
1981
1982 // Draw indicators for 6 positions in this section (3 X positions × 2 Y offsets)
1983 // Positions are: start_pos+0,1,2 (one Y offset) and start_pos+3,4,5 (other Y offset)
1984 for (uint8_t i = 0; i < 6; ++i) {
1985 uint8_t pos = start_pos + i;
1986 auto [tile_x, tile_y] =
1988 float pixel_x = tile_x * 8.0f;
1989 float pixel_y = tile_y * 8.0f;
1990
1991 ImVec2 snap_start(canvas_pos.x + pixel_x * scale,
1992 canvas_pos.y + pixel_y * scale);
1993 ImVec2 snap_end(snap_start.x + dims.width_pixels() * scale,
1994 snap_start.y + dims.height_pixels() * scale);
1995
1996 if (pos == nearest_snap) {
1997 // Highlighted nearest position - brighter with thicker border
1998 ImVec4 highlight = ImVec4(theme.dungeon_selection_primary.x,
1999 theme.dungeon_selection_primary.y,
2000 theme.dungeon_selection_primary.z, 0.75f);
2001 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(highlight),
2002 0.0f, 0, 2.5f);
2003 } else {
2004 // Ghosted other positions - semi-transparent thin border
2005 ImVec4 ghost = ImVec4(1.0f, 1.0f, 1.0f, 0.25f);
2006 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
2007 0, 1.0f);
2008 }
2009 }
2010}
2011
2012} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:128
Handles object selection, placement, and interaction within the dungeon canvas.
void SetCurrentRoom(std::array< zelda3::Room, dungeon_coords::kRoomCount > *rooms, int room_id)
std::pair< int, int > CalculateObjectBounds(const zelda3::RoomObject &object)
void PlaceDoorAtPosition(int canvas_x, int canvas_y)
bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin=32) const
std::unique_ptr< gfx::BackgroundBuffer > ghost_preview_buffer_
std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y) const
void SetItemPlacementMode(bool enabled, uint8_t item_id=0)
bool IsObjectInSelectBox(const zelda3::RoomObject &object) const
void DrawHoverHighlight(const std::vector< zelda3::RoomObject > &objects)
void PlaceSpriteAtPosition(int canvas_x, int canvas_y)
std::function< void(const zelda3::RoomObject &) object_placed_callback_)
std::unique_ptr< zelda3::ObjectDrawer > object_drawer_
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
std::array< zelda3::Room, dungeon_coords::kRoomCount > * rooms_
std::vector< zelda3::RoomObject > clipboard_
std::optional< SelectedEntity > GetEntityAtPosition(int canvas_x, int canvas_y) const
void SelectEntity(EntityType type, size_t index)
void SetDoorPlacementMode(bool enabled, zelda3::DoorType type=zelda3::DoorType::NormalDoor)
std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y) const
void SetPreviewObject(const zelda3::RoomObject &object, bool loaded)
void PlaceItemAtPosition(int canvas_x, int canvas_y)
void SetContext(InteractionContext *ctx)
Set the shared interaction context.
void SetMode(InteractionMode mode)
Set interaction mode.
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
bool IsPlacementActive() const
Check if any placement mode is active.
bool IsObjectPlacementActive() const
Check if object placement mode is active.
bool IsRectangleSelectionActive() const
Check if a rectangle selection is in progress.
void UpdateRectangleSelection(int canvas_x, int canvas_y)
Update rectangle selection endpoint.
static std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y)
Convert room tile coordinates to canvas pixel coordinates.
bool IsObjectSelected(size_t index) const
Check if an object is selected.
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Complete rectangle selection operation.
void BeginRectangleSelection(int canvas_x, int canvas_y)
Begin a rectangle selection operation.
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::pair< int, int >(const zelda3::RoomObject &)> dimension_calculator)
Draw selection highlights for all selected objects.
ImVec4 GetLayerTypeColor(const zelda3::RoomObject &object) const
Get selection highlight color based on object layer and type.
void DrawRectangleSelectionBox(gui::Canvas *canvas)
Draw the active rectangle selection box.
bool HasSelection() const
Check if any objects are selected.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:35
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:115
static Arena & Get()
Definition arena.cc:20
auto global_scale() const
Definition canvas.h:494
auto draw_list() const
Definition canvas.h:442
auto canvas_size() const
Definition canvas.h:451
auto zero_point() const
Definition canvas.h:443
bool IsMouseHovering() const
Definition canvas.h:433
static uint8_t GetSectionStartPosition(DoorDirection direction, bool is_inner)
Get the starting position index for outer/inner section.
static bool DetectWallFromPosition(int canvas_x, int canvas_y, DoorDirection &out_direction)
Detect which wall the cursor is near.
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
static std::pair< int, int > PositionToTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to tile coordinates.
static bool DetectWallSection(int canvas_x, int canvas_y, DoorDirection &out_direction, bool &out_is_inner)
Detect wall with inner/outer section information.
static uint8_t SnapToNearestPosition(int canvas_x, int canvas_y, DoorDirection direction)
Convert canvas coordinates to nearest valid door position.
static ObjectDimensionTable & Get()
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr)
Draw a room object to background buffers.
A class for managing sprites in the overworld and underworld.
Definition sprite.h:35
#define ICON_MD_DRAG_INDICATOR
Definition icons.h:624
#define ICON_MD_TOUCH_APP
Definition icons.h:2000
#define ICON_MD_MOUSE
Definition icons.h:1251
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
EntityType
Type of entity that can be selected in the dungeon editor.
constexpr DoorDimensions GetDoorDimensions(DoorDirection dir)
Get door dimensions based on direction.
Definition door_types.h:192
DoorType
Door types from ALTTP.
Definition door_types.h:33
int GetObjectSubtype(int object_id)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:161
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
DoorDirection
Door direction on room walls.
Definition door_types.h:18
void NotifyEntityChanged() const
Notify that entity has changed.
std::array< zelda3::Room, dungeon_coords::kRoomCount > * rooms
void NotifyInvalidateCache() const
Notify that cache invalidation is needed.
void NotifyMutation() const
Notify that a mutation is about to happen.
std::optional< zelda3::DoorType > preview_door_type
zelda3::DoorDirection detected_door_direction
std::optional< uint8_t > preview_item_id
std::optional< uint8_t > preview_sprite_id
std::optional< zelda3::RoomObject > preview_object
Represents a selected entity in the dungeon editor.
uint16_t position
Definition room.h:136
Represents a door in a dungeon room.
Definition room.h:263
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:268
DoorType type
Door type (determines appearance/behavior)
Definition room.h:265
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:302
DoorDirection direction
Which wall the door is on.
Definition room.h:266
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:264
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:269