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 SelectDoor(*door_index);
62 is_dragging_ = true;
64 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
66 return true;
67 }
68
70 return false;
71}
72
73void DoorInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
74 if (!is_dragging_ || !selected_door_index_.has_value())
75 return;
76
77 drag_current_pos_ = current_pos;
78}
79
81 if (!is_dragging_ || !selected_door_index_.has_value()) {
82 is_dragging_ = false;
83 return;
84 }
85
86 auto* room = GetCurrentRoom();
87 if (!room) {
88 is_dragging_ = false;
89 return;
90 }
91
92 auto& doors = room->GetDoors();
93 if (*selected_door_index_ >= doors.size()) {
94 is_dragging_ = false;
95 return;
96 }
97
98 // HandleClick primes drag state so a selected door can move immediately, but
99 // a plain click must not canonicalize distinct ROM positions that share the
100 // same visual snap slot.
103 is_dragging_ = false;
104 return;
105 }
106
107 int drag_x = static_cast<int>(drag_current_pos_.x);
108 int drag_y = static_cast<int>(drag_current_pos_.y);
109
110 // Detect wall from final position
111 zelda3::DoorDirection direction;
113 direction)) {
115 drag_x, drag_y, direction);
116
117 if (zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
118 auto& door = doors[*selected_door_index_];
119 if (door.position != position || door.direction != direction) {
121 door.position = position;
122 door.direction = direction;
123
124 // Re-encode bytes for ROM storage
125 auto [b1, b2] = door.EncodeBytes();
126 door.byte1 = b1;
127 door.byte2 = b2;
128
129 room->MarkObjectStreamDirty();
131 }
132 }
133 }
134
135 is_dragging_ = false;
136}
137
138bool DoorInteractionHandler::HandleOverlayClick(int canvas_x, int canvas_y) {
139 if (!selected_door_index_.has_value() || !HasValidContext() || is_dragging_) {
140 return false;
141 }
142
143 auto* room = GetCurrentRoom();
144 if (!room) {
145 return false;
146 }
147
148 const auto& doors = room->GetDoors();
149 if (*selected_door_index_ >= doors.size()) {
150 return false;
151 }
152
153 const auto& door = doors[*selected_door_index_];
154 const auto [tile_x, tile_y] = door.GetTileCoords();
155 const auto dims = door.GetEditorDimensions();
156 const DungeonCanvasTransform transform = GetCanvasTransform();
157 const float scale = transform.scale();
158 const ImVec2 door_pos =
159 transform.RoomPixelsToScreen(ImVec2(tile_x * 8.0f, tile_y * 8.0f));
160 const ImVec2 door_size = transform.RoomSizeToScreen(
161 ImVec2(dims.width_tiles * 8.0f, dims.height_tiles * 8.0f));
162 const auto badge = BuildPairBadgeOverlay(door, door_pos, door_size, scale);
163 if (!badge.has_value() || badge->target_room_id < 0) {
164 return false;
165 }
166
167 const ImVec2 screen_pos = transform.RoomPixelsToScreen(
168 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y)));
169 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
170 badge->screen_pos.y + badge->screen_size.y);
171 if (screen_pos.x < badge->screen_pos.x ||
172 screen_pos.y < badge->screen_pos.y || screen_pos.x > badge_max.x ||
173 screen_pos.y > badge_max.y) {
174 return false;
175 }
176
177 NavigateToPairBadge(*badge);
178 return true;
179}
180
183 auto* room = GetCurrentRoom();
184 const size_t current_door_count = room ? room->GetDoors().size() : 0;
185 return static_cast<GhostCapacityState>(
186 GetPlacementCapacityState(current_door_count, zelda3::kMaxDoors));
187}
188
191 return;
192
193 const auto pointer_screen_pos = GetPointerScreenPosition();
194 if (!pointer_screen_pos.has_value())
195 return;
196
197 const DungeonCanvasTransform transform = GetCanvasTransform();
198 const auto [canvas_x, canvas_y] =
199 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
200
201 // Try to update snapped position
202 if (!UpdateSnappedPosition(canvas_x, canvas_y)) {
203 // Placement guidance: make invalid hover state explicit instead of showing
204 // no preview.
205 const auto& theme = AgentUI::GetTheme();
206 ImDrawList* draw_list = ImGui::GetWindowDrawList();
207 ImVec2 hint_pos(pointer_screen_pos->x + 14.0f,
208 pointer_screen_pos->y + 10.0f);
209 draw_list->AddText(hint_pos, ImGui::GetColorU32(theme.status_warning),
210 "Move cursor near a wall to place door");
211 ImGui::SetTooltip(tr("Door placement requires a wall-adjacent position."));
212 return; // Not near a wall
213 }
214
215 // Get door position in tile coordinates
218
219 // Get door dimensions
222 int door_width_px = dims.width_tiles * 8;
223 int door_height_px = dims.height_tiles * 8;
224
225 // Convert to canvas pixel coordinates
226 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvas(tile_x, tile_y);
227
228 // Draw ghost preview
229 ImDrawList* draw_list = ImGui::GetWindowDrawList();
230 const float scale = transform.scale();
231 const ImVec2 preview_start = transform.RoomPixelsToScreen(ImVec2(
232 static_cast<float>(snap_canvas_x), static_cast<float>(snap_canvas_y)));
233 const ImVec2 preview_size = transform.RoomSizeToScreen(ImVec2(
234 static_cast<float>(door_width_px), static_cast<float>(door_height_px)));
235 const ImVec2 preview_end(preview_start.x + preview_size.x,
236 preview_start.y + preview_size.y);
237
238 const auto& theme = AgentUI::GetTheme();
239
240 const auto capacity_state = GetPlacementGhostCapacityState();
241 const auto placement_state = ToPlacementCapacityState(capacity_state);
242 auto* room = GetCurrentRoom();
243 const size_t current_door_count = room ? room->GetDoors().size() : 0;
244
245 const ImVec4 base_color = GetPlacementAccentColor(
246 theme, placement_state, theme.dungeon_selection_primary);
247
248 // Draw semi-transparent filled rectangle
249 ImVec4 fill_vec(base_color.x, base_color.y, base_color.z, 0.31f);
250 draw_list->AddRectFilled(preview_start, preview_end,
251 ImGui::GetColorU32(fill_vec));
252
253 // Draw outline
254 ImVec4 outline_color(base_color.x, base_color.y, base_color.z, 0.9f);
255 draw_list->AddRect(preview_start, preview_end,
256 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
257
258 // Draw door type label
259 std::string type_name(zelda3::GetDoorTypeName(preview_door_type_));
261 std::string label = type_name + " (" + dir_name + ")";
262
263 ImVec2 text_pos(preview_start.x, preview_start.y - 16 * scale);
264 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 200), label.c_str());
265
266 const std::string badge_text =
267 absl::StrFormat("Doors %zu/%zu", current_door_count, zelda3::kMaxDoors);
268 ImVec2 badge_min;
269 switch (detected_door_direction_) {
271 badge_min = ImVec2(preview_start.x, preview_end.y + 6.0f);
272 break;
274 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
275 const ImVec2 status_size = ImGui::CalcTextSize(
276 GetPlacementCapacityStatusText(placement_state).data());
277 const float badge_height = badge_size.y + status_size.y + 14.0f;
278 badge_min =
279 ImVec2(preview_start.x, preview_start.y - badge_height - 6.0f);
280 break;
281 }
283 badge_min = ImVec2(preview_end.x + 6.0f, preview_start.y);
284 break;
286 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
287 const ImVec2 status_size = ImGui::CalcTextSize(
288 GetPlacementCapacityStatusText(placement_state).data());
289 const float badge_width = std::max(badge_size.x, status_size.x) + 14.0f;
290 badge_min = ImVec2(preview_start.x - badge_width - 6.0f, preview_start.y);
291 break;
292 }
293 }
294 DrawPlacementCapacityBadge(draw_list, badge_min, theme, placement_state,
295 badge_text);
296
297 // Capacity tooltip when at/near limit
298 if (capacity_state != GhostCapacityState::kNormal &&
299 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
300 ImGui::SetTooltip(
301 tr("Doors: %zu/%zu\n%s"), current_door_count, zelda3::kMaxDoors,
302 GetPlacementCapacityTooltipSuffix(placement_state).data());
303 }
304}
305
307 if (!selected_door_index_.has_value() || !HasValidContext())
308 return;
309
310 auto* room = GetCurrentRoom();
311 if (!room)
312 return;
313
314 const auto& doors = room->GetDoors();
315 if (*selected_door_index_ >= doors.size())
316 return;
317
318 const auto& door = doors[*selected_door_index_];
319 auto [tile_x, tile_y] = door.GetTileCoords();
320 auto dims = door.GetEditorDimensions();
321
322 // If dragging, use current drag position for door preview
323 if (is_dragging_) {
324 int drag_x = static_cast<int>(drag_current_pos_.x);
325 int drag_y = static_cast<int>(drag_current_pos_.y);
326
328 bool is_inner = false;
330 is_inner)) {
332 drag_x, drag_y, dir);
333 auto [snap_x, snap_y] =
335 tile_x = snap_x;
336 tile_y = snap_y;
337 dims = zelda3::GetEditorDoorDimensions(dir, door.type);
338 }
339 }
340
341 ImDrawList* draw_list = ImGui::GetWindowDrawList();
342 const DungeonCanvasTransform transform = GetCanvasTransform();
343 const float scale = transform.scale();
344 const ImVec2 pos =
345 transform.RoomPixelsToScreen(ImVec2(tile_x * 8.0f, tile_y * 8.0f));
346 const ImVec2 size = transform.RoomSizeToScreen(
347 ImVec2(dims.width_tiles * 8.0f, dims.height_tiles * 8.0f));
348
349 // Animated selection
350 static float pulse = 0.0f;
351 pulse += ImGui::GetIO().DeltaTime * 3.0f;
352 float alpha = 0.5f + 0.3f * sinf(pulse);
353
354 ImU32 color = IM_COL32(255, 165, 0, 180); // Orange
355 ImU32 fill_color =
356 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
357
358 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
359 fill_color);
360 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
361 0, 2.0f);
362
363 ImVec2 label_pos(pos.x, pos.y - 14 * scale);
364 draw_list->AddText(label_pos, IM_COL32(255, 255, 255, 220), "Door");
365
366 if (auto badge = BuildPairBadgeOverlay(door, pos, size, scale)) {
367 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
368 badge->screen_pos.y + badge->screen_size.y);
369 const bool interactive =
370 badge->target_room_id >= 0 && ctx_ && ctx_->on_door_pair_navigation;
371 const bool hovered =
372 interactive && ImGui::IsMouseHoveringRect(badge->screen_pos, badge_max);
373
374 draw_list->AddText(badge->screen_pos, badge->color, badge->label.c_str());
375 if (hovered) {
376 const auto& theme = AgentUI::GetTheme();
377 const ImU32 hover_color = ImGui::GetColorU32(theme.accent_color);
378 draw_list->AddLine(ImVec2(badge->screen_pos.x, badge_max.y + 1.0f),
379 ImVec2(badge_max.x, badge_max.y + 1.0f), hover_color,
380 1.0f);
381 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
382 ImGui::SetTooltip(tr("Open room 0x%03X"), badge->target_room_id);
383 }
384 }
385
386 // Draw snap indicators when dragging
387 if (is_dragging_) {
389 }
390}
391
392std::optional<DoorInteractionHandler::PairBadgeOverlay>
394 ImVec2 door_pos, ImVec2 door_size,
395 float scale) const {
396 if (is_dragging_ || !ctx_ || !ctx_->rooms) {
397 return std::nullopt;
398 }
399
400 PairBadgeOverlay badge;
401 const int neighbor = NeighborRoomId(ctx_->current_room_id, door.direction);
402 if (neighbor < 0) {
403 badge.label = "edge";
404 badge.color = IM_COL32(170, 170, 170, 220);
405 } else {
406 badge.target_room_id = neighbor;
407 const auto opposite = OppositeDir(door.direction);
408 const auto& neighbor_doors = (*ctx_->rooms)[neighbor].GetDoors();
409 bool any_on_opposite = false;
410 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
411 const auto& nd = neighbor_doors[i];
412 if (nd.direction != opposite) {
413 continue;
414 }
415
416 any_on_opposite = true;
417 if (!badge.target_door_index.has_value()) {
418 badge.target_door_index = i;
419 }
420 if (nd.position == door.position) {
421 badge.target_door_index = i;
422 break;
423 }
424 }
425
426 if (badge.target_door_index.has_value()) {
427 const auto& target_door = neighbor_doors[*badge.target_door_index];
428 const auto [target_tile_x, target_tile_y] = target_door.GetTileCoords();
429 badge.target_tile_x = target_tile_x;
430 badge.target_tile_y = target_tile_y;
431 } else {
432 const auto [target_tile_x, target_tile_y] =
434 opposite);
435 badge.target_tile_x = target_tile_x;
436 badge.target_tile_y = target_tile_y;
437 }
438
439 if (badge.target_door_index.has_value() &&
440 neighbor_doors[*badge.target_door_index].position == door.position) {
441 badge.label = absl::StrFormat("pair 0x%03X", neighbor);
442 badge.color = IM_COL32(120, 220, 150, 235); // green
443 } else if (any_on_opposite) {
444 badge.label = absl::StrFormat("~0x%03X", neighbor);
445 badge.color = IM_COL32(255, 200, 90, 235); // amber
446 } else {
447 badge.label = absl::StrFormat("no pair 0x%03X", neighbor);
448 badge.color = IM_COL32(255, 130, 90, 235); // red-orange
449 }
450 }
451
452 switch (door.direction) {
454 badge.screen_pos = ImVec2(door_pos.x + 40.0f, door_pos.y - 14 * scale);
455 break;
457 badge.screen_pos = ImVec2(door_pos.x, door_pos.y + door_size.y + 2.0f);
458 break;
460 badge.screen_pos =
461 ImVec2(door_pos.x - 88.0f, door_pos.y + door_size.y * 0.5f - 7.0f);
462 break;
464 badge.screen_pos = ImVec2(door_pos.x + door_size.x + 6.0f,
465 door_pos.y + door_size.y * 0.5f - 7.0f);
466 break;
467 }
468 badge.screen_size = EstimateBadgeTextSize(badge.label);
469 return badge;
470}
471
473 const PairBadgeOverlay& badge) const {
474 if (!ctx_ || !ctx_->on_door_pair_navigation || badge.target_room_id < 0) {
475 return;
476 }
478 badge.target_tile_x, badge.target_tile_y);
479}
480
482 int canvas_x, int canvas_y) const {
483 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
484 return std::nullopt;
485
486 auto* room = ctx_->GetCurrentRoomConst();
487 if (!room)
488 return std::nullopt;
489
490 const auto& doors = room->GetDoors();
491 for (size_t i = 0; i < doors.size(); ++i) {
492 const auto& door = doors[i];
493
494 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
495
496 if (canvas_x >= door_x && canvas_x < door_x + door_w &&
497 canvas_y >= door_y && canvas_y < door_y + door_h) {
498 return i;
499 }
500 }
501
502 return std::nullopt;
503}
504
509
511 selected_door_index_ = std::nullopt;
512 is_dragging_ = false;
513}
514
516 if (!selected_door_index_.has_value() || !HasValidContext())
517 return;
518
519 auto* room = GetCurrentRoom();
520 if (!room)
521 return;
522
523 auto& doors = room->GetDoors();
524 if (*selected_door_index_ >= doors.size())
525 return;
526
528 doors.erase(doors.begin() + static_cast<ptrdiff_t>(*selected_door_index_));
529 room->MarkObjectStreamDirty();
533}
534
536 if (!HasValidContext()) {
537 return;
538 }
539
540 auto* room = GetCurrentRoom();
541 if (!room || room->GetDoors().empty()) {
542 return;
543 }
544
546 room->GetDoors().clear();
547 room->MarkObjectStreamDirty();
551}
552
553bool DoorInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
554 if (!selected_door_index_.has_value() || !HasValidContext()) {
555 return false;
556 }
557
558 auto* room = GetCurrentRoom();
559 if (!room) {
560 return false;
561 }
562
563 auto& doors = room->GetDoors();
564 if (*selected_door_index_ >= doors.size()) {
565 return false;
566 }
567
568 auto& door = doors[*selected_door_index_];
569 int position_delta = 0;
570 switch (door.direction) {
573 position_delta = delta_x;
574 break;
577 position_delta = delta_y;
578 break;
579 }
580
581 if (position_delta == 0) {
582 return false;
583 }
584
585 const int next_position =
586 std::clamp(static_cast<int>(door.position) + position_delta, 0,
588 if (next_position == door.position ||
590 static_cast<uint8_t>(next_position), door.direction)) {
591 return false;
592 }
593
595 door.position = static_cast<uint8_t>(next_position);
596 auto [b1, b2] = door.EncodeBytes();
597 door.byte1 = b1;
598 door.byte2 = b2;
599 room->MarkObjectStreamDirty();
602 return true;
603}
604
606 zelda3::DoorType new_type) {
607 if (!HasValidContext())
608 return false;
609
610 auto* room = GetCurrentRoom();
611 if (!room)
612 return false;
613
614 auto& doors = room->GetDoors();
615 if (index >= doors.size())
616 return false;
617
618 auto& door = doors[index];
619 if (door.type == new_type)
620 return false;
621
623
624 door.type = new_type;
625 auto [b1, b2] = door.EncodeBytes();
626 door.byte1 = b1;
627 door.byte2 = b2;
628
629 room->MarkObjectStreamDirty();
632 return true;
633}
634
636 int canvas_y) {
637 if (!HasValidContext()) {
639 return;
640 }
641
642 auto* room = GetCurrentRoom();
643 if (!room) {
645 return;
646 }
647
648 // Enforce door limit at placement time (matches DungeonValidator::zelda3::kMaxDoors)
649
650 if (room->GetDoors().size() >= zelda3::kMaxDoors) {
652 return;
653 }
654
655 // Detect wall from position
656 zelda3::DoorDirection direction;
658 direction)) {
660 return;
661 }
662
663 // Snap to nearest valid position
665 canvas_x, canvas_y, direction);
666
667 // Validate position
668 if (!zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
670 return;
671 }
672
674
676
677 // Create the door
678 zelda3::Room::Door new_door;
679 new_door.position = position;
680 new_door.type = preview_door_type_;
681 new_door.direction = direction;
682
683 // Encode bytes for ROM storage
684 auto [byte1, byte2] = new_door.EncodeBytes();
685 new_door.byte1 = byte1;
686 new_door.byte2 = byte2;
687
688 // Add door to room
689 room->AddDoor(new_door);
691
693}
694
695bool DoorInteractionHandler::UpdateSnappedPosition(int canvas_x, int canvas_y) {
696 zelda3::DoorDirection direction;
698 direction)) {
699 return false;
700 }
701
702 detected_door_direction_ = direction;
704 canvas_x, canvas_y, direction);
705 return true;
706}
707
709 if (!is_dragging_ || !HasValidContext())
710 return;
711
712 int drag_x = static_cast<int>(drag_current_pos_.x);
713 int drag_y = static_cast<int>(drag_current_pos_.y);
714
715 zelda3::DoorDirection direction;
716 bool is_inner = false;
717 if (!zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, direction,
718 is_inner)) {
719 return;
720 }
721
722 uint8_t start_pos =
725 drag_x, drag_y, direction);
726
727 ImDrawList* draw_list = ImGui::GetWindowDrawList();
728 const DungeonCanvasTransform transform = GetCanvasTransform();
729 const auto& theme = AgentUI::GetTheme();
730 zelda3::DoorType indicator_type = preview_door_type_;
731 auto* room = GetCurrentRoom();
732 if (room && selected_door_index_.has_value() &&
733 *selected_door_index_ < room->GetDoors().size()) {
734 indicator_type = room->GetDoors()[*selected_door_index_].type;
735 }
736 auto dims = zelda3::GetEditorDoorDimensions(direction, indicator_type);
737
738 // Draw indicators for 6 positions in this section
739 for (uint8_t i = 0; i < 6; ++i) {
740 uint8_t pos = start_pos + i;
741 auto [tile_x, tile_y] =
743 float pixel_x = tile_x * 8.0f;
744 float pixel_y = tile_y * 8.0f;
745
746 const ImVec2 snap_start =
747 transform.RoomPixelsToScreen(ImVec2(pixel_x, pixel_y));
748 const ImVec2 snap_size = transform.RoomSizeToScreen(
749 ImVec2(dims.width_pixels(), dims.height_pixels()));
750 const ImVec2 snap_end(snap_start.x + snap_size.x,
751 snap_start.y + snap_size.y);
752
753 if (pos == nearest_snap) {
754 ImVec4 highlight(theme.dungeon_selection_primary.x,
755 theme.dungeon_selection_primary.y,
756 theme.dungeon_selection_primary.z, 0.75f);
757 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(highlight),
758 0.0f, 0, 2.5f);
759 } else {
760 ImVec4 ghost(1.0f, 1.0f, 1.0f, 0.25f);
761 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
762 0, 1.0f);
763 }
764 }
765}
766
767} // 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 > RoomToCanvas(int room_x, int room_y) const
Convert room tile coordinates to canvas pixel coordinates.
bool HandleOverlayClick(int canvas_x, int canvas_y)
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 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:350
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 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)
constexpr DoorDimensions GetEditorDoorDimensions(DoorDirection dir, DoorType type)
Get editor interaction dimensions for a door.
Definition door_types.h:257
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:276
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:281
DoorType type
Door type (determines appearance/behavior)
Definition room.h:278
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:326
DoorDirection direction
Which wall the door is on.
Definition room.h:279
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:277
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:282