yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_interaction_handler.cc
Go to the documentation of this file.
1// Related header
3
4// C++ standard library
5#include <algorithm>
6
7// Third-party library headers
8#include "absl/strings/str_format.h"
9#include "imgui/imgui.h"
10
11// Project headers
15
16namespace yaze::editor {
17
22
23bool SpriteInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
24 if (!HasValidContext())
25 return false;
26
28 PlaceSpriteAtPosition(canvas_x, canvas_y);
29 return true;
30 }
31
32 // Try to select sprite at position
33 auto sprite_index = GetEntityAtPosition(canvas_x, canvas_y);
34 if (sprite_index.has_value()) {
35 SelectSprite(*sprite_index);
36 is_dragging_ = true;
38 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
40 return true;
41 }
42
44 return false;
45}
46
47void SpriteInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
48 if (!is_dragging_ || !selected_sprite_index_.has_value())
49 return;
50 drag_current_pos_ = current_pos;
51}
52
54 if (!is_dragging_ || !selected_sprite_index_.has_value()) {
55 is_dragging_ = false;
56 return;
57 }
58
59 auto* room = GetCurrentRoom();
60 if (!room) {
61 is_dragging_ = false;
62 return;
63 }
64
65 // Convert to sprite coordinates (16-pixel units)
66 auto [tile_x, tile_y] =
67 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
68 static_cast<int>(drag_current_pos_.y));
69
70 // Clamp to valid range (sprites use 0-31 range)
71 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
72 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
73
74 auto& sprites = room->GetSprites();
75 if (*selected_sprite_index_ < sprites.size()) {
77
78 sprites[*selected_sprite_index_].set_x(tile_x);
79 sprites[*selected_sprite_index_].set_y(tile_y);
80
82 }
83
84 is_dragging_ = false;
85}
86
89 return;
90
91 auto* canvas = ctx_->canvas;
92 if (!canvas->IsMouseHovering())
93 return;
94
95 const ImGuiIO& io = ImGui::GetIO();
96 ImVec2 canvas_pos = canvas->zero_point();
97 float scale = GetCanvasScale();
98
99 // Convert to room coordinates (sprites use 16-pixel grid)
100 int canvas_x = static_cast<int>((io.MousePos.x - canvas_pos.x) / scale);
101 int canvas_y = static_cast<int>((io.MousePos.y - canvas_pos.y) / scale);
102
103 // Snap to 16-pixel grid
104 int snapped_x = (canvas_x / dungeon_coords::kSpriteTileSize) *
106 int snapped_y = (canvas_y / dungeon_coords::kSpriteTileSize) *
108
109 auto* room = GetCurrentRoom();
110 size_t current_sprite_count = room ? room->GetSprites().size() : 0;
111 const bool at_sprite_limit = (current_sprite_count >= zelda3::kMaxTotalSprites);
112 const bool near_sprite_limit = (current_sprite_count >= zelda3::kMaxTotalSprites * 9 / 10);
113
114 // Draw ghost rectangle for sprite preview
115 ImVec2 rect_min(canvas_pos.x + snapped_x * scale,
116 canvas_pos.y + snapped_y * scale);
117 ImVec2 rect_max(rect_min.x + dungeon_coords::kSpriteTileSize * scale,
118 rect_min.y + dungeon_coords::kSpriteTileSize * scale);
119
120 const auto& theme = AgentUI::GetTheme();
121 ImVec4 fill_color = theme.status_success;
122 fill_color.w = 0.40f;
123 ImVec4 outline_color = theme.status_success;
124 outline_color.w = 0.85f;
125 if (at_sprite_limit) {
126 fill_color = theme.status_error;
127 fill_color.w = 0.40f;
128 outline_color = theme.status_error;
129 outline_color.w = 0.85f;
130 } else if (near_sprite_limit) {
131 fill_color = theme.status_warning;
132 fill_color.w = 0.40f;
133 outline_color = theme.status_warning;
134 outline_color.w = 0.85f;
135 }
136
137 canvas->draw_list()->AddRectFilled(rect_min, rect_max,
138 ImGui::GetColorU32(fill_color));
139 canvas->draw_list()->AddRect(rect_min, rect_max, ImGui::GetColorU32(outline_color),
140 0.0f, 0, 2.0f);
141
142 // Draw sprite ID label
143 std::string label = absl::StrFormat("%02X", preview_sprite_id_);
144 canvas->draw_list()->AddText(rect_min, ImGui::GetColorU32(theme.text_primary),
145 label.c_str());
146
147 // Capacity tooltip when at/near limit
148 if ((at_sprite_limit || near_sprite_limit) &&
149 ImGui::IsMouseHoveringRect(rect_min, rect_max)) {
150 ImGui::SetTooltip("Sprites: %zu/%zu%s", current_sprite_count, zelda3::kMaxTotalSprites,
151 at_sprite_limit ? "\nPlacement blocked" : "\nNear limit");
152 }
153
154}
155
157 if (!selected_sprite_index_.has_value() || !HasValidContext())
158 return;
159
160 auto* room = GetCurrentRoom();
161 if (!room)
162 return;
163
164 const auto& sprites = room->GetSprites();
165 if (*selected_sprite_index_ >= sprites.size())
166 return;
167
168 const auto& sprite = sprites[*selected_sprite_index_];
169
170 // Sprites use 16-pixel coordinate system
171 int pixel_x = sprite.x() * dungeon_coords::kSpriteTileSize;
172 int pixel_y = sprite.y() * dungeon_coords::kSpriteTileSize;
173
174 // If dragging, use current drag position (snapped to 16-pixel grid)
175 if (is_dragging_) {
176 auto [tile_x, tile_y] =
177 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
178 static_cast<int>(drag_current_pos_.y));
179 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
180 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
181 pixel_x = tile_x * dungeon_coords::kSpriteTileSize;
182 pixel_y = tile_y * dungeon_coords::kSpriteTileSize;
183 }
184
185 ImDrawList* draw_list = ImGui::GetWindowDrawList();
186 ImVec2 canvas_pos = GetCanvasZeroPoint();
187 float scale = GetCanvasScale();
188
189 ImVec2 pos(canvas_pos.x + pixel_x * scale, canvas_pos.y + pixel_y * scale);
190 ImVec2 size(dungeon_coords::kSpriteTileSize * scale,
192
193 // Animated selection
194 static float pulse = 0.0f;
195 pulse += ImGui::GetIO().DeltaTime * 3.0f;
196 float alpha = 0.5f + 0.3f * sinf(pulse);
197
198 ImU32 color = IM_COL32(0, 255, 0, 180); // Green
199 ImU32 fill_color =
200 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
201
202 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
203 fill_color);
204 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
205 0, 2.0f);
206
207 // Draw label
208 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
209 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Sprite");
210}
211
213 int canvas_x, int canvas_y) const {
214 if (!HasValidContext())
215 return std::nullopt;
216
217 auto* room = ctx_->GetCurrentRoomConst();
218 if (!room)
219 return std::nullopt;
220
221 // Convert screen coordinates to room coordinates
222 float scale = GetCanvasScale();
223 int room_x = static_cast<int>(canvas_x / scale);
224 int room_y = static_cast<int>(canvas_y / scale);
225
226 // Check sprites (16x16 hitbox)
227 const auto& sprites = room->GetSprites();
228 for (size_t i = 0; i < sprites.size(); ++i) {
229 const auto& sprite = sprites[i];
230
231 // Sprites use 16-pixel coordinate system
232 int sprite_x = sprite.x() * dungeon_coords::kSpriteTileSize;
233 int sprite_y = sprite.y() * dungeon_coords::kSpriteTileSize;
234
235 // 16x16 hitbox
236 if (room_x >= sprite_x &&
237 room_x < sprite_x + dungeon_coords::kSpriteTileSize &&
238 room_y >= sprite_y &&
239 room_y < sprite_y + dungeon_coords::kSpriteTileSize) {
240 return i;
241 }
242 }
243
244 return std::nullopt;
245}
246
251
256
258 if (!selected_sprite_index_.has_value() || !HasValidContext())
259 return;
260
261 auto* room = GetCurrentRoom();
262 if (!room)
263 return;
264
265 auto& sprites = room->GetSprites();
266 if (*selected_sprite_index_ >= sprites.size())
267 return;
268
270 sprites.erase(sprites.begin() +
271 static_cast<ptrdiff_t>(*selected_sprite_index_));
274}
275
277 int canvas_y) {
278 if (!HasValidContext()) {
280 return;
281 }
282
283 auto* room = GetCurrentRoom();
284 if (!room) {
286 return;
287 }
288
289 // Enforce sprite limit at placement time (matches DungeonValidator::kMaxTotalSprites).
290 if (room->GetSprites().size() >= zelda3::kMaxTotalSprites) {
292 return;
293 }
294
296
297 auto [sprite_x, sprite_y] = CanvasToSpriteCoords(canvas_x, canvas_y);
298
299 // Clamp to valid range
300 sprite_x = std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
301 sprite_y = std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
302
304
305 // Create the sprite
306 zelda3::Sprite new_sprite(preview_sprite_id_, static_cast<uint8_t>(sprite_x),
307 static_cast<uint8_t>(sprite_y), 0, 0);
308
309 // Add sprite to room
310 room->GetSprites().push_back(new_sprite);
312
314}
315
317 int canvas_x, int canvas_y) const {
318 float scale = GetCanvasScale();
319 // Convert to pixel coordinates, then to sprite tile coordinates
320 int pixel_x = static_cast<int>(canvas_x / scale);
321 int pixel_y = static_cast<int>(canvas_y / scale);
322 return {pixel_x / dungeon_coords::kSpriteTileSize,
324}
325
326} // namespace yaze::editor
float GetCanvasScale() const
Get canvas global scale.
zelda3::Room * GetCurrentRoom() const
Get current room (convenience method)
bool HasValidContext() const
Check if context is valid.
ImVec2 GetCanvasZeroPoint() const
Get canvas zero point (for screen coordinate conversion)
void PlaceSpriteAtPosition(int canvas_x, int canvas_y)
Place sprite at position.
void HandleRelease() override
Handle mouse release.
void SelectSprite(size_t index)
Select sprite at index.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::pair< int, int > CanvasToSpriteCoords(int canvas_x, int canvas_y) const
Convert canvas to sprite coordinates (16-pixel grid)
void BeginPlacement() override
Begin placement mode.
A class for managing sprites in the overworld and underworld.
Definition sprite.h:35
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
constexpr size_t kMaxTotalSprites
const zelda3::Room * GetCurrentRoomConst() const
Get const pointer to current room.
void NotifyEntityChanged() const
Notify that entity has changed.
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.