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