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) {
958 const std::string custom_base_path = obj_manager.GetBasePath();
962 ImGui::SetNextWindowSize(ImVec2(860.0f, 620.0f), ImGuiCond_FirstUseEver);
963 if (!ImGui::BeginPopupModal(
"Custom Object Workshop",
nullptr,
964 ImGuiWindowFlags_None)) {
968 ImGui::TextColored(theme.text_info,
971 theme.text_secondary_gray,
972 tr(
"Create and place custom dungeon objects without mixing "
973 "them into the main selector grid."));
976 if (ImGui::BeginTable(
977 "##CustomObjectToolbar", 2,
978 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
979 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 1.5f);
980 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthStretch,
982 ImGui::TableNextRow();
984 ImGui::TableNextColumn();
985 if (custom_base_path.empty()) {
987 " Custom object folder is not configured.");
989 ImGui::TextColored(theme.text_secondary_gray,
991 if (ImGui::IsItemHovered()) {
992 ImGui::SetTooltip(
"%s", custom_base_path.c_str());
996 ImGui::TableNextColumn();
998 if (ImGui::Button(
ICON_MD_ADD " New Custom Object", ImVec2(-1, 0))) {
1006 if (ImGui::IsItemHovered()) {
1007 ImGui::SetTooltip(tr(
"Create a new custom object from scratch"));
1010 if (ImGui::Button(
ICON_MD_REFRESH " Reload Workshop", ImVec2(-1, 0))) {
1011 obj_manager.ReloadAll();
1014 if (ImGui::IsItemHovered()) {
1016 tr(
"Reload custom object binaries and refresh their previews"));
1023 " Corner overrides map to 0x31 subtypes 02, 04, 03, and 05.");
1026 if (ImGui::BeginChild(
"##CustomObjectGrid",
1027 ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4.0f),
1029 ImGuiWindowFlags_AlwaysVerticalScrollbar |
1030 ImGuiWindowFlags_HorizontalScrollbar)) {
1031 const float item_spacing = 6.0f;
1032 const int columns = std::max(
1033 1,
static_cast<int>((ImGui::GetContentRegionAvail().x - item_spacing) /
1034 (item_size + item_spacing)));
1036 for (
int obj_id : {0x31, 0x32}) {
1040 const int subtype_count = std::min(obj_manager.GetSubtypeCount(obj_id),
1041 kPersistedCustomSubtypeSlots);
1042 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
1044 std::string subtype_name =
1045 absl::StrFormat(
"%s %02X", base_name.c_str(), subtype);
1050 if (custom_col > 0) {
1054 ImGui::PushID(obj_id * 1000 + subtype);
1058 ImVec2 button_size(item_size, item_size);
1060 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
1062 ImGui::CloseCurrentPopup();
1064 const bool item_visible = ImGui::IsItemVisible();
1065 ImVec2 button_pos = ImGui::GetItemRectMin();
1069 static_cast<uint8_t
>(subtype)));
1070 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1072 bool rendered =
false;
1076 obj_id,
static_cast<uint8_t
>(subtype));
1080 if (item_visible && !rendered) {
1081 std::string sub_text = absl::StrFormat(
"%02X", subtype);
1082 DrawFallbackPreviewTile(draw_list, button_pos, item_size,
1083 theme.status_success, sub_text.c_str());
1086 ImU32 border_color =
1087 is_selected ? ImGui::GetColorU32(theme.dungeon_selection_primary)
1088 : ImGui::GetColorU32(theme.panel_bg_darker);
1089 float border_thickness = is_selected ? 3.0f : 1.0f;
1093 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1094 border_color, 0.0f, 0, border_thickness);
1096 std::string id_text = absl::StrFormat(
"%02X:%02X", obj_id, subtype);
1097 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1098 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1099 button_pos.y + item_size - id_size.y - 2);
1100 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1104 if (ImGui::IsItemHovered()) {
1106 {{ImGuiCol_PopupBg, theme.panel_bg_color},
1107 {ImGuiCol_Border, theme.panel_border_color}});
1108 if (ImGui::BeginTooltip()) {
1109 const std::string filename =
1110 obj_manager.ResolveFilename(obj_id, subtype);
1111 const bool has_base = !custom_base_path.empty();
1112 std::filesystem::path full_path =
1113 has_base ? (std::filesystem::path(custom_base_path) / filename)
1114 : std::filesystem::path();
1115 const bool file_exists = has_base && !filename.empty() &&
1116 std::filesystem::exists(full_path);
1118 ImGui::TextColored(theme.selection_primary,
1119 tr(
"Custom 0x%02X:%02X"), obj_id, subtype);
1120 ImGui::Text(
"%s", subtype_name.c_str());
1122 rendered ? theme.status_success : theme.status_warning,
1124 rendered ?
"rendered custom layout"
1126 :
"thumbnails off"));
1128 ImGui::Text(tr(
"File: %s"),
1129 filename.empty() ?
"(unmapped)" : filename.c_str());
1131 ImGui::TextColored(theme.text_warning_yellow,
1132 tr(
"Folder not configured in project"));
1133 }
else if (file_exists) {
1134 ImGui::TextColored(theme.status_success, tr(
"File found"));
1136 ImGui::TextColored(theme.status_error, tr(
"File missing: %s"),
1137 full_path.string().c_str());
1140 if (obj_id == 0x31 && subtype >= 2 && subtype <= 5) {
1141 const char* corner_id =
"";
1143 corner_id =
"0x100 (TL)";
1144 }
else if (subtype == 3) {
1145 corner_id =
"0x102 (TR)";
1146 }
else if (subtype == 4) {
1147 corner_id =
"0x101 (BL)";
1149 corner_id =
"0x103 (BR)";
1152 ImGui::TextColored(theme.status_active,
1153 tr(
"Also used by corner override %s"),
1156 ImGui::EndTooltip();
1161 custom_col = (custom_col + 1) % columns;
1169 ImGui::CloseCurrentPopup();