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 const auto& sprites = room->GetSprites();
87 if (*selected_sprite_index_ < sprites.size()) {
88 const auto& sprite = sprites[*selected_sprite_index_];
89 UpdateSprite(*selected_sprite_index_, sprite.id(), tile_x, tile_y,
90 sprite.subtype(), sprite.layer(), sprite.key_drop());
91 }
92
93 is_dragging_ = false;
94}
95
98 return;
99
100 auto* canvas = ctx_->canvas;
101 const auto pointer_screen_pos = GetPointerScreenPosition();
102 if (!pointer_screen_pos.has_value())
103 return;
104
105 const DungeonCanvasTransform transform = GetCanvasTransform();
106 const auto [canvas_x, canvas_y] =
107 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
108
109 // Snap to 16-pixel grid
110 int snapped_x = (canvas_x / dungeon_coords::kSpriteTileSize) *
112 int snapped_y = (canvas_y / dungeon_coords::kSpriteTileSize) *
114
115 auto* room = GetCurrentRoom();
116 const size_t current_sprite_count = room ? room->GetSprites().size() : 0;
117 const auto capacity_state = GetPlacementGhostCapacityState();
118
119 // Draw ghost rectangle for sprite preview
120 const ImVec2 rect_min = transform.RoomPixelsToScreen(
121 ImVec2(static_cast<float>(snapped_x), static_cast<float>(snapped_y)));
122 const ImVec2 rect_size = transform.RoomSizeToScreen(
124 const ImVec2 rect_max(rect_min.x + rect_size.x, rect_min.y + rect_size.y);
125
126 const auto& theme = AgentUI::GetTheme();
127 const ImVec4 base_color =
128 GetPlacementAccentColor(theme, capacity_state, theme.status_success);
129 ImVec4 fill_color = base_color;
130 fill_color.w = 0.40f;
131 ImVec4 outline_color = base_color;
132 outline_color.w = 0.85f;
133
134 canvas->draw_list()->AddRectFilled(rect_min, rect_max,
135 ImGui::GetColorU32(fill_color));
136 canvas->draw_list()->AddRect(
137 rect_min, rect_max, ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
138
139 // Draw sprite ID label
140 std::string label = absl::StrFormat("%02X", preview_sprite_id_);
141 canvas->draw_list()->AddText(rect_min, ImGui::GetColorU32(theme.text_primary),
142 label.c_str());
143
144 // Capacity tooltip when at/near limit
145 if (capacity_state != GhostCapacityState::kNormal &&
146 ImGui::IsMouseHoveringRect(rect_min, rect_max)) {
147 ImGui::SetTooltip(tr("Sprites: %zu/%zu\n%s"), current_sprite_count,
149 GetPlacementCapacityTooltipSuffix(capacity_state).data());
150 }
151
152 const std::string badge_text = absl::StrFormat(
153 "Sprites %zu/%zu", current_sprite_count, zelda3::kMaxTotalSprites);
154 DrawPlacementCapacityBadge(canvas->draw_list(),
155 ImVec2(rect_min.x, rect_max.y + 6.0f), theme,
156 capacity_state, badge_text);
157}
158
160 if (!selected_sprite_index_.has_value() || !HasValidContext())
161 return;
162
163 auto* room = GetCurrentRoom();
164 if (!room)
165 return;
166
167 const auto& sprites = room->GetSprites();
168 if (*selected_sprite_index_ >= sprites.size())
169 return;
170
171 const auto& sprite = sprites[*selected_sprite_index_];
172
173 // Sprites use 16-pixel coordinate system
174 int pixel_x = sprite.x() * dungeon_coords::kSpriteTileSize;
175 int pixel_y = sprite.y() * dungeon_coords::kSpriteTileSize;
176
177 // If dragging, use current drag position (snapped to 16-pixel grid)
178 if (is_dragging_) {
179 auto [tile_x, tile_y] =
180 CanvasToSpriteCoords(static_cast<int>(drag_current_pos_.x),
181 static_cast<int>(drag_current_pos_.y));
182 tile_x = std::clamp(tile_x, 0, dungeon_coords::kSpriteGridMax);
183 tile_y = std::clamp(tile_y, 0, dungeon_coords::kSpriteGridMax);
184 pixel_x = tile_x * dungeon_coords::kSpriteTileSize;
185 pixel_y = tile_y * dungeon_coords::kSpriteTileSize;
186 }
187
188 ImDrawList* draw_list = ImGui::GetWindowDrawList();
189 const DungeonCanvasTransform transform = GetCanvasTransform();
190 const float scale = transform.scale();
191 const ImVec2 pos = transform.RoomPixelsToScreen(
192 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
193 const ImVec2 size = transform.RoomSizeToScreen(
195
196 // Animated selection
197 static float pulse = 0.0f;
198 pulse += ImGui::GetIO().DeltaTime * 3.0f;
199 float alpha = 0.5f + 0.3f * sinf(pulse);
200
201 ImU32 color = IM_COL32(0, 255, 0, 180); // Green
202 ImU32 fill_color =
203 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
204
205 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
206 fill_color);
207 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
208 0, 2.0f);
209
210 // Draw label
211 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
212 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Sprite");
213}
214
216 int canvas_x, int canvas_y) const {
217 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
218 return std::nullopt;
219
220 auto* room = ctx_->GetCurrentRoomConst();
221 if (!room)
222 return std::nullopt;
223
224 // Check sprites (16x16 hitbox)
225 const auto& sprites = room->GetSprites();
226 for (size_t i = 0; i < sprites.size(); ++i) {
227 const auto& sprite = sprites[i];
228
229 // Sprites use 16-pixel coordinate system
230 int sprite_x = sprite.x() * dungeon_coords::kSpriteTileSize;
231 int sprite_y = sprite.y() * dungeon_coords::kSpriteTileSize;
232
233 // 16x16 hitbox
234 if (canvas_x >= sprite_x &&
235 canvas_x < sprite_x + dungeon_coords::kSpriteTileSize &&
236 canvas_y >= sprite_y &&
237 canvas_y < sprite_y + dungeon_coords::kSpriteTileSize) {
238 return i;
239 }
240 }
241
242 return std::nullopt;
243}
244
249
254
256 const std::vector<zelda3::Sprite>& candidate) {
257 mutation_status_ = absl::OkStatus();
259 return true;
260 }
261 const auto* source_context = ctx_;
262 const int room_id = ctx_->current_room_id;
263 const auto* source_room = GetCurrentRoom();
264 mutation_status_ = sprite_mutation_preflight_(room_id, candidate);
265 if (mutation_status_.ok() &&
266 (ctx_ != source_context || !HasValidContext() ||
267 ctx_->current_room_id != room_id || GetCurrentRoom() != source_room)) {
268 mutation_status_ = absl::FailedPreconditionError(
269 "Sprite room changed during edit preflight; repeat the edit");
270 }
273 }
274 return mutation_status_.ok();
275}
276
278 mutation_status_ = absl::OkStatus();
279 if (!selected_sprite_index_.has_value() || !HasValidContext())
280 return;
281
282 auto* room = GetCurrentRoom();
283 if (!room)
284 return;
285
286 auto candidate = room->GetSprites();
287 if (*selected_sprite_index_ >= candidate.size())
288 return;
289
290 candidate.erase(candidate.begin() +
291 static_cast<ptrdiff_t>(*selected_sprite_index_));
292 if (!PreflightSprites(candidate)) {
293 return;
294 }
295 room = GetCurrentRoom();
297 room->GetSprites() = std::move(candidate);
298 room->MarkSpritesDirty();
302}
303
305 mutation_status_ = absl::OkStatus();
306 if (!HasValidContext()) {
307 return;
308 }
309
310 auto* room = GetCurrentRoom();
311 if (!room || room->GetSprites().empty()) {
312 return;
313 }
314
315 if (!PreflightSprites({})) {
316 return;
317 }
318 room = GetCurrentRoom();
320 room->GetSprites().clear();
321 room->MarkSpritesDirty();
325}
326
327bool SpriteInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
328 mutation_status_ = absl::OkStatus();
329 if (!selected_sprite_index_.has_value() || !HasValidContext()) {
330 return false;
331 }
332
333 auto* room = GetCurrentRoom();
334 if (!room) {
335 return false;
336 }
337
338 auto& sprites = room->GetSprites();
339 if (*selected_sprite_index_ >= sprites.size()) {
340 return false;
341 }
342
343 auto& sprite = sprites[*selected_sprite_index_];
344 const int64_t requested_x = static_cast<int64_t>(sprite.x()) + delta_x;
345 const int64_t requested_y = static_cast<int64_t>(sprite.y()) + delta_y;
346 const int next_x = static_cast<int>(
347 std::clamp<int64_t>(requested_x, 0, dungeon_coords::kSpriteGridMax));
348 const int next_y = static_cast<int>(
349 std::clamp<int64_t>(requested_y, 0, dungeon_coords::kSpriteGridMax));
350 if (next_x == sprite.x() && next_y == sprite.y()) {
351 return false;
352 }
353
354 return UpdateSprite(*selected_sprite_index_, sprite.id(), next_x, next_y,
355 sprite.subtype(), sprite.layer(), sprite.key_drop());
356}
357
359 uint8_t id, int x, int y, int subtype, int layer, int key_drop) {
360 if (x < 0 || x > 31 || y < 0 || y > 31 || subtype < 0 || subtype > 31 ||
361 layer < 0 || layer > 1 || key_drop < 0 || key_drop > 2) {
362 return absl::InvalidArgumentError(
363 "Sprite fields are outside their ROM ranges");
364 }
365 // EncodeSprites writes Y, the high subtype bits and layer to the first
366 // byte. 0xFF terminates the stream even when the remaining fields are valid.
367 const int first_byte = y | ((subtype & 0x18) << 2) | (layer << 7);
368 if (first_byte == 0xFF) {
369 return absl::InvalidArgumentError(
370 "Lower-layer sprites with subtype 24-31 cannot use Y 31: the ROM "
371 "reads that encoding as the end of the sprite list");
372 }
373 // LoadSprites folds these exact records into the preceding sprite. Authors
374 // should use its key-drop property instead of placing an ambiguous marker.
375 if (id == 0xE4 && x == 0 && (y == 29 || y == 30) && subtype == 24 &&
376 layer == 1) {
377 return absl::InvalidArgumentError(
378 "This E4 position/subtype/layer encodes a hidden key marker; use "
379 "the owning sprite's Key drop property");
380 }
381 return absl::OkStatus();
382}
383
384bool SpriteInteractionHandler::UpdateSprite(size_t index, uint8_t id, int x,
385 int y, int subtype, int layer,
386 int key_drop) {
387 mutation_status_ = absl::OkStatus();
388 if (!HasValidContext() ||
389 !ValidateSpriteProperties(id, x, y, subtype, layer, key_drop).ok()) {
390 return false;
391 }
392 auto* room = GetCurrentRoom();
393 if (!room || index >= room->GetSprites().size()) {
394 return false;
395 }
396 const auto sprite = room->GetSprites()[index];
397 if (sprite.id() == id && sprite.x() == x && sprite.y() == y &&
398 sprite.subtype() == subtype && sprite.layer() == layer &&
399 sprite.key_drop() == key_drop) {
400 return false;
401 }
402 // Rebuild identity metadata and invalidate cached previews when changing
403 // between ordinary sprites and overlords; raw setters leave those stale.
404 zelda3::Sprite updated(id, static_cast<uint8_t>(x), static_cast<uint8_t>(y),
405 static_cast<uint8_t>(subtype),
406 static_cast<uint8_t>(layer));
407 updated.set_key_drop(key_drop);
408 updated.set_deleted(sprite.deleted());
409 auto candidate = room->GetSprites();
410 candidate[index] = std::move(updated);
411 if (!PreflightSprites(candidate)) {
412 return false;
413 }
414 room = GetCurrentRoom();
416 room->GetSprites() = std::move(candidate);
417 room->MarkSpritesDirty();
420 return true;
421}
422
424 int canvas_y) {
425 mutation_status_ = absl::OkStatus();
426 if (!HasValidContext()) {
428 return;
429 }
430
431 auto* room = GetCurrentRoom();
432 if (!room) {
434 return;
435 }
436
437 // Enforce sprite limit at placement time (matches DungeonValidator::kMaxTotalSprites).
438 if (room->GetSprites().size() >= zelda3::kMaxTotalSprites) {
440 return;
441 }
442
444
445 auto [sprite_x, sprite_y] = CanvasToSpriteCoords(canvas_x, canvas_y);
446
447 // Clamp to valid range
448 sprite_x = std::clamp(sprite_x, 0, dungeon_coords::kSpriteGridMax);
449 sprite_y = std::clamp(sprite_y, 0, dungeon_coords::kSpriteGridMax);
450
451 // Create the sprite
452 zelda3::Sprite new_sprite(preview_sprite_id_, static_cast<uint8_t>(sprite_x),
453 static_cast<uint8_t>(sprite_y), 0, 0);
454 auto candidate = room->GetSprites();
455 candidate.push_back(std::move(new_sprite));
456 if (!PreflightSprites(candidate)) {
458 return;
459 }
460 room = GetCurrentRoom();
462
463 // Add sprite to room
464 room->GetSprites() = std::move(candidate);
465 room->MarkSpritesDirty();
467
469}
470
472 int canvas_x, int canvas_y) const {
473 return {canvas_x / dungeon_coords::kSpriteTileSize,
475}
476
477} // 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
bool PreflightSprites(const std::vector< zelda3::Sprite > &candidate)
bool UpdateSprite(size_t index, uint8_t id, int x, int y, int subtype, int layer, int key_drop)
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::function< void(const absl::Status &) mutation_error_callback_)
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.
static absl::Status ValidateSpriteProperties(uint8_t id, int x, int y, int subtype, int layer, int key_drop)
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:316
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
auto set_key_drop(int key)
Definition sprite.h:134
auto set_deleted(bool deleted)
Definition sprite.h:133
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.