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 <array>
5#include <cstdint>
6#include <functional>
7#include <string>
8#include "util/i18n/tr.h"
9
10#include "absl/strings/str_format.h"
15#include "app/gui/core/icons.h"
17#include "imgui/imgui.h"
18#include "zelda3/dungeon/room.h"
20
21namespace yaze {
22namespace editor {
23
35 public:
36 SpriteEditorPanel(int* current_room_id, DungeonRoomStore* rooms,
37 DungeonCanvasViewer* canvas_viewer = nullptr)
38 : current_room_id_(current_room_id),
39 rooms_(rooms),
40 canvas_viewer_(canvas_viewer) {}
41
42 // ==========================================================================
43 // WindowContent Identity
44 // ==========================================================================
45
46 std::string GetId() const override { return "dungeon.sprite_editor"; }
47 std::string GetDisplayName() const override { return "Sprite Editor"; }
48 std::string GetIcon() const override { return ICON_MD_PERSON; }
49 std::string GetEditorCategory() const override { return "Dungeon"; }
50 int GetPriority() const override { return 65; }
51
52 // ==========================================================================
53 // WindowContent Drawing
54 // ==========================================================================
55
56 void Draw(bool* p_open) override {
57 if (!current_room_id_ || !rooms_) {
58 ImGui::TextDisabled(tr("No room data available"));
59 return;
60 }
61
62 if (*current_room_id_ < 0 ||
63 *current_room_id_ >= static_cast<int>(rooms_->size())) {
64 ImGui::TextDisabled(tr("No room selected"));
65 return;
66 }
67
69 ImGui::Separator();
71 ImGui::Separator();
73 }
74
75 // ==========================================================================
76 // Panel-Specific Methods
77 // ==========================================================================
78
80
82 std::function<void(const zelda3::Sprite&)> callback) {
83 sprite_placed_callback_ = std::move(callback);
84 }
85 void SetOpenSelectionInspectorCallback(std::function<void()> callback) {
86 open_selection_inspector_callback_ = std::move(callback);
87 }
88
89 private:
91 const auto& theme = AgentUI::GetTheme();
92 // Placement mode indicator
93 if (placement_mode_) {
94 ImGui::TextColored(
95 theme.status_warning, ICON_MD_PLACE " Placing: %s (0x%02X)",
97 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
98 placement_mode_ = false;
99 if (canvas_viewer_) {
101 }
102 }
103 } else {
104 ImGui::TextColored(theme.text_secondary_gray,
105 ICON_MD_INFO " Select a sprite to place");
106 }
107 }
108
110 const auto& theme = AgentUI::GetTheme();
111 ImGui::Text(ICON_MD_PERSON " Select Sprite:");
112
113 // Filter by category
114 static const char* kCategories[] = {"All", "Enemies", "NPCs", "Bosses",
115 "Items"};
116 ImGui::SetNextItemWidth(100);
117 ImGui::Combo("##Category", &selected_category_, kCategories,
118 IM_ARRAYSIZE(kCategories));
119 ImGui::SameLine();
120
121 // Search filter
122 ImGui::SetNextItemWidth(120);
123 ImGui::InputTextWithHint("##Search", "Search...", search_filter_,
124 sizeof(search_filter_));
125
126 // Sprite grid with responsive sizing
127 float available_height = ImGui::GetContentRegionAvail().y;
128 // Reserve space for room sprites section
129 float reserved_height = 120.0f;
130 // Calculate grid height: at least 150px, responsive to available space
131 float grid_height =
132 std::max(150.0f, std::min(400.0f, available_height - reserved_height));
133
134 // Responsive sprite size based on panel width
135 float panel_width = ImGui::GetContentRegionAvail().x;
136 float sprite_size =
137 std::max(28.0f, std::min(40.0f, (panel_width - 40.0f) / 8.0f));
138 int items_per_row =
139 std::max(1, static_cast<int>(panel_width / (sprite_size + 6)));
140
141 ImGui::BeginChild("##SpriteGrid", ImVec2(0, grid_height), true,
142 ImGuiWindowFlags_HorizontalScrollbar);
143
144 int col = 0;
145 for (int i = 0; i < 256; ++i) {
146 // Apply filters
147 if (!MatchesFilter(i))
148 continue;
149
150 bool is_selected = (selected_sprite_id_ == i);
151
152 ImGui::PushID(i);
153
154 // Color-coded button based on sprite type using theme colors
155 ImVec4 button_color = GetSpriteTypeColor(i, theme);
156 if (is_selected) {
157 button_color.x = std::min(1.0f, button_color.x + 0.2f);
158 button_color.y = std::min(1.0f, button_color.y + 0.2f);
159 button_color.z = std::min(1.0f, button_color.z + 0.2f);
160 }
161
162 {
163 gui::StyleColorGuard sprite_btn_guard(
164 {{ImGuiCol_Button, button_color},
165 {ImGuiCol_ButtonHovered,
166 ImVec4(std::min(1.0f, button_color.x + 0.1f),
167 std::min(1.0f, button_color.y + 0.1f),
168 std::min(1.0f, button_color.z + 0.1f), 1.0f)},
169 {ImGuiCol_ButtonActive,
170 ImVec4(std::min(1.0f, button_color.x + 0.2f),
171 std::min(1.0f, button_color.y + 0.2f),
172 std::min(1.0f, button_color.z + 0.2f), 1.0f)}});
173
174 // Get category icon based on sprite type
175 const char* icon = GetSpriteTypeIcon(i);
176 std::string label = absl::StrFormat("%s\n%02X", icon, i);
177 if (ImGui::Button(label.c_str(), ImVec2(sprite_size, sprite_size))) {
179 placement_mode_ = true;
180 if (canvas_viewer_) {
182 i);
183 }
184 }
185 }
186
187 if (ImGui::IsItemHovered()) {
188 const char* category = GetSpriteCategoryName(i);
189 ImGui::SetTooltip(
190 tr("%s (0x%02X)\n[%s]\nClick to select for placement"),
191 zelda3::ResolveSpriteName(i), i, category);
192 }
193
194 // Selection highlight using theme color
195 if (is_selected) {
196 ImVec2 min = ImGui::GetItemRectMin();
197 ImVec2 max = ImGui::GetItemRectMax();
198 ImU32 sel_color =
199 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
200 ImGui::GetWindowDrawList()->AddRect(min, max, sel_color, 0.0f, 0, 2.0f);
201 }
202
203 ImGui::PopID();
204
205 col++;
206 if (col < items_per_row) {
207 ImGui::SameLine();
208 } else {
209 col = 0;
210 }
211 }
212
213 ImGui::EndChild();
214 }
215
217 const auto& theme = AgentUI::GetTheme();
218 auto& room = (*rooms_)[*current_room_id_];
219 auto& sprites = room.GetSprites();
220 int selected_sprite_list_index = -1;
221 if (canvas_viewer_ &&
223 const auto selected_entity =
225 if (selected_entity.type == EntityType::Sprite) {
226 selected_sprite_list_index = static_cast<int>(selected_entity.index);
227 }
228 }
229
230 // Sprite count with limit warning
231 int sprite_count = static_cast<int>(sprites.size());
232 ImVec4 count_color =
233 sprite_count > 16 ? theme.text_error_red : theme.text_primary;
234 ImGui::TextColored(count_color, ICON_MD_LIST " Room Sprites: %d/16",
235 sprite_count);
236 if (selected_sprite_list_index >= 0 && open_selection_inspector_callback_) {
237 ImGui::SameLine();
238 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW " Inspect Selected")) {
240 }
241 }
242
243 if (sprite_count > 16) {
244 ImGui::SameLine();
245 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING);
246 if (ImGui::IsItemHovered()) {
247 ImGui::SetTooltip(
248 tr("Room exceeds sprite limit (16 max)!\n"
249 "This may cause game crashes."));
250 }
251 }
252
253 if (sprites.empty()) {
254 ImGui::TextColored(theme.text_secondary_gray,
255 ICON_MD_INFO " No sprites in this room");
256 return;
257 }
258
259 // Split view: list on top, properties below
260 float available = ImGui::GetContentRegionAvail().y;
261 float list_height = std::max(100.0f, available * 0.4f);
262
263 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
264 for (size_t i = 0; i < sprites.size(); ++i) {
265 const auto& sprite = sprites[i];
266 bool is_selected = (selected_sprite_list_index == static_cast<int>(i));
267
268 ImGui::PushID(static_cast<int>(i));
269
270 // Build display string with indicators
271 std::string label = absl::StrFormat(
272 "[%02X] %s", sprite.id(), zelda3::ResolveSpriteName(sprite.id()));
273
274 // Add key drop indicator
275 if (sprite.key_drop() == 1) {
276 label += " " ICON_MD_KEY; // Small key
277 } else if (sprite.key_drop() == 2) {
278 label += " " ICON_MD_VPN_KEY; // Big key
279 }
280
281 // Add overlord indicator
282 if (sprite.IsOverlord()) {
283 label += " " ICON_MD_STAR;
284 }
285
286 if (ImGui::Selectable(label.c_str(), is_selected)) {
287 if (canvas_viewer_) {
289 i);
290 }
291 }
292
293 // Show position on same line
294 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
295 ImGui::TextColored(theme.text_secondary_gray, tr("(%d,%d) L%d"),
296 sprite.x(), sprite.y(), sprite.layer());
297
298 ImGui::PopID();
299 }
300 ImGui::EndChild();
301 }
302
303 bool MatchesFilter(int sprite_id) {
304 // Category filter
305 if (selected_category_ > 0) {
306 // Simplified category matching - in real implementation, use proper categorization
307 bool is_enemy = (sprite_id >= 0x09 && sprite_id <= 0x7F);
308 bool is_npc = (sprite_id >= 0x80 && sprite_id <= 0xBF);
309 bool is_boss = (sprite_id >= 0xC0 && sprite_id <= 0xD8);
310 bool is_item = (sprite_id >= 0xD9 && sprite_id <= 0xFF);
311
312 if (selected_category_ == 1 && !is_enemy)
313 return false;
314 if (selected_category_ == 2 && !is_npc)
315 return false;
316 if (selected_category_ == 3 && !is_boss)
317 return false;
318 if (selected_category_ == 4 && !is_item)
319 return false;
320 }
321
322 // Text search filter
323 if (search_filter_[0] != '\0') {
324 const char* name = zelda3::ResolveSpriteName(sprite_id);
325 // Simple case-insensitive substring search
326 std::string name_lower = name;
327 std::string filter_lower = search_filter_;
328 for (auto& c : name_lower)
329 c = static_cast<char>(tolower(c));
330 for (auto& c : filter_lower)
331 c = static_cast<char>(tolower(c));
332 if (name_lower.find(filter_lower) == std::string::npos) {
333 return false;
334 }
335 }
336
337 return true;
338 }
339
340 ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme& theme) {
341 // Color-code based on sprite type using theme colors
342 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
343 return theme.status_error; // Red for bosses
344 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
345 return theme.dungeon_sprite_layer0; // Green for NPCs
346 } else if (sprite_id >= 0xD9) {
347 return theme.dungeon_object_chest; // Gold for items
348 }
349 return theme.dungeon_sprite_layer1; // Blue for enemies
350 }
351
352 const char* GetSpriteTypeIcon(int sprite_id) {
353 // Return category-appropriate icons
354 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
355 return ICON_MD_DANGEROUS; // Skull for bosses
356 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
357 return ICON_MD_PERSON; // Person for NPCs
358 } else if (sprite_id >= 0xD9) {
359 return ICON_MD_STAR; // Star for items
360 }
361 return ICON_MD_PEST_CONTROL; // Bug for enemies
362 }
363
364 const char* GetSpriteCategoryName(int sprite_id) {
365 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
366 return "Boss";
367 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
368 return "NPC";
369 } else if (sprite_id >= 0xD9) {
370 return "Item";
371 }
372 return "Enemy";
373 }
374
375 int* current_room_id_ = nullptr;
378
379 // Selection state
382 char search_filter_[64] = {0};
383 bool placement_mode_ = false;
384
385 std::function<void(const zelda3::Sprite&)> sprite_placed_callback_;
387};
388
389} // namespace editor
390} // namespace yaze
391
392#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
DungeonObjectInteraction & object_interaction()
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
void SelectEntity(EntityType type, size_t index)
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 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()
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19