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"
8#include "app/core/window.h"
9#include "app/gfx/arena.h"
11#include "imgui/imgui.h"
12
13namespace yaze {
14namespace zelda3 {
15
18
20 if (rom_ == nullptr) {
21 return absl::InvalidArgumentError("ROM is null");
22 }
23
24 // Set default configuration
25 config_.snap_to_grid = true;
26 config_.grid_size = 16;
27 config_.show_grid = true;
28 config_.show_preview = true;
29 config_.auto_save = false;
33
34 // Set default editing state
37 editing_state_.current_object_type = 0x10; // Default to wall
39
40 // Initialize empty room
41 current_room_ = std::make_unique<Room>(0, rom_);
42
43 return absl::OkStatus();
44}
45
46absl::Status DungeonObjectEditor::LoadRoom(int room_id) {
47 if (rom_ == nullptr) {
48 return absl::InvalidArgumentError("ROM is null");
49 }
50
51 if (room_id < 0 || room_id >= NumberOfRooms) {
52 return absl::InvalidArgumentError("Invalid room ID");
53 }
54
55 // Create undo point before loading
56 auto status = CreateUndoPoint();
57 if (!status.ok()) {
58 // Continue anyway, but log the issue
59 }
60
61 // Load room from ROM
62 current_room_ = std::make_unique<Room>(room_id, rom_);
63
64 // Clear selection
66
67 // Reset editing state
71
72 // Notify callbacks
75 }
76
77 return absl::OkStatus();
78}
79
81 if (current_room_ == nullptr) {
82 return absl::FailedPreconditionError("No room loaded");
83 }
84
85 // Validate room before saving
87 auto validation_status = ValidateRoom();
88 if (!validation_status.ok()) {
89 return validation_status;
90 }
91 }
92
93 // Save room objects back to ROM (Phase 1, Task 1.3)
94 return current_room_->SaveObjects();
95}
96
98 if (current_room_ == nullptr) {
99 return absl::FailedPreconditionError("No room loaded");
100 }
101
102 // Create undo point before clearing
103 auto status = CreateUndoPoint();
104 if (!status.ok()) {
105 return status;
106 }
107
108 // Clear all objects
109 current_room_->ClearTileObjects();
110
111 // Clear selection
113
114 // Notify callbacks
117 }
118
119 return absl::OkStatus();
120}
121
122absl::Status DungeonObjectEditor::InsertObject(int x, int y, int object_type, int size, int layer) {
123 if (current_room_ == nullptr) {
124 return absl::FailedPreconditionError("No room loaded");
125 }
126
127 // Validate parameters
128 if (object_type < 0 || object_type > 0x3FF) {
129 return absl::InvalidArgumentError("Invalid object type");
130 }
131
132 if (size < kMinObjectSize || size > kMaxObjectSize) {
133 return absl::InvalidArgumentError("Invalid object size");
134 }
135
136 if (layer < kMinLayer || layer > kMaxLayer) {
137 return absl::InvalidArgumentError("Invalid layer");
138 }
139
140 // Snap coordinates to grid if enabled
141 if (config_.snap_to_grid) {
142 x = SnapToGrid(x);
143 y = SnapToGrid(y);
144 }
145
146 // Create undo point
147 auto status = CreateUndoPoint();
148 if (!status.ok()) {
149 return status;
150 }
151
152 // Create new object
153 RoomObject new_object(object_type, x, y, size, layer);
154 new_object.set_rom(rom_);
155 new_object.EnsureTilesLoaded();
156
157 // Check for collisions if validation is enabled
159 for (const auto& existing_obj : current_room_->GetTileObjects()) {
160 if (ObjectsCollide(new_object, existing_obj)) {
161 return absl::FailedPreconditionError("Object placement would cause collision");
162 }
163 }
164 }
165
166 // Add object to room using new method (Phase 3)
167 auto add_status = current_room_->AddObject(new_object);
168 if (!add_status.ok()) {
169 return add_status;
170 }
171
172 // Select the new object
174 selection_state_.selected_objects.push_back(current_room_->GetTileObjectCount() - 1);
175
176 // Notify callbacks
178 object_changed_callback_(current_room_->GetTileObjectCount() - 1, new_object);
179 }
180
183 }
184
187 }
188
189 return absl::OkStatus();
190}
191
192absl::Status DungeonObjectEditor::DeleteObject(size_t object_index) {
193 if (current_room_ == nullptr) {
194 return absl::FailedPreconditionError("No room loaded");
195 }
196
197 if (object_index >= current_room_->GetTileObjectCount()) {
198 return absl::OutOfRangeError("Object index out of range");
199 }
200
201 // Create undo point
202 auto status = CreateUndoPoint();
203 if (!status.ok()) {
204 return status;
205 }
206
207 // Remove object from room using new method (Phase 3)
208 auto remove_status = current_room_->RemoveObject(object_index);
209 if (!remove_status.ok()) {
210 return remove_status;
211 }
212
213 // Update selection indices
214 for (auto& selected_index : selection_state_.selected_objects) {
215 if (selected_index > object_index) {
216 selected_index--;
217 } else if (selected_index == object_index) {
218 // Remove the deleted object from selection
220 std::remove(selection_state_.selected_objects.begin(),
221 selection_state_.selected_objects.end(), object_index),
223 }
224 }
225
226 // Notify callbacks
229 }
230
233 }
234
235 return absl::OkStatus();
236}
237
239 if (current_room_ == nullptr) {
240 return absl::FailedPreconditionError("No room loaded");
241 }
242
244 return absl::FailedPreconditionError("No objects selected");
245 }
246
247 // Create undo point
248 auto status = CreateUndoPoint();
249 if (!status.ok()) {
250 return status;
251 }
252
253 // Sort selected indices in descending order to avoid index shifting issues
254 std::vector<size_t> sorted_selection = selection_state_.selected_objects;
255 std::sort(sorted_selection.begin(), sorted_selection.end(), std::greater<size_t>());
256
257 // Delete objects in reverse order
258 for (size_t index : sorted_selection) {
259 if (index < current_room_->GetTileObjectCount()) {
260 current_room_->RemoveTileObject(index);
261 }
262 }
263
264 // Clear selection
266
267 // Notify callbacks
270 }
271
272 return absl::OkStatus();
273}
274
275absl::Status DungeonObjectEditor::MoveObject(size_t object_index, int new_x, int new_y) {
276 if (current_room_ == nullptr) {
277 return absl::FailedPreconditionError("No room loaded");
278 }
279
280 if (object_index >= current_room_->GetTileObjectCount()) {
281 return absl::OutOfRangeError("Object index out of range");
282 }
283
284 // Snap coordinates to grid if enabled
285 if (config_.snap_to_grid) {
286 new_x = SnapToGrid(new_x);
287 new_y = SnapToGrid(new_y);
288 }
289
290 // Create undo point
291 auto status = CreateUndoPoint();
292 if (!status.ok()) {
293 return status;
294 }
295
296 // Get the object
297 auto& object = current_room_->GetTileObject(object_index);
298
299 // Check for collisions if validation is enabled
301 RoomObject test_object = object;
302 test_object.set_x(new_x);
303 test_object.set_y(new_y);
304
305 for (size_t i = 0; i < current_room_->GetTileObjects().size(); i++) {
306 if (i != object_index && ObjectsCollide(test_object, current_room_->GetTileObjects()[i])) {
307 return absl::FailedPreconditionError("Object move would cause collision");
308 }
309 }
310 }
311
312 // Move the object
313 object.set_x(new_x);
314 object.set_y(new_y);
315
316 // Notify callbacks
318 object_changed_callback_(object_index, object);
319 }
320
323 }
324
325 return absl::OkStatus();
326}
327
328absl::Status DungeonObjectEditor::ResizeObject(size_t object_index, int new_size) {
329 if (current_room_ == nullptr) {
330 return absl::FailedPreconditionError("No room loaded");
331 }
332
333 if (object_index >= current_room_->GetTileObjectCount()) {
334 return absl::OutOfRangeError("Object index out of range");
335 }
336
337 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
338 return absl::InvalidArgumentError("Invalid object size");
339 }
340
341 // Create undo point
342 auto status = CreateUndoPoint();
343 if (!status.ok()) {
344 return status;
345 }
346
347 // Resize the object
348 auto& object = current_room_->GetTileObject(object_index);
349 object.set_size(new_size);
350
351 // Notify callbacks
353 object_changed_callback_(object_index, object);
354 }
355
358 }
359
360 return absl::OkStatus();
361}
362
363absl::Status DungeonObjectEditor::HandleScrollWheel(int delta, int x, int y, bool ctrl_pressed) {
364 if (current_room_ == nullptr) {
365 return absl::FailedPreconditionError("No room loaded");
366 }
367
368 // Convert screen coordinates to room coordinates
369 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
370
371 // Handle size editing with scroll wheel
374
375 return HandleSizeEdit(delta, room_x, room_y);
376 }
377
378 // Handle layer switching with Ctrl+scroll
379 if (ctrl_pressed) {
380 int layer_delta = delta > 0 ? 1 : -1;
381 int new_layer = editing_state_.current_layer + layer_delta;
382 new_layer = std::max(kMinLayer, std::min(kMaxLayer, new_layer));
383
384 if (new_layer != editing_state_.current_layer) {
385 SetCurrentLayer(new_layer);
386 }
387
388 return absl::OkStatus();
389 }
390
391 return absl::OkStatus();
392}
393
394absl::Status DungeonObjectEditor::HandleSizeEdit(int delta, int x, int y) {
395 // Handle size editing for preview object
397 int new_size = GetNextSize(editing_state_.preview_size, delta);
398 if (IsValidSize(new_size)) {
399 editing_state_.preview_size = new_size;
401 }
402 return absl::OkStatus();
403 }
404
405 // Handle size editing for selected objects
407 for (size_t object_index : selection_state_.selected_objects) {
408 if (object_index < current_room_->GetTileObjectCount()) {
409 auto& object = current_room_->GetTileObject(object_index);
410 int new_size = GetNextSize(object.size_, delta);
411 if (IsValidSize(new_size)) {
412 auto status = ResizeObject(object_index, new_size);
413 if (!status.ok()) {
414 return status;
415 }
416 }
417 }
418 }
419 return absl::OkStatus();
420 }
421
422 return absl::OkStatus();
423}
424
425int DungeonObjectEditor::GetNextSize(int current_size, int delta) {
426 // Define size increments based on object type
427 // This is a simplified implementation - in practice, you'd have
428 // different size rules for different object types
429
430 if (delta > 0) {
431 // Increase size
432 if (current_size < 0x40) {
433 return current_size + 0x10; // Large increments for small sizes
434 } else if (current_size < 0x80) {
435 return current_size + 0x08; // Medium increments
436 } else {
437 return current_size + 0x04; // Small increments for large sizes
438 }
439 } else {
440 // Decrease size
441 if (current_size > 0x80) {
442 return current_size - 0x04; // Small decrements for large sizes
443 } else if (current_size > 0x40) {
444 return current_size - 0x08; // Medium decrements
445 } else {
446 return current_size - 0x10; // Large decrements for small sizes
447 }
448 }
449}
450
452 return size >= kMinObjectSize && size <= kMaxObjectSize;
453}
454
455absl::Status DungeonObjectEditor::HandleMouseClick(int x, int y, bool left_button, bool right_button, bool shift_pressed) {
456 if (current_room_ == nullptr) {
457 return absl::FailedPreconditionError("No room loaded");
458 }
459
460 // Convert screen coordinates to room coordinates
461 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
462
463 if (left_button) {
465 case Mode::kSelect:
466 if (shift_pressed) {
467 // Add to selection
468 auto object_index = FindObjectAt(room_x, room_y);
469 if (object_index.has_value()) {
470 return AddToSelection(object_index.value());
471 }
472 } else {
473 // Select object
474 return SelectObject(x, y);
475 }
476 break;
477
478 case Mode::kInsert:
479 // Insert object at clicked position
480 return InsertObject(room_x, room_y, editing_state_.current_object_type,
482
483 case Mode::kDelete:
484 // Delete object at clicked position
485 {
486 auto object_index = FindObjectAt(room_x, room_y);
487 if (object_index.has_value()) {
488 return DeleteObject(object_index.value());
489 }
490 }
491 break;
492
493 case Mode::kEdit:
494 // Select object for editing
495 return SelectObject(x, y);
496
497 default:
498 break;
499 }
500 }
501
502 if (right_button) {
503 // Context menu or alternate action
505 case Mode::kSelect:
506 // Show context menu for object
507 {
508 auto object_index = FindObjectAt(room_x, room_y);
509 if (object_index.has_value()) {
510 // TODO: Show context menu
511 }
512 }
513 break;
514
515 default:
516 break;
517 }
518 }
519
520 return absl::OkStatus();
521}
522
523absl::Status DungeonObjectEditor::HandleMouseDrag(int start_x, int start_y, int current_x, int current_y) {
524 if (current_room_ == nullptr) {
525 return absl::FailedPreconditionError("No room loaded");
526 }
527
528 // Enable dragging if not already (Phase 4)
533
534 // Create undo point before drag
535 auto undo_status = CreateUndoPoint();
536 if (!undo_status.ok()) {
537 return undo_status;
538 }
539 }
540
541 // Handle the drag operation (Phase 4)
542 return HandleDragOperation(current_x, current_y);
543}
544
545absl::Status DungeonObjectEditor::HandleMouseRelease(int x, int y) {
546 if (current_room_ == nullptr) {
547 return absl::FailedPreconditionError("No room loaded");
548 }
549
550 // End dragging operation (Phase 4)
553
554 // Notify callbacks about the final positions
557 }
558 }
559
560 return absl::OkStatus();
561}
562
563absl::Status DungeonObjectEditor::SelectObject(int screen_x, int screen_y) {
564 if (current_room_ == nullptr) {
565 return absl::FailedPreconditionError("No room loaded");
566 }
567
568 // Convert screen coordinates to room coordinates
569 auto [room_x, room_y] = ScreenToRoomCoordinates(screen_x, screen_y);
570
571 // Find object at position
572 auto object_index = FindObjectAt(room_x, room_y);
573
574 if (object_index.has_value()) {
575 // Select the found object
577 selection_state_.selected_objects.push_back(object_index.value());
578
579 // Notify callbacks
582 }
583
584 return absl::OkStatus();
585 } else {
586 // Clear selection if no object found
587 return ClearSelection();
588 }
589}
590
595
596 // Notify callbacks
599 }
600
601 return absl::OkStatus();
602}
603
604absl::Status DungeonObjectEditor::AddToSelection(size_t object_index) {
605 if (current_room_ == nullptr) {
606 return absl::FailedPreconditionError("No room loaded");
607 }
608
609 if (object_index >= current_room_->GetTileObjectCount()) {
610 return absl::OutOfRangeError("Object index out of range");
611 }
612
613 // Check if already selected
614 auto it = std::find(selection_state_.selected_objects.begin(),
615 selection_state_.selected_objects.end(), object_index);
616
617 if (it == selection_state_.selected_objects.end()) {
618 selection_state_.selected_objects.push_back(object_index);
620
621 // Notify callbacks
624 }
625 }
626
627 return absl::OkStatus();
628}
629
632
633 // Update preview object based on mode
635}
636
638 if (layer >= kMinLayer && layer <= kMaxLayer) {
641 }
642}
643
645 if (object_type >= 0 && object_type <= 0x3FF) {
648 }
649}
650
651std::optional<size_t> DungeonObjectEditor::FindObjectAt(int room_x, int room_y) {
652 if (current_room_ == nullptr) {
653 return std::nullopt;
654 }
655
656 // Search from back to front (last objects are on top)
657 for (int i = static_cast<int>(current_room_->GetTileObjectCount()) - 1; i >= 0; i--) {
658 if (IsObjectAtPosition(current_room_->GetTileObject(i), room_x, room_y)) {
659 return static_cast<size_t>(i);
660 }
661 }
662
663 return std::nullopt;
664}
665
666bool DungeonObjectEditor::IsObjectAtPosition(const RoomObject& object, int x, int y) {
667 // Convert object position to pixel coordinates
668 int obj_x = object.x_ * 16;
669 int obj_y = object.y_ * 16;
670
671 // Check if point is within object bounds
672 // This is a simplified implementation - in practice, you'd check
673 // against the actual tile data
674
675 int obj_width = 16; // Default object width
676 int obj_height = 16; // Default object height
677
678 // Adjust size based on object size value
679 if (object.size_ > 0x80) {
680 obj_width *= 2;
681 obj_height *= 2;
682 }
683
684 return (x >= obj_x && x < obj_x + obj_width &&
685 y >= obj_y && y < obj_y + obj_height);
686}
687
689 // Simple bounding box collision detection
690 // In practice, you'd use the actual tile data for more accurate collision
691
692 int obj1_x = obj1.x_ * 16;
693 int obj1_y = obj1.y_ * 16;
694 int obj1_w = 16;
695 int obj1_h = 16;
696
697 int obj2_x = obj2.x_ * 16;
698 int obj2_y = obj2.y_ * 16;
699 int obj2_w = 16;
700 int obj2_h = 16;
701
702 // Adjust sizes based on object size values
703 if (obj1.size_ > 0x80) {
704 obj1_w *= 2;
705 obj1_h *= 2;
706 }
707
708 if (obj2.size_ > 0x80) {
709 obj2_w *= 2;
710 obj2_h *= 2;
711 }
712
713 return !(obj1_x + obj1_w <= obj2_x ||
714 obj2_x + obj2_w <= obj1_x ||
715 obj1_y + obj1_h <= obj2_y ||
716 obj2_y + obj2_h <= obj1_y);
717}
718
719std::pair<int, int> DungeonObjectEditor::ScreenToRoomCoordinates(int screen_x, int screen_y) {
720 // Convert screen coordinates to room tile coordinates
721 // This is a simplified implementation - in practice, you'd account for
722 // camera position, zoom level, etc.
723
724 int room_x = screen_x / 16; // 16 pixels per tile
725 int room_y = screen_y / 16;
726
727 return {room_x, room_y};
728}
729
730std::pair<int, int> DungeonObjectEditor::RoomToScreenCoordinates(int room_x, int room_y) {
731 // Convert room tile coordinates to screen coordinates
732 int screen_x = room_x * 16;
733 int screen_y = room_y * 16;
734
735 return {screen_x, screen_y};
736}
737
739 if (!config_.snap_to_grid) {
740 return coordinate;
741 }
742
743 return (coordinate / config_.grid_size) * config_.grid_size;
744}
745
760
762 if (current_room_ == nullptr) {
763 return absl::FailedPreconditionError("No room loaded");
764 }
765
766 // Create undo point
767 UndoPoint undo_point;
768 undo_point.objects = current_room_->GetTileObjects();
769 undo_point.selection = selection_state_;
770 undo_point.editing = editing_state_;
771 undo_point.timestamp = std::chrono::steady_clock::now();
772
773 // Add to undo history
774 undo_history_.push_back(undo_point);
775
776 // Limit undo history size
777 if (undo_history_.size() > kMaxUndoHistory) {
778 undo_history_.erase(undo_history_.begin());
779 }
780
781 // Clear redo history when new action is performed
782 redo_history_.clear();
783
784 return absl::OkStatus();
785}
786
788 if (!CanUndo()) {
789 return absl::FailedPreconditionError("Nothing to undo");
790 }
791
792 // Move current state to redo history
793 UndoPoint current_state;
794 current_state.objects = current_room_->GetTileObjects();
795 current_state.selection = selection_state_;
796 current_state.editing = editing_state_;
797 current_state.timestamp = std::chrono::steady_clock::now();
798
799 redo_history_.push_back(current_state);
800
801 // Apply undo point
802 UndoPoint undo_point = undo_history_.back();
803 undo_history_.pop_back();
804
805 return ApplyUndoPoint(undo_point);
806}
807
809 if (!CanRedo()) {
810 return absl::FailedPreconditionError("Nothing to redo");
811 }
812
813 // Move current state to undo history
814 UndoPoint current_state;
815 current_state.objects = current_room_->GetTileObjects();
816 current_state.selection = selection_state_;
817 current_state.editing = editing_state_;
818 current_state.timestamp = std::chrono::steady_clock::now();
819
820 undo_history_.push_back(current_state);
821
822 // Apply redo point
823 UndoPoint redo_point = redo_history_.back();
824 redo_history_.pop_back();
825
826 return ApplyUndoPoint(redo_point);
827}
828
829absl::Status DungeonObjectEditor::ApplyUndoPoint(const UndoPoint& undo_point) {
830 if (current_room_ == nullptr) {
831 return absl::FailedPreconditionError("No room loaded");
832 }
833
834 // Restore room state
835 current_room_->SetTileObjects(undo_point.objects);
836
837 // Restore editor state
838 selection_state_ = undo_point.selection;
839 editing_state_ = undo_point.editing;
840
841 // Update preview
843
844 // Notify callbacks
847 }
848
851 }
852
853 return absl::OkStatus();
854}
855
857 return !undo_history_.empty();
858}
859
861 return !redo_history_.empty();
862}
863
865 undo_history_.clear();
866 redo_history_.clear();
867}
868
869// ============================================================================
870// Phase 4: Visual Feedback and GUI Methods
871// ============================================================================
872
873// Helper for color blending
874static uint32_t BlendColors(uint32_t base, uint32_t tint) {
875 uint8_t a_tint = (tint >> 24) & 0xFF;
876 if (a_tint == 0) return base;
877
878 uint8_t r_base = (base >> 16) & 0xFF;
879 uint8_t g_base = (base >> 8) & 0xFF;
880 uint8_t b_base = base & 0xFF;
881
882 uint8_t r_tint = (tint >> 16) & 0xFF;
883 uint8_t g_tint = (tint >> 8) & 0xFF;
884 uint8_t b_tint = tint & 0xFF;
885
886 float alpha = a_tint / 255.0f;
887 uint8_t r = r_base * (1.0f - alpha) + r_tint * alpha;
888 uint8_t g = g_base * (1.0f - alpha) + g_tint * alpha;
889 uint8_t b = b_base * (1.0f - alpha) + b_tint * alpha;
890
891 return 0xFF000000 | (r << 16) | (g << 8) | b;
892}
893
896 return;
897 }
898
899 // Draw highlight rectangles around selected objects
900 for (size_t obj_idx : selection_state_.selected_objects) {
901 if (obj_idx >= current_room_->GetTileObjectCount()) continue;
902
903 const auto& obj = current_room_->GetTileObject(obj_idx);
904 int x = obj.x() * 16;
905 int y = obj.y() * 16;
906 int w = 16 + (obj.size() * 4); // Approximate width
907 int h = 16 + (obj.size() * 4); // Approximate height
908
909 // Draw yellow selection box (2px border) - using SetPixel
910 uint8_t r = (config_.selection_color >> 16) & 0xFF;
911 uint8_t g = (config_.selection_color >> 8) & 0xFF;
912 uint8_t b = config_.selection_color & 0xFF;
913 gfx::SnesColor sel_color(r, g, b);
914
915 for (int py = y; py < y + h; py++) {
916 for (int px = x; px < x + w; px++) {
917 if (px < canvas.width() && py < canvas.height() &&
918 (px < x + 2 || px >= x + w - 2 || py < y + 2 || py >= y + h - 2)) {
919 canvas.SetPixel(px, py, sel_color);
920 }
921 }
922 }
923 }
924}
925
928 return;
929 }
930
931 // Apply subtle color tints based on layer (simplified - just mark with colored border)
932 for (const auto& obj : current_room_->GetTileObjects()) {
933 int x = obj.x() * 16;
934 int y = obj.y() * 16;
935 int w = 16;
936 int h = 16;
937
938 uint32_t tint_color = 0xFF000000;
939 switch (obj.GetLayerValue()) {
940 case 0: tint_color = config_.layer0_color; break;
941 case 1: tint_color = config_.layer1_color; break;
942 case 2: tint_color = config_.layer2_color; break;
943 }
944
945 // Draw 1px border in layer color
946 uint8_t r = (tint_color >> 16) & 0xFF;
947 uint8_t g = (tint_color >> 8) & 0xFF;
948 uint8_t b = tint_color & 0xFF;
949 gfx::SnesColor layer_color(r, g, b);
950
951 for (int py = y; py < y + h && py < canvas.height(); py++) {
952 for (int px = x; px < x + w && px < canvas.width(); px++) {
953 if (px == x || px == x + w - 1 || py == y || py == y + h - 1) {
954 canvas.SetPixel(px, py, layer_color);
955 }
956 }
957 }
958 }
959}
960
963 return;
964 }
965
966 ImGui::Begin("Object Properties", &config_.show_property_panel);
967
968 if (selection_state_.selected_objects.size() == 1) {
969 size_t obj_idx = selection_state_.selected_objects[0];
970 if (obj_idx < current_room_->GetTileObjectCount()) {
971 auto& obj = current_room_->GetTileObject(obj_idx);
972
973 ImGui::Text("Object #%zu", obj_idx);
974 ImGui::Separator();
975
976 // ID (hex)
977 int id = obj.id_;
978 if (ImGui::InputInt("ID (0x)", &id, 1, 16, ImGuiInputTextFlags_CharsHexadecimal)) {
979 if (id >= 0 && id <= 0xFFF) {
980 obj.id_ = id;
982 object_changed_callback_(obj_idx, obj);
983 }
984 }
985 }
986
987 // Position
988 int x = obj.x();
989 int y = obj.y();
990 if (ImGui::InputInt("X Position", &x, 1, 4)) {
991 if (x >= 0 && x < 64) {
992 obj.set_x(x);
994 object_changed_callback_(obj_idx, obj);
995 }
996 }
997 }
998 if (ImGui::InputInt("Y Position", &y, 1, 4)) {
999 if (y >= 0 && y < 64) {
1000 obj.set_y(y);
1002 object_changed_callback_(obj_idx, obj);
1003 }
1004 }
1005 }
1006
1007 // Size (for Type 1 objects only)
1008 if (obj.id_ < 0x100) {
1009 int size = obj.size();
1010 if (ImGui::SliderInt("Size", &size, 0, 15)) {
1011 obj.set_size(size);
1013 object_changed_callback_(obj_idx, obj);
1014 }
1015 }
1016 }
1017
1018 // Layer
1019 int layer = obj.GetLayerValue();
1020 if (ImGui::Combo("Layer", &layer, "Layer 0\0Layer 1\0Layer 2\0")) {
1021 obj.layer_ = static_cast<RoomObject::LayerType>(layer);
1023 object_changed_callback_(obj_idx, obj);
1024 }
1025 }
1026
1027 ImGui::Separator();
1028
1029 // Action buttons
1030 if (ImGui::Button("Delete Object")) {
1031 auto status = DeleteObject(obj_idx);
1032 (void)status; // Ignore return value for now
1033 }
1034 ImGui::SameLine();
1035 if (ImGui::Button("Duplicate")) {
1036 RoomObject duplicate = obj;
1037 duplicate.set_x(obj.x() + 1);
1038 auto status = current_room_->AddObject(duplicate);
1039 (void)status; // Ignore return value for now
1040 }
1041 }
1042 } else {
1043 // Multiple objects selected
1044 ImGui::Text("%zu objects selected", selection_state_.selected_objects.size());
1045 ImGui::Separator();
1046
1047 if (ImGui::Button("Delete All Selected")) {
1048 auto status = DeleteSelectedObjects();
1049 (void)status; // Ignore return value for now
1050 }
1051
1052 if (ImGui::Button("Clear Selection")) {
1053 auto status = ClearSelection();
1054 (void)status; // Ignore return value for now
1055 }
1056 }
1057
1058 ImGui::End();
1059}
1060
1062 ImGui::Begin("Layer Controls");
1063
1064 // Current layer selection
1065 ImGui::Text("Current Layer:");
1066 ImGui::RadioButton("Layer 0", &editing_state_.current_layer, 0);
1067 ImGui::SameLine();
1068 ImGui::RadioButton("Layer 1", &editing_state_.current_layer, 1);
1069 ImGui::SameLine();
1070 ImGui::RadioButton("Layer 2", &editing_state_.current_layer, 2);
1071
1072 ImGui::Separator();
1073
1074 // Layer visibility toggles
1075 static bool layer_visible[3] = {true, true, true};
1076 ImGui::Text("Layer Visibility:");
1077 ImGui::Checkbox("Show Layer 0", &layer_visible[0]);
1078 ImGui::Checkbox("Show Layer 1", &layer_visible[1]);
1079 ImGui::Checkbox("Show Layer 2", &layer_visible[2]);
1080
1081 ImGui::Separator();
1082
1083 // Layer colors
1084 ImGui::Checkbox("Show Layer Colors", &config_.show_layer_colors);
1086 ImGui::ColorEdit4("Layer 0 Tint", (float*)&config_.layer0_color);
1087 ImGui::ColorEdit4("Layer 1 Tint", (float*)&config_.layer1_color);
1088 ImGui::ColorEdit4("Layer 2 Tint", (float*)&config_.layer2_color);
1089 }
1090
1091 ImGui::Separator();
1092
1093 // Object counts per layer
1094 if (current_room_) {
1095 int count0 = 0, count1 = 0, count2 = 0;
1096 for (const auto& obj : current_room_->GetTileObjects()) {
1097 switch (obj.GetLayerValue()) {
1098 case 0: count0++; break;
1099 case 1: count1++; break;
1100 case 2: count2++; break;
1101 }
1102 }
1103 ImGui::Text("Layer 0: %d objects", count0);
1104 ImGui::Text("Layer 1: %d objects", count1);
1105 ImGui::Text("Layer 2: %d objects", count2);
1106 }
1107
1108 ImGui::End();
1109}
1110
1111absl::Status DungeonObjectEditor::HandleDragOperation(int current_x, int current_y) {
1113 return absl::OkStatus();
1114 }
1115
1116 // Calculate delta from drag start
1117 int dx = current_x - selection_state_.drag_start_x;
1118 int dy = current_y - selection_state_.drag_start_y;
1119
1120 // Convert pixel delta to grid delta
1121 int grid_dx = dx / config_.grid_size;
1122 int grid_dy = dy / config_.grid_size;
1123
1124 if (grid_dx == 0 && grid_dy == 0) {
1125 return absl::OkStatus(); // No meaningful movement yet
1126 }
1127
1128 // Move all selected objects
1129 for (size_t obj_idx : selection_state_.selected_objects) {
1130 if (obj_idx >= current_room_->GetTileObjectCount()) continue;
1131
1132 auto& obj = current_room_->GetTileObject(obj_idx);
1133 int new_x = obj.x() + grid_dx;
1134 int new_y = obj.y() + grid_dy;
1135
1136 // Clamp to valid range
1137 new_x = std::max(0, std::min(63, new_x));
1138 new_y = std::max(0, std::min(63, new_y));
1139
1140 obj.set_x(new_x);
1141 obj.set_y(new_y);
1142
1144 object_changed_callback_(obj_idx, obj);
1145 }
1146 }
1147
1148 // Update drag start position
1149 selection_state_.drag_start_x = current_x;
1150 selection_state_.drag_start_y = current_y;
1151
1152 return absl::OkStatus();
1153}
1154
1156 if (current_room_ == nullptr) {
1157 return absl::FailedPreconditionError("No room loaded for validation");
1158 }
1159
1160 // Validate objects don't overlap if collision checking is enabled
1162 const auto& objects = current_room_->GetTileObjects();
1163 for (size_t i = 0; i < objects.size(); i++) {
1164 for (size_t j = i + 1; j < objects.size(); j++) {
1165 if (ObjectsCollide(objects[i], objects[j])) {
1166 return absl::FailedPreconditionError(
1167 absl::StrFormat("Objects at indices %d and %d collide", i, j));
1168 }
1169 }
1170 }
1171 }
1172
1173 return absl::OkStatus();
1174}
1175
1179
1183
1187
1189 config_ = config;
1190}
1191
1193 rom_ = rom;
1194 // Reinitialize editor with new ROM
1196}
1197
1198// Factory function
1199std::unique_ptr<DungeonObjectEditor> CreateDungeonObjectEditor(Rom* rom) {
1200 return std::make_unique<DungeonObjectEditor>(rom);
1201}
1202
1203// Object Categories implementation
1204namespace ObjectCategories {
1205
1206std::vector<ObjectCategory> GetObjectCategories() {
1207 return {
1208 {"Walls", {0x10, 0x11, 0x12, 0x13}, "Basic wall objects"},
1209 {"Floors", {0x20, 0x21, 0x22, 0x23}, "Floor tile objects"},
1210 {"Decorations", {0x30, 0x31, 0x32, 0x33}, "Decorative objects"},
1211 {"Interactive", {0xF9, 0xFA, 0xFB}, "Interactive objects like chests"},
1212 {"Stairs", {0x13, 0x14, 0x15, 0x16}, "Staircase objects"},
1213 {"Doors", {0x17, 0x18, 0x19, 0x1A}, "Door objects"},
1214 {"Special", {0x200, 0x201, 0x202, 0x203}, "Special dungeon objects"}
1215 };
1216}
1217
1218absl::StatusOr<std::vector<int>> GetObjectsInCategory(const std::string& category_name) {
1219 auto categories = GetObjectCategories();
1220
1221 for (const auto& category : categories) {
1222 if (category.name == category_name) {
1223 return category.object_ids;
1224 }
1225 }
1226
1227 return absl::NotFoundError("Category not found");
1228}
1229
1230absl::StatusOr<std::string> GetObjectCategory(int object_id) {
1231 auto categories = GetObjectCategories();
1232
1233 for (const auto& category : categories) {
1234 for (int id : category.object_ids) {
1235 if (id == object_id) {
1236 return category.name;
1237 }
1238 }
1239 }
1240
1241 return absl::NotFoundError("Object category not found");
1242}
1243
1244absl::StatusOr<ObjectInfo> GetObjectInfo(int object_id) {
1245 ObjectInfo info;
1246 info.id = object_id;
1247
1248 // This is a simplified implementation - in practice, you'd have
1249 // a comprehensive database of object information
1250
1251 if (object_id >= 0x10 && object_id <= 0x1F) {
1252 info.name = "Wall";
1253 info.description = "Basic wall object";
1254 info.valid_sizes = {{0x12, 0x12}};
1255 info.valid_layers = {0, 1, 2};
1256 info.is_interactive = false;
1257 info.is_collidable = true;
1258 } else if (object_id >= 0x20 && object_id <= 0x2F) {
1259 info.name = "Floor";
1260 info.description = "Floor tile object";
1261 info.valid_sizes = {{0x12, 0x12}};
1262 info.valid_layers = {0, 1, 2};
1263 info.is_interactive = false;
1264 info.is_collidable = false;
1265 } else if (object_id == 0xF9) {
1266 info.name = "Small Chest";
1267 info.description = "Small treasure chest";
1268 info.valid_sizes = {{0x12, 0x12}};
1269 info.valid_layers = {0, 1};
1270 info.is_interactive = true;
1271 info.is_collidable = true;
1272 } else {
1273 info.name = "Unknown Object";
1274 info.description = "Unknown object type";
1275 info.valid_sizes = {{0x12, 0x12}};
1276 info.valid_layers = {0};
1277 info.is_interactive = false;
1278 info.is_collidable = true;
1279 }
1280
1281 return info;
1282}
1283
1284} // namespace ObjectCategories
1285
1286} // namespace zelda3
1287} // namespace yaze
The Rom class is used to load, save, and modify Rom data.
Definition rom.h:71
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:66
int height() const
Definition bitmap.h:254
void SetPixel(int x, int y, const SnesColor &color)
Set a pixel at the given x,y coordinates with SNES color.
Definition bitmap.cc:519
int width() const
Definition bitmap.h:253
SNES Color container.
Definition snes_color.h:38
std::function< void(size_t object_index, const RoomObject &object)> ObjectChangedCallback
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)
absl::Status DeleteObject(size_t object_index)
void SetSelectionChangedCallback(SelectionChangedCallback callback)
absl::Status SelectObject(int screen_x, int screen_y)
std::optional< size_t > FindObjectAt(int room_x, int room_y)
absl::Status AddToSelection(size_t object_index)
void RenderLayerVisualization(gfx::Bitmap &canvas)
void SetConfig(const EditorConfig &config)
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 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_
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)
void set_x(uint8_t x)
Definition room_object.h:72
void set_rom(Rom *rom)
Definition room_object.h:67
void set_y(uint8_t y)
Definition room_object.h:73
std::vector< ObjectCategory > GetObjectCategories()
Get all available object categories.
absl::StatusOr< ObjectInfo > GetObjectInfo(int object_id)
absl::StatusOr< std::string > GetObjectCategory(int object_id)
Get category for a specific object.
absl::StatusOr< std::vector< int > > GetObjectsInCategory(const std::string &category_name)
Get objects in a specific category.
constexpr int NumberOfRooms
Definition room.h:60
std::unique_ptr< DungeonObjectEditor > CreateDungeonObjectEditor(Rom *rom)
Factory function to create dungeon object editor.
Main namespace for the application.
std::chrono::steady_clock::time_point timestamp
std::vector< std::pair< int, int > > valid_sizes