yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_selector.cc
Go to the documentation of this file.
1// Related header
3#include "absl/strings/str_format.h"
4
5// C system headers
6#include <cstring>
7
8// C++ standard library headers
9#include <algorithm>
10#include <iterator>
11
12// Third-party library headers
13#include "imgui/imgui.h"
14
15// Project headers
18#include "app/platform/window.h"
19#include "core/features.h"
20#include "rom/rom.h"
21#include "zelda3/dungeon/custom_object.h" // For CustomObjectManager
27#include "zelda3/dungeon/room.h"
28#include "zelda3/dungeon/room_object.h" // For GetObjectName()
29
30namespace yaze::editor {
31
32using ImGui::BeginChild;
33using ImGui::EndChild;
34using ImGui::EndTabBar;
35using ImGui::EndTabItem;
36using ImGui::Separator;
37
40 if (ImGui::BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) {
41 if (ImGui::BeginTabItem("Room Graphics")) {
42 if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)3);
43 BeginChild(child_id, ImGui::GetContentRegionAvail(), true,
44 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
46 }
47 EndChild();
48 EndTabItem();
49 }
50
51 if (ImGui::BeginTabItem("Object Renderer")) {
53 EndTabItem();
54 }
55 EndTabBar();
56 }
57}
58
61 // Use AssetBrowser for better object selection
62 if (ImGui::BeginTable(
63 "DungeonObjectEditorTable", 2,
64 ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
65 ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter |
66 ImGuiTableFlags_BordersV,
67 ImVec2(0, 0))) {
68 ImGui::TableSetupColumn("Object Browser", ImGuiTableColumnFlags_WidthFixed,
69 400);
70 ImGui::TableSetupColumn("Preview Canvas",
71 ImGuiTableColumnFlags_WidthStretch);
72 ImGui::TableHeadersRow();
73
74 // Left column: AssetBrowser for object selection
75 ImGui::TableNextColumn();
76 ImGui::BeginChild("AssetBrowser", ImVec2(0, 0), true,
77 ImGuiWindowFlags_AlwaysVerticalScrollbar);
78
80
81 ImGui::EndChild();
82
83 // Right column: Preview and placement controls
84 ImGui::TableNextColumn();
85 ImGui::BeginChild("PreviewCanvas", ImVec2(0, 0), true);
86
87 // Object placement controls
88 ImGui::SeparatorText("Object Placement");
89 ImGui::InputInt("X Position", &place_x_);
90 ImGui::InputInt("Y Position", &place_y_);
91
92 if (ImGui::Button("Place Object") && object_loaded_) {
94 }
95
96 ImGui::Separator();
97
98 // Preview canvas
99 gui::CanvasFrameOptions frame_opts;
100 frame_opts.canvas_size = ImVec2(256 + 1, 0x10 * 0x40 + 1);
101 frame_opts.draw_grid = true;
102 frame_opts.grid_step = 32.0f;
103 frame_opts.render_popups = true;
104 gui::CanvasFrame frame(object_canvas_, frame_opts);
105
106 // Render selected object preview with primitive fallback
107 if (object_loaded_ && preview_object_.id_ >= 0) {
108 int preview_x = 128 - 16; // Center horizontally
109 int preview_y = 128 - 16; // Center vertically
110
111 // TODO: Implement preview using ObjectDrawer + small BackgroundBuffer
112 // For now, use primitive shape rendering (shows object ID and rough
113 // dimensions)
114 RenderObjectPrimitive(preview_object_, preview_x, preview_y);
115 }
116
117 ImGui::EndChild();
118 ImGui::EndTable();
119 }
120
121 // Object details window
122 if (object_loaded_) {
123 ImGui::Begin("Object Details", &object_loaded_, 0);
124 ImGui::Text("Object ID: 0x%02X", preview_object_.id_);
125 ImGui::Text("Position: (%d, %d)", preview_object_.x_, preview_object_.y_);
126 ImGui::Text("Size: 0x%02X", preview_object_.size_);
127 ImGui::Text("Layer: %d", static_cast<int>(preview_object_.layer_));
128
129 // Add object placement controls
130 ImGui::Separator();
131 ImGui::Text("Placement Controls:");
132 ImGui::InputInt("X Position", &place_x_);
133 ImGui::InputInt("Y Position", &place_y_);
134
135 if (ImGui::Button("Place Object")) {
137 }
138
139 ImGui::End();
140 }
141}
142
144 if (ImGui::BeginTabBar("##ObjectSelectorTabBar")) {
145 // Object Selector tab - for placing objects with new AssetBrowser
146 if (ImGui::BeginTabItem("Object Selector")) {
148 ImGui::EndTabItem();
149 }
150
151 // Room Graphics tab - 8 bitmaps viewer
152 if (ImGui::BeginTabItem("Room Graphics")) {
154 ImGui::EndTabItem();
155 }
156
157 // Object Editor tab - experimental editor
158 if (ImGui::BeginTabItem("Object Editor")) {
160 ImGui::EndTabItem();
161 }
162
163 ImGui::EndTabBar();
164 }
165}
166
168 const auto height = 0x40;
169 gui::CanvasFrameOptions frame_opts;
170 frame_opts.draw_grid = true;
171 frame_opts.grid_step = 32.0f;
172 frame_opts.render_popups = true;
173 gui::CanvasFrame frame(room_gfx_canvas_, frame_opts);
175
176 if (rom_ && rom_->is_loaded() && rooms_) {
177 int active_room_id = current_room_id_;
178 auto& room = (*rooms_)[active_room_id];
179 auto blocks = room.blocks();
180
181 // Load graphics for this room if not already loaded
182 if (blocks.empty()) {
183 room.LoadRoomGraphics(room.blockset);
184 blocks = room.blocks();
185 }
186
187 int current_block = 0;
188 const int max_blocks_per_row = 2; // 2 blocks per row for 300px column
189 const int block_width = 128; // Reduced size to fit column
190 const int block_height = 32; // Reduced height
191
192 for (int block : blocks) {
193 if (current_block >= 16)
194 break; // Only show first 16 blocks
195
196 // Ensure the graphics sheet is loaded and has a valid texture
197 if (block < gfx::Arena::Get().gfx_sheets().size()) {
198 auto& gfx_sheet = gfx::Arena::Get().gfx_sheets()[block];
199
200 // Calculate position in a grid layout instead of horizontal
201 // concatenation
202 int row = current_block / max_blocks_per_row;
203 int col = current_block % max_blocks_per_row;
204
205 ImVec2 local_pos(2 + (col * block_width), 2 + (row * block_height));
206
207 // Ensure we don't exceed canvas bounds
208 if (local_pos.x + block_width <= room_gfx_canvas_.width() &&
209 local_pos.y + block_height <= room_gfx_canvas_.height()) {
210 if (gfx_sheet.texture() != 0) {
212 (ImTextureID)(intptr_t)gfx_sheet.texture(), local_pos,
213 ImVec2(block_width, block_height));
214 }
215 }
216 }
217 current_block += 1;
218 }
219 }
220}
221
224 ImGui::Text("Editor systems not initialized");
225 return;
226 }
227
228 // Create a tabbed interface for different editing modes
229 if (ImGui::BeginTabBar("##EditingPanels")) {
230 // Object Editor Tab
231 if (ImGui::BeginTabItem("Objects")) {
233 ImGui::EndTabItem();
234 }
235
236 // Sprite Editor Tab
237 if (ImGui::BeginTabItem("Sprites")) {
239 ImGui::EndTabItem();
240 }
241
242 // Item Editor Tab
243 if (ImGui::BeginTabItem("Items")) {
245 ImGui::EndTabItem();
246 }
247
248 // Entrance Editor Tab
249 if (ImGui::BeginTabItem("Entrances")) {
251 ImGui::EndTabItem();
252 }
253
254 // Door Editor Tab
255 if (ImGui::BeginTabItem("Doors")) {
257 ImGui::EndTabItem();
258 }
259
260 // Chest Editor Tab
261 if (ImGui::BeginTabItem("Chests")) {
263 ImGui::EndTabItem();
264 }
265
266 // Properties Tab
267 if (ImGui::BeginTabItem("Properties")) {
269 ImGui::EndTabItem();
270 }
271
272 // Minecart Editor Tab
273 ImGui::EndTabBar();
274 }
275}
276
278 if (!object_editor_) {
279 ImGui::Text("Object editor not initialized");
280 return;
281 }
282
283 auto& editor = *object_editor_;
284
285 ImGui::Text("Object Editor");
286 Separator();
287
288 // Display current editing mode
289 auto mode = editor.GetMode();
290 const char* mode_names[] = {"Select", "Insert", "Delete",
291 "Edit", "Layer", "Preview"};
292 ImGui::Text("Mode: %s", mode_names[static_cast<int>(mode)]);
293
294 // Compact mode selection
295 if (ImGui::Button("Select"))
297 ImGui::SameLine();
298 if (ImGui::Button("Insert"))
300 ImGui::SameLine();
301 if (ImGui::Button("Edit"))
303
304 // Layer and object type selection
305 int current_layer = editor.GetCurrentLayer();
306 if (ImGui::SliderInt("Layer", &current_layer, 0, 2)) {
307 editor.SetCurrentLayer(current_layer);
308 }
309
310 int current_object_type = editor.GetCurrentObjectType();
311 if (ImGui::InputInt("Object Type", &current_object_type, 1, 16)) {
312 if (current_object_type >= 0 && current_object_type <= 0x3FF) {
313 editor.SetCurrentObjectType(current_object_type);
314 }
315 }
316
317 // Quick configuration checkboxes
318 auto config = editor.GetConfig();
319 if (ImGui::Checkbox("Snap to Grid", &config.snap_to_grid)) {
320 editor.SetConfig(config);
321 }
322 ImGui::SameLine();
323 if (ImGui::Checkbox("Show Grid", &config.show_grid)) {
324 editor.SetConfig(config);
325 }
326
327 // Object count and selection info
328 Separator();
329 ImGui::Text("Objects: %zu", editor.GetObjectCount());
330
331 auto selection = editor.GetSelection();
332 if (!selection.selected_objects.empty()) {
333 ImGui::Text("Selected: %zu", selection.selected_objects.size());
334 }
335
336 // Undo/Redo buttons
337 Separator();
338 if (ImGui::Button("Undo") && editor.CanUndo()) {
339 (void)editor.Undo();
340 }
341 ImGui::SameLine();
342 if (ImGui::Button("Redo") && editor.CanRedo()) {
343 (void)editor.Redo();
344 }
345}
346
348 const auto& theme = AgentUI::GetTheme();
349
350 // Type 3 objects (0xF80-0xFFF) - Special room features
351 if (object_id >= 0xF80) {
352 if (object_id >= 0xF80 && object_id <= 0xF8F) {
353 return IM_COL32(100, 200, 255, 255); // Light blue for layer indicators
354 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
355 return IM_COL32(255, 200, 100, 255); // Orange for door indicators
356 } else {
357 return IM_COL32(200, 150, 255, 255); // Purple for misc Type 3
358 }
359 }
360
361 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
362 if (object_id >= 0x100 && object_id < 0x200) {
363 if (object_id >= 0x100 && object_id <= 0x10F) {
364 return IM_COL32(255, 150, 50, 255); // Orange for torches
365 } else if (object_id >= 0x110 && object_id <= 0x11F) {
366 return IM_COL32(150, 150, 200, 255); // Blue-gray for blocks
367 } else if (object_id >= 0x120 && object_id <= 0x12F) {
368 return IM_COL32(100, 200, 100, 255); // Green for switches
369 } else if (object_id >= 0x130 && object_id <= 0x13F) {
370 return ImGui::GetColorU32(
371 theme.dungeon_selection_primary); // Yellow for stairs
372 } else {
373 return IM_COL32(180, 180, 180, 255); // Gray for other Type 2
374 }
375 }
376
377 // Type 1 objects (0x00-0xFF) - Base room objects
378 if (object_id >= 0x10 && object_id <= 0x1F) {
379 return ImGui::GetColorU32(theme.dungeon_object_wall); // Gray for walls
380 } else if (object_id >= 0x20 && object_id <= 0x2F) {
381 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for floors
382 } else if (object_id == 0xF9 || object_id == 0xFA) {
383 return ImGui::GetColorU32(theme.dungeon_object_chest); // Gold for chests
384 } else if (object_id >= 0x17 && object_id <= 0x1E) {
385 return ImGui::GetColorU32(theme.dungeon_object_floor); // Brown for doors
386 } else if (object_id == 0x2F || object_id == 0x2B) {
387 return ImGui::GetColorU32(
388 theme.dungeon_object_pot); // Saddle brown for pots
389 } else if (object_id >= 0x30 && object_id <= 0x3F) {
390 return ImGui::GetColorU32(
391 theme.dungeon_object_decoration); // Dim gray for decorations
392 } else if (object_id >= 0x00 && object_id <= 0x0F) {
393 return IM_COL32(120, 120, 180, 255); // Blue-gray for corners
394 } else {
395 return ImGui::GetColorU32(theme.dungeon_object_default); // Default gray
396 }
397}
398
400 // Type 3 objects (0xF80-0xFFF) - Special room features
401 if (object_id >= 0xF80) {
402 if (object_id >= 0xF80 && object_id <= 0xF8F) {
403 return "L"; // Layer
404 } else if (object_id >= 0xF90 && object_id <= 0xF9F) {
405 return "D"; // Door indicator
406 } else {
407 return "S"; // Special
408 }
409 }
410
411 // Type 2 objects (0x100-0x141) - Torches, blocks, switches
412 if (object_id >= 0x100 && object_id < 0x200) {
413 if (object_id >= 0x100 && object_id <= 0x10F) {
414 return "*"; // Torch (flame)
415 } else if (object_id >= 0x110 && object_id <= 0x11F) {
416 return "#"; // Block
417 } else if (object_id >= 0x120 && object_id <= 0x12F) {
418 return "o"; // Switch
419 } else if (object_id >= 0x130 && object_id <= 0x13F) {
420 return "^"; // Stairs
421 } else {
422 return "2"; // Type 2
423 }
424 }
425
426 // Type 1 objects (0x00-0xFF) - Base room objects
427 if (object_id >= 0x10 && object_id <= 0x1F) {
428 return "|"; // Wall
429 } else if (object_id >= 0x20 && object_id <= 0x2F) {
430 return "_"; // Floor
431 } else if (object_id == 0xF9 || object_id == 0xFA) {
432 return "C"; // Chest
433 } else if (object_id >= 0x17 && object_id <= 0x1E) {
434 return "+"; // Door
435 } else if (object_id == 0x2F || object_id == 0x2B) {
436 return "o"; // Pot
437 } else if (object_id >= 0x30 && object_id <= 0x3F) {
438 return "~"; // Decoration
439 } else if (object_id >= 0x00 && object_id <= 0x0F) {
440 return "/"; // Corner
441 } else {
442 return "?"; // Unknown
443 }
444}
445
447 const zelda3::RoomObject& object, int x, int y) {
448 const auto& theme = AgentUI::GetTheme();
449 // Render object as primitive shape on canvas
450 ImU32 color = GetObjectTypeColor(object.id_);
451
452 // Calculate object size with proper wall length handling
453 int obj_width, obj_height;
454 CalculateObjectDimensions(object, obj_width, obj_height);
455
456 // Draw object rectangle
457 ImVec4 color_vec = ImGui::ColorConvertU32ToFloat4(color);
458 object_canvas_.DrawRect(x, y, obj_width, obj_height, color_vec);
459 object_canvas_.DrawRect(x, y, obj_width, obj_height, theme.panel_bg_darker);
460
461 // Draw object ID as text
462 std::string obj_text = absl::StrFormat("0x%X", object.id_);
463 object_canvas_.DrawText(obj_text, x + obj_width + 2, y + 4);
464}
465
467 selected_object_id_ = obj_id;
468
469 // Create and update preview object
470 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, 0x12, 0);
472 if (game_data_) {
473 auto palette =
475 preview_palette_ = palette;
476 }
477 object_loaded_ = true;
478
479 // Notify callback
482 }
483}
484
486 const auto& theme = AgentUI::GetTheme();
487
488 // Object ranges: Type 1 (0x00-0xFF), Type 2 (0x100-0x141), Type 3 (0xF80-0xFFF)
489 struct ObjectRange {
490 int start;
491 int end;
492 const char* label;
493 ImU32 header_color;
494 };
495 static const ObjectRange ranges[] = {
496 {0x00, 0xFF, "Type 1", IM_COL32(80, 120, 180, 255)},
497 {0x100, 0x141, "Type 2", IM_COL32(120, 80, 180, 255)},
498 {0xF80, 0xFFF, "Type 3", IM_COL32(180, 120, 80, 255)},
499 };
500
501 // Total object count
502 int total_objects =
503 (0xFF - 0x00 + 1) + (0x141 - 0x100 + 1) + (0xFFF - 0xF80 + 1);
504
505 // Preview toggle (disabled by default for performance)
506 ImGui::Checkbox(ICON_MD_IMAGE " Previews", &enable_object_previews_);
507 if (ImGui::IsItemHovered()) {
508 ImGui::SetTooltip(
509 "Enable to show actual object graphics.\n"
510 "Requires a room to be loaded.\n"
511 "May impact performance.");
512 }
513 ImGui::SameLine();
514 ImGui::TextDisabled("(%d objects)", total_objects);
515
516 // Create asset browser-style grid
517 const float item_size = 72.0f;
518 const float item_spacing = 6.0f;
519 const int columns = std::max(
520 1, static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
521 (item_size + item_spacing)));
522
523 // Scrollable child region for grid - use all available space
524 float child_height = ImGui::GetContentRegionAvail().y;
525 if (ImGui::BeginChild("##ObjectGrid", ImVec2(0, child_height), false,
526 ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
527
528 // Iterate through all object ranges
529 for (const auto& range : ranges) {
530 // Section header for each type
531 ImGui::PushStyleColor(ImGuiCol_Header, range.header_color);
532 ImGui::PushStyleColor(
533 ImGuiCol_HeaderHovered,
534 IM_COL32((range.header_color & 0xFF) + 30,
535 ((range.header_color >> 8) & 0xFF) + 30,
536 ((range.header_color >> 16) & 0xFF) + 30, 255));
537 bool section_open = ImGui::CollapsingHeader(
538 absl::StrFormat("%s (0x%03X-0x%03X)", range.label, range.start,
539 range.end)
540 .c_str(),
541 ImGuiTreeNodeFlags_DefaultOpen);
542 ImGui::PopStyleColor(2);
543
544 if (!section_open)
545 continue;
546
547 int current_column = 0;
548
549 for (int obj_id = range.start; obj_id <= range.end; ++obj_id) {
550 if (current_column > 0) {
551 ImGui::SameLine();
552 }
553
554 ImGui::PushID(obj_id);
555
556 // Create selectable button for object
557 bool is_selected = (selected_object_id_ == obj_id);
558 ImVec2 button_size(item_size, item_size);
559
560 if (ImGui::Selectable("", is_selected,
561 ImGuiSelectableFlags_AllowDoubleClick,
562 button_size)) {
563 selected_object_id_ = obj_id;
564
565 // Create and update preview object
566 preview_object_ = zelda3::RoomObject(obj_id, 0, 0, 0x12, 0);
568 if (game_data_ &&
571 auto palette = game_data_->palette_groups
573 preview_palette_ = palette;
574 }
575 object_loaded_ = true;
576
577 // Notify callbacks
580 }
581
582 // Handle double-click to open static object editor
583 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
586 }
587 }
588 }
589
590 // Draw object preview on the button; fall back to styled placeholder
591 ImVec2 button_pos = ImGui::GetItemRectMin();
592 ImDrawList* draw_list = ImGui::GetWindowDrawList();
593
594 // Only attempt graphical preview if enabled (performance optimization)
595 bool rendered = false;
597 rendered = DrawObjectPreview(MakePreviewObject(obj_id), button_pos,
598 item_size);
599 }
600
601 if (!rendered) {
602 // Draw a styled fallback with gradient background
603 ImU32 obj_color = GetObjectTypeColor(obj_id);
604 ImU32 darker_color = IM_COL32((obj_color & 0xFF) * 0.6f,
605 ((obj_color >> 8) & 0xFF) * 0.6f,
606 ((obj_color >> 16) & 0xFF) * 0.6f, 255);
607
608 // Gradient background
609 draw_list->AddRectFilledMultiColor(
610 button_pos,
611 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
612 darker_color, darker_color, obj_color, obj_color);
613
614 // Draw object type symbol in center
615 std::string symbol = GetObjectTypeSymbol(obj_id);
616 ImVec2 symbol_size = ImGui::CalcTextSize(symbol.c_str());
617 ImVec2 symbol_pos(
618 button_pos.x + (item_size - symbol_size.x) / 2,
619 button_pos.y + (item_size - symbol_size.y) / 2 - 10);
620 draw_list->AddText(symbol_pos, IM_COL32(255, 255, 255, 180),
621 symbol.c_str());
622 }
623
624 // Draw border with special highlight for static editor object
625 bool is_static_editor_obj = (obj_id == static_editor_object_id_);
626 ImU32 border_color;
627 float border_thickness;
628
629 if (is_static_editor_obj) {
630 border_color = IM_COL32(0, 200, 255, 255);
631 border_thickness = 3.0f;
632 } else if (is_selected) {
633 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
634 border_thickness = 3.0f;
635 } else {
636 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
637 border_thickness = 1.0f;
638 }
639
640 draw_list->AddRect(
641 button_pos,
642 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
643 border_color, 0.0f, 0, border_thickness);
644
645 // Static editor indicator icon
646 if (is_static_editor_obj) {
647 ImVec2 icon_pos(button_pos.x + item_size - 14, button_pos.y + 2);
648 draw_list->AddCircleFilled(ImVec2(icon_pos.x + 6, icon_pos.y + 6), 6,
649 IM_COL32(0, 200, 255, 200));
650 draw_list->AddText(icon_pos, IM_COL32(255, 255, 255, 255), "i");
651 }
652
653 // Get object name for display
654 std::string full_name = zelda3::GetObjectName(obj_id);
655
656 // Truncate name for display
657 std::string display_name = full_name;
658 const size_t kMaxDisplayChars = 12;
659 if (display_name.length() > kMaxDisplayChars) {
660 display_name = display_name.substr(0, kMaxDisplayChars - 2) + "..";
661 }
662
663 // Draw object name (smaller, above ID)
664 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
665 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
666 button_pos.y + item_size - 26);
667 draw_list->AddText(name_pos,
668 ImGui::GetColorU32(theme.text_secondary_gray),
669 display_name.c_str());
670
671 // Draw object ID at bottom (hex format)
672 std::string id_text = absl::StrFormat("%03X", obj_id);
673 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
674 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
675 button_pos.y + item_size - id_size.y - 2);
676 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
677 id_text.c_str());
678
679 // Enhanced tooltip
680 if (ImGui::IsItemHovered()) {
681 ImGui::BeginTooltip();
682 ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "Object 0x%03X",
683 obj_id);
684 ImGui::Text("%s", full_name.c_str());
685 int subtype = zelda3::GetObjectSubtype(obj_id);
686 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Subtype %d",
687 subtype);
688 ImGui::Separator();
689 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
690 "Click to select for placement");
691 ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f),
692 "Double-click to view details");
693 ImGui::EndTooltip();
694 }
695
696 ImGui::PopID();
697
698 current_column = (current_column + 1) % columns;
699 } // end object loop
700 } // end range loop
701
702 // Custom Objects Section
703 ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(100, 180, 120, 255));
704 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(130, 210, 150, 255));
705 bool custom_open = ImGui::CollapsingHeader("Custom Objects",
706 ImGuiTreeNodeFlags_DefaultOpen);
707 ImGui::PopStyleColor(2);
708
709 if (custom_open) {
710 int custom_col = 0;
711 auto& obj_manager = zelda3::CustomObjectManager::Get();
712
713 // Initialize if needed (hacky lazy init if drawer hasn't done it yet)
714 // Ideally should be initialized by system.
715 // We'll skip init here and assume ObjectDrawer did it or will do it.
716 // But we need counts. If uninitialized, counts might be wrong?
717 // GetSubtypeCount checks static lists, so it's safe even if not fully init with paths.
718
719 for (int obj_id : {0x31, 0x32}) {
720 int subtype_count = obj_manager.GetSubtypeCount(obj_id);
721 for (int subtype = 0; subtype < subtype_count; ++subtype) {
722 if (custom_col > 0)
723 ImGui::SameLine();
724
725 ImGui::PushID(obj_id * 1000 + subtype);
726
727 bool is_selected = (selected_object_id_ == obj_id &&
728 (preview_object_.size_ & 0x1F) == subtype);
729 ImVec2 button_size(item_size, item_size);
730
731 if (ImGui::Selectable("", is_selected,
732 ImGuiSelectableFlags_AllowDoubleClick,
733 button_size)) {
734 SelectObject(obj_id);
735 // Update size to subtype
736 preview_object_.size_ = subtype;
737
738 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
741 }
742 }
743
744 // Draw Preview
745 ImVec2 button_pos = ImGui::GetItemRectMin();
746 ImDrawList* draw_list = ImGui::GetWindowDrawList();
747
748 bool rendered = false;
749 // Native preview requires loaded ROM and correct pathing, might fail if not init.
750 // But we can try constructing a temp object with correct subtype.
752 auto temp_obj = MakePreviewObject(obj_id);
753 temp_obj.size_ = subtype;
754 rendered = DrawObjectPreview(temp_obj, button_pos, item_size);
755 }
756
757 if (!rendered) {
758 // Fallback visuals
759 ImU32 obj_color = IM_COL32(100, 180, 120, 255);
760 ImU32 darker_color = IM_COL32(60, 100, 70, 255);
761
762 draw_list->AddRectFilledMultiColor(
763 button_pos,
764 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
765 darker_color, darker_color, obj_color, obj_color);
766
767 std::string symbol = (obj_id == 0x31) ? "Trk" : "Cus";
768 // Subtype
769 std::string sub_text = absl::StrFormat("%02X", subtype);
770 ImVec2 sub_size = ImGui::CalcTextSize(sub_text.c_str());
771 ImVec2 sub_pos(button_pos.x + (item_size - sub_size.x) / 2,
772 button_pos.y + (item_size - sub_size.y) / 2);
773 draw_list->AddText(sub_pos, IM_COL32(255, 255, 255, 220),
774 sub_text.c_str());
775 }
776
777 // Border
778 bool is_static_editor_obj = (obj_id == static_editor_object_id_ &&
780 // Static editor doesn't track subtype currently, so highlighting all subtypes of 0x31 is correct
781 // if we are editing 0x31 generic. But maybe we only edit specific subtype?
782 // Static editor usually edits the code/logic common to ID.
783 ImU32 border_color =
784 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
785 : ImGui::GetColorU32(theme.panel_bg_darker);
786 float border_thickness = is_selected ? 3.0f : 1.0f;
787 draw_list->AddRect(
788 button_pos,
789 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
790 border_color, 0.0f, 0, border_thickness);
791
792 // Name/ID
793 std::string id_text = absl::StrFormat("%02X:%02X", obj_id, subtype);
794 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
795 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
796 button_pos.y + item_size - id_size.y - 2);
797 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
798 id_text.c_str());
799
800 ImGui::PopID();
801 custom_col = (custom_col + 1) % columns;
802 }
803 }
804 }
805 }
806
807 ImGui::EndChild();
808}
809
810bool DungeonObjectSelector::MatchesObjectFilter(int obj_id, int filter_type) {
811 switch (filter_type) {
812 case 1: // Walls
813 return obj_id >= 0x10 && obj_id <= 0x1F;
814 case 2: // Floors
815 return obj_id >= 0x20 && obj_id <= 0x2F;
816 case 3: // Chests
817 return obj_id == 0xF9 || obj_id == 0xFA;
818 case 4: // Doors
819 return obj_id >= 0x17 && obj_id <= 0x1E;
820 case 5: // Decorations
821 return obj_id >= 0x30 && obj_id <= 0x3F;
822 case 6: // Stairs
823 return obj_id >= 0x138 && obj_id <= 0x13B;
824 default: // All
825 return true;
826 }
827}
828
830 const zelda3::RoomObject& object, int& width, int& height) {
831 // Size is a single 4-bit value (0-15), NOT two separate nibbles
832 // Size represents repetition count for the object's draw routine
833 int size = object.size_ & 0x0F;
834
835 // Base 16x16 (2x2 tiles), extension depends on object orientation
836 // Most objects extend horizontally, some (0x60-0x7F) extend vertically
837 if (object.id_ >= 0x60 && object.id_ <= 0x7F) {
838 // Vertical objects
839 width = 16;
840 height = 16 + size * 16;
841 } else {
842 // Horizontal objects (default)
843 width = 16 + size * 16;
844 height = 16;
845 }
846
847 width = std::min(width, 256);
848 height = std::min(height, 256);
849}
850
853 return;
854 }
855
856 // Create object with specified position
857 auto placed_object = preview_object_;
858 placed_object.set_x(static_cast<uint8_t>(x));
859 placed_object.set_y(static_cast<uint8_t>(y));
860
861 // Call placement callback
862 object_placement_callback_(placed_object);
863}
864
866 ImGui::Text("Sprite Editor");
867 Separator();
868
869 // Display current room sprites from Room data
870 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
871 const auto& room = (*rooms_)[current_room_id_];
872 const auto& sprites = room.GetSprites();
873
874 ImGui::Text("Sprites in room: %zu", sprites.size());
875
876 // Show first few sprites in compact format
877 int display_count = std::min(3, static_cast<int>(sprites.size()));
878 for (int i = 0; i < display_count; ++i) {
879 const auto& sprite = sprites[i];
880 ImGui::Text("ID:%02X (%d,%d) L%d", sprite.id(), sprite.x(), sprite.y(),
881 sprite.layer());
882 }
883 if (sprites.size() > 3) {
884 ImGui::Text("... and %zu more", sprites.size() - 3);
885 }
886 } else {
887 ImGui::TextDisabled("No room selected");
888 }
889
890 Separator();
891 ImGui::TextDisabled("Use Sprite Editor panel for editing");
892}
893
895 ImGui::Text("Item Editor");
896 Separator();
897
898 // Display current room pot items from Room data
899 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
900 const auto& room = (*rooms_)[current_room_id_];
901 const auto& pot_items = room.GetPotItems();
902
903 ImGui::Text("Pot items in room: %zu", pot_items.size());
904
905 // Show first few items in compact format
906 int display_count = std::min(3, static_cast<int>(pot_items.size()));
907 for (int i = 0; i < display_count; ++i) {
908 const auto& item = pot_items[i];
909 ImGui::Text("Item:%02X (%d,%d)", item.item, item.GetTileX(),
910 item.GetTileY());
911 }
912 if (pot_items.size() > 3) {
913 ImGui::Text("... and %zu more", pot_items.size() - 3);
914 }
915 } else {
916 ImGui::TextDisabled("No room selected");
917 }
918
919 Separator();
920 ImGui::TextDisabled("Use Item Editor panel for editing");
921}
922
924 ImGui::Text("Entrance Editor");
925 Separator();
926
927 // Entrances are managed through the dedicated Entrances panel
928 // which accesses the entrances_ array in DungeonEditorV2
929 ImGui::TextDisabled("Use Entrances panel for editing");
930 ImGui::TextDisabled("Room entrances and connections");
931}
932
934 const auto& theme = AgentUI::GetTheme();
935
936 ImGui::Text("Door Editor");
937 Separator();
938
939 // Show doors from the Room data (if available)
940 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
941 const auto& room = (*rooms_)[current_room_id_];
942 const auto& doors = room.GetDoors();
943
944 ImGui::Text("Room Doors: %zu", doors.size());
945
946 if (!doors.empty()) {
947 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.15f, 0.5f));
948 if (ImGui::BeginChild("##DoorList", ImVec2(-1, 120), true)) {
949 for (size_t i = 0; i < doors.size(); ++i) {
950 const auto& door = doors[i];
951 auto [tile_x, tile_y] = door.GetTileCoords();
952
953 ImGui::PushID(static_cast<int>(i));
954
955 // Draw door info with type name
956 std::string type_name(zelda3::GetDoorTypeName(door.type));
957 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
958 ImGui::Text("[%zu] %s (%s) at tile(%d,%d)", i, type_name.c_str(),
959 dir_name.c_str(), tile_x, tile_y);
960
961 // Delete button
962 ImGui::SameLine();
963 if (ImGui::SmallButton("X")) {
964 // Remove door (mutable ref needed)
965 auto& mutable_room = (*rooms_)[current_room_id_];
966 mutable_room.RemoveDoor(i);
967 }
968
969 ImGui::PopID();
970 }
971 }
972 ImGui::EndChild();
973 ImGui::PopStyleColor();
974 }
975
976 // Door type selector
977 Separator();
978 ImGui::Text("Door Type:");
979 static int selected_door_type =
980 static_cast<int>(zelda3::DoorType::NormalDoor);
981
982 // Build door type combo items (common types)
983 constexpr std::array<zelda3::DoorType, 20> door_types = {
1004 };
1005
1006 if (ImGui::BeginCombo(
1007 "##DoorType",
1008 std::string(zelda3::GetDoorTypeName(
1009 static_cast<zelda3::DoorType>(selected_door_type)))
1010 .c_str())) {
1011 for (auto door_type : door_types) {
1012 bool is_selected = (selected_door_type == static_cast<int>(door_type));
1013 if (ImGui::Selectable(
1014 std::string(zelda3::GetDoorTypeName(door_type)).c_str(),
1015 is_selected)) {
1016 selected_door_type = static_cast<int>(door_type);
1017 }
1018 if (is_selected) {
1019 ImGui::SetItemDefaultFocus();
1020 }
1021 }
1022 ImGui::EndCombo();
1023 }
1024
1025 // Instructions
1026 ImGui::TextWrapped(
1027 "Click on a room wall edge to place a door. Doors snap to valid "
1028 "positions.");
1029 } else {
1030 ImGui::Text("No room selected");
1031 }
1032}
1033
1035 ImGui::Text("Chest Editor");
1036 Separator();
1037
1038 // Display current room chests from Room data
1039 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
1040 const auto& room = (*rooms_)[current_room_id_];
1041 const auto& chests = room.GetChests();
1042
1043 ImGui::Text("Chests in room: %zu", chests.size());
1044
1045 // Show chests in compact format
1046 for (size_t i = 0; i < chests.size(); ++i) {
1047 const auto& chest = chests[i];
1048 ImGui::Text("[%zu] Item:%02X %s", i, chest.id,
1049 chest.size ? "(Big)" : "(Small)");
1050 }
1051 } else {
1052 ImGui::TextDisabled("No room selected");
1053 }
1054
1055 Separator();
1056 ImGui::TextDisabled("Chest editing through Room data");
1057}
1058
1060 ImGui::Text("Room Properties");
1061 Separator();
1062
1063 // Display current room properties from Room data
1064 if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
1065 const auto& room = (*rooms_)[current_room_id_];
1066
1067 ImGui::Text("Room ID: %03X", current_room_id_);
1068 ImGui::Text("Blockset: %d", room.blockset);
1069 ImGui::Text("Spriteset: %d", room.spriteset);
1070 ImGui::Text("Palette: %d", room.palette);
1071 ImGui::Text("Layout: %d", room.layout);
1072
1073 Separator();
1074 ImGui::Text("Header Data");
1075 ImGui::Text("Floor1: %d", room.floor1());
1076 ImGui::Text("Floor2: %d", room.floor2());
1077 ImGui::Text("Effect: %d", static_cast<int>(room.effect()));
1078 ImGui::Text("Tag1: %d", static_cast<int>(room.tag1()));
1079 ImGui::Text("Tag2: %d", static_cast<int>(room.tag2()));
1080 } else {
1081 ImGui::TextDisabled("No room selected");
1082 }
1083
1084 Separator();
1085 ImGui::TextDisabled("Full editing in Room Properties panel");
1086}
1087
1094
1096 zelda3::RoomObject obj(obj_id, 0, 0, 0x12, 0);
1097 obj.SetRom(rom_);
1098 obj.EnsureTilesLoaded();
1099 return obj;
1100}
1101
1105
1107 gfx::BackgroundBuffer** out) {
1108 if (!rom_ || !rom_->is_loaded()) {
1109 return false;
1110 }
1111
1112 // Check if room context changed - invalidate cache if so
1113 if (rooms_ && current_room_id_ < static_cast<int>(rooms_->size())) {
1114 const auto& room = (*rooms_)[current_room_id_];
1115 if (!room.IsLoaded()) {
1116 return false; // Can't render without loaded room
1117 }
1118
1119 // Invalidate cache if room/palette/blockset changed
1121 room.blockset != cached_preview_blockset_ ||
1122 room.palette != cached_preview_palette_) {
1125 cached_preview_blockset_ = room.blockset;
1126 cached_preview_palette_ = room.palette;
1127 }
1128 } else {
1129 return false;
1130 }
1131
1132 // Check if already in cache
1133 auto it = preview_cache_.find(obj_id);
1134 if (it != preview_cache_.end()) {
1135 *out = it->second.get();
1136 return (*out)->bitmap().texture() != nullptr;
1137 }
1138
1139 // Create new preview buffer
1140 auto& room = (*rooms_)[current_room_id_];
1141 const uint8_t* gfx_data = room.get_gfx_buffer().data();
1142
1143 // Create preview buffer large enough for object
1144 // Use a reasonable size based on object dimensions (minimum 64x64)
1145 int buffer_size = std::max(static_cast<int>(size), 128);
1146 auto preview =
1147 std::make_unique<gfx::BackgroundBuffer>(buffer_size, buffer_size);
1148
1149 // CRITICAL: Initialize bitmap before drawing
1150 preview->EnsureBitmapInitialized();
1151
1152 // Create object and render it at (1,1) for preview with small margin
1153 // This places the object at pixel (8,8) giving some padding from edges
1154 zelda3::RoomObject obj(obj_id, 1, 1, 0x12, 0);
1155 obj.SetRom(rom_);
1156 obj.EnsureTilesLoaded();
1157
1158 if (obj.tiles().empty()) {
1159 // Try to render even without tiles (some objects may still work)
1160 // Fall through to drawer which has fallback handling
1161 }
1162
1163 // Apply palette to bitmap surface (match Room::RenderRoomGraphics approach)
1164 auto& bitmap = preview->bitmap();
1165 {
1166 std::vector<SDL_Color> colors(256);
1167 // Flatten palette group into SDL colors
1168 // Dungeon palettes have 6 sub-palettes of 15 colors each = 90 colors
1169 size_t color_index = 0;
1170 for (size_t pal_idx = 0;
1171 pal_idx < current_palette_group_.size() && color_index < 256;
1172 ++pal_idx) {
1173 const auto& pal = current_palette_group_[pal_idx];
1174 for (size_t i = 0; i < pal.size() && color_index < 256; ++i) {
1175 ImVec4 rgb = pal[i].rgb();
1176 colors[color_index++] = {static_cast<Uint8>(rgb.x),
1177 static_cast<Uint8>(rgb.y),
1178 static_cast<Uint8>(rgb.z), 255};
1179 }
1180 }
1181 // Transparent color key at index 255
1182 colors[255] = {0, 0, 0, 0};
1183 bitmap.SetPalette(colors);
1184 if (bitmap.surface()) {
1185 SDL_SetColorKey(bitmap.surface(), SDL_TRUE, 255);
1186 SDL_SetSurfaceBlendMode(bitmap.surface(), SDL_BLENDMODE_BLEND);
1187 }
1188 }
1189
1190 zelda3::ObjectDrawer drawer(rom_, current_room_id_, gfx_data);
1191 drawer.InitializeDrawRoutines();
1192
1193 auto status =
1194 drawer.DrawObject(obj, *preview, *preview, current_palette_group_);
1195 if (!status.ok()) {
1196 return false;
1197 }
1198
1199 // Sync bitmap data to SDL surface after drawing
1200 if (bitmap.modified() && bitmap.surface() &&
1201 bitmap.mutable_data().size() > 0) {
1202 SDL_LockSurface(bitmap.surface());
1203 size_t surface_size = bitmap.surface()->h * bitmap.surface()->pitch;
1204 size_t data_size = bitmap.mutable_data().size();
1205 if (surface_size >= data_size) {
1206 memcpy(bitmap.surface()->pixels, bitmap.mutable_data().data(), data_size);
1207 }
1208 SDL_UnlockSurface(bitmap.surface());
1209 }
1210
1211 // Check if bitmap has content
1212 if (bitmap.size() == 0) {
1213 return false;
1214 }
1215
1216 // Create texture
1218 &bitmap);
1220
1221 if (!bitmap.texture()) {
1222 return false;
1223 }
1224
1225 // Store in cache and return
1226 *out = preview.get();
1227 preview_cache_[obj_id] = std::move(preview);
1228 return true;
1229}
1230
1232 ImVec2 top_left, float size) {
1233 gfx::BackgroundBuffer* preview = nullptr;
1234 if (!GetOrCreatePreview(object.id_, size, &preview)) {
1235 return false;
1236 }
1237
1238 // Draw the cached preview image
1239 auto& bitmap = preview->bitmap();
1240 if (!bitmap.texture()) {
1241 return false;
1242 }
1243
1244 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1245 ImVec2 bottom_right(top_left.x + size, top_left.y + size);
1246 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), top_left,
1247 bottom_right);
1248 return true;
1249}
1250
1251} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:128
std::function< void(const zelda3::RoomObject &) object_placement_callback_)
std::map< int, std::unique_ptr< gfx::BackgroundBuffer > > preview_cache_
bool GetOrCreatePreview(int obj_id, float size, gfx::BackgroundBuffer **out)
zelda3::DungeonObjectEditor * object_editor_
std::array< zelda3::Room, 0x128 > * rooms_
void CalculateObjectDimensions(const zelda3::RoomObject &object, int &width, int &height)
zelda3::DungeonObjectRegistry object_registry_
std::function< void(int)> object_double_click_callback_
void RenderObjectPrimitive(const zelda3::RoomObject &object, int x, int y)
std::unique_ptr< zelda3::DungeonEditorSystem > * dungeon_editor_system_
std::function< void(const zelda3::RoomObject &) object_selected_callback_)
zelda3::RoomObject MakePreviewObject(int obj_id) const
bool DrawObjectPreview(const zelda3::RoomObject &object, ImVec2 top_left, float size)
bool MatchesObjectFilter(int obj_id, int filter_type)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:35
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:115
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:102
static Arena & Get()
Definition arena.cc:20
Lightweight RAII guard for existing Canvas instances.
Definition canvas.h:872
auto height() const
Definition canvas.h:498
auto width() const
Definition canvas.h:497
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
void AddImageAt(ImTextureID texture, ImVec2 local_top_left, ImVec2 size)
Definition canvas.cc:2407
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1341
void DrawText(const std::string &text, int x, int y)
Definition canvas.cc:1346
static CustomObjectManager & Get()
void RegisterVanillaRange(int16_t start_id, int16_t end_id)
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr)
Draw a room object to background buffers.
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:88
void set_x(uint8_t x)
Definition room_object.h:73
void SetRom(Rom *rom)
Definition room_object.h:68
#define ICON_MD_IMAGE
Definition icons.h:982
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ SmallKeyStairsUp
Small key stairs (upwards)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ ExitMarker
Exit marker.
@ DoubleSidedShutter
Double sided shutter door.
int GetObjectSubtype(int object_id)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:161
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
Treasure chest.
Definition zelda.h:425
std::optional< float > grid_step
Definition canvas.h:70
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89