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"
14#include "imgui/imgui.h"
15#include "zelda3/dungeon/room.h"
17
18namespace yaze {
19namespace editor {
20
32 public:
33 SpriteEditorPanel(int* current_room_id,
34 std::array<zelda3::Room, 0x128>* rooms,
35 DungeonCanvasViewer* canvas_viewer = nullptr)
36 : current_room_id_(current_room_id),
37 rooms_(rooms),
38 canvas_viewer_(canvas_viewer) {}
39
40 // ==========================================================================
41 // EditorPanel Identity
42 // ==========================================================================
43
44 std::string GetId() const override { return "dungeon.sprite_editor"; }
45 std::string GetDisplayName() const override { return "Sprite Editor"; }
46 std::string GetIcon() const override { return ICON_MD_PERSON; }
47 std::string GetEditorCategory() const override { return "Dungeon"; }
48 int GetPriority() const override { return 65; }
49
50 // ==========================================================================
51 // EditorPanel Drawing
52 // ==========================================================================
53
54 void Draw(bool* p_open) override {
55 if (!current_room_id_ || !rooms_) {
56 ImGui::TextDisabled("No room data available");
57 return;
58 }
59
60 if (*current_room_id_ < 0 ||
61 *current_room_id_ >= static_cast<int>(rooms_->size())) {
62 ImGui::TextDisabled("No room selected");
63 return;
64 }
65
67 ImGui::Separator();
69 ImGui::Separator();
71 }
72
73 // ==========================================================================
74 // Panel-Specific Methods
75 // ==========================================================================
76
78 canvas_viewer_ = viewer;
79 }
80
82 std::function<void(const zelda3::Sprite&)> callback) {
83 sprite_placed_callback_ = std::move(callback);
84 }
85
86 private:
88 const auto& theme = AgentUI::GetTheme();
89 // Placement mode indicator
90 if (placement_mode_) {
91 ImGui::TextColored(theme.status_warning,
92 ICON_MD_PLACE " Placing: %s (0x%02X)",
94 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel")) {
95 placement_mode_ = false;
96 if (canvas_viewer_) {
98 }
99 }
100 } else {
101 ImGui::TextColored(theme.text_secondary_gray,
102 ICON_MD_INFO " Select a sprite to place");
103 }
104 }
105
107 const auto& theme = AgentUI::GetTheme();
108 ImGui::Text(ICON_MD_PERSON " Select Sprite:");
109
110 // Filter by category
111 static const char* kCategories[] = {
112 "All", "Enemies", "NPCs", "Bosses", "Items"
113 };
114 ImGui::SetNextItemWidth(100);
115 ImGui::Combo("##Category", &selected_category_, kCategories, IM_ARRAYSIZE(kCategories));
116 ImGui::SameLine();
117
118 // Search filter
119 ImGui::SetNextItemWidth(120);
120 ImGui::InputTextWithHint("##Search", "Search...", search_filter_, 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 = std::max(150.0f, std::min(400.0f, available_height - reserved_height));
128
129 // Responsive sprite size based on panel width
130 float panel_width = ImGui::GetContentRegionAvail().x;
131 float sprite_size = std::max(28.0f, std::min(40.0f, (panel_width - 40.0f) / 8.0f));
132 int items_per_row = std::max(1, static_cast<int>(panel_width / (sprite_size + 6)));
133
134 ImGui::BeginChild("##SpriteGrid", ImVec2(0, grid_height), true,
135 ImGuiWindowFlags_HorizontalScrollbar);
136
137 int col = 0;
138 for (int i = 0; i < 256; ++i) {
139 // Apply filters
140 if (!MatchesFilter(i)) continue;
141
142 bool is_selected = (selected_sprite_id_ == i);
143
144 ImGui::PushID(i);
145
146 // Color-coded button based on sprite type using theme colors
147 ImVec4 button_color = GetSpriteTypeColor(i, theme);
148 if (is_selected) {
149 button_color.x = std::min(1.0f, button_color.x + 0.2f);
150 button_color.y = std::min(1.0f, button_color.y + 0.2f);
151 button_color.z = std::min(1.0f, button_color.z + 0.2f);
152 }
153
154 ImGui::PushStyleColor(ImGuiCol_Button, button_color);
155 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
156 ImVec4(std::min(1.0f, button_color.x + 0.1f),
157 std::min(1.0f, button_color.y + 0.1f),
158 std::min(1.0f, button_color.z + 0.1f), 1.0f));
159 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
160 ImVec4(std::min(1.0f, button_color.x + 0.2f),
161 std::min(1.0f, button_color.y + 0.2f),
162 std::min(1.0f, button_color.z + 0.2f), 1.0f));
163
164 // Get category icon based on sprite type
165 const char* icon = GetSpriteTypeIcon(i);
166 std::string label = absl::StrFormat("%s\n%02X", icon, i);
167 if (ImGui::Button(label.c_str(), ImVec2(sprite_size, sprite_size))) {
169 placement_mode_ = true;
170 if (canvas_viewer_) {
172 }
173 }
174
175 ImGui::PopStyleColor(3);
176
177 if (ImGui::IsItemHovered()) {
178 const char* category = GetSpriteCategoryName(i);
179 ImGui::SetTooltip("%s (0x%02X)\n[%s]\nClick to select for placement",
180 zelda3::ResolveSpriteName(i), i, category);
181 }
182
183 // Selection highlight using theme color
184 if (is_selected) {
185 ImVec2 min = ImGui::GetItemRectMin();
186 ImVec2 max = ImGui::GetItemRectMax();
187 ImU32 sel_color = ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
188 ImGui::GetWindowDrawList()->AddRect(min, max, sel_color, 0.0f, 0, 2.0f);
189 }
190
191 ImGui::PopID();
192
193 col++;
194 if (col < items_per_row) {
195 ImGui::SameLine();
196 } else {
197 col = 0;
198 }
199 }
200
201 ImGui::EndChild();
202 }
203
205 const auto& theme = AgentUI::GetTheme();
206 auto& room = (*rooms_)[*current_room_id_];
207 const auto& sprites = room.GetSprites();
208
209 ImGui::Text(ICON_MD_LIST " Room Sprites (%zu):", sprites.size());
210
211 if (sprites.empty()) {
212 ImGui::TextColored(theme.text_secondary_gray,
213 ICON_MD_INFO " No sprites in this room");
214 return;
215 }
216
217 // Responsive list height - use remaining available space
218 float list_height = std::max(120.0f, ImGui::GetContentRegionAvail().y - 10.0f);
219 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
220 for (size_t i = 0; i < sprites.size(); ++i) {
221 const auto& sprite = sprites[i];
222
223 ImGui::PushID(static_cast<int>(i));
224
225 ImGui::Text("[%zu] %s (0x%02X)", i,
226 zelda3::ResolveSpriteName(sprite.id()), sprite.id());
227 ImGui::SameLine();
228 ImGui::TextColored(theme.text_secondary_gray,
229 "@ (%d,%d) L%d", sprite.x(), sprite.y(), sprite.layer());
230
231 ImGui::SameLine();
232 if (ImGui::SmallButton(ICON_MD_DELETE "##Del")) {
233 auto& mutable_room = (*rooms_)[*current_room_id_];
234 mutable_room.GetSprites().erase(
235 mutable_room.GetSprites().begin() + static_cast<long>(i));
236 }
237
238 ImGui::PopID();
239 }
240 ImGui::EndChild();
241 }
242
243 bool MatchesFilter(int sprite_id) {
244 // Category filter
245 if (selected_category_ > 0) {
246 // Simplified category matching - in real implementation, use proper categorization
247 bool is_enemy = (sprite_id >= 0x09 && sprite_id <= 0x7F);
248 bool is_npc = (sprite_id >= 0x80 && sprite_id <= 0xBF);
249 bool is_boss = (sprite_id >= 0xC0 && sprite_id <= 0xD8);
250 bool is_item = (sprite_id >= 0xD9 && sprite_id <= 0xFF);
251
252 if (selected_category_ == 1 && !is_enemy) return false;
253 if (selected_category_ == 2 && !is_npc) return false;
254 if (selected_category_ == 3 && !is_boss) return false;
255 if (selected_category_ == 4 && !is_item) return false;
256 }
257
258 // Text search filter
259 if (search_filter_[0] != '\0') {
260 const char* name = zelda3::ResolveSpriteName(sprite_id);
261 // Simple case-insensitive substring search
262 std::string name_lower = name;
263 std::string filter_lower = search_filter_;
264 for (auto& c : name_lower) c = static_cast<char>(tolower(c));
265 for (auto& c : filter_lower) c = static_cast<char>(tolower(c));
266 if (name_lower.find(filter_lower) == std::string::npos) {
267 return false;
268 }
269 }
270
271 return true;
272 }
273
274 ImVec4 GetSpriteTypeColor(int sprite_id, const AgentUITheme& theme) {
275 // Color-code based on sprite type using theme colors
276 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
277 return theme.status_error; // Red for bosses
278 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
279 return theme.dungeon_sprite_layer0; // Green for NPCs
280 } else if (sprite_id >= 0xD9) {
281 return theme.dungeon_object_chest; // Gold for items
282 }
283 return theme.dungeon_sprite_layer1; // Blue for enemies
284 }
285
286 const char* GetSpriteTypeIcon(int sprite_id) {
287 // Return category-appropriate icons
288 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
289 return ICON_MD_DANGEROUS; // Skull for bosses
290 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
291 return ICON_MD_PERSON; // Person for NPCs
292 } else if (sprite_id >= 0xD9) {
293 return ICON_MD_STAR; // Star for items
294 }
295 return ICON_MD_PEST_CONTROL; // Bug for enemies
296 }
297
298 const char* GetSpriteCategoryName(int sprite_id) {
299 if (sprite_id >= 0xC0 && sprite_id <= 0xD8) {
300 return "Boss";
301 } else if (sprite_id >= 0x80 && sprite_id <= 0xBF) {
302 return "NPC";
303 } else if (sprite_id >= 0xD9) {
304 return "Item";
305 }
306 return "Enemy";
307 }
308
309 int* current_room_id_ = nullptr;
310 std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
312
313 // Selection state
316 char search_filter_[64] = {0};
317 bool placement_mode_ = false;
318
319 std::function<void(const zelda3::Sprite&)> sprite_placed_callback_;
320};
321
322} // namespace editor
323} // namespace yaze
324
325#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_SPRITE_EDITOR_PANEL_H_
326
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.
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_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_DELETE
Definition icons.h:530
const AgentUITheme & GetTheme()
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
Centralized theme colors for Agent UI components.