yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_editor_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
2#define YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
3
4#include <algorithm>
5#include <array>
6#include <cstdint>
7#include <functional>
8#include <string>
9#include "util/i18n/tr.h"
10
11#include "absl/strings/str_format.h"
18#include "app/gui/core/icons.h"
19#include "app/gui/core/input.h"
21#include "imgui/imgui.h"
22#include "zelda3/dungeon/room.h"
24
25namespace yaze {
26namespace editor {
27
39 public:
40 SpriteEditorPanel(int* current_room_id, DungeonRoomStore* rooms,
41 DungeonCanvasViewer* canvas_viewer = nullptr)
42 : current_room_id_(current_room_id),
43 rooms_(rooms),
44 canvas_viewer_(canvas_viewer) {}
45
46 // ==========================================================================
47 // WindowContent Identity
48 // ==========================================================================
49
50 std::string GetId() const override { return "dungeon.sprite_editor"; }
51 std::string GetDisplayName() const override { return "Sprite Editor"; }
52 std::string GetIcon() const override { return ICON_MD_PERSON; }
53 std::string GetEditorCategory() const override { return "Dungeon"; }
54 int GetPriority() const override { return 65; }
55 std::string GetWorkflowGroup() const override { return "Editors"; }
56
57 // ==========================================================================
58 // WindowContent Drawing
59 // ==========================================================================
60
61 void Draw(bool* p_open) override {
62 gui::AutoWidgetScope automation_scope("Dungeon/SpriteEditor");
63 if (!current_room_id_ || !rooms_) {
64 ImGui::TextDisabled(tr("No room data available"));
65 return;
66 }
67
68 if (*current_room_id_ < 0 ||
69 *current_room_id_ >= static_cast<int>(rooms_->size())) {
70 ImGui::TextDisabled(tr("No room selected"));
71 return;
72 }
73
75 ImGui::Separator();
77 ImGui::Separator();
79 }
80
81 // ==========================================================================
82 // Panel-Specific Methods
83 // ==========================================================================
84
86
88 std::function<void(const zelda3::Sprite&)> callback) {
89 sprite_placed_callback_ = std::move(callback);
90 }
91 void SetOpenSelectionInspectorCallback(std::function<void()> callback) {
92 open_selection_inspector_callback_ = std::move(callback);
93 }
94
95 private:
97 const auto& theme = AgentUI::GetTheme();
98 // Placement mode indicator
99 if (placement_mode_) {
100 ImGui::TextColored(
101 theme.status_warning, ICON_MD_PLACE " Placing: %s (0x%02X)",
103 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
104 placement_mode_ = false;
105 if (canvas_viewer_) {
107 }
108 }
109 } else {
110 ImGui::TextColored(theme.text_secondary_gray,
111 ICON_MD_INFO " Select a sprite to place");
112 }
113 }
114
116 const auto& theme = AgentUI::GetTheme();
117 ImGui::Text(ICON_MD_PERSON " Select Sprite:");
118
119 // Filter by category
120 static const char* kCategories[] = {"All", "Enemies", "NPCs", "Bosses",
121 "Items"};
122 ImGui::SetNextItemWidth(100);
123 ImGui::Combo("##Category", &selected_category_, kCategories,
124 IM_ARRAYSIZE(kCategories));
125 ImGui::SameLine();
126
127 // Search filter
128 ImGui::SetNextItemWidth(120);
129 ImGui::InputTextWithHint("##Search", "Search...", search_filter_,
130 sizeof(search_filter_));
131
132 // Sprite grid with responsive sizing
133 float available_height = ImGui::GetContentRegionAvail().y;
134 // Reserve space for room sprites section
135 float reserved_height = 120.0f;
136 // Calculate grid height: at least 150px, responsive to available space
137 float grid_height =
138 std::max(150.0f, std::min(400.0f, available_height - reserved_height));
139
140 // Responsive sprite size based on panel width
141 float panel_width = ImGui::GetContentRegionAvail().x;
142 float sprite_size =
143 std::max(28.0f, std::min(40.0f, (panel_width - 40.0f) / 8.0f));
144 int items_per_row =
145 std::max(1, static_cast<int>(panel_width / (sprite_size + 6)));
146
147 ImGui::BeginChild("##SpriteGrid", ImVec2(0, grid_height), true,
148 ImGuiWindowFlags_HorizontalScrollbar);
149
150 int col = 0;
151 for (int i = 0; i < 256; ++i) {
152 // Apply filters
153 if (!MatchesFilter(i))
154 continue;
155
156 bool is_selected = (selected_sprite_id_ == i);
157
158 ImGui::PushID(i);
159
160 // Color-coded button based on sprite type using theme colors
161 ImVec4 button_color = GetSpriteTypeColor(i, theme);
162 if (is_selected) {
163 button_color.x = std::min(1.0f, button_color.x + 0.2f);
164 button_color.y = std::min(1.0f, button_color.y + 0.2f);
165 button_color.z = std::min(1.0f, button_color.z + 0.2f);
166 }
167
168 {
169 gui::StyleColorGuard sprite_btn_guard(
170 {{ImGuiCol_Button, button_color},
171 {ImGuiCol_ButtonHovered,
172 ImVec4(std::min(1.0f, button_color.x + 0.1f),
173 std::min(1.0f, button_color.y + 0.1f),
174 std::min(1.0f, button_color.z + 0.1f), 1.0f)},
175 {ImGuiCol_ButtonActive,
176 ImVec4(std::min(1.0f, button_color.x + 0.2f),
177 std::min(1.0f, button_color.y + 0.2f),
178 std::min(1.0f, button_color.z + 0.2f), 1.0f)}});
179
180 // Get category icon based on sprite type
181 const char* icon = GetSpriteTypeIcon(i);
182 std::string label = absl::StrFormat("%s\n%02X", icon, i);
183 if (ImGui::Button(label.c_str(), ImVec2(sprite_size, sprite_size))) {
185 placement_mode_ = true;
186 if (canvas_viewer_) {
188 i);
189 }
190 }
191 }
192
193 if (ImGui::IsItemHovered()) {
194 const char* category = GetSpriteCategoryName(i);
195 ImGui::SetTooltip(
196 tr("%s (0x%02X)\n[%s]\nClick to select for placement"),
197 zelda3::ResolveSpriteName(i), i, category);
198 }
199
200 // Selection highlight using theme color
201 if (is_selected) {
202 ImVec2 min = ImGui::GetItemRectMin();
203 ImVec2 max = ImGui::GetItemRectMax();
204 ImU32 sel_color =
205 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
206 ImGui::GetWindowDrawList()->AddRect(min, max, sel_color, 0.0f, 0, 2.0f);
207 }
208
209 ImGui::PopID();
210
211 col++;
212 if (col < items_per_row) {
213 ImGui::SameLine();
214 } else {
215 col = 0;
216 }
217 }
218
219 ImGui::EndChild();
220 }
221
223 const auto& theme = AgentUI::GetTheme();
224 auto& room = (*rooms_)[*current_room_id_];
225 auto& sprites = room.GetSprites();
226 int selected_sprite_list_index = -1;
227 if (canvas_viewer_) {
228 const auto& selected_entities = canvas_viewer_->object_interaction()
231 if (selected_entities.size() == 1 &&
232 selected_entities.front().type == EntityType::Sprite) {
233 selected_sprite_list_index =
234 static_cast<int>(selected_entities.front().index);
235 }
236 }
237
238 // Sprite count with limit warning
239 int sprite_count = static_cast<int>(sprites.size());
240 ImVec4 count_color =
241 sprite_count > 16 ? theme.text_error_red : theme.text_primary;
242 ImGui::TextColored(count_color, ICON_MD_LIST " Room Sprites: %d/16",
243 sprite_count);
244 if (selected_sprite_list_index >= 0 && open_selection_inspector_callback_) {
245 ImGui::SameLine();
246 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW " Inspect Selected")) {
248 }
249 }
250
251 if (sprite_count > 16) {
252 ImGui::SameLine();
253 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING);
254 if (ImGui::IsItemHovered()) {
255 ImGui::SetTooltip(
256 tr("Room exceeds sprite limit (16 max)!\n"
257 "This may cause game crashes."));
258 }
259 }
260
261 if (sprites.empty()) {
262 ImGui::TextColored(theme.text_secondary_gray,
263 ICON_MD_INFO " No sprites in this room");
264 return;
265 }
266
267 if (selected_sprite_list_index >= 0 &&
268 selected_sprite_list_index < static_cast<int>(sprites.size()) &&
270 const auto& sprite = sprites[selected_sprite_list_index];
271 int sprite_x = sprite.x();
272 int sprite_y = sprite.y();
273 ImGui::TextUnformatted(tr("Selected position"));
274 ImGui::SameLine();
275 ImGui::SetNextItemWidth(64.0f);
276 const bool x_changed = gui::InputScalarDeferred(
277 "##SelectedSpriteX", ImGuiDataType_S32, &sprite_x, "%d", 0,
278 {reinterpret_cast<uintptr_t>(rooms_),
279 static_cast<uint64_t>(*current_room_id_),
280 static_cast<uint64_t>(selected_sprite_list_index)});
281 gui::AutoRegisterLastItem("input_int", "selected_sprite_x",
282 "Selected sprite X coordinate");
283 ImGui::SameLine();
284 ImGui::SetNextItemWidth(64.0f);
285 const bool y_changed = gui::InputScalarDeferred(
286 "##SelectedSpriteY", ImGuiDataType_S32, &sprite_y, "%d", 0,
287 {reinterpret_cast<uintptr_t>(rooms_),
288 static_cast<uint64_t>(*current_room_id_),
289 static_cast<uint64_t>(selected_sprite_list_index)});
290 gui::AutoRegisterLastItem("input_int", "selected_sprite_y",
291 "Selected sprite Y coordinate");
292 if (x_changed || y_changed) {
293 const int clamped_x =
294 std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
295 const int clamped_y =
296 std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
297 auto& handler = canvas_viewer_->object_interaction()
300 handler.NudgeSelected(clamped_x - static_cast<int>(sprite.x()),
301 clamped_y - static_cast<int>(sprite.y()));
302 }
303 }
304
305 // Split view: list on top, properties below
306 float available = ImGui::GetContentRegionAvail().y;
307 float list_height = std::max(100.0f, available * 0.4f);
308
309 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
310 for (size_t i = 0; i < sprites.size(); ++i) {
311 const auto& sprite = sprites[i];
312 bool is_selected = (selected_sprite_list_index == static_cast<int>(i));
313
314 ImGui::PushID(static_cast<int>(i));
315
316 // Build display string with indicators
317 std::string label = absl::StrFormat(
318 "[%02X] %s", sprite.id(), zelda3::ResolveSpriteName(sprite.id()));
319
320 // Add key drop indicator
321 if (sprite.key_drop() == 1) {
322 label += " " ICON_MD_KEY; // Small key
323 } else if (sprite.key_drop() == 2) {
324 label += " " ICON_MD_VPN_KEY; // Big key
325 }
326
327 // Add overlord indicator
328 if (sprite.IsOverlord()) {
329 label += " " ICON_MD_STAR;
330 }
331
332 const bool row_selected = ImGui::Selectable(label.c_str(), is_selected);
333 gui::AutoRegisterLastItem("selectable",
334 absl::StrFormat("sprite_row_%zu", i),
335 "Select a sprite in the current dungeon room");
336 if (row_selected) {
337 if (canvas_viewer_) {
339 i);
340 }
341 }
342
343 // Show position on same line
344 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
345 ImGui::TextColored(theme.text_secondary_gray, tr("(%d,%d) L%d"),
346 sprite.x(), sprite.y(), sprite.layer());
347
348 ImGui::PopID();
349 }
350 ImGui::EndChild();
351 }
352
353 bool MatchesFilter(int sprite_id) {
354 // Category filter
355 if (selected_category_ > 0) {
356 // Simplified category matching - in real implementation, use proper categorization
357 bool is_enemy = (sprite_id >= 0x09 && sprite_id <= 0x7F);
358 bool is_npc = (sprite_id >= 0x80 && sprite_id <= 0xBF);
359 bool is_boss = (sprite_id >= 0xC0 && sprite_id <= 0xD8);
360 bool is_item = (sprite_id >= 0xD9 && sprite_id <= 0xFF);
361
362 if (selected_category_ == 1 && !is_enemy)
363 return false;
364 if (selected_category_ == 2 && !is_npc)
365 return false;
366 if (selected_category_ == 3 && !is_boss)
367 return false;
368 if (selected_category_ == 4 && !is_item)
369 return false;
370 }
371
372 // Text search filter
373 if (search_filter_[0] != '\0') {
374 const char* name = zelda3::ResolveSpriteName(sprite_id);
375 // Simple case-insensitive substring search
376 std::string name_lower = name;
377 std::string filter_lower = search_filter_;
378 for (auto& c : name_lower)
379 c = static_cast<char>(tolower(c));
380 for (auto& c : filter_lower)
381 c = static_cast<char>(tolower(c));
382 if (name_lower.find(filter_lower) == std::string::npos) {
383 return false;
384 }
385 }
386
387 return true;
388 }
389
390 ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme& theme) {
391 // Color-code based on sprite type using theme colors
392 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
393 return theme.status_error; // Red for bosses
394 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
395 return theme.dungeon_sprite_layer0; // Green for NPCs
396 } else if (sprite_id >= 0xD9) {
397 return theme.dungeon_object_chest; // Gold for items
398 }
399 return theme.dungeon_sprite_layer1; // Blue for enemies
400 }
401
402 const char* GetSpriteTypeIcon(int sprite_id) {
403 // Return category-appropriate icons
404 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
405 return ICON_MD_DANGEROUS; // Skull for bosses
406 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
407 return ICON_MD_PERSON; // Person for NPCs
408 } else if (sprite_id >= 0xD9) {
409 return ICON_MD_STAR; // Star for items
410 }
411 return ICON_MD_PEST_CONTROL; // Bug for enemies
412 }
413
414 const char* GetSpriteCategoryName(int sprite_id) {
415 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
416 return "Boss";
417 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
418 return "NPC";
419 } else if (sprite_id >= 0xD9) {
420 return "Item";
421 }
422 return "Enemy";
423 }
424
425 int* current_room_id_ = nullptr;
428
429 // Selection state
432 char search_filter_[64] = {0};
433 bool placement_mode_ = false;
434
435 std::function<void(const zelda3::Sprite&)> sprite_placed_callback_;
437};
438
439} // namespace editor
440} // namespace yaze
441
442#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
DungeonObjectInteraction & object_interaction()
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SelectEntity(EntityType type, size_t index)
const std::vector< SelectedEntity > & GetSelectedEntities() const
SpriteInteractionHandler & sprite_handler()
WindowContent for placing and managing dungeon sprites.
std::function< void(const zelda3::Sprite &) sprite_placed_callback_)
void SetOpenSelectionInspectorCallback(std::function< void()> callback)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::string GetEditorCategory() const override
Editor category this panel belongs to.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void Draw(bool *p_open) override
Draw the panel content.
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme &theme)
const char * GetSpriteCategoryName(int sprite_id)
const char * GetSpriteTypeIcon(int sprite_id)
std::function< void()> open_selection_inspector_callback_
void SetSpritePlacedCallback(std::function< void(const zelda3::Sprite &)> callback)
std::string GetId() const override
Unique identifier for this panel.
int GetPriority() const override
Get display priority for menu ordering.
SpriteEditorPanel(int *current_room_id, DungeonRoomStore *rooms, DungeonCanvasViewer *canvas_viewer=nullptr)
std::string GetIcon() const override
Material Design icon for this panel.
Base interface for all logical window content components.
RAII scope that enables automatic widget registration.
RAII guard for ImGui style colors.
Definition style_guard.h:27
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_PLACE
Definition icons.h:1477
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_DANGEROUS
Definition icons.h:515
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_KEY
Definition icons.h:1026
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_VPN_KEY
Definition icons.h:2113
const AgentUITheme & GetTheme()
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:392
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:287
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19
Automatic widget registration helpers for ImGui Test Engine integration.