yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6
7#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
14#include "app/platform/window.h"
15#include "imgui/imgui.h"
17
18namespace yaze {
19namespace zelda3 {
20
22
24 if (rom_ == nullptr) {
25 return absl::InvalidArgumentError("ROM is null");
26 }
27
28 // Set default configuration
29 config_.snap_to_grid = true;
30 config_.grid_size = 16;
31 config_.show_grid = true;
32 config_.show_preview = true;
33 config_.auto_save = false;
37
38 // Set default editing state
41 editing_state_.current_object_type = 0x10; // Default to wall
43
44 // Initialize empty room
45 owned_room_ = std::make_unique<Room>(0, rom_);
47
48 // Load templates
49 // TODO: Make this path configurable or platform-aware
50 template_manager_.LoadTemplates("assets/templates/dungeon");
51
52 return absl::OkStatus();
53}
54
55absl::Status DungeonObjectEditor::LoadRoom(int room_id) {
56 if (rom_ == nullptr) {
57 return absl::InvalidArgumentError("ROM is null");
58 }
59
60 if (room_id < 0 || room_id >= kNumberOfRooms) {
61 return absl::InvalidArgumentError("Invalid room ID");
62 }
63
64 // Create undo point before loading
65 auto status = CreateUndoPoint();
66 if (!status.ok()) {
67 // Continue anyway, but log the issue
68 }
69
70 // Load room from ROM
71 owned_room_ = std::make_unique<Room>(room_id, rom_);
73
74 // Clear selection
76
77 // Reset editing state
81
82 // Notify callbacks
85 }
86
87 return absl::OkStatus();
88}
89
91 if (current_room_ == nullptr) {
92 return absl::FailedPreconditionError("No room loaded");
93 }
94
95 // Validate room before saving
97 auto validation_result = ValidateRoom();
98 if (!validation_result.is_valid) {
99 std::string error_msg = "Validation failed";
100 if (!validation_result.errors.empty()) {
101 error_msg += ": " + validation_result.errors[0];
102 }
103 return absl::FailedPreconditionError(error_msg);
104 }
105 }
106
107 // Save room objects back to ROM (Phase 1, Task 1.3)
108 return current_room_->SaveObjects();
109}
110
112 if (current_room_ == nullptr) {
113 return absl::FailedPreconditionError("No room loaded");
114 }
115
116 // Create undo point before clearing
117 auto status = CreateUndoPoint();
118 if (!status.ok()) {
119 return status;
120 }
121
122 // Clear all objects
124
125 // Clear selection
127
128 // Notify callbacks
131 }
132
133 return absl::OkStatus();
134}
135
136absl::Status DungeonObjectEditor::InsertObject(int x, int y, int object_type,
137 int size, int layer) {
138 if (current_room_ == nullptr) {
139 return absl::FailedPreconditionError("No room loaded");
140 }
141
142 // Validate parameters
143 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
144 if (object_type < 0 || object_type > 0xFFF) {
145 return absl::InvalidArgumentError("Invalid object type");
146 }
147
148 if (size < kMinObjectSize || size > kMaxObjectSize) {
149 return absl::InvalidArgumentError("Invalid object size");
150 }
151
152 if (layer < kMinLayer || layer > kMaxLayer) {
153 return absl::InvalidArgumentError("Invalid layer");
154 }
155
156 // Snap coordinates to grid if enabled
157 if (config_.snap_to_grid) {
158 x = SnapToGrid(x);
159 y = SnapToGrid(y);
160 }
161
162 // Create undo point
163 auto status = CreateUndoPoint();
164 if (!status.ok()) {
165 return status;
166 }
167
168 // Create new object
169 RoomObject new_object(object_type, x, y, size, layer);
170 new_object.SetRom(rom_);
171 new_object.EnsureTilesLoaded();
172
173 // Check for collisions if validation is enabled
175 for (const auto& existing_obj : current_room_->GetTileObjects()) {
176 if (ObjectsCollide(new_object, existing_obj)) {
177 return absl::FailedPreconditionError(
178 "Object placement would cause collision");
179 }
180 }
181 }
182
183 // Add object to room using new method (Phase 3)
184 auto add_status = current_room_->AddObject(new_object);
185 if (!add_status.ok()) {
186 return add_status;
187 }
188
189 // Select the new object
193
194 // Notify callbacks
197 new_object);
198 }
199
202 }
203
206 }
207
208 return absl::OkStatus();
209}
210
211absl::Status DungeonObjectEditor::DeleteObject(size_t object_index) {
212 if (current_room_ == nullptr) {
213 return absl::FailedPreconditionError("No room loaded");
214 }
215
216 if (object_index >= current_room_->GetTileObjectCount()) {
217 return absl::OutOfRangeError("Object index out of range");
218 }
219
220 // Create undo point
221 auto status = CreateUndoPoint();
222 if (!status.ok()) {
223 return status;
224 }
225
226 // Remove object from room using new method (Phase 3)
227 auto remove_status = current_room_->RemoveObject(object_index);
228 if (!remove_status.ok()) {
229 return remove_status;
230 }
231
232 // Update selection indices
233 for (auto& selected_index : selection_state_.selected_objects) {
234 if (selected_index > object_index) {
235 selected_index--;
236 } else if (selected_index == object_index) {
237 // Remove the deleted object from selection
239 std::remove(selection_state_.selected_objects.begin(),
240 selection_state_.selected_objects.end(), object_index),
242 }
243 }
244
245 // Notify callbacks
248 }
249
252 }
253
254 return absl::OkStatus();
255}
256
258 if (current_room_ == nullptr) {
259 return absl::FailedPreconditionError("No room loaded");
260 }
261
263 return absl::FailedPreconditionError("No objects selected");
264 }
265
266 // Create undo point
267 auto status = CreateUndoPoint();
268 if (!status.ok()) {
269 return status;
270 }
271
272 // Sort selected indices in descending order to avoid index shifting issues
273 std::vector<size_t> sorted_selection = selection_state_.selected_objects;
274 std::sort(sorted_selection.begin(), sorted_selection.end(),
275 std::greater<size_t>());
276
277 // Delete objects in reverse order
278 for (size_t index : sorted_selection) {
279 if (index < current_room_->GetTileObjectCount()) {
281 }
282 }
283
284 // Clear selection
286
287 // Notify callbacks
290 }
291
292 return absl::OkStatus();
293}
294
295absl::Status DungeonObjectEditor::MoveObject(size_t object_index, int new_x,
296 int new_y) {
297 if (current_room_ == nullptr) {
298 return absl::FailedPreconditionError("No room loaded");
299 }
300
301 if (object_index >= current_room_->GetTileObjectCount()) {
302 return absl::OutOfRangeError("Object index out of range");
303 }
304
305 // Snap coordinates to grid if enabled
306 if (config_.snap_to_grid) {
307 new_x = SnapToGrid(new_x);
308 new_y = SnapToGrid(new_y);
309 }
310
311 // Create undo point
312 auto status = CreateUndoPoint();
313 if (!status.ok()) {
314 return status;
315 }
316
317 // Get the object
318 auto& object = current_room_->GetTileObject(object_index);
319
320 // Check for collisions if validation is enabled
322 RoomObject test_object = object;
323 test_object.set_x(new_x);
324 test_object.set_y(new_y);
325
326 for (size_t i = 0; i < current_room_->GetTileObjects().size(); i++) {
327 if (i != object_index &&
328 ObjectsCollide(test_object, current_room_->GetTileObjects()[i])) {
329 return absl::FailedPreconditionError(
330 "Object move would cause collision");
331 }
332 }
333 }
334
335 // Move the object
336 object.set_x(new_x);
337 object.set_y(new_y);
338
339 // Notify callbacks
341 object_changed_callback_(object_index, object);
342 }
343
346 }
347
348 return absl::OkStatus();
349}
350
351absl::Status DungeonObjectEditor::ResizeObject(size_t object_index,
352 int new_size) {
353 if (current_room_ == nullptr) {
354 return absl::FailedPreconditionError("No room loaded");
355 }
356
357 if (object_index >= current_room_->GetTileObjectCount()) {
358 return absl::OutOfRangeError("Object index out of range");
359 }
360
361 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
362 return absl::InvalidArgumentError("Invalid object size");
363 }
364
365 // Create undo point
366 auto status = CreateUndoPoint();
367 if (!status.ok()) {
368 return status;
369 }
370
371 // Resize the object
372 auto& object = current_room_->GetTileObject(object_index);
373 object.set_size(new_size);
374
375 // Notify callbacks
377 object_changed_callback_(object_index, object);
378 }
379
382 }
383
384 return absl::OkStatus();
385}
386
388 const std::vector<size_t>& indices, int dx, int dy) {
389 if (current_room_ == nullptr) {
390 return absl::FailedPreconditionError("No room loaded");
391 }
392
393 if (indices.empty()) {
394 return absl::OkStatus();
395 }
396
397 // Create single undo point for the batch operation
398 auto status = CreateUndoPoint();
399 if (!status.ok()) {
400 return status;
401 }
402
403 // Apply moves
404 for (size_t index : indices) {
405 if (index >= current_room_->GetTileObjectCount())
406 continue;
407
408 auto& object = current_room_->GetTileObject(index);
409 int new_x = object.x() + dx;
410 int new_y = object.y() + dy;
411
412 // Clamp to room bounds
413 new_x = std::max(0, std::min(63, new_x));
414 new_y = std::max(0, std::min(63, new_y));
415
416 object.set_x(new_x);
417 object.set_y(new_y);
418
420 object_changed_callback_(index, object);
421 }
422 }
423
426 }
427
428 return absl::OkStatus();
429}
430
432 const std::vector<size_t>& indices, int new_layer) {
433 if (current_room_ == nullptr) {
434 return absl::FailedPreconditionError("No room loaded");
435 }
436
437 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
438 return absl::InvalidArgumentError("Invalid layer");
439 }
440
441 // Create undo point
442 auto status = CreateUndoPoint();
443 if (!status.ok()) {
444 return status;
445 }
446
447 for (size_t index : indices) {
448 if (index >= current_room_->GetTileObjectCount())
449 continue;
450
451 auto& object = current_room_->GetTileObject(index);
453 // BothBG objects are rendered to BG1+BG2 regardless of their stored layer.
454 continue;
455 }
456 object.layer_ = static_cast<RoomObject::LayerType>(new_layer);
457
459 object_changed_callback_(index, object);
460 }
461 }
462
465 }
466
467 return absl::OkStatus();
468}
469
471 const std::vector<size_t>& indices, int new_size) {
472 if (current_room_ == nullptr) {
473 return absl::FailedPreconditionError("No room loaded");
474 }
475
476 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
477 return absl::InvalidArgumentError("Invalid object size");
478 }
479
480 // Create undo point
481 auto status = CreateUndoPoint();
482 if (!status.ok()) {
483 return status;
484 }
485
486 for (size_t index : indices) {
487 if (index >= current_room_->GetTileObjectCount())
488 continue;
489
490 auto& object = current_room_->GetTileObject(index);
491 // Only Type 1 objects typically support arbitrary sizing, but we allow it
492 // for all here as the validation logic might vary.
493 object.set_size(new_size);
494
496 object_changed_callback_(index, object);
497 }
498 }
499
502 }
503
504 return absl::OkStatus();
505}
506
507std::optional<size_t> DungeonObjectEditor::DuplicateObject(size_t object_index,
508 int offset_x,
509 int offset_y) {
510 if (current_room_ == nullptr) {
511 return std::nullopt;
512 }
513
514 if (object_index >= current_room_->GetTileObjectCount()) {
515 return std::nullopt;
516 }
517
518 // Create undo point
520
521 auto object = current_room_->GetTileObject(object_index);
522
523 // Offset position
524 int new_x = object.x() + offset_x;
525 int new_y = object.y() + offset_y;
526
527 // Clamp
528 new_x = std::max(0, std::min(63, new_x));
529 new_y = std::max(0, std::min(63, new_y));
530
531 object.set_x(new_x);
532 object.set_y(new_y);
533
534 // Add object
535 if (current_room_->AddObject(object).ok()) {
536 size_t new_index = current_room_->GetTileObjectCount() - 1;
537
540 }
541
542 return new_index;
543 }
544
545 return std::nullopt;
546}
547
549 const std::vector<size_t>& indices) {
550 if (current_room_ == nullptr)
551 return;
552
553 clipboard_.clear();
554
555 for (size_t index : indices) {
556 if (index < current_room_->GetTileObjectCount()) {
557 clipboard_.push_back(current_room_->GetTileObject(index));
558 }
559 }
560}
561
563 if (current_room_ == nullptr || clipboard_.empty()) {
564 return {};
565 }
566
567 // Create undo point
569
570 std::vector<size_t> new_indices;
571 size_t start_index = current_room_->GetTileObjectCount();
572
573 for (const auto& obj : clipboard_) {
574 // Paste with slight offset to make it visible
575 RoomObject new_obj = obj;
576
577 // Logic to ensure it stays in bounds if we were to support mouse-position pasting
578 // For now, just paste at original location + offset, or perhaps center of screen
579 // Let's do original + 1,1 for now to match duplicate behavior if we just copy/paste
580 // But better might be to keep relative positions if we had a "cursor" position.
581
582 int new_x = std::min(63, new_obj.x() + 1);
583 int new_y = std::min(63, new_obj.y() + 1);
584 new_obj.set_x(new_x);
585 new_obj.set_y(new_y);
586
587 if (current_room_->AddObject(new_obj).ok()) {
588 new_indices.push_back(start_index++);
589 }
590 }
591
594 }
595
596 return new_indices;
597}
598
599absl::Status DungeonObjectEditor::ChangeObjectType(size_t object_index,
600 int new_type) {
601 if (current_room_ == nullptr) {
602 return absl::FailedPreconditionError("No room loaded");
603 }
604
605 if (object_index >= current_room_->GetTileObjectCount()) {
606 return absl::OutOfRangeError("Object index out of range");
607 }
608
609 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
610 if (new_type < 0 || new_type > 0xFFF) {
611 return absl::InvalidArgumentError("Invalid object type");
612 }
613
614 // Create undo point
615 auto status = CreateUndoPoint();
616 if (!status.ok()) {
617 return status;
618 }
619
620 auto& object = current_room_->GetTileObject(object_index);
621 object.set_id(static_cast<int16_t>(new_type));
622
624 object_changed_callback_(object_index, object);
625 }
626
629 }
630
631 return absl::OkStatus();
632}
633
635 int x, int y) {
636 if (current_room_ == nullptr) {
637 return absl::FailedPreconditionError("No room loaded");
638 }
639
640 // Snap coordinates to grid if enabled
641 if (config_.snap_to_grid) {
642 x = SnapToGrid(x);
643 y = SnapToGrid(y);
644 }
645
646 // Create undo point
647 auto status = CreateUndoPoint();
648 if (!status.ok()) {
649 return status;
650 }
651
652 // Instantiate template objects
653 std::vector<RoomObject> new_objects =
655
656 // Check for collisions if enabled
658 for (const auto& new_obj : new_objects) {
659 for (const auto& existing_obj : current_room_->GetTileObjects()) {
660 if (ObjectsCollide(new_obj, existing_obj)) {
661 return absl::FailedPreconditionError(
662 "Template placement would cause collision");
663 }
664 }
665 }
666 }
667
668 // Add objects to room
669 for (const auto& obj : new_objects) {
671 }
672
673 // Select the new objects
675 size_t count = current_room_->GetTileObjectCount();
676 size_t added_count = new_objects.size();
677 for (size_t i = 0; i < added_count; ++i) {
678 selection_state_.selected_objects.push_back(count - added_count + i);
679 }
680 if (!selection_state_.selected_objects.empty()) {
682 }
683
686 }
687
688 return absl::OkStatus();
689}
690
692 const std::string& name, const std::string& description) {
694 return absl::FailedPreconditionError("No objects selected");
695 }
696
697 std::vector<RoomObject> objects;
698 int min_x = 64, min_y = 64;
699
700 // Collect selected objects and find bounds
701 for (size_t index : selection_state_.selected_objects) {
702 if (index < current_room_->GetTileObjectCount()) {
703 const auto& obj = current_room_->GetTileObject(index);
704 objects.push_back(obj);
705 if (obj.x() < min_x)
706 min_x = obj.x();
707 if (obj.y() < min_y)
708 min_y = obj.y();
709 }
710 }
711
712 // Create template
714 name, description, objects, min_x, min_y);
715
716 // Save template
717 return template_manager_.SaveTemplate(tmpl, "assets/templates/dungeon");
718}
719
720const std::vector<ObjectTemplate>& DungeonObjectEditor::GetTemplates() const {
722}
723
725 if (current_room_ == nullptr) {
726 return absl::FailedPreconditionError("No room loaded");
727 }
728
729 if (selection_state_.selected_objects.size() < 2) {
730 return absl::OkStatus(); // Nothing to align
731 }
732
733 // Create undo point
734 auto status = CreateUndoPoint();
735 if (!status.ok()) {
736 return status;
737 }
738
739 // Find reference value (min/max/avg)
740 int ref_val = 0;
741 const auto& indices = selection_state_.selected_objects;
742
743 if (alignment == Alignment::Left || alignment == Alignment::Top) {
744 ref_val = 64; // Max possible
745 } else if (alignment == Alignment::Right || alignment == Alignment::Bottom) {
746 ref_val = 0; // Min possible
747 }
748
749 // First pass: calculate reference
750 int sum = 0;
751 int count = 0;
752
753 for (size_t index : indices) {
754 if (index >= current_room_->GetTileObjectCount())
755 continue;
756 const auto& obj = current_room_->GetTileObject(index);
757
758 switch (alignment) {
759 case Alignment::Left:
760 if (obj.x() < ref_val)
761 ref_val = obj.x();
762 break;
763 case Alignment::Right:
764 if (obj.x() > ref_val)
765 ref_val = obj.x();
766 break;
767 case Alignment::Top:
768 if (obj.y() < ref_val)
769 ref_val = obj.y();
770 break;
772 if (obj.y() > ref_val)
773 ref_val = obj.y();
774 break;
776 sum += obj.x();
777 count++;
778 break;
780 sum += obj.y();
781 count++;
782 break;
783 }
784 }
785
786 if (alignment == Alignment::CenterX || alignment == Alignment::CenterY) {
787 if (count > 0)
788 ref_val = sum / count;
789 }
790
791 // Second pass: apply alignment
792 for (size_t index : indices) {
793 if (index >= current_room_->GetTileObjectCount())
794 continue;
795 auto& obj = current_room_->GetTileObject(index);
796
797 switch (alignment) {
798 case Alignment::Left:
799 case Alignment::Right:
801 obj.set_x(ref_val);
802 break;
803 case Alignment::Top:
806 obj.set_y(ref_val);
807 break;
808 }
809
811 object_changed_callback_(index, obj);
812 }
813 }
814
817 }
818
819 return absl::OkStatus();
820}
821
822absl::Status DungeonObjectEditor::ChangeObjectLayer(size_t object_index,
823 int new_layer) {
824 if (current_room_ == nullptr) {
825 return absl::FailedPreconditionError("No room loaded");
826 }
827
828 if (object_index >= current_room_->GetTileObjectCount()) {
829 return absl::OutOfRangeError("Object index out of range");
830 }
831
832 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
833 return absl::InvalidArgumentError("Invalid layer");
834 }
835
836 // Create undo point
837 auto status = CreateUndoPoint();
838 if (!status.ok()) {
839 return status;
840 }
841
842 auto& object = current_room_->GetTileObject(object_index);
843 if (GetObjectLayerSemantics(object).draws_to_both_bgs) {
844 // BothBG objects are rendered to BG1+BG2 regardless of their stored layer.
845 return absl::OkStatus();
846 }
847 object.layer_ = static_cast<RoomObject::LayerType>(new_layer);
848
850 object_changed_callback_(object_index, object);
851 }
852
855 }
856
857 return absl::OkStatus();
858}
859
860absl::Status DungeonObjectEditor::HandleScrollWheel(int delta, int x, int y,
861 bool ctrl_pressed) {
862 if (current_room_ == nullptr) {
863 return absl::FailedPreconditionError("No room loaded");
864 }
865
866 // Convert screen coordinates to room coordinates
867 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
868
869 // Handle size editing with scroll wheel
873 return HandleSizeEdit(delta, room_x, room_y);
874 }
875
876 // Handle layer switching with Ctrl+scroll
877 if (ctrl_pressed) {
878 int layer_delta = delta > 0 ? 1 : -1;
879 int new_layer = editing_state_.current_layer + layer_delta;
880 new_layer = std::max(kMinLayer, std::min(kMaxLayer, new_layer));
881
882 if (new_layer != editing_state_.current_layer) {
883 SetCurrentLayer(new_layer);
884 }
885
886 return absl::OkStatus();
887 }
888
889 return absl::OkStatus();
890}
891
892absl::Status DungeonObjectEditor::HandleSizeEdit(int delta, int x, int y) {
893 // Handle size editing for preview object
895 int new_size = GetNextSize(editing_state_.preview_size, delta);
896 if (IsValidSize(new_size)) {
897 editing_state_.preview_size = new_size;
899 }
900 return absl::OkStatus();
901 }
902
903 // Handle size editing for selected objects
906 for (size_t object_index : selection_state_.selected_objects) {
907 if (object_index < current_room_->GetTileObjectCount()) {
908 auto& object = current_room_->GetTileObject(object_index);
909 int new_size = GetNextSize(object.size_, delta);
910 if (IsValidSize(new_size)) {
911 auto status = ResizeObject(object_index, new_size);
912 if (!status.ok()) {
913 return status;
914 }
915 }
916 }
917 }
918 return absl::OkStatus();
919 }
920
921 return absl::OkStatus();
922}
923
924int DungeonObjectEditor::GetNextSize(int current_size, int delta) {
925 // Define size increments based on object type
926 // This is a simplified implementation - in practice, you'd have
927 // different size rules for different object types
928
929 if (delta > 0) {
930 // Increase size
931 if (current_size < 0x40) {
932 return current_size + 0x10; // Large increments for small sizes
933 } else if (current_size < 0x80) {
934 return current_size + 0x08; // Medium increments
935 } else {
936 return current_size + 0x04; // Small increments for large sizes
937 }
938 } else {
939 // Decrease size
940 if (current_size > 0x80) {
941 return current_size - 0x04; // Small decrements for large sizes
942 } else if (current_size > 0x40) {
943 return current_size - 0x08; // Medium decrements
944 } else {
945 return current_size - 0x10; // Large decrements for small sizes
946 }
947 }
948}
949
951 return size >= kMinObjectSize && size <= kMaxObjectSize;
952}
953
955 bool left_button,
956 bool right_button,
957 bool shift_pressed) {
958 if (current_room_ == nullptr) {
959 return absl::FailedPreconditionError("No room loaded");
960 }
961
962 // Convert screen coordinates to room coordinates
963 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
964
965 if (left_button) {
967 case Mode::kSelect:
968 if (shift_pressed) {
969 // Add to selection
970 auto object_index = FindObjectAt(room_x, room_y);
971 if (object_index.has_value()) {
972 return AddToSelection(object_index.value());
973 }
974 } else {
975 // Select object
976 return SelectObject(x, y);
977 }
978 break;
979
980 case Mode::kInsert:
981 // Insert object at clicked position
982 return InsertObject(room_x, room_y, editing_state_.current_object_type,
985
986 case Mode::kDelete:
987 // Delete object at clicked position
988 {
989 auto object_index = FindObjectAt(room_x, room_y);
990 if (object_index.has_value()) {
991 return DeleteObject(object_index.value());
992 }
993 }
994 break;
995
996 case Mode::kEdit:
997 // Select object for editing
998 return SelectObject(x, y);
999
1000 default:
1001 break;
1002 }
1003 }
1004
1005 if (right_button) {
1006 // Context menu or alternate action
1007 switch (editing_state_.current_mode) {
1008 case Mode::kSelect:
1009 // Show context menu for object
1010 {
1011 auto object_index = FindObjectAt(room_x, room_y);
1012 if (object_index.has_value()) {
1013 // TODO: Show context menu
1014 }
1015 }
1016 break;
1017
1018 default:
1019 break;
1020 }
1021 }
1022
1023 return absl::OkStatus();
1024}
1025
1026absl::Status DungeonObjectEditor::HandleMouseDrag(int start_x, int start_y,
1027 int current_x,
1028 int current_y) {
1029 if (current_room_ == nullptr) {
1030 return absl::FailedPreconditionError("No room loaded");
1031 }
1032
1033 // Enable dragging if not already (Phase 4)
1039
1040 // Create undo point before drag
1041 auto undo_status = CreateUndoPoint();
1042 if (!undo_status.ok()) {
1043 return undo_status;
1044 }
1045 }
1046
1047 // Handle the drag operation (Phase 4)
1048 return HandleDragOperation(current_x, current_y);
1049}
1050
1052 if (current_room_ == nullptr) {
1053 return absl::FailedPreconditionError("No room loaded");
1054 }
1055
1056 // End dragging operation (Phase 4)
1059
1060 // Notify callbacks about the final positions
1063 }
1064 }
1065
1066 return absl::OkStatus();
1067}
1068
1069absl::Status DungeonObjectEditor::SelectObject(int screen_x, int screen_y) {
1070 if (current_room_ == nullptr) {
1071 return absl::FailedPreconditionError("No room loaded");
1072 }
1073
1074 // Convert screen coordinates to room coordinates
1075 auto [room_x, room_y] = ScreenToRoomCoordinates(screen_x, screen_y);
1076
1077 // Find object at position
1078 auto object_index = FindObjectAt(room_x, room_y);
1079
1080 if (object_index.has_value()) {
1081 // Select the found object
1083 selection_state_.selected_objects.push_back(object_index.value());
1084
1085 // Notify callbacks
1088 }
1089
1090 return absl::OkStatus();
1091 } else {
1092 // Clear selection if no object found
1093 return ClearSelection();
1094 }
1095}
1096
1101
1102 // Notify callbacks
1105 }
1106
1107 return absl::OkStatus();
1108}
1109
1110absl::Status DungeonObjectEditor::AddToSelection(size_t object_index) {
1111 if (current_room_ == nullptr) {
1112 return absl::FailedPreconditionError("No room loaded");
1113 }
1114
1115 if (object_index >= current_room_->GetTileObjectCount()) {
1116 return absl::OutOfRangeError("Object index out of range");
1117 }
1118
1119 // Check if already selected
1120 auto it = std::find(selection_state_.selected_objects.begin(),
1121 selection_state_.selected_objects.end(), object_index);
1122
1123 if (it == selection_state_.selected_objects.end()) {
1124 selection_state_.selected_objects.push_back(object_index);
1126
1127 // Notify callbacks
1130 }
1131 }
1132
1133 return absl::OkStatus();
1134}
1135
1138
1139 // Update preview object based on mode
1141}
1142
1144 if (layer >= kMinLayer && layer <= kMaxLayer) {
1147 }
1148}
1149
1151 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
1152 if (object_type >= 0 && object_type <= 0xFFF) {
1153 editing_state_.current_object_type = object_type;
1155 }
1156}
1157
1158std::optional<size_t> DungeonObjectEditor::FindObjectAt(int room_x,
1159 int room_y) {
1160 if (current_room_ == nullptr) {
1161 return std::nullopt;
1162 }
1163
1164 // Search from back to front (last objects are on top)
1165 for (int i = static_cast<int>(current_room_->GetTileObjectCount()) - 1;
1166 i >= 0; i--) {
1167 if (IsObjectAtPosition(current_room_->GetTileObject(i), room_x, room_y)) {
1168 return static_cast<size_t>(i);
1169 }
1170 }
1171
1172 return std::nullopt;
1173}
1174
1176 int y) {
1177 // Coordinates are in room tiles.
1178 int obj_x = object.x_;
1179 int obj_y = object.y_;
1180
1181 // Simplified bounds: default to 1x1 tile, grow to 2x2 for large objects.
1182 int obj_width = 1;
1183 int obj_height = 1;
1184 if (object.size_ > 0x80) {
1185 obj_width = 2;
1186 obj_height = 2;
1187 }
1188
1189 return (x >= obj_x && x < obj_x + obj_width && y >= obj_y &&
1190 y < obj_y + obj_height);
1191}
1192
1194 const RoomObject& obj2) {
1195 // Simple bounding box collision detection
1196 // In practice, you'd use the actual tile data for more accurate collision
1197
1198 int obj1_x = obj1.x_ * 16;
1199 int obj1_y = obj1.y_ * 16;
1200 int obj1_w = 16;
1201 int obj1_h = 16;
1202
1203 int obj2_x = obj2.x_ * 16;
1204 int obj2_y = obj2.y_ * 16;
1205 int obj2_w = 16;
1206 int obj2_h = 16;
1207
1208 // Adjust sizes based on object size values
1209 if (obj1.size_ > 0x80) {
1210 obj1_w *= 2;
1211 obj1_h *= 2;
1212 }
1213
1214 if (obj2.size_ > 0x80) {
1215 obj2_w *= 2;
1216 obj2_h *= 2;
1217 }
1218
1219 return !(obj1_x + obj1_w <= obj2_x || obj2_x + obj2_w <= obj1_x ||
1220 obj1_y + obj1_h <= obj2_y || obj2_y + obj2_h <= obj1_y);
1221}
1222
1223std::pair<int, int> DungeonObjectEditor::ScreenToRoomCoordinates(int screen_x,
1224 int screen_y) {
1225 // Convert screen coordinates to room tile coordinates
1226 // This is a simplified implementation - in practice, you'd account for
1227 // camera position, zoom level, etc.
1228
1229 int room_x = screen_x / 16; // 16 pixels per tile
1230 int room_y = screen_y / 16;
1231
1232 return {room_x, room_y};
1233}
1234
1236 int room_y) {
1237 // Convert room tile coordinates to screen coordinates
1238 int screen_x = room_x * 16;
1239 int screen_y = room_y * 16;
1240
1241 return {screen_x, screen_y};
1242}
1243
1245 if (!config_.snap_to_grid) {
1246 return coordinate;
1247 }
1248
1249 int grid_size = config_.grid_size;
1250 if (grid_size <= 0) {
1251 return coordinate;
1252 }
1253
1254 // Coordinates are in room tiles; map pixel grid size to tile steps.
1255 int tile_step = std::max(1, grid_size / 16);
1256 return (coordinate / tile_step) * tile_step;
1257}
1258
1272
1274 if (current_room_ == nullptr) {
1275 return absl::FailedPreconditionError("No room loaded");
1276 }
1277
1278 // Create undo point
1279 UndoPoint undo_point;
1280 undo_point.objects = current_room_->GetTileObjects();
1281 undo_point.selection = selection_state_;
1282 undo_point.editing = editing_state_;
1283 undo_point.timestamp = std::chrono::steady_clock::now();
1284
1285 // Add to undo history
1286 undo_history_.push_back(undo_point);
1287
1288 // Limit undo history size
1289 if (undo_history_.size() > kMaxUndoHistory) {
1290 undo_history_.erase(undo_history_.begin());
1291 }
1292
1293 // Clear redo history when new action is performed
1294 redo_history_.clear();
1295
1296 return absl::OkStatus();
1297}
1298
1300 if (!CanUndo()) {
1301 return absl::FailedPreconditionError("Nothing to undo");
1302 }
1303
1304 // Move current state to redo history
1305 UndoPoint current_state;
1306 current_state.objects = current_room_->GetTileObjects();
1307 current_state.selection = selection_state_;
1308 current_state.editing = editing_state_;
1309 current_state.timestamp = std::chrono::steady_clock::now();
1310
1311 redo_history_.push_back(current_state);
1312
1313 // Apply undo point
1314 UndoPoint undo_point = undo_history_.back();
1315 undo_history_.pop_back();
1316
1317 return ApplyUndoPoint(undo_point);
1318}
1319
1321 if (!CanRedo()) {
1322 return absl::FailedPreconditionError("Nothing to redo");
1323 }
1324
1325 // Move current state to undo history
1326 UndoPoint current_state;
1327 current_state.objects = current_room_->GetTileObjects();
1328 current_state.selection = selection_state_;
1329 current_state.editing = editing_state_;
1330 current_state.timestamp = std::chrono::steady_clock::now();
1331
1332 undo_history_.push_back(current_state);
1333
1334 // Apply redo point
1335 UndoPoint redo_point = redo_history_.back();
1336 redo_history_.pop_back();
1337
1338 return ApplyUndoPoint(redo_point);
1339}
1340
1341absl::Status DungeonObjectEditor::ApplyUndoPoint(const UndoPoint& undo_point) {
1342 if (current_room_ == nullptr) {
1343 return absl::FailedPreconditionError("No room loaded");
1344 }
1345
1346 // Restore room state
1348
1349 // Restore editor state
1350 selection_state_ = undo_point.selection;
1351 editing_state_ = undo_point.editing;
1352
1353 // Update preview
1355
1356 // Notify callbacks
1359 }
1360
1363 }
1364
1365 return absl::OkStatus();
1366}
1367
1369 return !undo_history_.empty();
1370}
1371
1373 return !redo_history_.empty();
1374}
1375
1377 undo_history_.clear();
1378 redo_history_.clear();
1379}
1380
1381// ============================================================================
1382// Phase 4: Visual Feedback and GUI Methods
1383// ============================================================================
1384
1385// Helper for color blending
1386static uint32_t BlendColors(uint32_t base, uint32_t tint) {
1387 uint8_t a_tint = (tint >> 24) & 0xFF;
1388 if (a_tint == 0)
1389 return base;
1390
1391 uint8_t r_base = (base >> 16) & 0xFF;
1392 uint8_t g_base = (base >> 8) & 0xFF;
1393 uint8_t b_base = base & 0xFF;
1394
1395 uint8_t r_tint = (tint >> 16) & 0xFF;
1396 uint8_t g_tint = (tint >> 8) & 0xFF;
1397 uint8_t b_tint = tint & 0xFF;
1398
1399 float alpha = a_tint / 255.0f;
1400 uint8_t r = r_base * (1.0f - alpha) + r_tint * alpha;
1401 uint8_t g = g_base * (1.0f - alpha) + g_tint * alpha;
1402 uint8_t b = b_base * (1.0f - alpha) + b_tint * alpha;
1403
1404 return 0xFF000000 | (r << 16) | (g << 8) | b;
1405}
1406
1410 return;
1411 }
1412
1413 // Draw highlight rectangles around selected objects
1414 for (size_t obj_idx : selection_state_.selected_objects) {
1415 if (obj_idx >= current_room_->GetTileObjectCount())
1416 continue;
1417
1418 const auto& obj = current_room_->GetTileObject(obj_idx);
1419 int x = obj.x() * 16;
1420 int y = obj.y() * 16;
1421 int w = 16 + (obj.size() * 4); // Approximate width
1422 int h = 16 + (obj.size() * 4); // Approximate height
1423
1424 // Draw yellow selection box (2px border) - using SetPixel
1425 uint8_t r = (config_.selection_color >> 16) & 0xFF;
1426 uint8_t g = (config_.selection_color >> 8) & 0xFF;
1427 uint8_t b = config_.selection_color & 0xFF;
1428 gfx::SnesColor sel_color(r, g, b);
1429
1430 for (int py = y; py < y + h; py++) {
1431 for (int px = x; px < x + w; px++) {
1432 if (px < canvas.width() && py < canvas.height() &&
1433 (px < x + 2 || px >= x + w - 2 || py < y + 2 || py >= y + h - 2)) {
1434 canvas.SetPixel(px, py, sel_color);
1435 }
1436 }
1437 }
1438 }
1439}
1440
1443 return;
1444 }
1445
1446 // Apply subtle color tints based on layer (simplified - just mark with
1447 // colored border)
1448 for (const auto& obj : current_room_->GetTileObjects()) {
1449 int x = obj.x() * 16;
1450 int y = obj.y() * 16;
1451 int w = 16;
1452 int h = 16;
1453
1454 uint32_t tint_color = 0xFF000000;
1455 switch (obj.GetLayerValue()) {
1456 case 0:
1457 tint_color = config_.layer0_color;
1458 break;
1459 case 1:
1460 tint_color = config_.layer1_color;
1461 break;
1462 case 2:
1463 tint_color = config_.layer2_color;
1464 break;
1465 }
1466
1467 // Draw 1px border in layer color
1468 uint8_t r = (tint_color >> 16) & 0xFF;
1469 uint8_t g = (tint_color >> 8) & 0xFF;
1470 uint8_t b = tint_color & 0xFF;
1471 gfx::SnesColor layer_color(r, g, b);
1472
1473 for (int py = y; py < y + h && py < canvas.height(); py++) {
1474 for (int px = x; px < x + w && px < canvas.width(); px++) {
1475 if (px == x || px == x + w - 1 || py == y || py == y + h - 1) {
1476 canvas.SetPixel(px, py, layer_color);
1477 }
1478 }
1479 }
1480 }
1481}
1482
1484 const auto& theme = editor::AgentUI::GetTheme();
1485
1488 return;
1489 }
1490
1491 if (selection_state_.selected_objects.size() == 1) {
1492 size_t obj_idx = selection_state_.selected_objects[0];
1493 if (obj_idx < current_room_->GetTileObjectCount()) {
1494 auto& obj = current_room_->GetTileObject(obj_idx);
1495
1496 // ========== Identity Section ==========
1497 gui::SectionHeader(ICON_MD_TAG, "Identity", theme.text_info);
1498 if (gui::BeginPropertyTable("##IdentityProps")) {
1499 // Object index
1500 gui::PropertyRow("Object #", static_cast<int>(obj_idx));
1501
1502 // Object ID with name
1503 ImGui::TableNextRow();
1504 ImGui::TableNextColumn();
1505 ImGui::Text("ID");
1506 ImGui::TableNextColumn();
1507 std::string obj_name = GetObjectName(obj.id_);
1508 ImGui::Text("0x%03X", obj.id_);
1509 ImGui::SameLine();
1510 ImGui::TextColored(theme.text_secondary_gray, "(%s)", obj_name.c_str());
1511
1512 // Object type/subtype
1513 int subtype = GetObjectSubtype(obj.id_);
1514 ImGui::TableNextRow();
1515 ImGui::TableNextColumn();
1516 ImGui::Text("Type");
1517 ImGui::TableNextColumn();
1518 ImGui::Text("Subtype %d", subtype);
1519
1521 }
1522
1523 ImGui::Spacing();
1524
1525 // ========== Position Section ==========
1526 gui::SectionHeader(ICON_MD_PLACE, "Position", theme.text_info);
1527 if (gui::BeginPropertyTable("##PositionProps")) {
1528 // X Position
1529 ImGui::TableNextRow();
1530 ImGui::TableNextColumn();
1531 ImGui::Text("X");
1532 ImGui::TableNextColumn();
1533 int x = obj.x();
1534 ImGui::SetNextItemWidth(-1);
1535 if (ImGui::InputInt("##X", &x, 1, 4)) {
1536 if (x >= 0 && x < 64) {
1537 obj.set_x(x);
1539 object_changed_callback_(obj_idx, obj);
1540 }
1541 }
1542 }
1543
1544 // Y Position
1545 ImGui::TableNextRow();
1546 ImGui::TableNextColumn();
1547 ImGui::Text("Y");
1548 ImGui::TableNextColumn();
1549 int y = obj.y();
1550 ImGui::SetNextItemWidth(-1);
1551 if (ImGui::InputInt("##Y", &y, 1, 4)) {
1552 if (y >= 0 && y < 64) {
1553 obj.set_y(y);
1555 object_changed_callback_(obj_idx, obj);
1556 }
1557 }
1558 }
1559
1561 }
1562
1563 ImGui::Spacing();
1564
1565 // ========== Appearance Section ==========
1566 gui::SectionHeader(ICON_MD_PALETTE, "Appearance", theme.text_info);
1567 if (gui::BeginPropertyTable("##AppearanceProps")) {
1568 // Size (for Type 1 objects only)
1569 if (obj.id_ < 0x100) {
1570 ImGui::TableNextRow();
1571 ImGui::TableNextColumn();
1572 ImGui::Text("Size");
1573 ImGui::TableNextColumn();
1574 int size = obj.size();
1575 ImGui::SetNextItemWidth(-1);
1576 if (ImGui::SliderInt("##Size", &size, 0, 15, "0x%02X")) {
1577 obj.set_size(size);
1579 object_changed_callback_(obj_idx, obj);
1580 }
1581 }
1582 }
1583
1584 // Object ID (editable)
1585 ImGui::TableNextRow();
1586 ImGui::TableNextColumn();
1587 ImGui::Text("Change ID");
1588 ImGui::TableNextColumn();
1589 int id = obj.id_;
1590 ImGui::SetNextItemWidth(-1);
1591 if (ImGui::InputInt("##ID", &id, 1, 16,
1592 ImGuiInputTextFlags_CharsHexadecimal)) {
1593 if (id >= 0 && id <= 0xFFF) {
1594 obj.set_id(static_cast<int16_t>(id));
1596 object_changed_callback_(obj_idx, obj);
1597 }
1598 }
1599 }
1600
1602 }
1603
1604 ImGui::Spacing();
1605
1606 // ========== Layer Section ==========
1607 gui::SectionHeader(ICON_MD_LAYERS, "Layer", theme.text_info);
1608 if (gui::BeginPropertyTable("##LayerProps")) {
1609 const auto semantics = GetObjectLayerSemantics(obj);
1610
1611 ImGui::TableNextRow();
1612 ImGui::TableNextColumn();
1613 ImGui::Text("Draws To");
1614 ImGui::TableNextColumn();
1615 if (semantics.draws_to_both_bgs) {
1616 ImGui::TextColored(theme.text_warning_yellow, "Both (BG1 + BG2)");
1617 } else {
1618 ImGui::Text("%s", EffectiveBgLayerLabel(semantics.effective_bg_layer));
1619 }
1620
1621 ImGui::TableNextRow();
1622 ImGui::TableNextColumn();
1623 ImGui::Text("Layer");
1624 ImGui::TableNextColumn();
1625 int layer = obj.GetLayerValue();
1626 ImGui::SetNextItemWidth(-1);
1627 if (semantics.draws_to_both_bgs) {
1628 ImGui::BeginDisabled();
1629 }
1630 if (ImGui::Combo("##Layer", &layer,
1631 "BG1 (Floor)\0BG2 (Objects)\0BG3 (Overlay)\0")) {
1632 if (!semantics.draws_to_both_bgs) {
1633 obj.layer_ = static_cast<RoomObject::LayerType>(layer);
1635 object_changed_callback_(obj_idx, obj);
1636 }
1637 }
1638 }
1639 if (semantics.draws_to_both_bgs) {
1640 ImGui::EndDisabled();
1641 ImGui::SameLine();
1643 "This object draws to both BG1 and BG2 in the engine. The stored "
1644 "layer selection does not affect rendering.");
1645 }
1646
1648 }
1649
1650 ImGui::Spacing();
1651 ImGui::Separator();
1652 ImGui::Spacing();
1653
1654 // ========== Actions Section ==========
1655 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1656
1657 gui::StyleColorGuard delete_btn_guard(
1658 {{ImGuiCol_Button,
1659 ImVec4(theme.status_error.x * 0.7f, theme.status_error.y * 0.7f,
1660 theme.status_error.z * 0.7f, 1.0f)},
1661 {ImGuiCol_ButtonHovered, theme.status_error}});
1662 if (ImGui::Button(ICON_MD_DELETE " Delete", ImVec2(button_width, 0))) {
1663 auto status = DeleteObject(obj_idx);
1664 (void)status;
1665 }
1666
1667 ImGui::SameLine();
1668
1669 if (ImGui::Button(ICON_MD_CONTENT_COPY " Duplicate",
1670 ImVec2(button_width, 0))) {
1671 RoomObject duplicate = obj;
1672 duplicate.set_x(obj.x() + 1);
1673 auto status = current_room_->AddObject(duplicate);
1674 (void)status;
1675 }
1676 }
1677 } else {
1678 // ========== Multiple Selection Mode ==========
1679 ImGui::TextColored(theme.text_warning_yellow,
1680 ICON_MD_SELECT_ALL " %zu objects selected",
1682
1683 ImGui::Spacing();
1684
1685 // ========== Batch Layer ==========
1686 gui::SectionHeader(ICON_MD_LAYERS, "Batch Layer", theme.text_info);
1687 size_t both_bg_count = 0;
1688 for (size_t idx : selection_state_.selected_objects) {
1689 if (idx >= current_room_->GetTileObjectCount()) continue;
1690 const auto& obj = current_room_->GetTileObject(idx);
1692 ++both_bg_count;
1693 }
1694 }
1695 if (both_bg_count > 0) {
1696 ImGui::TextColored(theme.text_warning_yellow,
1697 ICON_MD_INFO " %zu object%s draw%s to Both BGs and "
1698 "won't be affected by layer changes",
1699 both_bg_count, both_bg_count == 1 ? "" : "s",
1700 both_bg_count == 1 ? "s" : "");
1701 ImGui::Spacing();
1702 }
1703 static int batch_layer = 0;
1704 ImGui::SetNextItemWidth(-1);
1705 const bool all_both_bg =
1706 (both_bg_count == selection_state_.selected_objects.size());
1707 if (all_both_bg) {
1708 ImGui::BeginDisabled();
1709 }
1710 if (ImGui::Combo("##BatchLayer", &batch_layer,
1711 "BG1 (Floor)\0BG2 (Objects)\0BG3 (Overlay)\0")) {
1713 }
1714 if (all_both_bg) {
1715 ImGui::EndDisabled();
1716 }
1717
1718 ImGui::Spacing();
1719
1720 // ========== Batch Size ==========
1721 gui::SectionHeader(ICON_MD_ASPECT_RATIO, "Batch Size", theme.text_info);
1722 static int batch_size = 0x12;
1723 ImGui::SetNextItemWidth(-1);
1724 if (ImGui::InputInt("##BatchSize", &batch_size, 1, 16,
1725 ImGuiInputTextFlags_CharsHexadecimal)) {
1727 }
1728
1729 ImGui::Spacing();
1730
1731 // ========== Nudge Section ==========
1732 gui::SectionHeader(ICON_MD_OPEN_WITH, "Nudge", theme.text_info);
1733 float nudge_btn_size = (ImGui::GetContentRegionAvail().x - 24) / 4;
1734 if (ImGui::Button(ICON_MD_ARROW_BACK, ImVec2(nudge_btn_size, 0))) {
1736 }
1737 ImGui::SameLine();
1738 if (ImGui::Button(ICON_MD_ARROW_UPWARD, ImVec2(nudge_btn_size, 0))) {
1740 }
1741 ImGui::SameLine();
1742 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD, ImVec2(nudge_btn_size, 0))) {
1744 }
1745 ImGui::SameLine();
1746 if (ImGui::Button(ICON_MD_ARROW_FORWARD, ImVec2(nudge_btn_size, 0))) {
1748 }
1749
1750 ImGui::Spacing();
1751 ImGui::Separator();
1752 ImGui::Spacing();
1753
1754 // ========== Actions ==========
1755 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1756
1757 gui::StyleColorGuard delete_all_btn_guard(
1758 {{ImGuiCol_Button,
1759 ImVec4(theme.status_error.x * 0.7f, theme.status_error.y * 0.7f,
1760 theme.status_error.z * 0.7f, 1.0f)},
1761 {ImGuiCol_ButtonHovered, theme.status_error}});
1762 if (ImGui::Button(ICON_MD_DELETE_SWEEP " Delete All",
1763 ImVec2(button_width, 0))) {
1764 auto status = DeleteSelectedObjects();
1765 (void)status;
1766 }
1767
1768 ImGui::SameLine();
1769
1770 if (ImGui::Button(ICON_MD_DESELECT " Clear Selection",
1771 ImVec2(button_width, 0))) {
1772 auto status = ClearSelection();
1773 (void)status;
1774 }
1775 }
1776}
1777
1779 ImGui::Begin("Layer Controls");
1780
1781 // Current layer selection
1782 ImGui::Text("Current Layer:");
1783 ImGui::RadioButton("Layer 0", &editing_state_.current_layer, 0);
1784 ImGui::SameLine();
1785 ImGui::RadioButton("Layer 1", &editing_state_.current_layer, 1);
1786 ImGui::SameLine();
1787 ImGui::RadioButton("Layer 2", &editing_state_.current_layer, 2);
1788
1789 ImGui::Separator();
1790
1791 // Layer visibility toggles
1792 static bool layer_visible[3] = {true, true, true};
1793 ImGui::Text("Layer Visibility:");
1794 ImGui::Checkbox("Show Layer 0", &layer_visible[0]);
1795 ImGui::Checkbox("Show Layer 1", &layer_visible[1]);
1796 ImGui::Checkbox("Show Layer 2", &layer_visible[2]);
1797
1798 ImGui::Separator();
1799
1800 // Layer colors
1801 ImGui::Checkbox("Show Layer Colors", &config_.show_layer_colors);
1803 ImGui::ColorEdit4("Layer 0 Tint", (float*)&config_.layer0_color);
1804 ImGui::ColorEdit4("Layer 1 Tint", (float*)&config_.layer1_color);
1805 ImGui::ColorEdit4("Layer 2 Tint", (float*)&config_.layer2_color);
1806 }
1807
1808 ImGui::Separator();
1809
1810 // Object counts per layer
1811 if (current_room_) {
1812 int count0 = 0, count1 = 0, count2 = 0;
1813 for (const auto& obj : current_room_->GetTileObjects()) {
1814 switch (obj.GetLayerValue()) {
1815 case 0:
1816 count0++;
1817 break;
1818 case 1:
1819 count1++;
1820 break;
1821 case 2:
1822 count2++;
1823 break;
1824 }
1825 }
1826 ImGui::Text("Layer 0: %d objects", count0);
1827 ImGui::Text("Layer 1: %d objects", count1);
1828 ImGui::Text("Layer 2: %d objects", count2);
1829 }
1830
1831 ImGui::End();
1832}
1833
1835 int current_y) {
1838 return absl::OkStatus();
1839 }
1840
1841 // Calculate delta from drag start
1842 int dx = current_x - selection_state_.drag_start_x;
1843 int dy = current_y - selection_state_.drag_start_y;
1844
1845 // Convert pixel delta to grid delta
1846 int grid_dx = dx / config_.grid_size;
1847 int grid_dy = dy / config_.grid_size;
1848
1849 if (grid_dx == 0 && grid_dy == 0) {
1850 return absl::OkStatus(); // No meaningful movement yet
1851 }
1852
1853 // Move all selected objects
1854 for (size_t obj_idx : selection_state_.selected_objects) {
1855 if (obj_idx >= current_room_->GetTileObjectCount())
1856 continue;
1857
1858 auto& obj = current_room_->GetTileObject(obj_idx);
1859 int new_x = obj.x() + grid_dx;
1860 int new_y = obj.y() + grid_dy;
1861
1862 // Clamp to valid range
1863 new_x = std::max(0, std::min(63, new_x));
1864 new_y = std::max(0, std::min(63, new_y));
1865
1866 obj.set_x(new_x);
1867 obj.set_y(new_y);
1868
1870 object_changed_callback_(obj_idx, obj);
1871 }
1872 }
1873
1874 // Update drag start position
1875 selection_state_.drag_start_x = current_x;
1876 selection_state_.drag_start_y = current_y;
1877
1878 return absl::OkStatus();
1879}
1880
1882 if (current_room_ == nullptr) {
1883 return {false, {}, {"No room loaded"}};
1884 }
1885
1886 // Use the dedicated validator
1888
1889 // Validate objects don't overlap if collision checking is enabled
1891 const auto& objects = current_room_->GetTileObjects();
1892 for (size_t i = 0; i < objects.size(); i++) {
1893 for (size_t j = i + 1; j < objects.size(); j++) {
1894 if (ObjectsCollide(objects[i], objects[j])) {
1895 result.errors.push_back(
1896 absl::StrFormat("Objects at indices %d and %d collide", i, j));
1897 result.is_valid = false;
1898 }
1899 }
1900 }
1901 }
1902
1903 return result;
1904}
1905
1907 auto result = ValidateRoom();
1908 std::vector<std::string> all_issues = result.errors;
1909 all_issues.insert(all_issues.end(), result.warnings.begin(),
1910 result.warnings.end());
1911 return all_issues;
1912}
1913
1918
1922
1927
1929 config_ = config;
1930}
1931
1933 rom_ = rom;
1934 // Reinitialize editor with new ROM
1936}
1937
1939 // Set the current room pointer to the external room
1940 current_room_ = room;
1941
1942 // Reset editing state for new room
1946
1947 // Clear selection as it's invalid for the new room
1949
1950 // Clear undo history as it applies to the previous room
1951 ClearHistory();
1952
1953 // Notify callbacks
1956 }
1957}
1958
1959// Factory function
1960std::unique_ptr<DungeonObjectEditor> CreateDungeonObjectEditor(Rom* rom) {
1961 return std::make_unique<DungeonObjectEditor>(rom);
1962}
1963
1964// Object Categories implementation
1965namespace ObjectCategories {
1966
1967std::vector<ObjectCategory> GetObjectCategories() {
1968 return {
1969 {"Walls", {0x10, 0x11, 0x12, 0x13}, "Basic wall objects"},
1970 {"Floors", {0x20, 0x21, 0x22, 0x23}, "Floor tile objects"},
1971 {"Decorations", {0x30, 0x31, 0x32, 0x33}, "Decorative objects"},
1972 {"Interactive", {0xF9, 0xFA, 0xFB}, "Interactive objects like chests"},
1973 {"Stairs", {0x13, 0x14, 0x15, 0x16}, "Staircase objects"},
1974 {"Doors", {0x17, 0x18, 0x19, 0x1A}, "Door objects"},
1975 {"Special",
1976 {0xF80, 0xF81, 0xF82, 0xF97},
1977 "Special dungeon objects (Type 3)"}};
1978}
1979
1980absl::StatusOr<std::vector<int>> GetObjectsInCategory(
1981 const std::string& category_name) {
1982 auto categories = GetObjectCategories();
1983
1984 for (const auto& category : categories) {
1985 if (category.name == category_name) {
1986 return category.object_ids;
1987 }
1988 }
1989
1990 return absl::NotFoundError("Category not found");
1991}
1992
1993absl::StatusOr<std::string> GetObjectCategory(int object_id) {
1994 auto categories = GetObjectCategories();
1995
1996 for (const auto& category : categories) {
1997 for (int id : category.object_ids) {
1998 if (id == object_id) {
1999 return category.name;
2000 }
2001 }
2002 }
2003
2004 return absl::NotFoundError("Object category not found");
2005}
2006
2007absl::StatusOr<ObjectInfo> GetObjectInfo(int object_id) {
2008 ObjectInfo info;
2009 info.id = object_id;
2010
2011 // This is a simplified implementation - in practice, you'd have
2012 // a comprehensive database of object information
2013
2014 if (object_id >= 0x10 && object_id <= 0x1F) {
2015 info.name = "Wall";
2016 info.description = "Basic wall object";
2017 info.valid_sizes = {{0x12, 0x12}};
2018 info.valid_layers = {0, 1, 2};
2019 info.is_interactive = false;
2020 info.is_collidable = true;
2021 } else if (object_id >= 0x20 && object_id <= 0x2F) {
2022 info.name = "Floor";
2023 info.description = "Floor tile object";
2024 info.valid_sizes = {{0x12, 0x12}};
2025 info.valid_layers = {0, 1, 2};
2026 info.is_interactive = false;
2027 info.is_collidable = false;
2028 } else if (object_id == 0xF9) {
2029 info.name = "Small Chest";
2030 info.description = "Small treasure chest";
2031 info.valid_sizes = {{0x12, 0x12}};
2032 info.valid_layers = {0, 1};
2033 info.is_interactive = true;
2034 info.is_collidable = true;
2035 } else {
2036 info.name = "Unknown Object";
2037 info.description = "Unknown object type";
2038 info.valid_sizes = {{0x12, 0x12}};
2039 info.valid_layers = {0};
2040 info.is_interactive = false;
2041 info.is_collidable = true;
2042 }
2043
2044 return info;
2045}
2046
2047} // namespace ObjectCategories
2048
2049} // namespace zelda3
2050} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
int height() const
Definition bitmap.h:374
void SetPixel(int x, int y, const SnesColor &color)
Set a pixel at the given x,y coordinates with SNES color.
Definition bitmap.cc:726
int width() const
Definition bitmap.h:373
SNES Color container.
Definition snes_color.h:110
RAII guard for ImGui style colors.
Definition style_guard.h:27
std::function< void(const SelectionState &)> SelectionChangedCallback
absl::Status HandleMouseDrag(int start_x, int start_y, int current_x, int current_y)
absl::Status HandleScrollWheel(int delta, int x, int y, bool ctrl_pressed)
std::pair< int, int > ScreenToRoomCoordinates(int screen_x, int screen_y)
int GetNextSize(int current_size, int delta)
void SetRoomChangedCallback(RoomChangedCallback callback)
std::pair< int, int > RoomToScreenCoordinates(int room_x, int room_y)
absl::Status InsertObject(int x, int y, int object_type, int size=0x12, int layer=0)
const std::vector< ObjectTemplate > & GetTemplates() const
absl::Status BatchMoveObjects(const std::vector< size_t > &indices, int dx, int dy)
absl::Status ChangeObjectLayer(size_t object_index, int new_layer)
absl::Status BatchChangeObjectLayer(const std::vector< size_t > &indices, int new_layer)
absl::Status DeleteObject(size_t object_index)
void CopySelectedObjects(const std::vector< size_t > &indices)
void SetSelectionChangedCallback(SelectionChangedCallback callback)
absl::Status SelectObject(int screen_x, int screen_y)
std::optional< size_t > DuplicateObject(size_t object_index, int offset_x=1, int offset_y=1)
absl::Status AlignSelectedObjects(Alignment alignment)
std::optional< size_t > FindObjectAt(int room_x, int room_y)
absl::Status AddToSelection(size_t object_index)
void RenderLayerVisualization(gfx::Bitmap &canvas)
std::vector< std::string > GetValidationErrors()
void SetConfig(const EditorConfig &config)
std::function< void(size_t object_index, const RoomObject &object)> ObjectChangedCallback
absl::Status HandleDragOperation(int current_x, int current_y)
absl::Status HandleSizeEdit(int delta, int x, int y)
bool ObjectsCollide(const RoomObject &obj1, const RoomObject &obj2)
bool IsObjectAtPosition(const RoomObject &object, int x, int y)
void SetObjectChangedCallback(ObjectChangedCallback callback)
absl::Status HandleMouseRelease(int x, int y)
absl::Status InsertTemplate(const ObjectTemplate &tmpl, int x, int y)
absl::Status MoveObject(size_t object_index, int new_x, int new_y)
absl::Status ResizeObject(size_t object_index, int new_size)
std::optional< RoomObject > preview_object_
absl::Status BatchResizeObjects(const std::vector< size_t > &indices, int new_size)
absl::Status ChangeObjectType(size_t object_index, int new_type)
SelectionChangedCallback selection_changed_callback_
absl::Status ApplyUndoPoint(const UndoPoint &undo_point)
absl::Status HandleMouseClick(int x, int y, bool left_button, bool right_button, bool shift_pressed)
void RenderSelectionHighlight(gfx::Bitmap &canvas)
absl::Status CreateTemplateFromSelection(const std::string &name, const std::string &description)
ValidationResult ValidateRoom(const Room &room)
static ObjectTemplate CreateFromObjects(const std::string &name, const std::string &description, const std::vector< RoomObject > &objects, int origin_x, int origin_y)
const std::vector< ObjectTemplate > & GetTemplates() const
std::vector< RoomObject > InstantiateTemplate(const ObjectTemplate &tmpl, int x, int y, Rom *rom)
absl::Status LoadTemplates(const std::string &directory_path)
absl::Status SaveTemplate(const ObjectTemplate &tmpl, const std::string &directory_path)
void set_size(uint8_t size)
Definition room_object.h:78
void set_x(uint8_t x)
Definition room_object.h:76
void set_id(int16_t id)
void SetRom(Rom *rom)
Definition room_object.h:70
void set_y(uint8_t y)
Definition room_object.h:77
void ClearTileObjects()
Definition room.h:320
absl::Status RemoveObject(size_t index)
Definition room.cc:1885
size_t GetTileObjectCount() const
Definition room.h:355
absl::Status SaveObjects()
Definition room.cc:1681
RoomObject & GetTileObject(size_t index)
Definition room.h:356
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:314
void SetTileObjects(const std::vector< RoomObject > &objects)
Definition room.h:466
absl::Status AddObject(const RoomObject &object)
Definition room.cc:1872
void RemoveTileObject(size_t index)
Definition room.h:349
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_PLACE
Definition icons.h:1477
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_ASPECT_RATIO
Definition icons.h:192
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_ARROW_BACK
Definition icons.h:173
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_DESELECT
Definition icons.h:540
#define ICON_MD_TAG
Definition icons.h:1940
#define ICON_MD_DELETE_SWEEP
Definition icons.h:533
const AgentUITheme & GetTheme()
void EndPropertyTable()
void PropertyRow(const char *label, const char *value)
void SectionHeader(const char *icon, const char *label, const ImVec4 &color)
bool BeginPropertyTable(const char *id, int columns, ImGuiTableFlags extra_flags)
void HelpMarker(const char *desc)
std::vector< ObjectCategory > GetObjectCategories()
Get all available object categories.
absl::StatusOr< ObjectInfo > GetObjectInfo(int object_id)
absl::StatusOr< std::string > GetObjectCategory(int object_id)
Get category for a specific object.
absl::StatusOr< std::vector< int > > GetObjectsInCategory(const std::string &category_name)
Get objects in a specific category.
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
std::unique_ptr< DungeonObjectEditor > CreateDungeonObjectEditor(Rom *rom)
Factory function to create dungeon object editor.
int GetObjectSubtype(int object_id)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
std::chrono::steady_clock::time_point timestamp
std::vector< std::pair< int, int > > valid_sizes
std::vector< std::string > errors