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
9#include "absl/strings/str_format.h"
14#include "app/gui/core/icons.h"
16#include "imgui/imgui.h"
17#include "zelda3/dungeon/room.h"
19
20namespace yaze {
21namespace editor {
22
34 public:
35 SpriteEditorPanel(int* current_room_id, DungeonRoomStore* rooms,
36 DungeonCanvasViewer* canvas_viewer = nullptr)
37 : current_room_id_(current_room_id),
38 rooms_(rooms),
39 canvas_viewer_(canvas_viewer) {}
40
41 // ==========================================================================
42 // WindowContent Identity
43 // ==========================================================================
44
45 std::string GetId() const override { return "dungeon.sprite_editor"; }
46 std::string GetDisplayName() const override { return "Sprite Editor"; }
47 std::string GetIcon() const override { return ICON_MD_PERSON; }
48 std::string GetEditorCategory() const override { return "Dungeon"; }
49 int GetPriority() const override { return 65; }
50
51 // ==========================================================================
52 // WindowContent Drawing
53 // ==========================================================================
54
55 void Draw(bool* p_open) override {
56 if (!current_room_id_ || !rooms_) {
57 ImGui::TextDisabled("No room data available");
58 return;
59 }
60
61 if (*current_room_id_ < 0 ||
62 *current_room_id_ >= static_cast<int>(rooms_->size())) {
63 ImGui::TextDisabled("No room selected");
64 return;
65 }
66
68 ImGui::Separator();
70 ImGui::Separator();
72 }
73
74 // ==========================================================================
75 // Panel-Specific Methods
76 // ==========================================================================
77
79
81 std::function<void(const zelda3::Sprite&)> callback) {
82 sprite_placed_callback_ = std::move(callback);
83 }
84
85 private:
87 const auto& theme = AgentUI::GetTheme();
88 // Placement mode indicator
89 if (placement_mode_) {
90 ImGui::TextColored(
91 theme.status_warning, ICON_MD_PLACE " Placing: %s (0x%02X)",
93 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
94 placement_mode_ = false;
95 if (canvas_viewer_) {
97 }
98 }
99 } else {
100 ImGui::TextColored(theme.text_secondary_gray,
101 ICON_MD_INFO " Select a sprite to place");
102 }
103 }
104
106 const auto& theme = AgentUI::GetTheme();
107 ImGui::Text(ICON_MD_PERSON " Select Sprite:");
108
109 // Filter by category
110 static const char* kCategories[] = {"All", "Enemies", "NPCs", "Bosses",
111 "Items"};
112 ImGui::SetNextItemWidth(100);
113 ImGui::Combo("##Category", &selected_category_, kCategories,
114 IM_ARRAYSIZE(kCategories));
115 ImGui::SameLine();
116
117 // Search filter
118 ImGui::SetNextItemWidth(120);
119 ImGui::InputTextWithHint("##Search", "Search...", search_filter_,
120 sizeof(search_filter_));
121
122 // Sprite grid with responsive sizing
123 float available_height = ImGui::GetContentRegionAvail().y;
124 // Reserve space for room sprites section
125 float reserved_height = 120.0f;
126 // Calculate grid height: at least 150px, responsive to available space
127 float grid_height =
128 std::max(150.0f, std::min(400.0f, available_height - reserved_height));
129
130 // Responsive sprite size based on panel width
131 float panel_width = ImGui::GetContentRegionAvail().x;
132 float sprite_size =
133 std::max(28.0f, std::min(40.0f, (panel_width - 40.0f) / 8.0f));
134 int items_per_row =
135 std::max(1, static_cast<int>(panel_width / (sprite_size + 6)));
136
137 ImGui::BeginChild("##SpriteGrid", ImVec2(0, grid_height), true,
138 ImGuiWindowFlags_HorizontalScrollbar);
139
140 int col = 0;
141 for (int i = 0; i < 256; ++i) {
142 // Apply filters
143 if (!MatchesFilter(i))
144 continue;
145
146 bool is_selected = (selected_sprite_id_ == i);
147
148 ImGui::PushID(i);
149
150 // Color-coded button based on sprite type using theme colors
151 ImVec4 button_color = GetSpriteTypeColor(i, theme);
152 if (is_selected) {
153 button_color.x = std::min(1.0f, button_color.x + 0.2f);
154 button_color.y = std::min(1.0f, button_color.y + 0.2f);
155 button_color.z = std::min(1.0f, button_color.z + 0.2f);
156 }
157
158 {
159 gui::StyleColorGuard sprite_btn_guard(
160 {{ImGuiCol_Button, button_color},
161 {ImGuiCol_ButtonHovered,
162 ImVec4(std::min(1.0f, button_color.x + 0.1f),
163 std::min(1.0f, button_color.y + 0.1f),
164 std::min(1.0f, button_color.z + 0.1f), 1.0f)},
165 {ImGuiCol_ButtonActive,
166 ImVec4(std::min(1.0f, button_color.x + 0.2f),
167 std::min(1.0f, button_color.y + 0.2f),
168 std::min(1.0f, button_color.z + 0.2f), 1.0f)}});
169
170 // Get category icon based on sprite type
171 const char* icon = GetSpriteTypeIcon(i);
172 std::string label = absl::StrFormat("%s\n%02X", icon, i);
173 if (ImGui::Button(label.c_str(), ImVec2(sprite_size, sprite_size))) {
175 placement_mode_ = true;
176 if (canvas_viewer_) {
178 i);
179 }
180 }
181 }
182
183 if (ImGui::IsItemHovered()) {
184 const char* category = GetSpriteCategoryName(i);
185 ImGui::SetTooltip("%s (0x%02X)\n[%s]\nClick to select for placement",
186 zelda3::ResolveSpriteName(i), i, category);
187 }
188
189 // Selection highlight using theme color
190 if (is_selected) {
191 ImVec2 min = ImGui::GetItemRectMin();
192 ImVec2 max = ImGui::GetItemRectMax();
193 ImU32 sel_color =
194 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
195 ImGui::GetWindowDrawList()->AddRect(min, max, sel_color, 0.0f, 0, 2.0f);
196 }
197
198 ImGui::PopID();
199
200 col++;
201 if (col < items_per_row) {
202 ImGui::SameLine();
203 } else {
204 col = 0;
205 }
206 }
207
208 ImGui::EndChild();
209 }
210
212 const auto& theme = AgentUI::GetTheme();
213 auto& room = (*rooms_)[*current_room_id_];
214 auto& sprites = room.GetSprites();
215
216 // Sprite count with limit warning
217 int sprite_count = static_cast<int>(sprites.size());
218 ImVec4 count_color =
219 sprite_count > 16 ? theme.text_error_red : theme.text_primary;
220 ImGui::TextColored(count_color, ICON_MD_LIST " Room Sprites: %d/16",
221 sprite_count);
222
223 if (sprite_count > 16) {
224 ImGui::SameLine();
225 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING);
226 if (ImGui::IsItemHovered()) {
227 ImGui::SetTooltip(
228 "Room exceeds sprite limit (16 max)!\n"
229 "This may cause game crashes.");
230 }
231 }
232
233 if (sprites.empty()) {
234 ImGui::TextColored(theme.text_secondary_gray,
235 ICON_MD_INFO " No sprites in this room");
236 return;
237 }
238
239 // Split view: list on top, properties below
240 float available = ImGui::GetContentRegionAvail().y;
241 float list_height = std::max(100.0f, available * 0.4f);
242
243 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
244 for (size_t i = 0; i < sprites.size(); ++i) {
245 const auto& sprite = sprites[i];
246 bool is_selected = (selected_sprite_list_index_ == static_cast<int>(i));
247
248 ImGui::PushID(static_cast<int>(i));
249
250 // Build display string with indicators
251 std::string label = absl::StrFormat(
252 "[%02X] %s", sprite.id(), zelda3::ResolveSpriteName(sprite.id()));
253
254 // Add key drop indicator
255 if (sprite.key_drop() == 1) {
256 label += " " ICON_MD_KEY; // Small key
257 } else if (sprite.key_drop() == 2) {
258 label += " " ICON_MD_VPN_KEY; // Big key
259 }
260
261 // Add overlord indicator
262 if (sprite.IsOverlord()) {
263 label += " " ICON_MD_STAR;
264 }
265
266 if (ImGui::Selectable(label.c_str(), is_selected)) {
267 selected_sprite_list_index_ = static_cast<int>(i);
268 }
269
270 // Show position on same line
271 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
272 ImGui::TextColored(theme.text_secondary_gray, "(%d,%d) L%d", sprite.x(),
273 sprite.y(), sprite.layer());
274
275 ImGui::PopID();
276 }
277 ImGui::EndChild();
278
279 // Sprite properties panel
281 }
282
284 const auto& theme = AgentUI::GetTheme();
285 auto& room = (*rooms_)[*current_room_id_];
286 auto& sprites = room.GetSprites();
287
289 selected_sprite_list_index_ >= static_cast<int>(sprites.size())) {
290 ImGui::TextColored(theme.text_secondary_gray,
291 ICON_MD_INFO " Select a sprite to edit properties");
292 return;
293 }
294
295 auto& sprite = sprites[selected_sprite_list_index_];
296
297 ImGui::Separator();
298 ImGui::Text(ICON_MD_EDIT " Sprite Properties");
299
300 // Overlord badge (read-only)
301 if (sprite.IsOverlord()) {
302 ImGui::SameLine();
303 ImGui::TextColored(theme.status_warning, ICON_MD_STAR " OVERLORD");
304 if (ImGui::IsItemHovered()) {
305 ImGui::SetTooltip(
306 "This is an Overlord sprite.\n"
307 "Overlords have separate limits (8 max).");
308 }
309 }
310
311 // ID and Name (read-only)
312 ImGui::Text("ID: 0x%02X - %s", sprite.id(),
313 zelda3::ResolveSpriteName(sprite.id()));
314
315 // Position (editable)
316 int pos_x = sprite.x();
317 int pos_y = sprite.y();
318 ImGui::SetNextItemWidth(60);
319 if (ImGui::InputInt("X##SpriteX", &pos_x, 1, 8)) {
320 pos_x = std::clamp(pos_x, 0, 63);
321 // Note: Need setter in Sprite class
322 }
323 ImGui::SameLine();
324 ImGui::SetNextItemWidth(60);
325 if (ImGui::InputInt("Y##SpriteY", &pos_y, 1, 8)) {
326 pos_y = std::clamp(pos_y, 0, 63);
327 // Note: Need setter in Sprite class
328 }
329
330 // Subtype selector (0-7)
331 int subtype = sprite.subtype();
332 ImGui::SetNextItemWidth(80);
333 if (ImGui::Combo("Subtype##SpriteSubtype", &subtype,
334 "0\0001\0002\0003\0004\0005\0006\0007\0")) {
335 sprite.set_subtype(subtype);
336 }
337 if (ImGui::IsItemHovered()) {
338 ImGui::SetTooltip(
339 "Controls sprite behavior variant.\n"
340 "Effect varies by sprite type.");
341 }
342
343 // Layer selector
344 int layer = sprite.layer();
345 ImGui::SetNextItemWidth(80);
346 if (ImGui::Combo("Layer##SpriteLayer", &layer,
347 "Upper (0)\0Lower (1)\0Both (2)\0")) {
348 sprite.set_layer(layer);
349 }
350 if (ImGui::IsItemHovered()) {
351 ImGui::SetTooltip(
352 "Which layer the sprite appears on.\n"
353 "Upper = main floor, Lower = basement.");
354 }
355
356 // Key drop selector
357 int key_drop = sprite.key_drop();
358 ImGui::Text("Key Drop:");
359 ImGui::SameLine();
360 if (ImGui::RadioButton("None##KeyNone", key_drop == 0)) {
361 sprite.set_key_drop(0);
362 }
363 ImGui::SameLine();
364 if (ImGui::RadioButton(ICON_MD_KEY " Small##KeySmall", key_drop == 1)) {
365 sprite.set_key_drop(1);
366 }
367 ImGui::SameLine();
368 if (ImGui::RadioButton(ICON_MD_VPN_KEY " Big##KeyBig", key_drop == 2)) {
369 sprite.set_key_drop(2);
370 }
371 if (ImGui::IsItemHovered()) {
372 ImGui::SetTooltip("Key dropped when sprite is defeated.");
373 }
374
375 // Delete button
376 ImGui::Spacing();
377 {
378 gui::StyleColorGuard del_guard(ImGuiCol_Button, theme.status_error);
379 if (ImGui::Button(ICON_MD_DELETE " Delete Sprite")) {
380 sprites.erase(sprites.begin() + selected_sprite_list_index_);
382 }
383 }
384
385 ImGui::SameLine();
386 if (ImGui::Button(ICON_MD_CONTENT_COPY " Duplicate")) {
387 zelda3::Sprite copy = sprite;
388 sprites.push_back(copy);
389 }
390 }
391
392 bool MatchesFilter(int sprite_id) {
393 // Category filter
394 if (selected_category_ > 0) {
395 // Simplified category matching - in real implementation, use proper categorization
396 bool is_enemy = (sprite_id >= 0x09 && sprite_id <= 0x7F);
397 bool is_npc = (sprite_id >= 0x80 && sprite_id <= 0xBF);
398 bool is_boss = (sprite_id >= 0xC0 && sprite_id <= 0xD8);
399 bool is_item = (sprite_id >= 0xD9 && sprite_id <= 0xFF);
400
401 if (selected_category_ == 1 && !is_enemy)
402 return false;
403 if (selected_category_ == 2 && !is_npc)
404 return false;
405 if (selected_category_ == 3 && !is_boss)
406 return false;
407 if (selected_category_ == 4 && !is_item)
408 return false;
409 }
410
411 // Text search filter
412 if (search_filter_[0] != '\0') {
413 const char* name = zelda3::ResolveSpriteName(sprite_id);
414 // Simple case-insensitive substring search
415 std::string name_lower = name;
416 std::string filter_lower = search_filter_;
417 for (auto& c : name_lower)
418 c = static_cast<char>(tolower(c));
419 for (auto& c : filter_lower)
420 c = static_cast<char>(tolower(c));
421 if (name_lower.find(filter_lower) == std::string::npos) {
422 return false;
423 }
424 }
425
426 return true;
427 }
428
429 ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme& theme) {
430 // Color-code based on sprite type using theme colors
431 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
432 return theme.status_error; // Red for bosses
433 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
434 return theme.dungeon_sprite_layer0; // Green for NPCs
435 } else if (sprite_id >= 0xD9) {
436 return theme.dungeon_object_chest; // Gold for items
437 }
438 return theme.dungeon_sprite_layer1; // Blue for enemies
439 }
440
441 const char* GetSpriteTypeIcon(int sprite_id) {
442 // Return category-appropriate icons
443 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
444 return ICON_MD_DANGEROUS; // Skull for bosses
445 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
446 return ICON_MD_PERSON; // Person for NPCs
447 } else if (sprite_id >= 0xD9) {
448 return ICON_MD_STAR; // Star for items
449 }
450 return ICON_MD_PEST_CONTROL; // Bug for enemies
451 }
452
453 const char* GetSpriteCategoryName(int sprite_id) {
454 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
455 return "Boss";
456 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
457 return "NPC";
458 } else if (sprite_id >= 0xD9) {
459 return "Item";
460 }
461 return "Enemy";
462 }
463
464 int* current_room_id_ = nullptr;
467
468 // Selection state
470 int selected_sprite_list_index_ = -1; // Selected sprite in room list
472 char search_filter_[64] = {0};
473 bool placement_mode_ = false;
474
475 std::function<void(const zelda3::Sprite&)> sprite_placed_callback_;
476};
477
478} // namespace editor
479} // namespace yaze
480
481#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
DungeonObjectInteraction & object_interaction()
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
WindowContent for placing and managing dungeon sprites.
std::function< void(const zelda3::Sprite &) sprite_placed_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)
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:35
#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_EDIT
Definition icons.h:645
#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_DELETE
Definition icons.h:530
#define ICON_MD_KEY
Definition icons.h:1026
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#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