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#include "util/i18n/tr.h"
4
5// C++ standard library
6#include <algorithm>
7
8// Third-party library headers
9#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11
12// Project headers
16
17namespace yaze::editor {
18
21 auto* room = GetCurrentRoom();
22 const size_t current_sprite_count = room ? room->GetSprites().size() : 0;
23 return GetPlacementCapacityState(current_sprite_count,
25}
26
31
32bool SpriteInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
33 if (!HasValidContext())
34 return false;
35
37 if (IsWithinBounds(canvas_x, canvas_y)) {
38 PlaceSpriteAtPosition(canvas_x, canvas_y);
39 }
40 return true;
41 }
42
43 // Try to select sprite at position
44 auto sprite_index = GetEntityAtPosition(canvas_x, canvas_y);
45 if (sprite_index.has_value()) {
46 SelectSprite(*sprite_index);
47 is_dragging_ = true;
49 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
51 return true;
52 }
53
55 return false;
56}
57
58void SpriteInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
59 if (!is_dragging_ || !selected_sprite_index_.has_value())
60 return;
61 drag_current_pos_ = current_pos;
62}
63
65 if (!is_dragging_ || !selected_sprite_index_.has_value()) {
66 is_dragging_ = false;
67 return;
68 }
69
70 auto* room = GetCurrentRoom();
71 if (!room) {
72 is_dragging_ = false;
73 return;
74 }
75
76 // Convert to sprite coordinates (16-pixel units)
77 auto [tile_x, tile_y] =
78 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
79 static_cast<int>(drag_current_pos_.y));
80
81 // Clamp to valid range (sprites use 0-31 range)
82 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
83 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
84
85 auto& sprites = room->GetSprites();
86 if (*selected_sprite_index_ < sprites.size()) {
88
89 sprites[*selected_sprite_index_].set_x(tile_x);
90 sprites[*selected_sprite_index_].set_y(tile_y);
91
93 }
94
95 is_dragging_ = false;
96}
97
100 return;
101
102 auto* canvas = ctx_->canvas;
103 const auto pointer_screen_pos = GetPointerScreenPosition();
104 if (!pointer_screen_pos.has_value())
105 return;
106
107 const DungeonCanvasTransform transform = GetCanvasTransform();
108 const auto [canvas_x, canvas_y] =
109 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
110
111 // Snap to 16-pixel grid
112 int snapped_x = (canvas_x / dungeon_coords::kSpriteTileSize) *
114 int snapped_y = (canvas_y / dungeon_coords::kSpriteTileSize) *
116
117 auto* room = GetCurrentRoom();
118 const size_t current_sprite_count = room ? room->GetSprites().size() : 0;
119 const auto capacity_state = GetPlacementGhostCapacityState();
120
121 // Draw ghost rectangle for sprite preview
122 const ImVec2 rect_min = transform.RoomPixelsToScreen(
123 ImVec2(static_cast<float>(snapped_x), static_cast<float>(snapped_y)));
124 const ImVec2 rect_size = transform.RoomSizeToScreen(
126 const ImVec2 rect_max(rect_min.x + rect_size.x, rect_min.y + rect_size.y);
127
128 const auto& theme = AgentUI::GetTheme();
129 const ImVec4 base_color =
130 GetPlacementAccentColor(theme, capacity_state, theme.status_success);
131 ImVec4 fill_color = base_color;
132 fill_color.w = 0.40f;
133 ImVec4 outline_color = base_color;
134 outline_color.w = 0.85f;
135
136 canvas->draw_list()->AddRectFilled(rect_min, rect_max,
137 ImGui::GetColorU32(fill_color));
138 canvas->draw_list()->AddRect(
139 rect_min, rect_max, ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
140
141 // Draw sprite ID label
142 std::string label = absl::StrFormat("%02X", preview_sprite_id_);
143 canvas->draw_list()->AddText(rect_min, ImGui::GetColorU32(theme.text_primary),
144 label.c_str());
145
146 // Capacity tooltip when at/near limit
147 if (capacity_state != GhostCapacityState::kNormal &&
148 ImGui::IsMouseHoveringRect(rect_min, rect_max)) {
149 ImGui::SetTooltip(tr("Sprites: %zu/%zu\n%s"), current_sprite_count,
151 GetPlacementCapacityTooltipSuffix(capacity_state).data());
152 }
153
154 const std::string badge_text = absl::StrFormat(
155 "Sprites %zu/%zu", current_sprite_count, zelda3::kMaxTotalSprites);
156 DrawPlacementCapacityBadge(canvas->draw_list(),
157 ImVec2(rect_min.x, rect_max.y + 6.0f), theme,
158 capacity_state, badge_text);
159}
160
162 if (!selected_sprite_index_.has_value() || !HasValidContext())
163 return;
164
165 auto* room = GetCurrentRoom();
166 if (!room)
167 return;
168
169 const auto& sprites = room->GetSprites();
170 if (*selected_sprite_index_ >= sprites.size())
171 return;
172
173 const auto& sprite = sprites[*selected_sprite_index_];
174
175 // Sprites use 16-pixel coordinate system
176 int pixel_x = sprite.x() * dungeon_coords::kSpriteTileSize;
177 int pixel_y = sprite.y() * dungeon_coords::kSpriteTileSize;
178
179 // If dragging, use current drag position (snapped to 16-pixel grid)
180 if (is_dragging_) {
181 auto [tile_x, tile_y] =
182 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
183 static_cast<int>(drag_current_pos_.y));
184 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
185 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
186 pixel_x = tile_x * dungeon_coords::kSpriteTileSize;
187 pixel_y = tile_y * dungeon_coords::kSpriteTileSize;
188 }
189
190 ImDrawList* draw_list = ImGui::GetWindowDrawList();
191 const DungeonCanvasTransform transform = GetCanvasTransform();
192 const float scale = transform.scale();
193 const ImVec2 pos = transform.RoomPixelsToScreen(
194 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
195 const ImVec2 size = transform.RoomSizeToScreen(
197
198 // Animated selection
199 static float pulse = 0.0f;
200 pulse += ImGui::GetIO().DeltaTime * 3.0f;
201 float alpha = 0.5f + 0.3f * sinf(pulse);
202
203 ImU32 color = IM_COL32(0, 255, 0, 180); // Green
204 ImU32 fill_color =
205 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
206
207 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
208 fill_color);
209 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
210 0, 2.0f);
211
212 // Draw label
213 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
214 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Sprite");
215}
216
218 int canvas_x, int canvas_y) const {
219 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
220 return std::nullopt;
221
222 auto* room = ctx_->GetCurrentRoomConst();
223 if (!room)
224 return std::nullopt;
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 (canvas_x >= sprite_x &&
237 canvas_x < sprite_x + dungeon_coords::kSpriteTileSize &&
238 canvas_y >= sprite_y &&
239 canvas_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_));
272 room->MarkSpritesDirty();
276}
277
279 if (!HasValidContext()) {
280 return;
281 }
282
283 auto* room = GetCurrentRoom();
284 if (!room || room->GetSprites().empty()) {
285 return;
286 }
287
289 room->GetSprites().clear();
290 room->MarkSpritesDirty();
294}
295
296bool SpriteInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
297 if (!selected_sprite_index_.has_value() || !HasValidContext()) {
298 return false;
299 }
300
301 auto* room = GetCurrentRoom();
302 if (!room) {
303 return false;
304 }
305
306 auto& sprites = room->GetSprites();
307 if (*selected_sprite_index_ >= sprites.size()) {
308 return false;
309 }
310
311 auto& sprite = sprites[*selected_sprite_index_];
312 const int next_x = std::clamp(static_cast<int>(sprite.x()) + delta_x, 0,
314 const int next_y = std::clamp(static_cast<int>(sprite.y()) + delta_y, 0,
316 if (next_x == sprite.x() && next_y == sprite.y()) {
317 return false;
318 }
319
321 sprite.set_x(next_x);
322 sprite.set_y(next_y);
323 room->MarkSpritesDirty();
326 return true;
327}
328
330 int canvas_y) {
331 if (!HasValidContext()) {
333 return;
334 }
335
336 auto* room = GetCurrentRoom();
337 if (!room) {
339 return;
340 }
341
342 // Enforce sprite limit at placement time (matches DungeonValidator::kMaxTotalSprites).
343 if (room->GetSprites().size() >= zelda3::kMaxTotalSprites) {
345 return;
346 }
347
349
350 auto [sprite_x, sprite_y] = CanvasToSpriteCoords(canvas_x, canvas_y);
351
352 // Clamp to valid range
353 sprite_x = std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
354 sprite_y = std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
355
357
358 // Create the sprite
359 zelda3::Sprite new_sprite(preview_sprite_id_, static_cast<uint8_t>(sprite_x),
360 static_cast<uint8_t>(sprite_y), 0, 0);
361
362 // Add sprite to room
363 room->GetSprites().push_back(new_sprite);
364 room->MarkSpritesDirty();
366
368}
369
371 int canvas_x, int canvas_y) const {
372 return {canvas_x / dungeon_coords::kSpriteTileSize,
374}
375
376} // namespace yaze::editor
std::optional< ImVec2 > GetPointerScreenPosition() const
DungeonCanvasTransform GetCanvasTransform() const
bool IsWithinBounds(int canvas_x, int canvas_y) const
Check if coordinates are within room bounds.
zelda3::Room * GetCurrentRoom() const
Get current room (convenience method)
bool HasValidContext() const
Check if context is valid.
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
GhostCapacityState GetPlacementGhostCapacityState() const
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.
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:253
A class for managing sprites in the overworld and underworld.
Definition sprite.h:37
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
void DrawPlacementCapacityBadge(ImDrawList *draw_list, const ImVec2 &badge_min, const AgentUITheme &theme, PlacementCapacityState state, std::string_view primary_text)
ImVec4 GetPlacementAccentColor(const AgentUITheme &theme, PlacementCapacityState state, const ImVec4 &normal_color)
std::string_view GetPlacementCapacityTooltipSuffix(PlacementCapacityState state)
PlacementCapacityState GetPlacementCapacityState(size_t current_count, size_t max_count)
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.