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
56 // ==========================================================================
57 // WindowContent Drawing
58 // ==========================================================================
59
60 void Draw(bool* p_open) override {
61 gui::AutoWidgetScope automation_scope("Dungeon/SpriteEditor");
62 if (!current_room_id_ || !rooms_) {
63 ImGui::TextDisabled(tr("No room data available"));
64 return;
65 }
66
67 if (*current_room_id_ < 0 ||
68 *current_room_id_ >= static_cast<int>(rooms_->size())) {
69 ImGui::TextDisabled(tr("No room selected"));
70 return;
71 }
72
74 ImGui::Separator();
76 ImGui::Separator();
78 }
79
80 // ==========================================================================
81 // Panel-Specific Methods
82 // ==========================================================================
83
85
87 std::function<void(const zelda3::Sprite&)> callback) {
88 sprite_placed_callback_ = std::move(callback);
89 }
90 void SetOpenSelectionInspectorCallback(std::function<void()> callback) {
91 open_selection_inspector_callback_ = std::move(callback);
92 }
93
94 private:
96 const auto& theme = AgentUI::GetTheme();
97 // Placement mode indicator
98 if (placement_mode_) {
99 ImGui::TextColored(
100 theme.status_warning, ICON_MD_PLACE " Placing: %s (0x%02X)",
102 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
103 placement_mode_ = false;
104 if (canvas_viewer_) {
106 }
107 }
108 } else {
109 ImGui::TextColored(theme.text_secondary_gray,
110 ICON_MD_INFO " Select a sprite to place");
111 }
112 }
113
115 const auto& theme = AgentUI::GetTheme();
116 ImGui::Text(ICON_MD_PERSON " Select Sprite:");
117
118 // Filter by category
119 static const char* kCategories[] = {"All", "Enemies", "NPCs", "Bosses",
120 "Items"};
121 ImGui::SetNextItemWidth(100);
122 ImGui::Combo("##Category", &selected_category_, kCategories,
123 IM_ARRAYSIZE(kCategories));
124 ImGui::SameLine();
125
126 // Search filter
127 ImGui::SetNextItemWidth(120);
128 ImGui::InputTextWithHint("##Search", "Search...", search_filter_,
129 sizeof(search_filter_));
130
131 // Sprite grid with responsive sizing
132 float available_height = ImGui::GetContentRegionAvail().y;
133 // Reserve space for room sprites section
134 float reserved_height = 120.0f;
135 // Calculate grid height: at least 150px, responsive to available space
136 float grid_height =
137 std::max(150.0f, std::min(400.0f, available_height - reserved_height));
138
139 // Responsive sprite size based on panel width
140 float panel_width = ImGui::GetContentRegionAvail().x;
141 float sprite_size =
142 std::max(28.0f, std::min(40.0f, (panel_width - 40.0f) / 8.0f));
143 int items_per_row =
144 std::max(1, static_cast<int>(panel_width / (sprite_size + 6)));
145
146 ImGui::BeginChild("##SpriteGrid", ImVec2(0, grid_height), true,
147 ImGuiWindowFlags_HorizontalScrollbar);
148
149 int col = 0;
150 for (int i = 0; i < 256; ++i) {
151 // Apply filters
152 if (!MatchesFilter(i))
153 continue;
154
155 bool is_selected = (selected_sprite_id_ == i);
156
157 ImGui::PushID(i);
158
159 // Color-coded button based on sprite type using theme colors
160 ImVec4 button_color = GetSpriteTypeColor(i, theme);
161 if (is_selected) {
162 button_color.x = std::min(1.0f, button_color.x + 0.2f);
163 button_color.y = std::min(1.0f, button_color.y + 0.2f);
164 button_color.z = std::min(1.0f, button_color.z + 0.2f);
165 }
166
167 {
168 gui::StyleColorGuard sprite_btn_guard(
169 {{ImGuiCol_Button, button_color},
170 {ImGuiCol_ButtonHovered,
171 ImVec4(std::min(1.0f, button_color.x + 0.1f),
172 std::min(1.0f, button_color.y + 0.1f),
173 std::min(1.0f, button_color.z + 0.1f), 1.0f)},
174 {ImGuiCol_ButtonActive,
175 ImVec4(std::min(1.0f, button_color.x + 0.2f),
176 std::min(1.0f, button_color.y + 0.2f),
177 std::min(1.0f, button_color.z + 0.2f), 1.0f)}});
178
179 // Get category icon based on sprite type
180 const char* icon = GetSpriteTypeIcon(i);
181 std::string label = absl::StrFormat("%s\n%02X", icon, i);
182 if (ImGui::Button(label.c_str(), ImVec2(sprite_size, sprite_size))) {
184 placement_mode_ = true;
185 if (canvas_viewer_) {
187 i);
188 }
189 }
190 }
191
192 if (ImGui::IsItemHovered()) {
193 const char* category = GetSpriteCategoryName(i);
194 ImGui::SetTooltip(
195 tr("%s (0x%02X)\n[%s]\nClick to select for placement"),
196 zelda3::ResolveSpriteName(i), i, category);
197 }
198
199 // Selection highlight using theme color
200 if (is_selected) {
201 ImVec2 min = ImGui::GetItemRectMin();
202 ImVec2 max = ImGui::GetItemRectMax();
203 ImU32 sel_color =
204 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
205 ImGui::GetWindowDrawList()->AddRect(min, max, sel_color, 0.0f, 0, 2.0f);
206 }
207
208 ImGui::PopID();
209
210 col++;
211 if (col < items_per_row) {
212 ImGui::SameLine();
213 } else {
214 col = 0;
215 }
216 }
217
218 ImGui::EndChild();
219 }
220
222 const auto& theme = AgentUI::GetTheme();
223 auto& room = (*rooms_)[*current_room_id_];
224 auto& sprites = room.GetSprites();
225 int selected_sprite_list_index = -1;
226 if (canvas_viewer_) {
227 const auto& selected_entities = canvas_viewer_->object_interaction()
230 if (selected_entities.size() == 1 &&
231 selected_entities.front().type == EntityType::Sprite) {
232 selected_sprite_list_index =
233 static_cast<int>(selected_entities.front().index);
234 }
235 }
236
237 // Sprite count with limit warning
238 int sprite_count = static_cast<int>(sprites.size());
239 ImVec4 count_color =
240 sprite_count > 16 ? theme.text_error_red : theme.text_primary;
241 ImGui::TextColored(count_color, ICON_MD_LIST " Room Sprites: %d/16",
242 sprite_count);
243 if (selected_sprite_list_index >= 0 && open_selection_inspector_callback_) {
244 ImGui::SameLine();
245 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW " Inspect Selected")) {
247 }
248 }
249
250 if (sprite_count > 16) {
251 ImGui::SameLine();
252 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING);
253 if (ImGui::IsItemHovered()) {
254 ImGui::SetTooltip(
255 tr("Room exceeds sprite limit (16 max)!\n"
256 "This may cause game crashes."));
257 }
258 }
259
260 if (sprites.empty()) {
261 ImGui::TextColored(theme.text_secondary_gray,
262 ICON_MD_INFO " No sprites in this room");
263 return;
264 }
265
266 if (selected_sprite_list_index >= 0 &&
267 selected_sprite_list_index < static_cast<int>(sprites.size()) &&
269 const auto& sprite = sprites[selected_sprite_list_index];
270 int sprite_x = sprite.x();
271 int sprite_y = sprite.y();
272 ImGui::TextUnformatted(tr("Selected position"));
273 ImGui::SameLine();
274 ImGui::SetNextItemWidth(64.0f);
275 const bool x_changed = gui::InputScalarDeferred(
276 "##SelectedSpriteX", ImGuiDataType_S32, &sprite_x, "%d", 0,
277 {reinterpret_cast<uintptr_t>(rooms_),
278 static_cast<uint64_t>(*current_room_id_),
279 static_cast<uint64_t>(selected_sprite_list_index)});
280 gui::AutoRegisterLastItem("input_int", "selected_sprite_x",
281 "Selected sprite X coordinate");
282 ImGui::SameLine();
283 ImGui::SetNextItemWidth(64.0f);
284 const bool y_changed = gui::InputScalarDeferred(
285 "##SelectedSpriteY", ImGuiDataType_S32, &sprite_y, "%d", 0,
286 {reinterpret_cast<uintptr_t>(rooms_),
287 static_cast<uint64_t>(*current_room_id_),
288 static_cast<uint64_t>(selected_sprite_list_index)});
289 gui::AutoRegisterLastItem("input_int", "selected_sprite_y",
290 "Selected sprite Y coordinate");
291 if (x_changed || y_changed) {
292 const int clamped_x =
293 std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
294 const int clamped_y =
295 std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
296 auto& handler = canvas_viewer_->object_interaction()
299 handler.NudgeSelected(clamped_x - static_cast<int>(sprite.x()),
300 clamped_y - static_cast<int>(sprite.y()));
301 }
302 }
303
304 // Split view: list on top, properties below
305 float available = ImGui::GetContentRegionAvail().y;
306 float list_height = std::max(100.0f, available * 0.4f);
307
308 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
309 for (size_t i = 0; i < sprites.size(); ++i) {
310 const auto& sprite = sprites[i];
311 bool is_selected = (selected_sprite_list_index == static_cast<int>(i));
312
313 ImGui::PushID(static_cast<int>(i));
314
315 // Build display string with indicators
316 std::string label = absl::StrFormat(
317 "[%02X] %s", sprite.id(), zelda3::ResolveSpriteName(sprite.id()));
318
319 // Add key drop indicator
320 if (sprite.key_drop() == 1) {
321 label += " " ICON_MD_KEY; // Small key
322 } else if (sprite.key_drop() == 2) {
323 label += " " ICON_MD_VPN_KEY; // Big key
324 }
325
326 // Add overlord indicator
327 if (sprite.IsOverlord()) {
328 label += " " ICON_MD_STAR;
329 }
330
331 const bool row_selected = ImGui::Selectable(label.c_str(), is_selected);
332 gui::AutoRegisterLastItem("selectable",
333 absl::StrFormat("sprite_row_%zu", i),
334 "Select a sprite in the current dungeon room");
335 if (row_selected) {
336 if (canvas_viewer_) {
338 i);
339 }
340 }
341
342 // Show position on same line
343 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
344 ImGui::TextColored(theme.text_secondary_gray, tr("(%d,%d) L%d"),
345 sprite.x(), sprite.y(), sprite.layer());
346
347 ImGui::PopID();
348 }
349 ImGui::EndChild();
350 }
351
352 bool MatchesFilter(int sprite_id) {
353 // Category filter
354 if (selected_category_ > 0) {
355 // Simplified category matching - in real implementation, use proper categorization
356 bool is_enemy = (sprite_id >= 0x09 && sprite_id <= 0x7F);
357 bool is_npc = (sprite_id >= 0x80 && sprite_id <= 0xBF);
358 bool is_boss = (sprite_id >= 0xC0 && sprite_id <= 0xD8);
359 bool is_item = (sprite_id >= 0xD9 && sprite_id <= 0xFF);
360
361 if (selected_category_ == 1 && !is_enemy)
362 return false;
363 if (selected_category_ == 2 && !is_npc)
364 return false;
365 if (selected_category_ == 3 && !is_boss)
366 return false;
367 if (selected_category_ == 4 && !is_item)
368 return false;
369 }
370
371 // Text search filter
372 if (search_filter_[0] != '\0') {
373 const char* name = zelda3::ResolveSpriteName(sprite_id);
374 // Simple case-insensitive substring search
375 std::string name_lower = name;
376 std::string filter_lower = search_filter_;
377 for (auto& c : name_lower)
378 c = static_cast<char>(tolower(c));
379 for (auto& c : filter_lower)
380 c = static_cast<char>(tolower(c));
381 if (name_lower.find(filter_lower) == std::string::npos) {
382 return false;
383 }
384 }
385
386 return true;
387 }
388
389 ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme& theme) {
390 // Color-code based on sprite type using theme colors
391 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
392 return theme.status_error; // Red for bosses
393 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
394 return theme.dungeon_sprite_layer0; // Green for NPCs
395 } else if (sprite_id >= 0xD9) {
396 return theme.dungeon_object_chest; // Gold for items
397 }
398 return theme.dungeon_sprite_layer1; // Blue for enemies
399 }
400
401 const char* GetSpriteTypeIcon(int sprite_id) {
402 // Return category-appropriate icons
403 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
404 return ICON_MD_DANGEROUS; // Skull for bosses
405 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
406 return ICON_MD_PERSON; // Person for NPCs
407 } else if (sprite_id >= 0xD9) {
408 return ICON_MD_STAR; // Star for items
409 }
410 return ICON_MD_PEST_CONTROL; // Bug for enemies
411 }
412
413 const char* GetSpriteCategoryName(int sprite_id) {
414 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
415 return "Boss";
416 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
417 return "NPC";
418 } else if (sprite_id >= 0xD9) {
419 return "Item";
420 }
421 return "Enemy";
422 }
423
424 int* current_room_id_ = nullptr;
427
428 // Selection state
431 char search_filter_[64] = {0};
432 bool placement_mode_ = false;
433
434 std::function<void(const zelda3::Sprite&)> sprite_placed_callback_;
436};
437
438} // namespace editor
439} // namespace yaze
440
441#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.
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:37
#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:388
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19
Automatic widget registration helpers for ImGui Test Engine integration.