120 if (object_id >= 0xF80) {
122 return ImGui::GetColorU32(theme.item_color);
123 }
else if (object_id >= 0xF80 && object_id <= 0xF8F) {
124 return ImGui::ColorConvertFloat4ToU32(
125 theme.selection_secondary);
126 }
else if (object_id >= 0xF90 && object_id <= 0xF9F) {
127 return ImGui::ColorConvertFloat4ToU32(
128 theme.transport_color);
130 return ImGui::ColorConvertFloat4ToU32(
131 theme.music_zone_color);
136 if (object_id >= 0x100 && object_id < 0x200) {
137 if (object_id >= 0x100 && object_id <= 0x10F) {
138 return ImGui::GetColorU32(theme.status_warning);
139 }
else if (object_id >= 0x110 && object_id <= 0x11F) {
140 return ImGui::GetColorU32(theme.dungeon_object_default);
141 }
else if (object_id >= 0x120 && object_id <= 0x12F) {
142 return ImGui::ColorConvertFloat4ToU32(
143 theme.status_success);
144 }
else if (object_id >= 0x130 && object_id <= 0x13F) {
145 return ImGui::GetColorU32(theme.selection_primary);
147 return ImGui::GetColorU32(theme.text_secondary_gray);
152 if (object_id >= 0x10 && object_id <= 0x1F) {
153 return ImGui::GetColorU32(theme.dungeon_object_wall);
154 }
else if (object_id >= 0x20 && object_id <= 0x2F) {
155 return ImGui::GetColorU32(theme.dungeon_object_floor);
156 }
else if (object_id >= 0x17 && object_id <= 0x1E) {
157 return ImGui::GetColorU32(theme.dungeon_object_floor);
158 }
else if (object_id == 0x2F || object_id == 0x2B) {
159 return ImGui::GetColorU32(
160 theme.dungeon_object_pot);
161 }
else if (object_id >= 0x30 && object_id <= 0x3F) {
162 return ImGui::GetColorU32(
163 theme.dungeon_object_decoration);
164 }
else if (object_id >= 0x00 && object_id <= 0x0F) {
165 return ImGui::GetColorU32(theme.dungeon_selection_secondary);
167 return ImGui::GetColorU32(theme.dungeon_object_default);
256 static const ObjectRange ranges[] = {
257 {0x00, 0xF7,
"Type 1"},
258 {0x100, 0x13F,
"Type 2"},
259 {0xF80, 0xFFF,
"Type 3"},
264 (0xF7 - 0x00 + 1) + (0x13F - 0x100 + 1) + (0xFFF - 0xF80 + 1);
268 const int custom_count =
269 std::min(obj_manager.GetSubtypeCount(0x31),
270 kPersistedCustomSubtypeSlots) +
271 std::min(obj_manager.GetSubtypeCount(0x32), kPersistedCustomSubtypeSlots);
274 ImGui::SetNextItemWidth(-1.0f);
275 ImGui::InputTextWithHint(
276 "##ObjectSearch",
ICON_MD_SEARCH " Filter objects by name or hex...",
282 static const char* kFilterLabels[] = {
"All",
"Walls",
"Floors",
"Chests",
283 "Doors",
"Decor",
"Stairs"};
284 const float controls_width = ImGui::GetContentRegionAvail().x;
285 const float filter_width = std::min(170.0f, controls_width);
286 ImGui::SetNextItemWidth(filter_width);
288 IM_ARRAYSIZE(kFilterLabels));
298 ImGui::OpenPopup(
"##ObjectSelectorDisplayPopup");
300 if (ImGui::BeginPopup(
"##ObjectSelectorDisplayPopup")) {
301 ImGui::TextDisabled(tr(
"Display"));
303 if (ImGui::IsItemHovered()) {
305 tr(
"Show rendered object thumbnails in the selector.\n"
306 "Requires a room to be loaded and may cost some performance."));
309 ImGui::TextDisabled(tr(
"Grid density"));
310 static constexpr const char* kDensityLabels[] = {
"Small",
"Medium",
312 constexpr float kDensitySegmentWidth = 84.0f;
313 const float density_spacing = ImGui::GetStyle().ItemSpacing.x;
314 for (
int density = 0; density < 3; ++density) {
316 ImGui::SameLine(0.0f, density_spacing);
320 ImVec2(kDensitySegmentWidth, 0.0f))) {
330 ImGui::TextColored(theme.text_secondary_gray, tr(
"%d vanilla objects"),
333 if (ImGui::GetContentRegionAvail().x > 220.0f) {
336 ImGui::TextColored(theme.text_info,
ICON_MD_LABEL " Queued 0x%03X %s",
340 if (ImGui::GetContentRegionAvail().x > 180.0f) {
346 const float item_spacing = 6.0f;
351 constexpr float kRightMargin = 16.0f;
352 constexpr float kMinItemSize = 32.0f;
353 const float available_width = ImGui::GetContentRegionAvail().x;
355 const float item_size =
356 std::min(requested_item_size,
357 std::max(kMinItemSize, available_width - kRightMargin));
359 std::max(1,
static_cast<int>((available_width - item_spacing) /
360 (item_size + item_spacing)));
366 float child_height = ImGui::GetContentRegionAvail().y;
367 if (ImGui::BeginChild(
"##ObjectGrid", ImVec2(0, child_height),
false,
368 ImGuiWindowFlags_AlwaysVerticalScrollbar |
369 ImGuiWindowFlags_HorizontalScrollbar)) {
372 for (
const auto& range : ranges) {
374 const ImU32 header_color = GetObjectRangeHeaderColor(range.start);
375 const ImU32 header_hover_color =
376 GetObjectRangeHeaderHoverColor(range.start);
378 {{ImGuiCol_Header, ImGui::ColorConvertU32ToFloat4(header_color)},
379 {ImGuiCol_HeaderHovered,
380 ImGui::ColorConvertU32ToFloat4(header_hover_color)}});
381 bool section_open = ImGui::CollapsingHeader(
382 absl::StrFormat(
"%s (0x%03X-0x%03X)", range.label, range.start,
385 ImGuiTreeNodeFlags_DefaultOpen);
390 int current_column = 0;
392 for (
int obj_id = range.start; obj_id <= range.end; ++obj_id) {
402 if (current_column > 0) {
406 ImGui::PushID(obj_id);
410 ImVec2 button_size(item_size, item_size);
412 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
434 const bool item_visible = ImGui::IsItemVisible();
435 ImVec2 button_pos = ImGui::GetItemRectMin();
440 ImDrawList* draw_list = ImGui::GetWindowDrawList();
443 bool rendered =
false;
449 if (item_visible && !rendered) {
452 DrawFallbackPreviewTile(
453 draw_list, button_pos, item_size,
460 float border_thickness;
463 border_color = ImGui::GetColorU32(theme.dungeon_selection_primary);
464 border_thickness = 3.0f;
466 border_color = ImGui::GetColorU32(theme.panel_bg_darker);
467 border_thickness = 1.0f;
473 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
474 border_color, 0.0f, 0, border_thickness);
479 std::string display_name = full_name;
480 const size_t max_display_chars =
481 std::max<size_t>(7,
static_cast<size_t>(item_size / 6.0f));
482 if (display_name.length() > max_display_chars) {
483 display_name = display_name.substr(0, max_display_chars - 2) +
"..";
488 ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
489 ImVec2 name_pos = ImVec2(button_pos.x + (item_size - name_size.x) / 2,
490 button_pos.y + item_size - 26);
491 draw_list->AddText(name_pos,
492 ImGui::GetColorU32(theme.text_secondary_gray),
493 display_name.c_str());
496 std::string id_text = absl::StrFormat(
"%03X", obj_id);
497 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
498 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
499 button_pos.y + item_size - id_size.y - 2);
500 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
505 if (ImGui::IsItemHovered()) {
507 {{ImGuiCol_PopupBg, theme.panel_bg_color},
508 {ImGuiCol_Border, theme.panel_border_color}});
510 if (ImGui::BeginTooltip()) {
511 ImGui::TextColored(theme.selection_primary, tr(
"Object 0x%03X"),
513 ImGui::Text(
"%s", full_name.c_str());
515 ImGui::TextColored(theme.text_secondary_gray, tr(
"Subtype %d"),
518 rendered ? theme.status_success : theme.status_warning,
520 rendered ?
"rendered tile layout"
522 :
"thumbnails off"));
525 const uint8_t preview_size =
527 uint32_t layout_key = (
static_cast<uint32_t
>(obj_id) << 16) |
528 static_cast<uint32_t
>(preview_size);
529 const bool can_capture_layout =
532 if (can_capture_layout &&
538 if (layout_or.ok()) {
545 ImGui::TextColored(theme.status_success, tr(
"Tiles: %zu"),
546 layout.cells.size());
548 if (can_capture_layout) {
551 room_ref.get_gfx_buffer().
data());
553 ImGui::TextColored(theme.status_active, tr(
"Draw Routine: %d"),
557 ImGui::Text(tr(
"Layout:"));
558 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
559 ImVec2 grid_start = ImGui::GetCursorScreenPos();
560 float cell_size = 4.0f;
561 for (
const auto& cell : layout.cells) {
562 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
563 grid_start.y + cell.rel_y * cell_size);
564 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
565 tooltip_draw_list->AddRectFilled(
566 p1, p2, ThemeColor(theme.dungeon_grid_cell_highlight));
567 tooltip_draw_list->AddRect(
568 p1, p2, ThemeColor(theme.panel_border_color));
570 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
571 layout.bounds_height * cell_size));
575 ImGui::TextColored(theme.text_secondary_gray,
576 tr(
"Click to select for placement"));
583 current_column = (current_column + 1) % columns;
612 const std::string& name,
618 auto to_lower = [](std::string value) {
619 std::transform(value.begin(), value.end(), value.begin(),
620 [](
unsigned char c) { return std::tolower(c); });
625 std::string name_lower = to_lower(name);
627 std::string id_hex = absl::StrFormat(
"%03X", obj_id);
628 std::string id_lower = to_lower(id_hex);
629 std::string id_pref =
"0x" + id_lower;
631 if (name_lower.find(needle) != std::string::npos) {
634 if (id_lower.find(needle) != std::string::npos ||
635 id_pref.find(needle) != std::string::npos) {
640 std::string sub_hex = absl::StrFormat(
"%02X", subtype);
641 std::string sub_lower = to_lower(sub_hex);
642 std::string combined = id_lower +
":" + sub_lower;
643 std::string combined_pref =
"0x" + combined;
644 if (combined.find(needle) != std::string::npos ||
645 combined_pref.find(needle) != std::string::npos) {
924 const std::string custom_base_path = obj_manager.GetBasePath();
928 ImGui::SetNextWindowSize(ImVec2(860.0f, 620.0f), ImGuiCond_FirstUseEver);
929 if (!ImGui::BeginPopupModal(
"Custom Object Workshop",
nullptr,
930 ImGuiWindowFlags_None)) {
934 ImGui::TextColored(theme.text_info,
937 theme.text_secondary_gray,
938 tr(
"Create and place custom dungeon objects without mixing "
939 "them into the main selector grid."));
942 if (ImGui::BeginTable(
943 "##CustomObjectToolbar", 2,
944 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
945 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 1.5f);
946 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthStretch,
948 ImGui::TableNextRow();
950 ImGui::TableNextColumn();
951 if (custom_base_path.empty()) {
953 " Custom object folder is not configured.");
955 ImGui::TextColored(theme.text_secondary_gray,
957 if (ImGui::IsItemHovered()) {
958 ImGui::SetTooltip(
"%s", custom_base_path.c_str());
962 ImGui::TableNextColumn();
964 if (ImGui::Button(
ICON_MD_ADD " New Custom Object", ImVec2(-1, 0))) {
971 if (ImGui::IsItemHovered()) {
972 ImGui::SetTooltip(tr(
"Create a new custom object from scratch"));
975 if (ImGui::Button(
ICON_MD_REFRESH " Reload Workshop", ImVec2(-1, 0))) {
976 obj_manager.ReloadAll();
979 if (ImGui::IsItemHovered()) {
981 tr(
"Reload custom object binaries and refresh their previews"));
988 " Corner overrides map to 0x31 subtypes 02, 04, 03, and 05.");
991 if (ImGui::BeginChild(
"##CustomObjectGrid",
992 ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4.0f),
994 ImGuiWindowFlags_AlwaysVerticalScrollbar |
995 ImGuiWindowFlags_HorizontalScrollbar)) {
996 const float item_spacing = 6.0f;
997 const int columns = std::max(
998 1,
static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
999 (item_size + item_spacing)));
1001 for (
int obj_id : {0x31, 0x32}) {
1005 const int subtype_count = std::min(obj_manager.GetSubtypeCount(obj_id),
1006 kPersistedCustomSubtypeSlots);
1007 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
1009 std::string subtype_name =
1010 absl::StrFormat(
"%s %02X", base_name.c_str(), subtype);
1015 if (custom_col > 0) {
1019 ImGui::PushID(obj_id * 1000 + subtype);
1023 ImVec2 button_size(item_size, item_size);
1025 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
1027 ImGui::CloseCurrentPopup();
1029 const bool item_visible = ImGui::IsItemVisible();
1030 ImVec2 button_pos = ImGui::GetItemRectMin();
1034 static_cast<uint8_t
>(subtype)));
1035 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1037 bool rendered =
false;
1041 obj_id,
static_cast<uint8_t
>(subtype));
1045 if (item_visible && !rendered) {
1046 std::string sub_text = absl::StrFormat(
"%02X", subtype);
1047 DrawFallbackPreviewTile(draw_list, button_pos, item_size,
1048 theme.status_success, sub_text.c_str());
1051 ImU32 border_color =
1052 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
1053 : ImGui::GetColorU32(theme.panel_bg_darker);
1054 float border_thickness = is_selected ? 3.0f : 1.0f;
1058 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1059 border_color, 0.0f, 0, border_thickness);
1061 std::string id_text = absl::StrFormat(
"%02X:%02X", obj_id, subtype);
1062 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1063 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1064 button_pos.y + item_size - id_size.y - 2);
1065 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1069 if (ImGui::IsItemHovered()) {
1071 {{ImGuiCol_PopupBg, theme.panel_bg_color},
1072 {ImGuiCol_Border, theme.panel_border_color}});
1073 if (ImGui::BeginTooltip()) {
1074 const std::string filename =
1075 obj_manager.ResolveFilename(obj_id, subtype);
1076 const bool has_base = !custom_base_path.empty();
1077 std::filesystem::path full_path =
1078 has_base ? (std::filesystem::path(custom_base_path) / filename)
1079 : std::filesystem::path();
1080 const bool file_exists = has_base && !filename.empty() &&
1081 std::filesystem::exists(full_path);
1083 ImGui::TextColored(theme.selection_primary,
1084 tr(
"Custom 0x%02X:%02X"), obj_id, subtype);
1085 ImGui::Text(
"%s", subtype_name.c_str());
1087 rendered ? theme.status_success : theme.status_warning,
1089 rendered ?
"rendered custom layout"
1091 :
"thumbnails off"));
1093 ImGui::Text(tr(
"File: %s"),
1094 filename.empty() ?
"(unmapped)" : filename.c_str());
1096 ImGui::TextColored(theme.text_warning_yellow,
1097 tr(
"Folder not configured in project"));
1098 }
else if (file_exists) {
1099 ImGui::TextColored(theme.status_success, tr(
"File found"));
1101 ImGui::TextColored(theme.status_error, tr(
"File missing: %s"),
1102 full_path.string().c_str());
1105 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
1106 const char* corner_id =
"";
1108 corner_id =
"0x100 (TL)";
1109 }
else if (subtype == 3) {
1110 corner_id =
"0x102 (TR)";
1111 }
else if (subtype == 4) {
1112 corner_id =
"0x101 (BL)";
1114 corner_id =
"0x103 (BR)";
1117 ImGui::TextColored(theme.status_active,
1118 tr(
"Also used by corner override %s"),
1121 ImGui::EndTooltip();
1126 custom_col = (custom_col + 1) % columns;
1134 ImGui::CloseCurrentPopup();