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