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