yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
item_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
14
15namespace yaze::editor {
16
22
23bool ItemInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
24 if (!HasValidContext())
25 return false;
26
28 if (IsWithinBounds(canvas_x, canvas_y)) {
29 PlaceItemAtPosition(canvas_x, canvas_y);
30 }
31 return true;
32 }
33
34 // Try to select item at position
35 auto item_index = GetEntityAtPosition(canvas_x, canvas_y);
36 if (item_index.has_value()) {
37 SelectItem(*item_index);
38 is_dragging_ = true;
40 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
42 return true;
43 }
44
46 return false;
47}
48
49void ItemInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
50 if (!is_dragging_ || !selected_item_index_.has_value())
51 return;
52 drag_current_pos_ = current_pos;
53}
54
56 if (!is_dragging_ || !selected_item_index_.has_value()) {
57 is_dragging_ = false;
58 return;
59 }
60
61 auto* room = GetCurrentRoom();
62 if (!room) {
63 is_dragging_ = false;
64 return;
65 }
66
67 const int pixel_x = std::clamp(static_cast<int>(drag_current_pos_.x), 0,
69 const int pixel_y = std::clamp(static_cast<int>(drag_current_pos_.y), 0,
71
72 auto& pot_items = room->GetPotItems();
73 if (*selected_item_index_ < pot_items.size()) {
74 const auto encoded = zelda3::EncodePotItemPosition(
75 pixel_x / 8 * 8, pixel_y / 8 * 8,
76 pot_items[*selected_item_index_].position);
77 if (!encoded) {
78 is_dragging_ = false;
79 return;
80 }
81 const uint16_t next_position = *encoded;
82 if (pot_items[*selected_item_index_].position == next_position) {
83 is_dragging_ = false;
84 return;
85 }
86
88 pot_items[*selected_item_index_].position = next_position;
89 room->MarkPotItemsDirty();
92 }
93
94 is_dragging_ = false;
95}
96
99 return;
100
101 auto* canvas = ctx_->canvas;
102 const auto pointer_screen_pos = GetPointerScreenPosition();
103 if (!pointer_screen_pos.has_value())
104 return;
105
106 const DungeonCanvasTransform transform = GetCanvasTransform();
107 const auto [canvas_x, canvas_y] =
108 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
109
110 // Snap to 8-pixel grid
111 int snapped_x =
113 int snapped_y =
115
116 // Draw ghost rectangle for item preview
117 const ImVec2 rect_min = transform.RoomPixelsToScreen(
118 ImVec2(static_cast<float>(snapped_x), static_cast<float>(snapped_y)));
119 const ImVec2 rect_size = transform.RoomSizeToScreen(ImVec2(16, 16));
120 const ImVec2 rect_max(rect_min.x + rect_size.x, rect_min.y + rect_size.y);
121
122 const auto& theme = AgentUI::GetTheme();
123 ImVec4 fill_color = theme.dungeon_selection_primary;
124 fill_color.w = 0.35f;
125 ImVec4 outline_color = theme.dungeon_selection_primary;
126 outline_color.w = 0.85f;
127
128 canvas->draw_list()->AddRectFilled(rect_min, rect_max,
129 ImGui::GetColorU32(fill_color));
130 canvas->draw_list()->AddRect(
131 rect_min, rect_max, ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
132
133 // Draw item ID label
134 std::string label = absl::StrFormat("%02X", preview_item_id_);
135 canvas->draw_list()->AddText(rect_min, ImGui::GetColorU32(theme.text_primary),
136 label.c_str());
137}
138
140 if (!selected_item_index_.has_value() || !HasValidContext())
141 return;
142
143 auto* room = GetCurrentRoom();
144 if (!room)
145 return;
146
147 const auto& pot_items = room->GetPotItems();
148 if (*selected_item_index_ >= pot_items.size())
149 return;
150
151 const auto& pot_item = pot_items[*selected_item_index_];
152 int pixel_x = pot_item.GetPixelX();
153 int pixel_y = pot_item.GetPixelY();
154
155 // If dragging, use current drag position
156 if (is_dragging_) {
157 pixel_x = static_cast<int>(drag_current_pos_.x);
158 pixel_y = static_cast<int>(drag_current_pos_.y);
159 // Snap to 8-pixel grid
162 }
163
164 ImDrawList* draw_list = ImGui::GetWindowDrawList();
165 const DungeonCanvasTransform transform = GetCanvasTransform();
166 const float scale = transform.scale();
167 const ImVec2 pos = transform.RoomPixelsToScreen(
168 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
169 const ImVec2 size = transform.RoomSizeToScreen(ImVec2(16, 16));
170
171 // Animated selection
172 static float pulse = 0.0f;
173 pulse += ImGui::GetIO().DeltaTime * 3.0f;
174 float alpha = 0.5f + 0.3f * sinf(pulse);
175
176 ImU32 color = IM_COL32(255, 255, 0, 180); // Yellow
177 ImU32 fill_color =
178 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
179
180 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
181 fill_color);
182 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
183 0, 2.0f);
184
185 // Draw label
186 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
187 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Item");
188}
189
191 int canvas_x, int canvas_y) const {
192 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
193 return std::nullopt;
194
195 auto* room = ctx_->GetCurrentRoomConst();
196 if (!room)
197 return std::nullopt;
198
199 // Check pot items
200 const auto& pot_items = room->GetPotItems();
201 for (size_t i = 0; i < pot_items.size(); ++i) {
202 const auto& pot_item = pot_items[i];
203
204 int item_x = pot_item.GetPixelX();
205 int item_y = pot_item.GetPixelY();
206
207 // 16x16 hitbox
208 if (canvas_x >= item_x && canvas_x < item_x + 16 && canvas_y >= item_y &&
209 canvas_y < item_y + 16) {
210 return i;
211 }
212 }
213
214 return std::nullopt;
215}
216
221
223 selected_item_index_ = std::nullopt;
224 is_dragging_ = false;
225}
226
228 if (!selected_item_index_.has_value() || !HasValidContext())
229 return;
230
231 auto* room = GetCurrentRoom();
232 if (!room)
233 return;
234
235 auto& pot_items = room->GetPotItems();
236 if (*selected_item_index_ >= pot_items.size())
237 return;
238
240 pot_items.erase(pot_items.begin() +
241 static_cast<ptrdiff_t>(*selected_item_index_));
242 room->MarkPotItemsDirty();
246}
247
249 if (!HasValidContext()) {
250 return;
251 }
252
253 auto* room = GetCurrentRoom();
254 if (!room || room->GetPotItems().empty()) {
255 return;
256 }
257
259 room->GetPotItems().clear();
260 room->MarkPotItemsDirty();
264}
265
267 int delta_pixel_y) {
268 if (!selected_item_index_.has_value() || !HasValidContext()) {
269 return false;
270 }
271
272 auto* room = GetCurrentRoom();
273 if (!room) {
274 return false;
275 }
276
277 auto& pot_items = room->GetPotItems();
278 if (*selected_item_index_ >= pot_items.size()) {
279 return false;
280 }
281
282 auto& pot_item = pot_items[*selected_item_index_];
283 constexpr int kRoomPixelMax = 511;
284 const int next_pixel_x =
285 std::clamp(pot_item.GetPixelX() + delta_pixel_x, 0, kRoomPixelMax);
286 const int next_pixel_y =
287 std::clamp(pot_item.GetPixelY() + delta_pixel_y, 0, kRoomPixelMax);
288 const auto encoded = zelda3::EncodePotItemPosition(
289 next_pixel_x / 8 * 8, next_pixel_y / 8 * 8, pot_item.position);
290 if (!encoded)
291 return false;
292 const uint16_t next_position = *encoded;
293 if (next_position == pot_item.position) {
294 return false;
295 }
296
298 pot_item.position = next_position;
299 room->MarkPotItemsDirty();
302 return true;
303}
304
305bool ItemInteractionHandler::MutateItemType(size_t index, uint8_t new_type) {
306 if (!HasValidContext()) {
307 return false;
308 }
309 auto* room = GetCurrentRoom();
310 if (!room || index >= room->GetPotItems().size()) {
311 return false;
312 }
313 auto& item = room->GetPotItems()[index];
314 if (item.item == new_type || item.position == 0xFFFF) {
315 return false;
316 }
317 // The ROM position is a tilemap byte offset with layer/control bits, not
318 // two independent pixel-coordinate bytes. A type-only edit must preserve it
319 // exactly, including fields outside the current position editor's subset.
321 item.item = new_type;
322 room->MarkPotItemsDirty();
325 return true;
326}
327
328bool ItemInteractionHandler::UpdateItem(size_t index, uint8_t type, int pixel_x,
329 int pixel_y) {
330 if (!HasValidContext()) {
331 return false;
332 }
333 auto* room = GetCurrentRoom();
334 if (!room || index >= room->GetPotItems().size()) {
335 return false;
336 }
337 auto& item = room->GetPotItems()[index];
338 if (pixel_x == item.GetPixelX() && pixel_y == item.GetPixelY()) {
339 return MutateItemType(index, type);
340 }
341 const auto encoded =
342 zelda3::EncodePotItemPosition(pixel_x, pixel_y, item.position);
343 if (!encoded)
344 return false;
345 const uint16_t position = *encoded;
346 if (item.item == type && item.position == position) {
347 return false;
348 }
350 item.item = type;
351 item.position = position;
352 room->MarkPotItemsDirty();
355 return true;
356}
357
359 const std::vector<zelda3::PotItem>& candidate) {
361 return true;
362 }
363 const auto* source_context = ctx_;
364 const int room_id = ctx_->current_room_id;
365 const auto* source_room = GetCurrentRoom();
366 mutation_status_ = item_placement_preflight_(room_id, candidate);
367 if (mutation_status_.ok() &&
368 (ctx_ != source_context || !HasValidContext() ||
369 ctx_->current_room_id != room_id || GetCurrentRoom() != source_room)) {
370 mutation_status_ = absl::FailedPreconditionError(
371 "Pot-item room changed during placement preflight; repeat the "
372 "placement");
373 }
376 }
377 return mutation_status_.ok();
378}
379
380void ItemInteractionHandler::PlaceItemAtPosition(int canvas_x, int canvas_y) {
381 mutation_status_ = absl::OkStatus();
382 if (!HasValidContext())
383 return;
384
385 auto* room = GetCurrentRoom();
386 if (!room)
387 return;
388
389 const auto encoded =
390 zelda3::EncodePotItemPosition(std::clamp(canvas_x, 0, 511) / 8 * 8,
391 std::clamp(canvas_y, 0, 511) / 8 * 8);
392 if (!encoded)
393 return;
394 zelda3::PotItem new_item;
395 new_item.position = *encoded;
396 new_item.item = preview_item_id_;
397
398 auto candidate = room->GetPotItems();
399 candidate.push_back(new_item);
400 if (!PreflightPlacement(candidate)) {
401 return;
402 }
403 room = GetCurrentRoom();
405 room->GetPotItems() = std::move(candidate);
406 room->MarkPotItemsDirty();
407
409}
410
411} // 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
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
std::function< void(const absl::Status &) mutation_error_callback_)
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
void DrawGhostPreview() override
Draw ghost preview during placement.
bool PreflightPlacement(const std::vector< zelda3::PotItem > &candidate)
void BeginPlacement() override
Begin placement mode.
void PlaceItemAtPosition(int canvas_x, int canvas_y)
Place item at position.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
bool NudgeSelected(int delta_pixel_x, int delta_pixel_y)
bool MutateItemType(size_t index, uint8_t new_type)
Change one pot item's raw type byte in place.
void HandleRelease() override
Handle mouse release.
void SelectItem(size_t index)
Select item at index.
bool UpdateItem(size_t index, uint8_t type, int pixel_x, int pixel_y)
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
constexpr std::optional< uint16_t > EncodePotItemPosition(int x, int y, uint16_t original=0)
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.
uint16_t position
Definition room.h:140