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()) continue;
404
405 auto& object = current_room_->GetTileObject(index);
406 int new_x = object.x() + dx;
407 int new_y = object.y() + dy;
408
409 // Clamp to room bounds
410 new_x = std::max(0, std::min(63, new_x));
411 new_y = std::max(0, std::min(63, new_y));
412
413 object.set_x(new_x);
414 object.set_y(new_y);
415
417 object_changed_callback_(index, object);
418 }
419 }
420
423 }
424
425 return absl::OkStatus();
426}
427
429 const std::vector<size_t>& indices, int new_layer) {
430 if (current_room_ == nullptr) {
431 return absl::FailedPreconditionError("No room loaded");
432 }
433
434 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
435 return absl::InvalidArgumentError("Invalid layer");
436 }
437
438 // Create undo point
439 auto status = CreateUndoPoint();
440 if (!status.ok()) {
441 return status;
442 }
443
444 for (size_t index : indices) {
445 if (index >= current_room_->GetTileObjectCount()) continue;
446
447 auto& object = current_room_->GetTileObject(index);
448 object.layer_ = static_cast<RoomObject::LayerType>(new_layer);
449
451 object_changed_callback_(index, object);
452 }
453 }
454
457 }
458
459 return absl::OkStatus();
460}
461
463 const std::vector<size_t>& indices, int new_size) {
464 if (current_room_ == nullptr) {
465 return absl::FailedPreconditionError("No room loaded");
466 }
467
468 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
469 return absl::InvalidArgumentError("Invalid object size");
470 }
471
472 // Create undo point
473 auto status = CreateUndoPoint();
474 if (!status.ok()) {
475 return status;
476 }
477
478 for (size_t index : indices) {
479 if (index >= current_room_->GetTileObjectCount()) continue;
480
481 auto& object = current_room_->GetTileObject(index);
482 // Only Type 1 objects typically support arbitrary sizing, but we allow it
483 // for all here as the validation logic might vary.
484 object.set_size(new_size);
485
487 object_changed_callback_(index, object);
488 }
489 }
490
493 }
494
495 return absl::OkStatus();
496}
497
498std::optional<size_t> DungeonObjectEditor::DuplicateObject(size_t object_index, int offset_x, int offset_y) {
499 if (current_room_ == nullptr) {
500 return std::nullopt;
501 }
502
503 if (object_index >= current_room_->GetTileObjectCount()) {
504 return std::nullopt;
505 }
506
507 // Create undo point
509
510 auto object = current_room_->GetTileObject(object_index);
511
512 // Offset position
513 int new_x = object.x() + offset_x;
514 int new_y = object.y() + offset_y;
515
516 // Clamp
517 new_x = std::max(0, std::min(63, new_x));
518 new_y = std::max(0, std::min(63, new_y));
519
520 object.set_x(new_x);
521 object.set_y(new_y);
522
523 // Add object
524 if (current_room_->AddObject(object).ok()) {
525 size_t new_index = current_room_->GetTileObjectCount() - 1;
526
529 }
530
531 return new_index;
532 }
533
534 return std::nullopt;
535}
536
537void DungeonObjectEditor::CopySelectedObjects(const std::vector<size_t>& indices) {
538 if (current_room_ == nullptr) return;
539
540 clipboard_.clear();
541
542 for (size_t index : indices) {
543 if (index < current_room_->GetTileObjectCount()) {
544 clipboard_.push_back(current_room_->GetTileObject(index));
545 }
546 }
547}
548
550 if (current_room_ == nullptr || clipboard_.empty()) {
551 return {};
552 }
553
554 // Create undo point
556
557 std::vector<size_t> new_indices;
558 size_t start_index = current_room_->GetTileObjectCount();
559
560 for (const auto& obj : clipboard_) {
561 // Paste with slight offset to make it visible
562 RoomObject new_obj = obj;
563
564 // Logic to ensure it stays in bounds if we were to support mouse-position pasting
565 // For now, just paste at original location + offset, or perhaps center of screen
566 // Let's do original + 1,1 for now to match duplicate behavior if we just copy/paste
567 // But better might be to keep relative positions if we had a "cursor" position.
568
569 int new_x = std::min(63, new_obj.x() + 1);
570 int new_y = std::min(63, new_obj.y() + 1);
571 new_obj.set_x(new_x);
572 new_obj.set_y(new_y);
573
574 if (current_room_->AddObject(new_obj).ok()) {
575 new_indices.push_back(start_index++);
576 }
577 }
578
581 }
582
583 return new_indices;
584}
585
586absl::Status DungeonObjectEditor::ChangeObjectType(size_t object_index,
587 int new_type) {
588 if (current_room_ == nullptr) {
589 return absl::FailedPreconditionError("No room loaded");
590 }
591
592 if (object_index >= current_room_->GetTileObjectCount()) {
593 return absl::OutOfRangeError("Object index out of range");
594 }
595
596 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
597 if (new_type < 0 || new_type > 0xFFF) {
598 return absl::InvalidArgumentError("Invalid object type");
599 }
600
601 // Create undo point
602 auto status = CreateUndoPoint();
603 if (!status.ok()) {
604 return status;
605 }
606
607 auto& object = current_room_->GetTileObject(object_index);
608 object.id_ = new_type;
609
611 object_changed_callback_(object_index, object);
612 }
613
616 }
617
618 return absl::OkStatus();
619}
620
622 int x, int y) {
623 if (current_room_ == nullptr) {
624 return absl::FailedPreconditionError("No room loaded");
625 }
626
627 // Snap coordinates to grid if enabled
628 if (config_.snap_to_grid) {
629 x = SnapToGrid(x);
630 y = SnapToGrid(y);
631 }
632
633 // Create undo point
634 auto status = CreateUndoPoint();
635 if (!status.ok()) {
636 return status;
637 }
638
639 // Instantiate template objects
640 std::vector<RoomObject> new_objects =
642
643 // Check for collisions if enabled
645 for (const auto& new_obj : new_objects) {
646 for (const auto& existing_obj : current_room_->GetTileObjects()) {
647 if (ObjectsCollide(new_obj, existing_obj)) {
648 return absl::FailedPreconditionError(
649 "Template placement would cause collision");
650 }
651 }
652 }
653 }
654
655 // Add objects to room
656 for (const auto& obj : new_objects) {
658 }
659
660 // Select the new objects
662 size_t count = current_room_->GetTileObjectCount();
663 size_t added_count = new_objects.size();
664 for (size_t i = 0; i < added_count; ++i) {
665 selection_state_.selected_objects.push_back(count - added_count + i);
666 }
667 if (!selection_state_.selected_objects.empty()) {
669 }
670
673 }
674
675 return absl::OkStatus();
676}
677
679 const std::string& name, const std::string& description) {
681 return absl::FailedPreconditionError("No objects selected");
682 }
683
684 std::vector<RoomObject> objects;
685 int min_x = 64, min_y = 64;
686
687 // Collect selected objects and find bounds
688 for (size_t index : selection_state_.selected_objects) {
689 if (index < current_room_->GetTileObjectCount()) {
690 const auto& obj = current_room_->GetTileObject(index);
691 objects.push_back(obj);
692 if (obj.x() < min_x) min_x = obj.x();
693 if (obj.y() < min_y) min_y = obj.y();
694 }
695 }
696
697 // Create template
699 name, description, objects, min_x, min_y);
700
701 // Save template
702 return template_manager_.SaveTemplate(tmpl, "assets/templates/dungeon");
703}
704
705const std::vector<ObjectTemplate>& DungeonObjectEditor::GetTemplates() const {
707}
708
710 if (current_room_ == nullptr) {
711 return absl::FailedPreconditionError("No room loaded");
712 }
713
714 if (selection_state_.selected_objects.size() < 2) {
715 return absl::OkStatus(); // Nothing to align
716 }
717
718 // Create undo point
719 auto status = CreateUndoPoint();
720 if (!status.ok()) {
721 return status;
722 }
723
724 // Find reference value (min/max/avg)
725 int ref_val = 0;
726 const auto& indices = selection_state_.selected_objects;
727
728 if (alignment == Alignment::Left || alignment == Alignment::Top) {
729 ref_val = 64; // Max possible
730 } else if (alignment == Alignment::Right || alignment == Alignment::Bottom) {
731 ref_val = 0; // Min possible
732 }
733
734 // First pass: calculate reference
735 int sum = 0;
736 int count = 0;
737
738 for (size_t index : indices) {
739 if (index >= current_room_->GetTileObjectCount()) continue;
740 const auto& obj = current_room_->GetTileObject(index);
741
742 switch (alignment) {
743 case Alignment::Left:
744 if (obj.x() < ref_val) ref_val = obj.x();
745 break;
746 case Alignment::Right:
747 if (obj.x() > ref_val) ref_val = obj.x();
748 break;
749 case Alignment::Top:
750 if (obj.y() < ref_val) ref_val = obj.y();
751 break;
753 if (obj.y() > ref_val) ref_val = obj.y();
754 break;
756 sum += obj.x();
757 count++;
758 break;
760 sum += obj.y();
761 count++;
762 break;
763 }
764 }
765
766 if (alignment == Alignment::CenterX || alignment == Alignment::CenterY) {
767 if (count > 0) ref_val = sum / count;
768 }
769
770 // Second pass: apply alignment
771 for (size_t index : indices) {
772 if (index >= current_room_->GetTileObjectCount()) continue;
773 auto& obj = current_room_->GetTileObject(index);
774
775 switch (alignment) {
776 case Alignment::Left:
777 case Alignment::Right:
779 obj.set_x(ref_val);
780 break;
781 case Alignment::Top:
784 obj.set_y(ref_val);
785 break;
786 }
787
789 object_changed_callback_(index, obj);
790 }
791 }
792
795 }
796
797 return absl::OkStatus();
798}
799
800absl::Status DungeonObjectEditor::ChangeObjectLayer(size_t object_index,
801 int new_layer) {
802 if (current_room_ == nullptr) {
803 return absl::FailedPreconditionError("No room loaded");
804 }
805
806 if (object_index >= current_room_->GetTileObjectCount()) {
807 return absl::OutOfRangeError("Object index out of range");
808 }
809
810 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
811 return absl::InvalidArgumentError("Invalid layer");
812 }
813
814 // Create undo point
815 auto status = CreateUndoPoint();
816 if (!status.ok()) {
817 return status;
818 }
819
820 auto& object = current_room_->GetTileObject(object_index);
821 object.layer_ = static_cast<RoomObject::LayerType>(new_layer);
822
824 object_changed_callback_(object_index, object);
825 }
826
829 }
830
831 return absl::OkStatus();
832}
833
834absl::Status DungeonObjectEditor::HandleScrollWheel(int delta, int x, int y,
835 bool ctrl_pressed) {
836 if (current_room_ == nullptr) {
837 return absl::FailedPreconditionError("No room loaded");
838 }
839
840 // Convert screen coordinates to room coordinates
841 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
842
843 // Handle size editing with scroll wheel
847 return HandleSizeEdit(delta, room_x, room_y);
848 }
849
850 // Handle layer switching with Ctrl+scroll
851 if (ctrl_pressed) {
852 int layer_delta = delta > 0 ? 1 : -1;
853 int new_layer = editing_state_.current_layer + layer_delta;
854 new_layer = std::max(kMinLayer, std::min(kMaxLayer, new_layer));
855
856 if (new_layer != editing_state_.current_layer) {
857 SetCurrentLayer(new_layer);
858 }
859
860 return absl::OkStatus();
861 }
862
863 return absl::OkStatus();
864}
865
866absl::Status DungeonObjectEditor::HandleSizeEdit(int delta, int x, int y) {
867 // Handle size editing for preview object
869 int new_size = GetNextSize(editing_state_.preview_size, delta);
870 if (IsValidSize(new_size)) {
871 editing_state_.preview_size = new_size;
873 }
874 return absl::OkStatus();
875 }
876
877 // Handle size editing for selected objects
880 for (size_t object_index : selection_state_.selected_objects) {
881 if (object_index < current_room_->GetTileObjectCount()) {
882 auto& object = current_room_->GetTileObject(object_index);
883 int new_size = GetNextSize(object.size_, delta);
884 if (IsValidSize(new_size)) {
885 auto status = ResizeObject(object_index, new_size);
886 if (!status.ok()) {
887 return status;
888 }
889 }
890 }
891 }
892 return absl::OkStatus();
893 }
894
895 return absl::OkStatus();
896}
897
898int DungeonObjectEditor::GetNextSize(int current_size, int delta) {
899 // Define size increments based on object type
900 // This is a simplified implementation - in practice, you'd have
901 // different size rules for different object types
902
903 if (delta > 0) {
904 // Increase size
905 if (current_size < 0x40) {
906 return current_size + 0x10; // Large increments for small sizes
907 } else if (current_size < 0x80) {
908 return current_size + 0x08; // Medium increments
909 } else {
910 return current_size + 0x04; // Small increments for large sizes
911 }
912 } else {
913 // Decrease size
914 if (current_size > 0x80) {
915 return current_size - 0x04; // Small decrements for large sizes
916 } else if (current_size > 0x40) {
917 return current_size - 0x08; // Medium decrements
918 } else {
919 return current_size - 0x10; // Large decrements for small sizes
920 }
921 }
922}
923
925 return size >= kMinObjectSize && size <= kMaxObjectSize;
926}
927
929 bool left_button,
930 bool right_button,
931 bool shift_pressed) {
932 if (current_room_ == nullptr) {
933 return absl::FailedPreconditionError("No room loaded");
934 }
935
936 // Convert screen coordinates to room coordinates
937 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
938
939 if (left_button) {
941 case Mode::kSelect:
942 if (shift_pressed) {
943 // Add to selection
944 auto object_index = FindObjectAt(room_x, room_y);
945 if (object_index.has_value()) {
946 return AddToSelection(object_index.value());
947 }
948 } else {
949 // Select object
950 return SelectObject(x, y);
951 }
952 break;
953
954 case Mode::kInsert:
955 // Insert object at clicked position
956 return InsertObject(room_x, room_y, editing_state_.current_object_type,
959
960 case Mode::kDelete:
961 // Delete object at clicked position
962 {
963 auto object_index = FindObjectAt(room_x, room_y);
964 if (object_index.has_value()) {
965 return DeleteObject(object_index.value());
966 }
967 }
968 break;
969
970 case Mode::kEdit:
971 // Select object for editing
972 return SelectObject(x, y);
973
974 default:
975 break;
976 }
977 }
978
979 if (right_button) {
980 // Context menu or alternate action
982 case Mode::kSelect:
983 // Show context menu for object
984 {
985 auto object_index = FindObjectAt(room_x, room_y);
986 if (object_index.has_value()) {
987 // TODO: Show context menu
988 }
989 }
990 break;
991
992 default:
993 break;
994 }
995 }
996
997 return absl::OkStatus();
998}
999
1000absl::Status DungeonObjectEditor::HandleMouseDrag(int start_x, int start_y,
1001 int current_x,
1002 int current_y) {
1003 if (current_room_ == nullptr) {
1004 return absl::FailedPreconditionError("No room loaded");
1005 }
1006
1007 // Enable dragging if not already (Phase 4)
1013
1014 // Create undo point before drag
1015 auto undo_status = CreateUndoPoint();
1016 if (!undo_status.ok()) {
1017 return undo_status;
1018 }
1019 }
1020
1021 // Handle the drag operation (Phase 4)
1022 return HandleDragOperation(current_x, current_y);
1023}
1024
1026 if (current_room_ == nullptr) {
1027 return absl::FailedPreconditionError("No room loaded");
1028 }
1029
1030 // End dragging operation (Phase 4)
1033
1034 // Notify callbacks about the final positions
1037 }
1038 }
1039
1040 return absl::OkStatus();
1041}
1042
1043absl::Status DungeonObjectEditor::SelectObject(int screen_x, int screen_y) {
1044 if (current_room_ == nullptr) {
1045 return absl::FailedPreconditionError("No room loaded");
1046 }
1047
1048 // Convert screen coordinates to room coordinates
1049 auto [room_x, room_y] = ScreenToRoomCoordinates(screen_x, screen_y);
1050
1051 // Find object at position
1052 auto object_index = FindObjectAt(room_x, room_y);
1053
1054 if (object_index.has_value()) {
1055 // Select the found object
1057 selection_state_.selected_objects.push_back(object_index.value());
1058
1059 // Notify callbacks
1062 }
1063
1064 return absl::OkStatus();
1065 } else {
1066 // Clear selection if no object found
1067 return ClearSelection();
1068 }
1069}
1070
1075
1076 // Notify callbacks
1079 }
1080
1081 return absl::OkStatus();
1082}
1083
1084absl::Status DungeonObjectEditor::AddToSelection(size_t object_index) {
1085 if (current_room_ == nullptr) {
1086 return absl::FailedPreconditionError("No room loaded");
1087 }
1088
1089 if (object_index >= current_room_->GetTileObjectCount()) {
1090 return absl::OutOfRangeError("Object index out of range");
1091 }
1092
1093 // Check if already selected
1094 auto it = std::find(selection_state_.selected_objects.begin(),
1095 selection_state_.selected_objects.end(), object_index);
1096
1097 if (it == selection_state_.selected_objects.end()) {
1098 selection_state_.selected_objects.push_back(object_index);
1100
1101 // Notify callbacks
1104 }
1105 }
1106
1107 return absl::OkStatus();
1108}
1109
1112
1113 // Update preview object based on mode
1115}
1116
1118 if (layer >= kMinLayer && layer <= kMaxLayer) {
1121 }
1122}
1123
1125 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
1126 if (object_type >= 0 && object_type <= 0xFFF) {
1127 editing_state_.current_object_type = object_type;
1129 }
1130}
1131
1132std::optional<size_t> DungeonObjectEditor::FindObjectAt(int room_x,
1133 int room_y) {
1134 if (current_room_ == nullptr) {
1135 return std::nullopt;
1136 }
1137
1138 // Search from back to front (last objects are on top)
1139 for (int i = static_cast<int>(current_room_->GetTileObjectCount()) - 1;
1140 i >= 0; i--) {
1141 if (IsObjectAtPosition(current_room_->GetTileObject(i), room_x, room_y)) {
1142 return static_cast<size_t>(i);
1143 }
1144 }
1145
1146 return std::nullopt;
1147}
1148
1150 int y) {
1151 // Convert object position to pixel coordinates
1152 int obj_x = object.x_ * 16;
1153 int obj_y = object.y_ * 16;
1154
1155 // Check if point is within object bounds
1156 // This is a simplified implementation - in practice, you'd check
1157 // against the actual tile data
1158
1159 int obj_width = 16; // Default object width
1160 int obj_height = 16; // Default object height
1161
1162 // Adjust size based on object size value
1163 if (object.size_ > 0x80) {
1164 obj_width *= 2;
1165 obj_height *= 2;
1166 }
1167
1168 return (x >= obj_x && x < obj_x + obj_width && y >= obj_y &&
1169 y < obj_y + obj_height);
1170}
1171
1173 const RoomObject& obj2) {
1174 // Simple bounding box collision detection
1175 // In practice, you'd use the actual tile data for more accurate collision
1176
1177 int obj1_x = obj1.x_ * 16;
1178 int obj1_y = obj1.y_ * 16;
1179 int obj1_w = 16;
1180 int obj1_h = 16;
1181
1182 int obj2_x = obj2.x_ * 16;
1183 int obj2_y = obj2.y_ * 16;
1184 int obj2_w = 16;
1185 int obj2_h = 16;
1186
1187 // Adjust sizes based on object size values
1188 if (obj1.size_ > 0x80) {
1189 obj1_w *= 2;
1190 obj1_h *= 2;
1191 }
1192
1193 if (obj2.size_ > 0x80) {
1194 obj2_w *= 2;
1195 obj2_h *= 2;
1196 }
1197
1198 return !(obj1_x + obj1_w <= obj2_x || obj2_x + obj2_w <= obj1_x ||
1199 obj1_y + obj1_h <= obj2_y || obj2_y + obj2_h <= obj1_y);
1200}
1201
1202std::pair<int, int> DungeonObjectEditor::ScreenToRoomCoordinates(int screen_x,
1203 int screen_y) {
1204 // Convert screen coordinates to room tile coordinates
1205 // This is a simplified implementation - in practice, you'd account for
1206 // camera position, zoom level, etc.
1207
1208 int room_x = screen_x / 16; // 16 pixels per tile
1209 int room_y = screen_y / 16;
1210
1211 return {room_x, room_y};
1212}
1213
1215 int room_y) {
1216 // Convert room tile coordinates to screen coordinates
1217 int screen_x = room_x * 16;
1218 int screen_y = room_y * 16;
1219
1220 return {screen_x, screen_y};
1221}
1222
1224 if (!config_.snap_to_grid) {
1225 return coordinate;
1226 }
1227
1228 return (coordinate / config_.grid_size) * config_.grid_size;
1229}
1230
1244
1246 if (current_room_ == nullptr) {
1247 return absl::FailedPreconditionError("No room loaded");
1248 }
1249
1250 // Create undo point
1251 UndoPoint undo_point;
1252 undo_point.objects = current_room_->GetTileObjects();
1253 undo_point.selection = selection_state_;
1254 undo_point.editing = editing_state_;
1255 undo_point.timestamp = std::chrono::steady_clock::now();
1256
1257 // Add to undo history
1258 undo_history_.push_back(undo_point);
1259
1260 // Limit undo history size
1261 if (undo_history_.size() > kMaxUndoHistory) {
1262 undo_history_.erase(undo_history_.begin());
1263 }
1264
1265 // Clear redo history when new action is performed
1266 redo_history_.clear();
1267
1268 return absl::OkStatus();
1269}
1270
1272 if (!CanUndo()) {
1273 return absl::FailedPreconditionError("Nothing to undo");
1274 }
1275
1276 // Move current state to redo history
1277 UndoPoint current_state;
1278 current_state.objects = current_room_->GetTileObjects();
1279 current_state.selection = selection_state_;
1280 current_state.editing = editing_state_;
1281 current_state.timestamp = std::chrono::steady_clock::now();
1282
1283 redo_history_.push_back(current_state);
1284
1285 // Apply undo point
1286 UndoPoint undo_point = undo_history_.back();
1287 undo_history_.pop_back();
1288
1289 return ApplyUndoPoint(undo_point);
1290}
1291
1293 if (!CanRedo()) {
1294 return absl::FailedPreconditionError("Nothing to redo");
1295 }
1296
1297 // Move current state to undo history
1298 UndoPoint current_state;
1299 current_state.objects = current_room_->GetTileObjects();
1300 current_state.selection = selection_state_;
1301 current_state.editing = editing_state_;
1302 current_state.timestamp = std::chrono::steady_clock::now();
1303
1304 undo_history_.push_back(current_state);
1305
1306 // Apply redo point
1307 UndoPoint redo_point = redo_history_.back();
1308 redo_history_.pop_back();
1309
1310 return ApplyUndoPoint(redo_point);
1311}
1312
1313absl::Status DungeonObjectEditor::ApplyUndoPoint(const UndoPoint& undo_point) {
1314 if (current_room_ == nullptr) {
1315 return absl::FailedPreconditionError("No room loaded");
1316 }
1317
1318 // Restore room state
1320
1321 // Restore editor state
1322 selection_state_ = undo_point.selection;
1323 editing_state_ = undo_point.editing;
1324
1325 // Update preview
1327
1328 // Notify callbacks
1331 }
1332
1335 }
1336
1337 return absl::OkStatus();
1338}
1339
1341 return !undo_history_.empty();
1342}
1343
1345 return !redo_history_.empty();
1346}
1347
1349 undo_history_.clear();
1350 redo_history_.clear();
1351}
1352
1353// ============================================================================
1354// Phase 4: Visual Feedback and GUI Methods
1355// ============================================================================
1356
1357// Helper for color blending
1358static uint32_t BlendColors(uint32_t base, uint32_t tint) {
1359 uint8_t a_tint = (tint >> 24) & 0xFF;
1360 if (a_tint == 0)
1361 return base;
1362
1363 uint8_t r_base = (base >> 16) & 0xFF;
1364 uint8_t g_base = (base >> 8) & 0xFF;
1365 uint8_t b_base = base & 0xFF;
1366
1367 uint8_t r_tint = (tint >> 16) & 0xFF;
1368 uint8_t g_tint = (tint >> 8) & 0xFF;
1369 uint8_t b_tint = tint & 0xFF;
1370
1371 float alpha = a_tint / 255.0f;
1372 uint8_t r = r_base * (1.0f - alpha) + r_tint * alpha;
1373 uint8_t g = g_base * (1.0f - alpha) + g_tint * alpha;
1374 uint8_t b = b_base * (1.0f - alpha) + b_tint * alpha;
1375
1376 return 0xFF000000 | (r << 16) | (g << 8) | b;
1377}
1378
1382 return;
1383 }
1384
1385 // Draw highlight rectangles around selected objects
1386 for (size_t obj_idx : selection_state_.selected_objects) {
1387 if (obj_idx >= current_room_->GetTileObjectCount())
1388 continue;
1389
1390 const auto& obj = current_room_->GetTileObject(obj_idx);
1391 int x = obj.x() * 16;
1392 int y = obj.y() * 16;
1393 int w = 16 + (obj.size() * 4); // Approximate width
1394 int h = 16 + (obj.size() * 4); // Approximate height
1395
1396 // Draw yellow selection box (2px border) - using SetPixel
1397 uint8_t r = (config_.selection_color >> 16) & 0xFF;
1398 uint8_t g = (config_.selection_color >> 8) & 0xFF;
1399 uint8_t b = config_.selection_color & 0xFF;
1400 gfx::SnesColor sel_color(r, g, b);
1401
1402 for (int py = y; py < y + h; py++) {
1403 for (int px = x; px < x + w; px++) {
1404 if (px < canvas.width() && py < canvas.height() &&
1405 (px < x + 2 || px >= x + w - 2 || py < y + 2 || py >= y + h - 2)) {
1406 canvas.SetPixel(px, py, sel_color);
1407 }
1408 }
1409 }
1410 }
1411}
1412
1415 return;
1416 }
1417
1418 // Apply subtle color tints based on layer (simplified - just mark with
1419 // colored border)
1420 for (const auto& obj : current_room_->GetTileObjects()) {
1421 int x = obj.x() * 16;
1422 int y = obj.y() * 16;
1423 int w = 16;
1424 int h = 16;
1425
1426 uint32_t tint_color = 0xFF000000;
1427 switch (obj.GetLayerValue()) {
1428 case 0:
1429 tint_color = config_.layer0_color;
1430 break;
1431 case 1:
1432 tint_color = config_.layer1_color;
1433 break;
1434 case 2:
1435 tint_color = config_.layer2_color;
1436 break;
1437 }
1438
1439 // Draw 1px border in layer color
1440 uint8_t r = (tint_color >> 16) & 0xFF;
1441 uint8_t g = (tint_color >> 8) & 0xFF;
1442 uint8_t b = tint_color & 0xFF;
1443 gfx::SnesColor layer_color(r, g, b);
1444
1445 for (int py = y; py < y + h && py < canvas.height(); py++) {
1446 for (int px = x; px < x + w && px < canvas.width(); px++) {
1447 if (px == x || px == x + w - 1 || py == y || py == y + h - 1) {
1448 canvas.SetPixel(px, py, layer_color);
1449 }
1450 }
1451 }
1452 }
1453}
1454
1456 const auto& theme = editor::AgentUI::GetTheme();
1457
1460 return;
1461 }
1462
1463 if (selection_state_.selected_objects.size() == 1) {
1464 size_t obj_idx = selection_state_.selected_objects[0];
1465 if (obj_idx < current_room_->GetTileObjectCount()) {
1466 auto& obj = current_room_->GetTileObject(obj_idx);
1467
1468 // ========== Identity Section ==========
1469 gui::SectionHeader(ICON_MD_TAG, "Identity", theme.text_info);
1470 if (gui::BeginPropertyTable("##IdentityProps")) {
1471 // Object index
1472 gui::PropertyRow("Object #", static_cast<int>(obj_idx));
1473
1474 // Object ID with name
1475 ImGui::TableNextRow();
1476 ImGui::TableNextColumn();
1477 ImGui::Text("ID");
1478 ImGui::TableNextColumn();
1479 std::string obj_name = GetObjectName(obj.id_);
1480 ImGui::Text("0x%03X", obj.id_);
1481 ImGui::SameLine();
1482 ImGui::TextColored(theme.text_secondary_gray, "(%s)", obj_name.c_str());
1483
1484 // Object type/subtype
1485 int subtype = GetObjectSubtype(obj.id_);
1486 ImGui::TableNextRow();
1487 ImGui::TableNextColumn();
1488 ImGui::Text("Type");
1489 ImGui::TableNextColumn();
1490 ImGui::Text("Subtype %d", subtype);
1491
1493 }
1494
1495 ImGui::Spacing();
1496
1497 // ========== Position Section ==========
1498 gui::SectionHeader(ICON_MD_PLACE, "Position", theme.text_info);
1499 if (gui::BeginPropertyTable("##PositionProps")) {
1500 // X Position
1501 ImGui::TableNextRow();
1502 ImGui::TableNextColumn();
1503 ImGui::Text("X");
1504 ImGui::TableNextColumn();
1505 int x = obj.x();
1506 ImGui::SetNextItemWidth(-1);
1507 if (ImGui::InputInt("##X", &x, 1, 4)) {
1508 if (x >= 0 && x < 64) {
1509 obj.set_x(x);
1511 object_changed_callback_(obj_idx, obj);
1512 }
1513 }
1514 }
1515
1516 // Y Position
1517 ImGui::TableNextRow();
1518 ImGui::TableNextColumn();
1519 ImGui::Text("Y");
1520 ImGui::TableNextColumn();
1521 int y = obj.y();
1522 ImGui::SetNextItemWidth(-1);
1523 if (ImGui::InputInt("##Y", &y, 1, 4)) {
1524 if (y >= 0 && y < 64) {
1525 obj.set_y(y);
1527 object_changed_callback_(obj_idx, obj);
1528 }
1529 }
1530 }
1531
1533 }
1534
1535 ImGui::Spacing();
1536
1537 // ========== Appearance Section ==========
1538 gui::SectionHeader(ICON_MD_PALETTE, "Appearance", theme.text_info);
1539 if (gui::BeginPropertyTable("##AppearanceProps")) {
1540 // Size (for Type 1 objects only)
1541 if (obj.id_ < 0x100) {
1542 ImGui::TableNextRow();
1543 ImGui::TableNextColumn();
1544 ImGui::Text("Size");
1545 ImGui::TableNextColumn();
1546 int size = obj.size();
1547 ImGui::SetNextItemWidth(-1);
1548 if (ImGui::SliderInt("##Size", &size, 0, 15, "0x%02X")) {
1549 obj.set_size(size);
1551 object_changed_callback_(obj_idx, obj);
1552 }
1553 }
1554 }
1555
1556 // Object ID (editable)
1557 ImGui::TableNextRow();
1558 ImGui::TableNextColumn();
1559 ImGui::Text("Change ID");
1560 ImGui::TableNextColumn();
1561 int id = obj.id_;
1562 ImGui::SetNextItemWidth(-1);
1563 if (ImGui::InputInt("##ID", &id, 1, 16,
1564 ImGuiInputTextFlags_CharsHexadecimal)) {
1565 if (id >= 0 && id <= 0xFFF) {
1566 obj.id_ = id;
1568 object_changed_callback_(obj_idx, obj);
1569 }
1570 }
1571 }
1572
1574 }
1575
1576 ImGui::Spacing();
1577
1578 // ========== Layer Section ==========
1579 gui::SectionHeader(ICON_MD_LAYERS, "Layer", theme.text_info);
1580 if (gui::BeginPropertyTable("##LayerProps")) {
1581 ImGui::TableNextRow();
1582 ImGui::TableNextColumn();
1583 ImGui::Text("Layer");
1584 ImGui::TableNextColumn();
1585 int layer = obj.GetLayerValue();
1586 ImGui::SetNextItemWidth(-1);
1587 if (ImGui::Combo("##Layer", &layer,
1588 "BG1 (Floor)\0BG2 (Objects)\0BG3 (Overlay)\0")) {
1589 obj.layer_ = static_cast<RoomObject::LayerType>(layer);
1591 object_changed_callback_(obj_idx, obj);
1592 }
1593 }
1594
1596 }
1597
1598 ImGui::Spacing();
1599 ImGui::Separator();
1600 ImGui::Spacing();
1601
1602 // ========== Actions Section ==========
1603 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1604
1605 ImGui::PushStyleColor(ImGuiCol_Button,
1606 ImVec4(theme.status_error.x * 0.7f,
1607 theme.status_error.y * 0.7f,
1608 theme.status_error.z * 0.7f, 1.0f));
1609 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, theme.status_error);
1610 if (ImGui::Button(ICON_MD_DELETE " Delete", ImVec2(button_width, 0))) {
1611 auto status = DeleteObject(obj_idx);
1612 (void)status;
1613 }
1614 ImGui::PopStyleColor(2);
1615
1616 ImGui::SameLine();
1617
1618 if (ImGui::Button(ICON_MD_CONTENT_COPY " Duplicate",
1619 ImVec2(button_width, 0))) {
1620 RoomObject duplicate = obj;
1621 duplicate.set_x(obj.x() + 1);
1622 auto status = current_room_->AddObject(duplicate);
1623 (void)status;
1624 }
1625 }
1626 } else {
1627 // ========== Multiple Selection Mode ==========
1628 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_SELECT_ALL " %zu objects selected",
1630
1631 ImGui::Spacing();
1632
1633 // ========== Batch Layer ==========
1634 gui::SectionHeader(ICON_MD_LAYERS, "Batch Layer", theme.text_info);
1635 static int batch_layer = 0;
1636 ImGui::SetNextItemWidth(-1);
1637 if (ImGui::Combo("##BatchLayer", &batch_layer,
1638 "BG1 (Floor)\0BG2 (Objects)\0BG3 (Overlay)\0")) {
1640 }
1641
1642 ImGui::Spacing();
1643
1644 // ========== Batch Size ==========
1645 gui::SectionHeader(ICON_MD_ASPECT_RATIO, "Batch Size", theme.text_info);
1646 static int batch_size = 0x12;
1647 ImGui::SetNextItemWidth(-1);
1648 if (ImGui::InputInt("##BatchSize", &batch_size, 1, 16,
1649 ImGuiInputTextFlags_CharsHexadecimal)) {
1651 }
1652
1653 ImGui::Spacing();
1654
1655 // ========== Nudge Section ==========
1656 gui::SectionHeader(ICON_MD_OPEN_WITH, "Nudge", theme.text_info);
1657 float nudge_btn_size = (ImGui::GetContentRegionAvail().x - 24) / 4;
1658 if (ImGui::Button(ICON_MD_ARROW_BACK, ImVec2(nudge_btn_size, 0))) {
1660 }
1661 ImGui::SameLine();
1662 if (ImGui::Button(ICON_MD_ARROW_UPWARD, ImVec2(nudge_btn_size, 0))) {
1664 }
1665 ImGui::SameLine();
1666 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD, ImVec2(nudge_btn_size, 0))) {
1668 }
1669 ImGui::SameLine();
1670 if (ImGui::Button(ICON_MD_ARROW_FORWARD, ImVec2(nudge_btn_size, 0))) {
1672 }
1673
1674 ImGui::Spacing();
1675 ImGui::Separator();
1676 ImGui::Spacing();
1677
1678 // ========== Actions ==========
1679 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1680
1681 ImGui::PushStyleColor(ImGuiCol_Button,
1682 ImVec4(theme.status_error.x * 0.7f,
1683 theme.status_error.y * 0.7f,
1684 theme.status_error.z * 0.7f, 1.0f));
1685 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, theme.status_error);
1686 if (ImGui::Button(ICON_MD_DELETE_SWEEP " Delete All",
1687 ImVec2(button_width, 0))) {
1688 auto status = DeleteSelectedObjects();
1689 (void)status;
1690 }
1691 ImGui::PopStyleColor(2);
1692
1693 ImGui::SameLine();
1694
1695 if (ImGui::Button(ICON_MD_DESELECT " Clear Selection",
1696 ImVec2(button_width, 0))) {
1697 auto status = ClearSelection();
1698 (void)status;
1699 }
1700 }
1701}
1702
1704 ImGui::Begin("Layer Controls");
1705
1706 // Current layer selection
1707 ImGui::Text("Current Layer:");
1708 ImGui::RadioButton("Layer 0", &editing_state_.current_layer, 0);
1709 ImGui::SameLine();
1710 ImGui::RadioButton("Layer 1", &editing_state_.current_layer, 1);
1711 ImGui::SameLine();
1712 ImGui::RadioButton("Layer 2", &editing_state_.current_layer, 2);
1713
1714 ImGui::Separator();
1715
1716 // Layer visibility toggles
1717 static bool layer_visible[3] = {true, true, true};
1718 ImGui::Text("Layer Visibility:");
1719 ImGui::Checkbox("Show Layer 0", &layer_visible[0]);
1720 ImGui::Checkbox("Show Layer 1", &layer_visible[1]);
1721 ImGui::Checkbox("Show Layer 2", &layer_visible[2]);
1722
1723 ImGui::Separator();
1724
1725 // Layer colors
1726 ImGui::Checkbox("Show Layer Colors", &config_.show_layer_colors);
1728 ImGui::ColorEdit4("Layer 0 Tint", (float*)&config_.layer0_color);
1729 ImGui::ColorEdit4("Layer 1 Tint", (float*)&config_.layer1_color);
1730 ImGui::ColorEdit4("Layer 2 Tint", (float*)&config_.layer2_color);
1731 }
1732
1733 ImGui::Separator();
1734
1735 // Object counts per layer
1736 if (current_room_) {
1737 int count0 = 0, count1 = 0, count2 = 0;
1738 for (const auto& obj : current_room_->GetTileObjects()) {
1739 switch (obj.GetLayerValue()) {
1740 case 0:
1741 count0++;
1742 break;
1743 case 1:
1744 count1++;
1745 break;
1746 case 2:
1747 count2++;
1748 break;
1749 }
1750 }
1751 ImGui::Text("Layer 0: %d objects", count0);
1752 ImGui::Text("Layer 1: %d objects", count1);
1753 ImGui::Text("Layer 2: %d objects", count2);
1754 }
1755
1756 ImGui::End();
1757}
1758
1760 int current_y) {
1763 return absl::OkStatus();
1764 }
1765
1766 // Calculate delta from drag start
1767 int dx = current_x - selection_state_.drag_start_x;
1768 int dy = current_y - selection_state_.drag_start_y;
1769
1770 // Convert pixel delta to grid delta
1771 int grid_dx = dx / config_.grid_size;
1772 int grid_dy = dy / config_.grid_size;
1773
1774 if (grid_dx == 0 && grid_dy == 0) {
1775 return absl::OkStatus(); // No meaningful movement yet
1776 }
1777
1778 // Move all selected objects
1779 for (size_t obj_idx : selection_state_.selected_objects) {
1780 if (obj_idx >= current_room_->GetTileObjectCount())
1781 continue;
1782
1783 auto& obj = current_room_->GetTileObject(obj_idx);
1784 int new_x = obj.x() + grid_dx;
1785 int new_y = obj.y() + grid_dy;
1786
1787 // Clamp to valid range
1788 new_x = std::max(0, std::min(63, new_x));
1789 new_y = std::max(0, std::min(63, new_y));
1790
1791 obj.set_x(new_x);
1792 obj.set_y(new_y);
1793
1795 object_changed_callback_(obj_idx, obj);
1796 }
1797 }
1798
1799 // Update drag start position
1800 selection_state_.drag_start_x = current_x;
1801 selection_state_.drag_start_y = current_y;
1802
1803 return absl::OkStatus();
1804}
1805
1807 if (current_room_ == nullptr) {
1808 return {false, {}, {"No room loaded"}};
1809 }
1810
1811 // Use the dedicated validator
1813
1814 // Validate objects don't overlap if collision checking is enabled
1816 const auto& objects = current_room_->GetTileObjects();
1817 for (size_t i = 0; i < objects.size(); i++) {
1818 for (size_t j = i + 1; j < objects.size(); j++) {
1819 if (ObjectsCollide(objects[i], objects[j])) {
1820 result.errors.push_back(
1821 absl::StrFormat("Objects at indices %d and %d collide", i, j));
1822 result.is_valid = false;
1823 }
1824 }
1825 }
1826 }
1827
1828 return result;
1829}
1830
1832 auto result = ValidateRoom();
1833 std::vector<std::string> all_issues = result.errors;
1834 all_issues.insert(all_issues.end(), result.warnings.begin(),
1835 result.warnings.end());
1836 return all_issues;
1837}
1838
1843
1847
1852
1854 config_ = config;
1855}
1856
1858 rom_ = rom;
1859 // Reinitialize editor with new ROM
1861}
1862
1864 // Set the current room pointer to the external room
1865 current_room_ = room;
1866
1867 // Reset editing state for new room
1871
1872 // Clear selection as it's invalid for the new room
1874
1875 // Clear undo history as it applies to the previous room
1876 ClearHistory();
1877
1878 // Notify callbacks
1881 }
1882}
1883
1884// Factory function
1885std::unique_ptr<DungeonObjectEditor> CreateDungeonObjectEditor(Rom* rom) {
1886 return std::make_unique<DungeonObjectEditor>(rom);
1887}
1888
1889// Object Categories implementation
1890namespace ObjectCategories {
1891
1892std::vector<ObjectCategory> GetObjectCategories() {
1893 return {
1894 {"Walls", {0x10, 0x11, 0x12, 0x13}, "Basic wall objects"},
1895 {"Floors", {0x20, 0x21, 0x22, 0x23}, "Floor tile objects"},
1896 {"Decorations", {0x30, 0x31, 0x32, 0x33}, "Decorative objects"},
1897 {"Interactive", {0xF9, 0xFA, 0xFB}, "Interactive objects like chests"},
1898 {"Stairs", {0x13, 0x14, 0x15, 0x16}, "Staircase objects"},
1899 {"Doors", {0x17, 0x18, 0x19, 0x1A}, "Door objects"},
1900 {"Special", {0xF80, 0xF81, 0xF82, 0xF97}, "Special dungeon objects (Type 3)"}};
1901}
1902
1903absl::StatusOr<std::vector<int>> GetObjectsInCategory(
1904 const std::string& category_name) {
1905 auto categories = GetObjectCategories();
1906
1907 for (const auto& category : categories) {
1908 if (category.name == category_name) {
1909 return category.object_ids;
1910 }
1911 }
1912
1913 return absl::NotFoundError("Category not found");
1914}
1915
1916absl::StatusOr<std::string> GetObjectCategory(int object_id) {
1917 auto categories = GetObjectCategories();
1918
1919 for (const auto& category : categories) {
1920 for (int id : category.object_ids) {
1921 if (id == object_id) {
1922 return category.name;
1923 }
1924 }
1925 }
1926
1927 return absl::NotFoundError("Object category not found");
1928}
1929
1930absl::StatusOr<ObjectInfo> GetObjectInfo(int object_id) {
1931 ObjectInfo info;
1932 info.id = object_id;
1933
1934 // This is a simplified implementation - in practice, you'd have
1935 // a comprehensive database of object information
1936
1937 if (object_id >= 0x10 && object_id <= 0x1F) {
1938 info.name = "Wall";
1939 info.description = "Basic wall object";
1940 info.valid_sizes = {{0x12, 0x12}};
1941 info.valid_layers = {0, 1, 2};
1942 info.is_interactive = false;
1943 info.is_collidable = true;
1944 } else if (object_id >= 0x20 && object_id <= 0x2F) {
1945 info.name = "Floor";
1946 info.description = "Floor tile object";
1947 info.valid_sizes = {{0x12, 0x12}};
1948 info.valid_layers = {0, 1, 2};
1949 info.is_interactive = false;
1950 info.is_collidable = false;
1951 } else if (object_id == 0xF9) {
1952 info.name = "Small Chest";
1953 info.description = "Small treasure chest";
1954 info.valid_sizes = {{0x12, 0x12}};
1955 info.valid_layers = {0, 1};
1956 info.is_interactive = true;
1957 info.is_collidable = true;
1958 } else {
1959 info.name = "Unknown Object";
1960 info.description = "Unknown object type";
1961 info.valid_sizes = {{0x12, 0x12}};
1962 info.valid_layers = {0};
1963 info.is_interactive = false;
1964 info.is_collidable = true;
1965 }
1966
1967 return info;
1968}
1969
1970} // namespace ObjectCategories
1971
1972} // namespace zelda3
1973} // 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:724
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:1522
size_t GetTileObjectCount() const
Definition room.h:387
absl::Status SaveObjects()
Definition room.cc:1376
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:1509
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