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#include <cstdint>
8
9// Third-party library headers
10#include "absl/strings/str_format.h"
11#include "imgui/imgui.h"
12
13// Project headers
17
18namespace yaze::editor {
19
22 auto* room = GetCurrentRoom();
23 const size_t current_sprite_count = room ? room->GetSprites().size() : 0;
24 return GetPlacementCapacityState(current_sprite_count,
26}
27
32
33bool SpriteInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
34 if (!HasValidContext())
35 return false;
36
38 if (IsWithinBounds(canvas_x, canvas_y)) {
39 PlaceSpriteAtPosition(canvas_x, canvas_y);
40 }
41 return true;
42 }
43
44 // Try to select sprite at position
45 auto sprite_index = GetEntityAtPosition(canvas_x, canvas_y);
46 if (sprite_index.has_value()) {
47 SelectSprite(*sprite_index);
48 is_dragging_ = true;
50 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
52 return true;
53 }
54
56 return false;
57}
58
59void SpriteInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
60 if (!is_dragging_ || !selected_sprite_index_.has_value())
61 return;
62 drag_current_pos_ = current_pos;
63}
64
66 if (!is_dragging_ || !selected_sprite_index_.has_value()) {
67 is_dragging_ = false;
68 return;
69 }
70
71 auto* room = GetCurrentRoom();
72 if (!room) {
73 is_dragging_ = false;
74 return;
75 }
76
77 // Convert to sprite coordinates (16-pixel units)
78 auto [tile_x, tile_y] =
79 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
80 static_cast<int>(drag_current_pos_.y));
81
82 // Clamp to valid range (sprites use 0-31 range)
83 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
84 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
85
86 auto& sprites = room->GetSprites();
87 if (*selected_sprite_index_ < sprites.size()) {
89
90 sprites[*selected_sprite_index_].set_x(tile_x);
91 sprites[*selected_sprite_index_].set_y(tile_y);
92
94 }
95
96 is_dragging_ = false;
97}
98
101 return;
102
103 auto* canvas = ctx_->canvas;
104 const auto pointer_screen_pos = GetPointerScreenPosition();
105 if (!pointer_screen_pos.has_value())
106 return;
107
108 const DungeonCanvasTransform transform = GetCanvasTransform();
109 const auto [canvas_x, canvas_y] =
110 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
111
112 // Snap to 16-pixel grid
113 int snapped_x = (canvas_x / dungeon_coords::kSpriteTileSize) *
115 int snapped_y = (canvas_y / dungeon_coords::kSpriteTileSize) *
117
118 auto* room = GetCurrentRoom();
119 const size_t current_sprite_count = room ? room->GetSprites().size() : 0;
120 const auto capacity_state = GetPlacementGhostCapacityState();
121
122 // Draw ghost rectangle for sprite preview
123 const ImVec2 rect_min = transform.RoomPixelsToScreen(
124 ImVec2(static_cast<float>(snapped_x), static_cast<float>(snapped_y)));
125 const ImVec2 rect_size = transform.RoomSizeToScreen(
127 const ImVec2 rect_max(rect_min.x + rect_size.x, rect_min.y + rect_size.y);
128
129 const auto& theme = AgentUI::GetTheme();
130 const ImVec4 base_color =
131 GetPlacementAccentColor(theme, capacity_state, theme.status_success);
132 ImVec4 fill_color = base_color;
133 fill_color.w = 0.40f;
134 ImVec4 outline_color = base_color;
135 outline_color.w = 0.85f;
136
137 canvas->draw_list()->AddRectFilled(rect_min, rect_max,
138 ImGui::GetColorU32(fill_color));
139 canvas->draw_list()->AddRect(
140 rect_min, rect_max, ImGui::GetColorU32(outline_color), 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 (capacity_state != GhostCapacityState::kNormal &&
149 ImGui::IsMouseHoveringRect(rect_min, rect_max)) {
150 ImGui::SetTooltip(tr("Sprites: %zu/%zu\n%s"), current_sprite_count,
152 GetPlacementCapacityTooltipSuffix(capacity_state).data());
153 }
154
155 const std::string badge_text = absl::StrFormat(
156 "Sprites %zu/%zu", current_sprite_count, zelda3::kMaxTotalSprites);
157 DrawPlacementCapacityBadge(canvas->draw_list(),
158 ImVec2(rect_min.x, rect_max.y + 6.0f), theme,
159 capacity_state, badge_text);
160}
161
163 if (!selected_sprite_index_.has_value() || !HasValidContext())
164 return;
165
166 auto* room = GetCurrentRoom();
167 if (!room)
168 return;
169
170 const auto& sprites = room->GetSprites();
171 if (*selected_sprite_index_ >= sprites.size())
172 return;
173
174 const auto& sprite = sprites[*selected_sprite_index_];
175
176 // Sprites use 16-pixel coordinate system
177 int pixel_x = sprite.x() * dungeon_coords::kSpriteTileSize;
178 int pixel_y = sprite.y() * dungeon_coords::kSpriteTileSize;
179
180 // If dragging, use current drag position (snapped to 16-pixel grid)
181 if (is_dragging_) {
182 auto [tile_x, tile_y] =
183 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
184 static_cast<int>(drag_current_pos_.y));
185 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
186 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
187 pixel_x = tile_x * dungeon_coords::kSpriteTileSize;
188 pixel_y = tile_y * dungeon_coords::kSpriteTileSize;
189 }
190
191 ImDrawList* draw_list = ImGui::GetWindowDrawList();
192 const DungeonCanvasTransform transform = GetCanvasTransform();
193 const float scale = transform.scale();
194 const ImVec2 pos = transform.RoomPixelsToScreen(
195 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
196 const ImVec2 size = transform.RoomSizeToScreen(
198
199 // Animated selection
200 static float pulse = 0.0f;
201 pulse += ImGui::GetIO().DeltaTime * 3.0f;
202 float alpha = 0.5f + 0.3f * sinf(pulse);
203
204 ImU32 color = IM_COL32(0, 255, 0, 180); // Green
205 ImU32 fill_color =
206 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
207
208 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
209 fill_color);
210 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
211 0, 2.0f);
212
213 // Draw label
214 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
215 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Sprite");
216}
217
219 int canvas_x, int canvas_y) const {
220 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
221 return std::nullopt;
222
223 auto* room = ctx_->GetCurrentRoomConst();
224 if (!room)
225 return std::nullopt;
226
227 // Check sprites (16x16 hitbox)
228 const auto& sprites = room->GetSprites();
229 for (size_t i = 0; i < sprites.size(); ++i) {
230 const auto& sprite = sprites[i];
231
232 // Sprites use 16-pixel coordinate system
233 int sprite_x = sprite.x() * dungeon_coords::kSpriteTileSize;
234 int sprite_y = sprite.y() * dungeon_coords::kSpriteTileSize;
235
236 // 16x16 hitbox
237 if (canvas_x >= sprite_x &&
238 canvas_x < sprite_x + dungeon_coords::kSpriteTileSize &&
239 canvas_y >= sprite_y &&
240 canvas_y < sprite_y + dungeon_coords::kSpriteTileSize) {
241 return i;
242 }
243 }
244
245 return std::nullopt;
246}
247
252
257
259 if (!selected_sprite_index_.has_value() || !HasValidContext())
260 return;
261
262 auto* room = GetCurrentRoom();
263 if (!room)
264 return;
265
266 auto& sprites = room->GetSprites();
267 if (*selected_sprite_index_ >= sprites.size())
268 return;
269
271 sprites.erase(sprites.begin() +
272 static_cast<ptrdiff_t>(*selected_sprite_index_));
273 room->MarkSpritesDirty();
277}
278
280 if (!HasValidContext()) {
281 return;
282 }
283
284 auto* room = GetCurrentRoom();
285 if (!room || room->GetSprites().empty()) {
286 return;
287 }
288
290 room->GetSprites().clear();
291 room->MarkSpritesDirty();
295}
296
297bool SpriteInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
298 if (!selected_sprite_index_.has_value() || !HasValidContext()) {
299 return false;
300 }
301
302 auto* room = GetCurrentRoom();
303 if (!room) {
304 return false;
305 }
306
307 auto& sprites = room->GetSprites();
308 if (*selected_sprite_index_ >= sprites.size()) {
309 return false;
310 }
311
312 auto& sprite = sprites[*selected_sprite_index_];
313 const int64_t requested_x = static_cast<int64_t>(sprite.x()) + delta_x;
314 const int64_t requested_y = static_cast<int64_t>(sprite.y()) + delta_y;
315 const int next_x = static_cast<int>(
316 std::clamp<int64_t>(requested_x, 0, dungeon_coords::kSpriteGridMax));
317 const int next_y = static_cast<int>(
318 std::clamp<int64_t>(requested_y, 0, dungeon_coords::kSpriteGridMax));
319 if (next_x == sprite.x() && next_y == sprite.y()) {
320 return false;
321 }
322
324 sprite.set_x(next_x);
325 sprite.set_y(next_y);
326 room->MarkSpritesDirty();
329 return true;
330}
331
333 int canvas_y) {
334 if (!HasValidContext()) {
336 return;
337 }
338
339 auto* room = GetCurrentRoom();
340 if (!room) {
342 return;
343 }
344
345 // Enforce sprite limit at placement time (matches DungeonValidator::kMaxTotalSprites).
346 if (room->GetSprites().size() >= zelda3::kMaxTotalSprites) {
348 return;
349 }
350
352
353 auto [sprite_x, sprite_y] = CanvasToSpriteCoords(canvas_x, canvas_y);
354
355 // Clamp to valid range
356 sprite_x = std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
357 sprite_y = std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
358
360
361 // Create the sprite
362 zelda3::Sprite new_sprite(preview_sprite_id_, static_cast<uint8_t>(sprite_x),
363 static_cast<uint8_t>(sprite_y), 0, 0);
364
365 // Add sprite to room
366 room->GetSprites().push_back(new_sprite);
367 room->MarkSpritesDirty();
369
371}
372
374 int canvas_x, int canvas_y) const {
375 return {canvas_x / dungeon_coords::kSpriteTileSize,
377}
378
379} // 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:254
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.