113 if (object_id >= 0xF80) {
114 if (object_id >= 0xF80 && object_id <= 0xF8F) {
115 return ImGui::ColorConvertFloat4ToU32(
116 theme.selection_secondary);
117 }
else if (object_id >= 0xF90 && object_id <= 0xF9F) {
118 return ImGui::ColorConvertFloat4ToU32(
119 theme.transport_color);
121 return ImGui::ColorConvertFloat4ToU32(
122 theme.music_zone_color);
127 if (object_id >= 0x100 && object_id < 0x200) {
128 if (object_id >= 0x100 && object_id <= 0x10F) {
129 return ImGui::GetColorU32(theme.status_warning);
130 }
else if (object_id >= 0x110 && object_id <= 0x11F) {
131 return ImGui::GetColorU32(theme.dungeon_object_default);
132 }
else if (object_id >= 0x120 && object_id <= 0x12F) {
133 return ImGui::ColorConvertFloat4ToU32(
134 theme.status_success);
135 }
else if (object_id >= 0x130 && object_id <= 0x13F) {
136 return ImGui::GetColorU32(theme.selection_primary);
138 return ImGui::GetColorU32(theme.text_secondary_gray);
143 if (object_id >= 0x10 && object_id <= 0x1F) {
144 return ImGui::GetColorU32(theme.dungeon_object_wall);
145 }
else if (object_id >= 0x20 && object_id <= 0x2F) {
146 return ImGui::GetColorU32(theme.dungeon_object_floor);
147 }
else if (object_id == 0xF9 || object_id == 0xFA) {
148 return ImGui::GetColorU32(theme.item_color);
149 }
else if (object_id >= 0x17 && object_id <= 0x1E) {
150 return ImGui::GetColorU32(theme.dungeon_object_floor);
151 }
else if (object_id == 0x2F || object_id == 0x2B) {
152 return ImGui::GetColorU32(
153 theme.dungeon_object_pot);
154 }
else if (object_id >= 0x30 && object_id <= 0x3F) {
155 return ImGui::GetColorU32(
156 theme.dungeon_object_decoration);
157 }
else if (object_id >= 0x00 && object_id <= 0x0F) {
158 return ImGui::GetColorU32(theme.dungeon_selection_secondary);
160 return ImGui::GetColorU32(theme.dungeon_object_default);
243 static const ObjectRange ranges[] = {
244 {0x00, 0xFF,
"Type 1"},
245 {0x100, 0x141,
"Type 2"},
246 {0xF80, 0xFFF,
"Type 3"},
251 (0xFF - 0x00 + 1) + (0x141 - 0x100 + 1) + (0xFFF - 0xF80 + 1);
255 const int custom_count =
256 obj_manager.GetSubtypeCount(0x31) + obj_manager.GetSubtypeCount(0x32);
259 ImGui::SetNextItemWidth(-1.0f);
260 ImGui::InputTextWithHint(
261 "##ObjectSearch",
ICON_MD_SEARCH " Filter objects by name or hex...",
267 static const char* kFilterLabels[] = {
"All",
"Walls",
"Floors",
"Chests",
268 "Doors",
"Decor",
"Stairs"};
269 const float controls_width = ImGui::GetContentRegionAvail().x;
270 const float filter_width = std::min(170.0f, controls_width);
271 ImGui::SetNextItemWidth(filter_width);
273 IM_ARRAYSIZE(kFilterLabels));
283 ImGui::OpenPopup(
"##ObjectSelectorDisplayPopup");
285 if (ImGui::BeginPopup(
"##ObjectSelectorDisplayPopup")) {
286 ImGui::TextDisabled(tr(
"Display"));
288 if (ImGui::IsItemHovered()) {
290 tr(
"Show rendered object thumbnails in the selector.\n"
291 "Requires a room to be loaded and may cost some performance."));
294 ImGui::TextDisabled(tr(
"Grid density"));
295 static constexpr const char* kDensityLabels[] = {
"Small",
"Medium",
297 constexpr float kDensitySegmentWidth = 84.0f;
298 const float density_spacing = ImGui::GetStyle().ItemSpacing.x;
299 for (
int density = 0; density < 3; ++density) {
301 ImGui::SameLine(0.0f, density_spacing);
305 ImVec2(kDensitySegmentWidth, 0.0f))) {
315 ImGui::TextColored(theme.text_secondary_gray, tr(
"%d vanilla objects"),
318 if (ImGui::GetContentRegionAvail().x > 220.0f) {
321 ImGui::TextColored(theme.text_info,
ICON_MD_LABEL " Queued 0x%03X %s",
325 if (ImGui::GetContentRegionAvail().x > 180.0f) {
331 const float item_spacing = 6.0f;
336 constexpr float kRightMargin = 16.0f;
337 constexpr float kMinItemSize = 32.0f;
338 const float available_width = ImGui::GetContentRegionAvail().x;
340 const float item_size =
341 std::min(requested_item_size,
342 std::max(kMinItemSize, available_width - kRightMargin));
344 std::max(1,
static_cast<int>((available_width - item_spacing) /
345 (item_size + item_spacing)));
351 float child_height = ImGui::GetContentRegionAvail().y;
352 if (ImGui::BeginChild(
"##ObjectGrid", ImVec2(0, child_height),
false,
353 ImGuiWindowFlags_AlwaysVerticalScrollbar |
354 ImGuiWindowFlags_HorizontalScrollbar)) {
357 for (
const auto& range : ranges) {
359 const ImU32 header_color = GetObjectRangeHeaderColor(range.start);
360 const ImU32 header_hover_color =
361 GetObjectRangeHeaderHoverColor(range.start);
363 {{ImGuiCol_Header, ImGui::ColorConvertU32ToFloat4(header_color)},
364 {ImGuiCol_HeaderHovered,
365 ImGui::ColorConvertU32ToFloat4(header_hover_color)}});
366 bool section_open = ImGui::CollapsingHeader(
367 absl::StrFormat(
"%s (0x%03X-0x%03X)", range.label, range.start,
370 ImGuiTreeNodeFlags_DefaultOpen);
375 int current_column = 0;
377 for (
int obj_id = range.start; obj_id <= range.end; ++obj_id) {
387 if (current_column > 0) {
391 ImGui::PushID(obj_id);
395 ImVec2 button_size(item_size, item_size);
397 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
417 const bool item_visible = ImGui::IsItemVisible();
418 ImVec2 button_pos = ImGui::GetItemRectMin();
422 ImDrawList* draw_list = ImGui::GetWindowDrawList();
425 bool rendered =
false;
431 if (item_visible && !rendered) {
434 DrawFallbackPreviewTile(
435 draw_list, button_pos, item_size,
442 float border_thickness;
445 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
446 border_thickness = 3.0f;
448 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
449 border_thickness = 1.0f;
455 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
456 border_color, 0.0f, 0, border_thickness);
461 std::string display_name = full_name;
462 const size_t max_display_chars =
463 std::max<size_t>(7,
static_cast<size_t>(item_size / 6.0f));
464 if (display_name.length() > max_display_chars) {
465 display_name = display_name.substr(0, max_display_chars - 2) +
"..";
470 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
471 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
472 button_pos.y + item_size - 26);
473 draw_list->AddText(name_pos,
474 ImGui::GetColorU32(theme.text_secondary_gray),
475 display_name.c_str());
478 std::string id_text = absl::StrFormat(
"%03X", obj_id);
479 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
480 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
481 button_pos.y + item_size - id_size.y - 2);
482 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
487 if (ImGui::IsItemHovered()) {
489 {{ImGuiCol_PopupBg, theme.panel_bg_color},
490 {ImGuiCol_Border, theme.panel_border_color}});
492 if (ImGui::BeginTooltip()) {
493 ImGui::TextColored(theme.selection_primary, tr(
"Object 0x%03X"),
495 ImGui::Text(
"%s", full_name.c_str());
497 ImGui::TextColored(theme.text_secondary_gray, tr(
"Subtype %d"),
500 rendered ? theme.status_success : theme.status_warning,
502 rendered ?
"rendered tile layout"
504 :
"thumbnails off"));
507 uint32_t layout_key = (
static_cast<uint32_t
>(obj_id) << 16) |
508 static_cast<uint32_t
>(subtype);
509 const bool can_capture_layout =
512 if (can_capture_layout &&
518 if (layout_or.ok()) {
525 ImGui::TextColored(theme.status_success, tr(
"Tiles: %zu"),
526 layout.cells.size());
528 if (can_capture_layout) {
531 room_ref.get_gfx_buffer().
data());
533 ImGui::TextColored(theme.status_active, tr(
"Draw Routine: %d"),
537 ImGui::Text(tr(
"Layout:"));
538 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
539 ImVec2 grid_start = ImGui::GetCursorScreenPos();
540 float cell_size = 4.0f;
541 for (
const auto& cell : layout.cells) {
542 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
543 grid_start.y + cell.rel_y * cell_size);
544 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
545 tooltip_draw_list->AddRectFilled(
546 p1, p2, ThemeColor(theme.dungeon_grid_cell_highlight));
547 tooltip_draw_list->AddRect(
548 p1, p2, ThemeColor(theme.panel_border_color));
550 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
551 layout.bounds_height * cell_size));
555 ImGui::TextColored(theme.text_secondary_gray,
556 tr(
"Click to select for placement"));
563 current_column = (current_column + 1) % columns;
592 const std::string& name,
598 auto to_lower = [](std::string value) {
599 std::transform(value.begin(), value.end(), value.begin(),
600 [](
unsigned char c) { return std::tolower(c); });
605 std::string name_lower = to_lower(name);
607 std::string id_hex = absl::StrFormat(
"%03X", obj_id);
608 std::string id_lower = to_lower(id_hex);
609 std::string id_pref =
"0x" + id_lower;
611 if (name_lower.find(needle) != std::string::npos) {
614 if (id_lower.find(needle) != std::string::npos ||
615 id_pref.find(needle) != std::string::npos) {
620 std::string sub_hex = absl::StrFormat(
"%02X", subtype);
621 std::string sub_lower = to_lower(sub_hex);
622 std::string combined = id_lower +
":" + sub_lower;
623 std::string combined_pref =
"0x" + combined;
624 if (combined.find(needle) != std::string::npos ||
625 combined_pref.find(needle) != std::string::npos) {
897 const std::string custom_base_path = obj_manager.GetBasePath();
901 ImGui::SetNextWindowSize(ImVec2(860.0f, 620.0f), ImGuiCond_FirstUseEver);
902 if (!ImGui::BeginPopupModal(
"Custom Object Workshop",
nullptr,
903 ImGuiWindowFlags_None)) {
907 ImGui::TextColored(theme.text_info,
910 theme.text_secondary_gray,
911 tr(
"Create and place custom dungeon objects without mixing "
912 "them into the main selector grid."));
915 if (ImGui::BeginTable(
916 "##CustomObjectToolbar", 2,
917 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
918 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 1.5f);
919 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthStretch,
921 ImGui::TableNextRow();
923 ImGui::TableNextColumn();
924 if (custom_base_path.empty()) {
926 " Custom object folder is not configured.");
928 ImGui::TextColored(theme.text_secondary_gray,
930 if (ImGui::IsItemHovered()) {
931 ImGui::SetTooltip(
"%s", custom_base_path.c_str());
935 ImGui::TableNextColumn();
937 if (ImGui::Button(
ICON_MD_ADD " New Custom Object", ImVec2(-1, 0))) {
944 if (ImGui::IsItemHovered()) {
945 ImGui::SetTooltip(tr(
"Create a new custom object from scratch"));
948 if (ImGui::Button(
ICON_MD_REFRESH " Reload Workshop", ImVec2(-1, 0))) {
949 obj_manager.ReloadAll();
952 if (ImGui::IsItemHovered()) {
954 tr(
"Reload custom object binaries and refresh their previews"));
961 " Corner overrides map to 0x31 subtypes 02, 04, 03, and 05.");
964 if (ImGui::BeginChild(
"##CustomObjectGrid",
965 ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4.0f),
967 ImGuiWindowFlags_AlwaysVerticalScrollbar |
968 ImGuiWindowFlags_HorizontalScrollbar)) {
969 const float item_spacing = 6.0f;
970 const int columns = std::max(
971 1,
static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
972 (item_size + item_spacing)));
974 for (
int obj_id : {0x31, 0x32}) {
978 int subtype_count = obj_manager.GetSubtypeCount(obj_id);
979 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
981 std::string subtype_name =
982 absl::StrFormat(
"%s %02X", base_name.c_str(), subtype);
987 if (custom_col > 0) {
991 ImGui::PushID(obj_id * 1000 + subtype);
995 ImVec2 button_size(item_size, item_size);
997 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
999 ImGui::CloseCurrentPopup();
1001 const bool item_visible = ImGui::IsItemVisible();
1002 ImVec2 button_pos = ImGui::GetItemRectMin();
1005 static_cast<uint8_t
>(subtype & 0x1F));
1006 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1008 bool rendered =
false;
1011 temp_obj.size_ = subtype;
1015 if (item_visible && !rendered) {
1016 std::string sub_text = absl::StrFormat(
"%02X", subtype);
1017 DrawFallbackPreviewTile(draw_list, button_pos, item_size,
1018 theme.status_success, sub_text.c_str());
1021 ImU32 border_color =
1022 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
1023 : ImGui::GetColorU32(theme.panel_bg_darker);
1024 float border_thickness = is_selected ? 3.0f : 1.0f;
1028 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1029 border_color, 0.0f, 0, border_thickness);
1031 std::string id_text = absl::StrFormat(
"%02X:%02X", obj_id, subtype);
1032 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1033 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1034 button_pos.y + item_size - id_size.y - 2);
1035 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1039 if (ImGui::IsItemHovered()) {
1041 {{ImGuiCol_PopupBg, theme.panel_bg_color},
1042 {ImGuiCol_Border, theme.panel_border_color}});
1043 if (ImGui::BeginTooltip()) {
1044 const std::string filename =
1045 obj_manager.ResolveFilename(obj_id, subtype);
1046 const bool has_base = !custom_base_path.empty();
1047 std::filesystem::path full_path =
1048 has_base ? (std::filesystem::path(custom_base_path) / filename)
1049 : std::filesystem::path();
1050 const bool file_exists = has_base && !filename.empty() &&
1051 std::filesystem::exists(full_path);
1053 ImGui::TextColored(theme.selection_primary,
1054 tr(
"Custom 0x%02X:%02X"), obj_id, subtype);
1055 ImGui::Text(
"%s", subtype_name.c_str());
1057 rendered ? theme.status_success : theme.status_warning,
1059 rendered ?
"rendered custom layout"
1061 :
"thumbnails off"));
1063 ImGui::Text(tr(
"File: %s"),
1064 filename.empty() ?
"(unmapped)" : filename.c_str());
1066 ImGui::TextColored(theme.text_warning_yellow,
1067 tr(
"Folder not configured in project"));
1068 }
else if (file_exists) {
1069 ImGui::TextColored(theme.status_success, tr(
"File found"));
1071 ImGui::TextColored(theme.status_error, tr(
"File missing: %s"),
1072 full_path.string().c_str());
1075 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
1076 const char* corner_id =
"";
1078 corner_id =
"0x100 (TL)";
1079 }
else if (subtype == 3) {
1080 corner_id =
"0x102 (TR)";
1081 }
else if (subtype == 4) {
1082 corner_id =
"0x101 (BL)";
1084 corner_id =
"0x103 (BR)";
1087 ImGui::TextColored(theme.status_active,
1088 tr(
"Also used by corner override %s"),
1091 ImGui::EndTooltip();
1096 custom_col = (custom_col + 1) % columns;
1104 ImGui::CloseCurrentPopup();