yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
door_interaction_handler.cc
Go to the documentation of this file.
1// Related header
3#include "util/i18n/tr.h"
4
5// Third-party library headers
6#include "imgui/imgui.h"
7
8#include <algorithm>
9#include <cmath>
10#include <string>
11
12// Project headers
13#include "absl/strings/str_format.h"
18
19namespace yaze::editor {
20
21namespace {
22
27
28ImVec2 EstimateBadgeTextSize(const std::string& label) {
29 // Overlay hit-testing can run from headless interaction tests before ImGui has
30 // baked a font atlas. Keep the clickable region independent of font state.
31 return ImVec2(std::max(1.0f, static_cast<float>(label.size()) * 7.0f), 14.0f);
32}
33
34} // namespace
35
40
46
47bool DoorInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
48 if (!HasValidContext())
49 return false;
50
52 if (IsWithinBounds(canvas_x, canvas_y)) {
53 PlaceDoorAtSnappedPosition(canvas_x, canvas_y);
54 }
55 return true;
56 }
57
58 // Try to select door at position
59 auto door_index = GetEntityAtPosition(canvas_x, canvas_y);
60 if (door_index.has_value()) {
61 const auto* room = ctx_->GetCurrentRoomConst();
62 if (!room || *door_index >= room->GetDoors().size()) {
63 return false;
64 }
65 const auto [anchor_x, anchor_y] =
66 room->GetDoors()[*door_index].GetPixelCoords();
67
68 SelectDoor(*door_index);
69 is_dragging_ = true;
71 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
74 ImVec2(static_cast<float>(canvas_x - anchor_x),
75 static_cast<float>(canvas_y - anchor_y));
76 return true;
77 }
78
80 return false;
81}
82
83void DoorInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
84 if (!is_dragging_ || !selected_door_index_.has_value())
85 return;
86
87 drag_current_pos_ = current_pos;
88}
89
91 if (!is_dragging_ || !selected_door_index_.has_value()) {
92 is_dragging_ = false;
93 return;
94 }
95
96 auto* room = GetCurrentRoom();
97 if (!room) {
98 is_dragging_ = false;
99 return;
100 }
101
102 auto& doors = room->GetDoors();
103 if (*selected_door_index_ >= doors.size()) {
104 is_dragging_ = false;
105 return;
106 }
107
108 // HandleClick primes drag state so a selected door can move immediately, but
109 // a plain click must not canonicalize distinct ROM positions that share the
110 // same visual snap slot.
113 is_dragging_ = false;
114 return;
115 }
116
117 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
118
119 // Detect wall from final position
120 zelda3::DoorDirection direction;
122 direction)) {
124 drag_x, drag_y, direction);
125
126 if (zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
127 auto& door = doors[*selected_door_index_];
128 if (door.position != position || door.direction != direction) {
130 door.position = position;
131 door.direction = direction;
132
133 // Re-encode bytes for ROM storage
134 auto [b1, b2] = door.EncodeBytes();
135 door.byte1 = b1;
136 door.byte2 = b2;
137
138 room->MarkObjectStreamDirty();
140 }
141 }
142 }
143
144 is_dragging_ = false;
145}
146
147bool DoorInteractionHandler::HandleOverlayClick(int canvas_x, int canvas_y) {
148 if (!selected_door_index_.has_value() || !HasValidContext() || is_dragging_) {
149 return false;
150 }
151
152 auto* room = GetCurrentRoom();
153 if (!room) {
154 return false;
155 }
156
157 const auto& doors = room->GetDoors();
158 if (*selected_door_index_ >= doors.size()) {
159 return false;
160 }
161
162 const auto& door = doors[*selected_door_index_];
163 const auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
164 const DungeonCanvasTransform transform = GetCanvasTransform();
165 const float scale = transform.scale();
166 const ImVec2 door_pos = transform.RoomPixelsToScreen(
167 ImVec2(static_cast<float>(door_x), static_cast<float>(door_y)));
168 const ImVec2 door_size = transform.RoomSizeToScreen(
169 ImVec2(static_cast<float>(door_w), static_cast<float>(door_h)));
170 const auto badge = BuildPairBadgeOverlay(door, door_pos, door_size, scale);
171 if (!badge.has_value() || badge->target_room_id < 0) {
172 return false;
173 }
174
175 const ImVec2 screen_pos = transform.RoomPixelsToScreen(
176 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y)));
177 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
178 badge->screen_pos.y + badge->screen_size.y);
179 if (screen_pos.x < badge->screen_pos.x ||
180 screen_pos.y < badge->screen_pos.y || screen_pos.x > badge_max.x ||
181 screen_pos.y > badge_max.y) {
182 return false;
183 }
184
185 NavigateToPairBadge(*badge);
186 return true;
187}
188
191 auto* room = GetCurrentRoom();
192 const size_t current_door_count = room ? room->GetDoors().size() : 0;
193 return static_cast<GhostCapacityState>(
194 GetPlacementCapacityState(current_door_count, zelda3::kMaxDoors));
195}
196
199 return;
200
201 const auto pointer_screen_pos = GetPointerScreenPosition();
202 if (!pointer_screen_pos.has_value())
203 return;
204
205 const DungeonCanvasTransform transform = GetCanvasTransform();
206 const auto [canvas_x, canvas_y] =
207 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
208
209 // Try to update snapped position
210 if (!UpdateSnappedPosition(canvas_x, canvas_y)) {
211 // Placement guidance: make invalid hover state explicit instead of showing
212 // no preview.
213 const auto& theme = AgentUI::GetTheme();
214 ImDrawList* draw_list = ImGui::GetWindowDrawList();
215 ImVec2 hint_pos(pointer_screen_pos->x + 14.0f,
216 pointer_screen_pos->y + 10.0f);
217 draw_list->AddText(hint_pos, ImGui::GetColorU32(theme.status_warning),
218 "Move cursor near a wall to place door");
219 ImGui::SetTooltip(tr("Door placement requires a wall-adjacent position."));
220 return; // Not near a wall
221 }
222
223 const auto [snap_canvas_x, snap_canvas_y, door_width_px, door_height_px] =
226
227 // Draw ghost preview
228 ImDrawList* draw_list = ImGui::GetWindowDrawList();
229 const float scale = transform.scale();
230 const ImVec2 preview_start = transform.RoomPixelsToScreen(ImVec2(
231 static_cast<float>(snap_canvas_x), static_cast<float>(snap_canvas_y)));
232 const ImVec2 preview_size = transform.RoomSizeToScreen(ImVec2(
233 static_cast<float>(door_width_px), static_cast<float>(door_height_px)));
234 const ImVec2 preview_end(preview_start.x + preview_size.x,
235 preview_start.y + preview_size.y);
236
237 const auto& theme = AgentUI::GetTheme();
238
239 const auto capacity_state = GetPlacementGhostCapacityState();
240 const auto placement_state = ToPlacementCapacityState(capacity_state);
241 auto* room = GetCurrentRoom();
242 const size_t current_door_count = room ? room->GetDoors().size() : 0;
243
244 const ImVec4 base_color = GetPlacementAccentColor(
245 theme, placement_state, theme.dungeon_selection_primary);
246
247 // Draw semi-transparent filled rectangle
248 ImVec4 fill_vec(base_color.x, base_color.y, base_color.z, 0.31f);
249 draw_list->AddRectFilled(preview_start, preview_end,
250 ImGui::GetColorU32(fill_vec));
251
252 // Draw outline
253 ImVec4 outline_color(base_color.x, base_color.y, base_color.z, 0.9f);
254 draw_list->AddRect(preview_start, preview_end,
255 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
256
257 // Draw door type label
258 std::string type_name(zelda3::GetDoorTypeName(preview_door_type_));
260 std::string label = type_name + " (" + dir_name + ")";
261
262 ImVec2 text_pos(preview_start.x, preview_start.y - 16 * scale);
263 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 200), label.c_str());
264
265 const std::string badge_text =
266 absl::StrFormat("Doors %zu/%zu", current_door_count, zelda3::kMaxDoors);
267 ImVec2 badge_min;
268 switch (detected_door_direction_) {
270 badge_min = ImVec2(preview_start.x, preview_end.y + 6.0f);
271 break;
273 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
274 const ImVec2 status_size = ImGui::CalcTextSize(
275 GetPlacementCapacityStatusText(placement_state).data());
276 const float badge_height = badge_size.y + status_size.y + 14.0f;
277 badge_min =
278 ImVec2(preview_start.x, preview_start.y - badge_height - 6.0f);
279 break;
280 }
282 badge_min = ImVec2(preview_end.x + 6.0f, preview_start.y);
283 break;
285 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
286 const ImVec2 status_size = ImGui::CalcTextSize(
287 GetPlacementCapacityStatusText(placement_state).data());
288 const float badge_width = std::max(badge_size.x, status_size.x) + 14.0f;
289 badge_min = ImVec2(preview_start.x - badge_width - 6.0f, preview_start.y);
290 break;
291 }
292 }
293 DrawPlacementCapacityBadge(draw_list, badge_min, theme, placement_state,
294 badge_text);
295
296 // Capacity tooltip when at/near limit
297 if (capacity_state != GhostCapacityState::kNormal &&
298 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
299 ImGui::SetTooltip(
300 tr("Doors: %zu/%zu\n%s"), current_door_count, zelda3::kMaxDoors,
301 GetPlacementCapacityTooltipSuffix(placement_state).data());
302 }
303}
304
306 if (!selected_door_index_.has_value() || !HasValidContext())
307 return;
308
309 auto* room = GetCurrentRoom();
310 if (!room)
311 return;
312
313 const auto& doors = room->GetDoors();
314 if (*selected_door_index_ >= doors.size())
315 return;
316
317 const auto& door = doors[*selected_door_index_];
318 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
319
320 // If dragging, use current drag position for door preview
321 if (is_dragging_) {
322 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
323
325 bool is_inner = false;
327 is_inner)) {
329 drag_x, drag_y, dir);
330 std::tie(door_x, door_y, door_w, door_h) =
332 door.type);
333 }
334 }
335
336 ImDrawList* draw_list = ImGui::GetWindowDrawList();
337 const DungeonCanvasTransform transform = GetCanvasTransform();
338 const float scale = transform.scale();
339 const ImVec2 pos = transform.RoomPixelsToScreen(
340 ImVec2(static_cast<float>(door_x), static_cast<float>(door_y)));
341 const ImVec2 size = transform.RoomSizeToScreen(
342 ImVec2(static_cast<float>(door_w), static_cast<float>(door_h)));
343
344 // Animated selection
345 static float pulse = 0.0f;
346 pulse += ImGui::GetIO().DeltaTime * 3.0f;
347 float alpha = 0.5f + 0.3f * sinf(pulse);
348
349 ImU32 color = IM_COL32(255, 165, 0, 180); // Orange
350 ImU32 fill_color =
351 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
352
353 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
354 fill_color);
355 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
356 0, 2.0f);
357
358 ImVec2 label_pos(pos.x, pos.y - 14 * scale);
359 draw_list->AddText(label_pos, IM_COL32(255, 255, 255, 220), "Door");
360
361 if (auto badge = BuildPairBadgeOverlay(door, pos, size, scale)) {
362 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
363 badge->screen_pos.y + badge->screen_size.y);
364 const bool interactive =
365 badge->target_room_id >= 0 && ctx_ && ctx_->on_door_pair_navigation;
366 const bool hovered =
367 interactive && ImGui::IsMouseHoveringRect(badge->screen_pos, badge_max);
368
369 draw_list->AddText(badge->screen_pos, badge->color, badge->label.c_str());
370 if (hovered) {
371 const auto& theme = AgentUI::GetTheme();
372 const ImU32 hover_color = ImGui::GetColorU32(theme.accent_color);
373 draw_list->AddLine(ImVec2(badge->screen_pos.x, badge_max.y + 1.0f),
374 ImVec2(badge_max.x, badge_max.y + 1.0f), hover_color,
375 1.0f);
376 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
377 ImGui::SetTooltip(tr("Open room 0x%03X"), badge->target_room_id);
378 }
379 }
380
381 // Draw snap indicators when dragging
382 if (is_dragging_) {
384 }
385}
386
387std::optional<DoorInteractionHandler::PairBadgeOverlay>
389 ImVec2 door_pos, ImVec2 door_size,
390 float scale) const {
391 if (is_dragging_ || !ctx_ || !ctx_->rooms ||
393 return std::nullopt;
394 }
395
396 PairBadgeOverlay badge;
397 const int neighbor = NeighborRoomId(ctx_->current_room_id, door.direction);
398 if (neighbor < 0) {
399 badge.label = "edge";
400 badge.color = IM_COL32(170, 170, 170, 220);
401 } else {
402 badge.target_room_id = neighbor;
403 const auto opposite = OppositeDir(door.direction);
404 const auto& neighbor_doors = (*ctx_->rooms)[neighbor].GetDoors();
405 bool any_on_opposite = false;
406 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
407 const auto& nd = neighbor_doors[i];
408 if (nd.direction != opposite ||
410 continue;
411 }
412
413 any_on_opposite = true;
414 if (!badge.target_door_index.has_value()) {
415 badge.target_door_index = i;
416 }
417 if (nd.position == door.position) {
418 badge.target_door_index = i;
419 break;
420 }
421 }
422
423 if (badge.target_door_index.has_value()) {
424 const auto& target_door = neighbor_doors[*badge.target_door_index];
425 const auto [target_tile_x, target_tile_y] = target_door.GetTileCoords();
426 badge.target_tile_x = target_tile_x;
427 badge.target_tile_y = target_tile_y;
428 } else {
429 const auto [target_tile_x, target_tile_y] =
431 opposite);
432 badge.target_tile_x = target_tile_x;
433 badge.target_tile_y = target_tile_y;
434 }
435
436 if (badge.target_door_index.has_value() &&
437 neighbor_doors[*badge.target_door_index].position == door.position) {
438 badge.label = absl::StrFormat("pair 0x%03X", neighbor);
439 badge.color = IM_COL32(120, 220, 150, 235); // green
440 } else if (any_on_opposite) {
441 badge.label = absl::StrFormat("~0x%03X", neighbor);
442 badge.color = IM_COL32(255, 200, 90, 235); // amber
443 } else {
444 badge.label = absl::StrFormat("no pair 0x%03X", neighbor);
445 badge.color = IM_COL32(255, 130, 90, 235); // red-orange
446 }
447 }
448
449 switch (door.direction) {
451 badge.screen_pos = ImVec2(door_pos.x + 40.0f, door_pos.y - 14 * scale);
452 break;
454 badge.screen_pos = ImVec2(door_pos.x, door_pos.y + door_size.y + 2.0f);
455 break;
457 badge.screen_pos =
458 ImVec2(door_pos.x - 88.0f, door_pos.y + door_size.y * 0.5f - 7.0f);
459 break;
461 badge.screen_pos = ImVec2(door_pos.x + door_size.x + 6.0f,
462 door_pos.y + door_size.y * 0.5f - 7.0f);
463 break;
464 }
465 badge.screen_size = EstimateBadgeTextSize(badge.label);
466 return badge;
467}
468
470 const PairBadgeOverlay& badge) const {
471 if (!ctx_ || !ctx_->on_door_pair_navigation || badge.target_room_id < 0) {
472 return;
473 }
475 badge.target_tile_x, badge.target_tile_y);
476}
477
479 int canvas_x, int canvas_y) const {
480 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
481 return std::nullopt;
482
483 auto* room = ctx_->GetCurrentRoomConst();
484 if (!room)
485 return std::nullopt;
486
487 const auto& doors = room->GetDoors();
488 for (size_t i = 0; i < doors.size(); ++i) {
489 const auto& door = doors[i];
490
491 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
492
493 if (canvas_x >= door_x && canvas_x < door_x + door_w &&
494 canvas_y >= door_y && canvas_y < door_y + door_h) {
495 return i;
496 }
497 }
498
499 return std::nullopt;
500}
501
506
508 selected_door_index_ = std::nullopt;
509 is_dragging_ = false;
510}
511
513 if (!selected_door_index_.has_value() || !HasValidContext())
514 return;
515
516 auto* room = GetCurrentRoom();
517 if (!room)
518 return;
519
520 auto& doors = room->GetDoors();
521 if (*selected_door_index_ >= doors.size())
522 return;
523
525 doors.erase(doors.begin() + static_cast<ptrdiff_t>(*selected_door_index_));
526 room->MarkObjectStreamDirty();
530}
531
533 if (!HasValidContext()) {
534 return;
535 }
536
537 auto* room = GetCurrentRoom();
538 if (!room || room->GetDoors().empty()) {
539 return;
540 }
541
543 room->GetDoors().clear();
544 room->MarkObjectStreamDirty();
548}
549
550bool DoorInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
551 if (!selected_door_index_.has_value() || !HasValidContext()) {
552 return false;
553 }
554
555 auto* room = GetCurrentRoom();
556 if (!room) {
557 return false;
558 }
559
560 auto& doors = room->GetDoors();
561 if (*selected_door_index_ >= doors.size()) {
562 return false;
563 }
564
565 auto& door = doors[*selected_door_index_];
566 int position_delta = 0;
567 switch (door.direction) {
570 position_delta = delta_x;
571 break;
574 position_delta = delta_y;
575 break;
576 }
577
578 if (position_delta == 0) {
579 return false;
580 }
581
582 const int next_position =
583 std::clamp(static_cast<int>(door.position) + position_delta, 0,
585 if (next_position == door.position ||
587 static_cast<uint8_t>(next_position), door.direction)) {
588 return false;
589 }
590
592 door.position = static_cast<uint8_t>(next_position);
593 auto [b1, b2] = door.EncodeBytes();
594 door.byte1 = b1;
595 door.byte2 = b2;
596 room->MarkObjectStreamDirty();
599 return true;
600}
601
603 zelda3::DoorType new_type) {
604 if (!HasValidContext())
605 return false;
606
607 const uint8_t raw_type = static_cast<uint8_t>(new_type);
608 if ((raw_type & 0x01) != 0 || raw_type > 0x66)
609 return false;
610
611 auto* room = GetCurrentRoom();
612 if (!room)
613 return false;
614
615 auto& doors = room->GetDoors();
616 if (index >= doors.size())
617 return false;
618
619 auto& door = doors[index];
620 if (door.type == new_type)
621 return false;
622
624
625 door.type = new_type;
626 auto [b1, b2] = door.EncodeBytes();
627 door.byte1 = b1;
628 door.byte2 = b2;
629
630 room->MarkObjectStreamDirty();
633 return true;
634}
635
637 int canvas_y) {
638 if (!HasValidContext()) {
640 return;
641 }
642
643 auto* room = GetCurrentRoom();
644 if (!room) {
646 return;
647 }
648
649 // Enforce door limit at placement time (matches DungeonValidator::zelda3::kMaxDoors)
650
651 if (room->GetDoors().size() >= zelda3::kMaxDoors) {
653 return;
654 }
655
656 // Detect wall from position
657 zelda3::DoorDirection direction;
659 direction)) {
661 return;
662 }
663
664 // Snap to nearest valid position
666 canvas_x, canvas_y, direction);
667
668 // Validate position
669 if (!zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
671 return;
672 }
673
675
677
678 // Create the door
679 zelda3::Room::Door new_door;
680 new_door.position = position;
681 new_door.type = preview_door_type_;
682 new_door.direction = direction;
683
684 // Encode bytes for ROM storage
685 auto [byte1, byte2] = new_door.EncodeBytes();
686 new_door.byte1 = byte1;
687 new_door.byte2 = byte2;
688
689 // Add door to room
690 room->AddDoor(new_door);
692
694}
695
696bool DoorInteractionHandler::UpdateSnappedPosition(int canvas_x, int canvas_y) {
697 zelda3::DoorDirection direction;
699 direction)) {
700 return false;
701 }
702
703 detected_door_direction_ = direction;
705 canvas_x, canvas_y, direction);
706 return true;
707}
708
710 const {
711 return {
712 static_cast<int>(drag_current_pos_.x - drag_grab_offset_from_anchor_.x),
713 static_cast<int>(drag_current_pos_.y - drag_grab_offset_from_anchor_.y),
714 };
715}
716
718 if (!is_dragging_ || !HasValidContext())
719 return;
720
721 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
722
723 zelda3::DoorDirection direction;
724 bool is_inner = false;
725 if (!zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, direction,
726 is_inner)) {
727 return;
728 }
729
730 uint8_t start_pos =
733 drag_x, drag_y, direction);
734
735 ImDrawList* draw_list = ImGui::GetWindowDrawList();
736 const DungeonCanvasTransform transform = GetCanvasTransform();
737 const auto& theme = AgentUI::GetTheme();
738 zelda3::DoorType indicator_type = preview_door_type_;
739 auto* room = GetCurrentRoom();
740 if (room && selected_door_index_.has_value() &&
741 *selected_door_index_ < room->GetDoors().size()) {
742 indicator_type = room->GetDoors()[*selected_door_index_].type;
743 }
744 // Draw indicators for 6 positions in this section
745 for (uint8_t i = 0; i < 6; ++i) {
746 uint8_t pos = start_pos + i;
747 const auto [pixel_x, pixel_y, width, height] =
749 indicator_type);
750
751 const ImVec2 snap_start = transform.RoomPixelsToScreen(
752 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
753 const ImVec2 snap_size = transform.RoomSizeToScreen(
754 ImVec2(static_cast<float>(width), static_cast<float>(height)));
755 const ImVec2 snap_end(snap_start.x + snap_size.x,
756 snap_start.y + snap_size.y);
757
758 if (pos == nearest_snap) {
759 ImVec4 highlight(theme.dungeon_selection_primary.x,
760 theme.dungeon_selection_primary.y,
761 theme.dungeon_selection_primary.z, 0.75f);
762 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(highlight),
763 0.0f, 0, 2.5f);
764 } else {
765 ImVec4 ghost(1.0f, 1.0f, 1.0f, 0.25f);
766 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
767 0, 1.0f);
768 }
769 }
770}
771
772} // 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.
bool HandleOverlayClick(int canvas_x, int canvas_y)
std::pair< int, int > GetCurrentDragAnchorPosition() const
bool NudgeSelected(int delta_x, int delta_y)
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
void DrawSnapIndicators()
Draw snap position indicators during door drag.
void SelectDoor(size_t index)
Select door at index.
void DrawGhostPreview() override
Draw ghost preview during placement.
void NavigateToPairBadge(const PairBadgeOverlay &badge) const
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
void CancelPlacement() override
Cancel current placement.
void BeginPlacement() override
Begin placement mode.
void HandleRelease() override
Handle mouse release.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
GhostCapacityState GetPlacementGhostCapacityState() const
bool UpdateSnappedPosition(int canvas_x, int canvas_y)
Update snapped position based on cursor.
void PlaceDoorAtSnappedPosition(int canvas_x, int canvas_y)
Place door at snapped position.
bool MutateDoorType(size_t index, zelda3::DoorType new_type)
Change the type of a door in place, re-encoding ROM bytes.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
std::optional< PairBadgeOverlay > BuildPairBadgeOverlay(const zelda3::Room::Door &door, ImVec2 door_pos, ImVec2 door_size, float scale) const
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
static uint8_t GetSectionStartPosition(DoorDirection direction, bool is_inner)
Get the starting position index for outer/inner section.
static constexpr int kMaxDoorPositions
static std::tuple< int, int, int, int > GetDoorEditorBounds(uint8_t position, DoorDirection direction, DoorType type)
Get the editor interaction bounds for a typed door.
static bool DetectWallFromPosition(int canvas_x, int canvas_y, DoorDirection &out_direction)
Detect which wall the cursor is near.
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
static std::pair< int, int > PositionToTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to tile coordinates.
static bool DetectWallSection(int canvas_x, int canvas_y, DoorDirection &out_direction, bool &out_is_inner)
Detect wall with inner/outer section information.
static uint8_t SnapToNearestPosition(int canvas_x, int canvas_y, DoorDirection direction)
Convert canvas coordinates to nearest valid door position.
const std::vector< Door > & GetDoors() const
Definition room.h:373
const AgentUITheme & GetTheme()
PlacementCapacityState ToPlacementCapacityState(DoorInteractionHandler::GhostCapacityState state)
Editors are the view controllers for the application.
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
std::string_view GetPlacementCapacityStatusText(PlacementCapacityState state)
void DrawPlacementCapacityBadge(ImDrawList *draw_list, const ImVec2 &badge_min, const AgentUITheme &theme, PlacementCapacityState state, std::string_view primary_text)
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
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)
DoorType
Door types from ALTTP.
Definition door_types.h:33
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
Definition door_types.h:333
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
constexpr size_t kMaxDoors
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
std::function< void(int target_room_id, std::optional< size_t > target_door_index, int target_tile_x, int target_tile_y)> on_door_pair_navigation
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.
Represents a door in a dungeon room.
Definition room.h:299
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:304
DoorType type
Door type (determines appearance/behavior)
Definition room.h:301
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:349
DoorDirection direction
Which wall the door is on.
Definition room.h:302
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:300
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:305