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