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