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"
19
20namespace yaze::editor {
21
22namespace {
23
28
29ImVec2 EstimateBadgeTextSize(const std::string& label) {
30 // Overlay hit-testing can run from headless interaction tests before ImGui has
31 // baked a font atlas. Keep the clickable region independent of font state.
32 return ImVec2(std::max(1.0f, static_cast<float>(label.size()) * 7.0f), 14.0f);
33}
34
35} // namespace
36
41
47
48bool DoorInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
49 if (!HasValidContext())
50 return false;
51
53 if (IsWithinBounds(canvas_x, canvas_y)) {
54 PlaceDoorAtSnappedPosition(canvas_x, canvas_y);
55 }
56 return true;
57 }
58
59 // Try to select door at position
60 auto door_index = GetEntityAtPosition(canvas_x, canvas_y);
61 if (door_index.has_value()) {
62 const auto* room = ctx_->GetCurrentRoomConst();
63 if (!room || *door_index >= room->GetDoors().size()) {
64 return false;
65 }
66 const auto [anchor_x, anchor_y] =
67 room->GetDoors()[*door_index].GetPixelCoords();
68
69 SelectDoor(*door_index);
70 is_dragging_ = true;
72 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y));
75 ImVec2(static_cast<float>(canvas_x - anchor_x),
76 static_cast<float>(canvas_y - anchor_y));
77 return true;
78 }
79
81 return false;
82}
83
84void DoorInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
85 if (!is_dragging_ || !selected_door_index_.has_value())
86 return;
87
88 drag_current_pos_ = current_pos;
89}
90
92 mutation_status_ = absl::OkStatus();
93 if (!is_dragging_ || !selected_door_index_.has_value()) {
94 is_dragging_ = false;
95 return;
96 }
97
98 auto* room = GetCurrentRoom();
99 if (!room) {
100 is_dragging_ = false;
101 return;
102 }
103
104 if (*selected_door_index_ >= room->GetDoors().size()) {
105 is_dragging_ = false;
106 return;
107 }
108
109 // HandleClick primes drag state so a selected door can move immediately, but
110 // a plain click must not canonicalize distinct ROM positions that share the
111 // same visual snap slot.
114 is_dragging_ = false;
115 return;
116 }
117
118 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
119
120 // Detect wall from final position
121 zelda3::DoorDirection direction;
123 direction)) {
125 drag_x, drag_y, direction);
126
127 if (zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
128 auto candidate = room->GetDoors();
129 auto& door = candidate[*selected_door_index_];
130 if (door.position != position || door.direction != direction) {
131 door.position = position;
132 door.direction = direction;
133
134 // Re-encode bytes for ROM storage
135 auto [b1, b2] = door.EncodeBytes();
136 door.byte1 = b1;
137 door.byte2 = b2;
138
139 if (!PreflightDoors(candidate)) {
140 is_dragging_ = false;
141 return;
142 }
143 room = GetCurrentRoom();
145 room->GetDoors() = std::move(candidate);
146 room->MarkObjectStreamDirty();
148 }
149 }
150 }
151
152 is_dragging_ = false;
153}
154
155bool DoorInteractionHandler::HandleOverlayClick(int canvas_x, int canvas_y) {
156 if (!selected_door_index_.has_value() || !HasValidContext() || is_dragging_) {
157 return false;
158 }
159
160 auto* room = GetCurrentRoom();
161 if (!room) {
162 return false;
163 }
164
165 const auto& doors = room->GetDoors();
166 if (*selected_door_index_ >= doors.size()) {
167 return false;
168 }
169
170 const auto& door = doors[*selected_door_index_];
171 const auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
172 const DungeonCanvasTransform transform = GetCanvasTransform();
173 const float scale = transform.scale();
174 const ImVec2 door_pos = transform.RoomPixelsToScreen(
175 ImVec2(static_cast<float>(door_x), static_cast<float>(door_y)));
176 const ImVec2 door_size = transform.RoomSizeToScreen(
177 ImVec2(static_cast<float>(door_w), static_cast<float>(door_h)));
178 const auto badge = BuildPairBadgeOverlay(door, door_pos, door_size, scale);
179 if (!badge.has_value() || badge->target_room_id < 0) {
180 return false;
181 }
182
183 const ImVec2 screen_pos = transform.RoomPixelsToScreen(
184 ImVec2(static_cast<float>(canvas_x), static_cast<float>(canvas_y)));
185 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
186 badge->screen_pos.y + badge->screen_size.y);
187 if (screen_pos.x < badge->screen_pos.x ||
188 screen_pos.y < badge->screen_pos.y || screen_pos.x > badge_max.x ||
189 screen_pos.y > badge_max.y) {
190 return false;
191 }
192
193 NavigateToPairBadge(*badge);
194 return true;
195}
196
199 auto* room = GetCurrentRoom();
200 const size_t current_door_count = room ? room->GetDoors().size() : 0;
201 return static_cast<GhostCapacityState>(
202 GetPlacementCapacityState(current_door_count, zelda3::kMaxDoors));
203}
204
207 return;
208
209 const auto pointer_screen_pos = GetPointerScreenPosition();
210 if (!pointer_screen_pos.has_value())
211 return;
212
213 const DungeonCanvasTransform transform = GetCanvasTransform();
214 const auto [canvas_x, canvas_y] =
215 transform.ScreenToRoomPixelCoordinates(*pointer_screen_pos);
216
217 // Try to update snapped position
218 if (!UpdateSnappedPosition(canvas_x, canvas_y)) {
219 // Placement guidance: make invalid hover state explicit instead of showing
220 // no preview.
221 const auto& theme = AgentUI::GetTheme();
222 ImDrawList* draw_list = ImGui::GetWindowDrawList();
223 ImVec2 hint_pos(pointer_screen_pos->x + 14.0f,
224 pointer_screen_pos->y + 10.0f);
225 draw_list->AddText(hint_pos, ImGui::GetColorU32(theme.status_warning),
226 "Move cursor near a wall to place door");
227 ImGui::SetTooltip(tr("Door placement requires a wall-adjacent position."));
228 return; // Not near a wall
229 }
230
231 const auto [snap_canvas_x, snap_canvas_y, door_width_px, door_height_px] =
234
235 // Draw ghost preview
236 ImDrawList* draw_list = ImGui::GetWindowDrawList();
237 const float scale = transform.scale();
238 const ImVec2 preview_start = transform.RoomPixelsToScreen(ImVec2(
239 static_cast<float>(snap_canvas_x), static_cast<float>(snap_canvas_y)));
240 const ImVec2 preview_size = transform.RoomSizeToScreen(ImVec2(
241 static_cast<float>(door_width_px), static_cast<float>(door_height_px)));
242 const ImVec2 preview_end(preview_start.x + preview_size.x,
243 preview_start.y + preview_size.y);
244
245 const auto& theme = AgentUI::GetTheme();
246
247 const auto capacity_state = GetPlacementGhostCapacityState();
248 const auto placement_state = ToPlacementCapacityState(capacity_state);
249 auto* room = GetCurrentRoom();
250 const size_t current_door_count = room ? room->GetDoors().size() : 0;
251
252 const ImVec4 base_color = GetPlacementAccentColor(
253 theme, placement_state, theme.dungeon_selection_primary);
254
255 // Draw semi-transparent filled rectangle
256 ImVec4 fill_vec(base_color.x, base_color.y, base_color.z, 0.31f);
257 draw_list->AddRectFilled(preview_start, preview_end,
258 ImGui::GetColorU32(fill_vec));
259
260 // Draw outline
261 ImVec4 outline_color(base_color.x, base_color.y, base_color.z, 0.9f);
262 draw_list->AddRect(preview_start, preview_end,
263 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
264
265 // Draw door type label
266 std::string type_name(zelda3::GetDoorTypeName(preview_door_type_));
268 std::string label = type_name + " (" + dir_name + ")";
269
270 ImVec2 text_pos(preview_start.x, preview_start.y - 16 * scale);
271 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 200), label.c_str());
272
273 const std::string badge_text =
274 absl::StrFormat("Doors %zu/%zu", current_door_count, zelda3::kMaxDoors);
275 ImVec2 badge_min;
276 switch (detected_door_direction_) {
278 badge_min = ImVec2(preview_start.x, preview_end.y + 6.0f);
279 break;
281 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
282 const ImVec2 status_size = ImGui::CalcTextSize(
283 GetPlacementCapacityStatusText(placement_state).data());
284 const float badge_height = badge_size.y + status_size.y + 14.0f;
285 badge_min =
286 ImVec2(preview_start.x, preview_start.y - badge_height - 6.0f);
287 break;
288 }
290 badge_min = ImVec2(preview_end.x + 6.0f, preview_start.y);
291 break;
293 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
294 const ImVec2 status_size = ImGui::CalcTextSize(
295 GetPlacementCapacityStatusText(placement_state).data());
296 const float badge_width = std::max(badge_size.x, status_size.x) + 14.0f;
297 badge_min = ImVec2(preview_start.x - badge_width - 6.0f, preview_start.y);
298 break;
299 }
300 }
301 DrawPlacementCapacityBadge(draw_list, badge_min, theme, placement_state,
302 badge_text);
303
304 // Capacity tooltip when at/near limit
305 if (capacity_state != GhostCapacityState::kNormal &&
306 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
307 ImGui::SetTooltip(
308 tr("Doors: %zu/%zu\n%s"), current_door_count, zelda3::kMaxDoors,
309 GetPlacementCapacityTooltipSuffix(placement_state).data());
310 }
311}
312
314 if (!selected_door_index_.has_value() || !HasValidContext())
315 return;
316
317 auto* room = GetCurrentRoom();
318 if (!room)
319 return;
320
321 const auto& doors = room->GetDoors();
322 if (*selected_door_index_ >= doors.size())
323 return;
324
325 const auto& door = doors[*selected_door_index_];
326 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
327
328 // If dragging, use current drag position for door preview
329 if (is_dragging_) {
330 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
331
333 bool is_inner = false;
335 is_inner)) {
337 drag_x, drag_y, dir);
338 std::tie(door_x, door_y, door_w, door_h) =
340 door.type);
341 }
342 }
343
344 ImDrawList* draw_list = ImGui::GetWindowDrawList();
345 const DungeonCanvasTransform transform = GetCanvasTransform();
346 const float scale = transform.scale();
347 const ImVec2 pos = transform.RoomPixelsToScreen(
348 ImVec2(static_cast<float>(door_x), static_cast<float>(door_y)));
349 const ImVec2 size = transform.RoomSizeToScreen(
350 ImVec2(static_cast<float>(door_w), static_cast<float>(door_h)));
351
352 // Animated selection
353 static float pulse = 0.0f;
354 pulse += ImGui::GetIO().DeltaTime * 3.0f;
355 float alpha = 0.5f + 0.3f * sinf(pulse);
356
357 ImU32 color = IM_COL32(255, 165, 0, 180); // Orange
358 ImU32 fill_color =
359 (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
360
361 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
362 fill_color);
363 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
364 0, 2.0f);
365
366 ImVec2 label_pos(pos.x, pos.y - 14 * scale);
367 draw_list->AddText(label_pos, IM_COL32(255, 255, 255, 220), "Door");
368
369 if (auto badge = BuildPairBadgeOverlay(door, pos, size, scale)) {
370 const ImVec2 badge_max(badge->screen_pos.x + badge->screen_size.x,
371 badge->screen_pos.y + badge->screen_size.y);
372 const bool interactive =
373 badge->target_room_id >= 0 && ctx_ && ctx_->on_door_pair_navigation;
374 const bool hovered =
375 interactive && ImGui::IsMouseHoveringRect(badge->screen_pos, badge_max);
376
377 draw_list->AddText(badge->screen_pos, badge->color, badge->label.c_str());
378 if (hovered) {
379 const auto& theme = AgentUI::GetTheme();
380 const ImU32 hover_color = ImGui::GetColorU32(theme.accent_color);
381 draw_list->AddLine(ImVec2(badge->screen_pos.x, badge_max.y + 1.0f),
382 ImVec2(badge_max.x, badge_max.y + 1.0f), hover_color,
383 1.0f);
384 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
385 ImGui::SetTooltip(tr("Open room 0x%03X"), badge->target_room_id);
386 }
387 }
388
389 // Draw snap indicators when dragging
390 if (is_dragging_) {
392 }
393}
394
395std::optional<DoorInteractionHandler::PairBadgeOverlay>
397 ImVec2 door_pos, ImVec2 door_size,
398 float scale) const {
399 if (is_dragging_ || !ctx_ || !ctx_->rooms ||
401 return std::nullopt;
402 }
403
404 PairBadgeOverlay badge;
405 const bool ordinary = door.type == zelda3::DoorType::NormalDoor ||
407 if (ordinary) {
408 const auto target =
410 if (!target.ok()) {
411 return std::nullopt;
412 }
413 const int neighbor = *target;
414 badge.target_room_id = neighbor;
415 const auto opposite = OppositeDir(door.direction);
416 const uint8_t return_position =
417 door.position < 6 ? door.position + 6 : door.position - 6;
418 std::tie(badge.target_tile_x, badge.target_tile_y) =
420 opposite);
421 // Drawing and hit-testing must not decode unopened rooms. The inspector
422 // loads target data explicitly when it builds an editable preview.
423 const auto* source_room = ctx_->rooms->GetIfLoaded(ctx_->current_room_id);
424 const auto* target_room = ctx_->rooms->GetIfLoaded(neighbor);
425 if (!source_room || !target_room || !source_room->AreObjectsLoaded() ||
426 !target_room->AreObjectsLoaded() || !selected_door_index_) {
427 badge.label = absl::StrFormat("inspect 0x%03X", neighbor);
428 badge.color = IM_COL32(170, 170, 170, 220);
429 } else {
430 const auto layer = door.type == zelda3::DoorType::NormalDoorLower
433 const auto plan = PlanDungeonDoorConnection(
434 *source_room, *target_room,
436 if (!plan.ok()) {
437 badge.label = absl::StrFormat("blocked 0x%03X", neighbor);
438 badge.color = IM_COL32(255, 200, 90, 235);
439 } else if (plan->creates_return) {
440 badge.label = absl::StrFormat("no pair 0x%03X", neighbor);
441 badge.color = IM_COL32(255, 130, 90, 235);
442 } else {
443 badge.target_door_index = plan->target_door_index;
444 if (plan->changed()) {
445 badge.label = absl::StrFormat("layer mismatch 0x%03X", neighbor);
446 badge.color = IM_COL32(255, 200, 90, 235);
447 } else {
448 badge.label = absl::StrFormat("pair 0x%03X", neighbor);
449 badge.color = IM_COL32(120, 220, 150, 235);
450 }
451 }
452 }
453 } else if (const int neighbor =
455 neighbor < 0) {
456 badge.label = "edge";
457 badge.color = IM_COL32(170, 170, 170, 220);
458 } else {
459 badge.target_room_id = neighbor;
460 const auto opposite = OppositeDir(door.direction);
461 const auto& neighbor_doors = (*ctx_->rooms)[neighbor].GetDoors();
462 bool any_on_opposite = false;
463 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
464 const auto& nd = neighbor_doors[i];
465 if (nd.direction != opposite ||
467 continue;
468 }
469
470 any_on_opposite = true;
471 if (!badge.target_door_index.has_value()) {
472 badge.target_door_index = i;
473 }
474 if (nd.position == door.position) {
475 badge.target_door_index = i;
476 break;
477 }
478 }
479
480 if (badge.target_door_index.has_value()) {
481 const auto& target_door = neighbor_doors[*badge.target_door_index];
482 const auto [target_tile_x, target_tile_y] = target_door.GetTileCoords();
483 badge.target_tile_x = target_tile_x;
484 badge.target_tile_y = target_tile_y;
485 } else {
486 const auto [target_tile_x, target_tile_y] =
488 opposite);
489 badge.target_tile_x = target_tile_x;
490 badge.target_tile_y = target_tile_y;
491 }
492
493 if (badge.target_door_index.has_value() &&
494 neighbor_doors[*badge.target_door_index].position == door.position) {
495 badge.label = absl::StrFormat("nearby 0x%03X", neighbor);
496 badge.color = IM_COL32(255, 200, 90, 235); // Unverified special door
497 } else if (any_on_opposite) {
498 badge.label = absl::StrFormat("~0x%03X", neighbor);
499 badge.color = IM_COL32(255, 200, 90, 235); // amber
500 } else {
501 badge.label = absl::StrFormat("no pair 0x%03X", neighbor);
502 badge.color = IM_COL32(255, 130, 90, 235); // red-orange
503 }
504 }
505
506 switch (door.direction) {
508 badge.screen_pos = ImVec2(door_pos.x + 40.0f, door_pos.y - 14 * scale);
509 break;
511 badge.screen_pos = ImVec2(door_pos.x, door_pos.y + door_size.y + 2.0f);
512 break;
514 badge.screen_pos =
515 ImVec2(door_pos.x - 88.0f, door_pos.y + door_size.y * 0.5f - 7.0f);
516 break;
518 badge.screen_pos = ImVec2(door_pos.x + door_size.x + 6.0f,
519 door_pos.y + door_size.y * 0.5f - 7.0f);
520 break;
521 }
522 badge.screen_size = EstimateBadgeTextSize(badge.label);
523 return badge;
524}
525
527 const PairBadgeOverlay& badge) const {
528 if (!ctx_ || !ctx_->on_door_pair_navigation || badge.target_room_id < 0) {
529 return;
530 }
532 badge.target_tile_x, badge.target_tile_y);
533}
534
536 int canvas_x, int canvas_y) const {
537 if (!HasValidContext() || !IsWithinBounds(canvas_x, canvas_y))
538 return std::nullopt;
539
540 auto* room = ctx_->GetCurrentRoomConst();
541 if (!room)
542 return std::nullopt;
543
544 const auto& doors = room->GetDoors();
545 for (size_t i = 0; i < doors.size(); ++i) {
546 const auto& door = doors[i];
547
548 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
549
550 if (canvas_x >= door_x && canvas_x < door_x + door_w &&
551 canvas_y >= door_y && canvas_y < door_y + door_h) {
552 return i;
553 }
554 }
555
556 return std::nullopt;
557}
558
563
565 selected_door_index_ = std::nullopt;
566 is_dragging_ = false;
567}
568
570 const std::vector<zelda3::Room::Door>& candidate) {
571 mutation_status_ = absl::OkStatus();
573 return true;
574 }
575 const auto* source_context = ctx_;
576 const int room_id = ctx_->current_room_id;
577 const auto* source_room = GetCurrentRoom();
578 mutation_status_ = door_mutation_preflight_(room_id, candidate);
579 if (mutation_status_.ok() &&
580 (ctx_ != source_context || !HasValidContext() ||
581 ctx_->current_room_id != room_id || GetCurrentRoom() != source_room)) {
582 mutation_status_ = absl::FailedPreconditionError(
583 "Door room changed during edit preflight; repeat the edit");
584 }
587 }
588 return mutation_status_.ok();
589}
590
592 mutation_status_ = absl::OkStatus();
593 if (!selected_door_index_.has_value() || !HasValidContext())
594 return;
595
596 auto* room = GetCurrentRoom();
597 if (!room)
598 return;
599
600 auto candidate = room->GetDoors();
601 if (*selected_door_index_ >= candidate.size())
602 return;
603
604 candidate.erase(candidate.begin() +
605 static_cast<ptrdiff_t>(*selected_door_index_));
606 if (!PreflightDoors(candidate)) {
607 return;
608 }
609 room = GetCurrentRoom();
611 room->GetDoors() = std::move(candidate);
612 room->MarkObjectStreamDirty();
616}
617
619 mutation_status_ = absl::OkStatus();
620 if (!HasValidContext()) {
621 return;
622 }
623
624 auto* room = GetCurrentRoom();
625 if (!room || room->GetDoors().empty()) {
626 return;
627 }
628
629 if (!PreflightDoors({})) {
630 return;
631 }
632 room = GetCurrentRoom();
634 room->GetDoors().clear();
635 room->MarkObjectStreamDirty();
639}
640
641bool DoorInteractionHandler::NudgeSelected(int delta_x, int delta_y) {
642 mutation_status_ = absl::OkStatus();
643 if (!selected_door_index_.has_value() || !HasValidContext()) {
644 return false;
645 }
646
647 auto* room = GetCurrentRoom();
648 if (!room) {
649 return false;
650 }
651
652 auto candidate = room->GetDoors();
653 if (*selected_door_index_ >= candidate.size()) {
654 return false;
655 }
656
657 auto& door = candidate[*selected_door_index_];
658 int position_delta = 0;
659 switch (door.direction) {
662 position_delta = delta_x;
663 break;
666 position_delta = delta_y;
667 break;
668 }
669
670 if (position_delta == 0) {
671 return false;
672 }
673
674 const int next_position =
675 std::clamp(static_cast<int>(door.position) + position_delta, 0,
677 if (next_position == door.position ||
679 static_cast<uint8_t>(next_position), door.direction)) {
680 return false;
681 }
682
683 door.position = static_cast<uint8_t>(next_position);
684 auto [b1, b2] = door.EncodeBytes();
685 door.byte1 = b1;
686 door.byte2 = b2;
687 if (!PreflightDoors(candidate)) {
688 return false;
689 }
690 room = GetCurrentRoom();
692 room->GetDoors() = std::move(candidate);
693 room->MarkObjectStreamDirty();
696 return true;
697}
698
700 zelda3::DoorType new_type) {
701 mutation_status_ = absl::OkStatus();
702 auto* room = GetCurrentRoom();
703 if (!room || index >= room->GetDoors().size()) {
704 return false;
705 }
706 const auto& door = room->GetDoors()[index];
707 return UpdateDoor(index, new_type, door.direction, door.position);
708}
709
711 zelda3::DoorDirection direction,
712 uint8_t position) {
713 mutation_status_ = absl::OkStatus();
714 if (!HasValidContext()) {
715 return false;
716 }
717 const auto raw_type = static_cast<uint8_t>(type);
718 if ((raw_type & 1) != 0 || raw_type > 0x66 ||
719 static_cast<unsigned>(direction) > 3 ||
720 !zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
721 return false;
722 }
723 auto* room = GetCurrentRoom();
724 if (!room || index >= room->GetDoors().size()) {
725 return false;
726 }
727 auto candidate = room->GetDoors();
728 auto& door = candidate[index];
729 if (door.type == type && door.direction == direction &&
730 door.position == position) {
731 return false;
732 }
733 door.type = type;
734 door.direction = direction;
735 door.position = position;
736 const auto [b1, b2] = door.EncodeBytes();
737 door.byte1 = b1;
738 door.byte2 = b2;
739 if (!PreflightDoors(candidate)) {
740 return false;
741 }
742 room = GetCurrentRoom();
744 room->GetDoors() = std::move(candidate);
745 room->MarkObjectStreamDirty();
748 return true;
749}
750
752 int canvas_y) {
753 mutation_status_ = absl::OkStatus();
754 if (!HasValidContext()) {
756 return;
757 }
758
759 auto* room = GetCurrentRoom();
760 if (!room) {
762 return;
763 }
764
765 // Enforce door limit at placement time (matches DungeonValidator::zelda3::kMaxDoors)
766
767 if (room->GetDoors().size() >= zelda3::kMaxDoors) {
769 return;
770 }
771
772 // Detect wall from position
773 zelda3::DoorDirection direction;
775 direction)) {
777 return;
778 }
779
780 // Snap to nearest valid position
782 canvas_x, canvas_y, direction);
783
784 // Validate position
785 if (!zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
787 return;
788 }
789
791
792 // Create the door
793 zelda3::Room::Door new_door;
794 new_door.position = position;
795 new_door.type = preview_door_type_;
796 new_door.direction = direction;
797
798 // Encode bytes for ROM storage
799 auto [byte1, byte2] = new_door.EncodeBytes();
800 new_door.byte1 = byte1;
801 new_door.byte2 = byte2;
802
803 auto candidate = room->GetDoors();
804 candidate.push_back(new_door);
805 if (!PreflightDoors(candidate)) {
807 return;
808 }
809 room = GetCurrentRoom();
811
812 // Add door to room
813 room->AddDoor(new_door);
815
817}
818
819bool DoorInteractionHandler::UpdateSnappedPosition(int canvas_x, int canvas_y) {
820 zelda3::DoorDirection direction;
822 direction)) {
823 return false;
824 }
825
826 detected_door_direction_ = direction;
828 canvas_x, canvas_y, direction);
829 return true;
830}
831
833 const {
834 return {
835 static_cast<int>(drag_current_pos_.x - drag_grab_offset_from_anchor_.x),
836 static_cast<int>(drag_current_pos_.y - drag_grab_offset_from_anchor_.y),
837 };
838}
839
841 if (!is_dragging_ || !HasValidContext())
842 return;
843
844 const auto [drag_x, drag_y] = GetCurrentDragAnchorPosition();
845
846 zelda3::DoorDirection direction;
847 bool is_inner = false;
848 if (!zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, direction,
849 is_inner)) {
850 return;
851 }
852
853 uint8_t start_pos =
856 drag_x, drag_y, direction);
857
858 ImDrawList* draw_list = ImGui::GetWindowDrawList();
859 const DungeonCanvasTransform transform = GetCanvasTransform();
860 const auto& theme = AgentUI::GetTheme();
861 zelda3::DoorType indicator_type = preview_door_type_;
862 auto* room = GetCurrentRoom();
863 if (room && selected_door_index_.has_value() &&
864 *selected_door_index_ < room->GetDoors().size()) {
865 indicator_type = room->GetDoors()[*selected_door_index_].type;
866 }
867 // Draw indicators for 6 positions in this section
868 for (uint8_t i = 0; i < 6; ++i) {
869 uint8_t pos = start_pos + i;
870 const auto [pixel_x, pixel_y, width, height] =
872 indicator_type);
873
874 const ImVec2 snap_start = transform.RoomPixelsToScreen(
875 ImVec2(static_cast<float>(pixel_x), static_cast<float>(pixel_y)));
876 const ImVec2 snap_size = transform.RoomSizeToScreen(
877 ImVec2(static_cast<float>(width), static_cast<float>(height)));
878 const ImVec2 snap_end(snap_start.x + snap_size.x,
879 snap_start.y + snap_size.y);
880
881 if (pos == nearest_snap) {
882 ImVec4 highlight(theme.dungeon_selection_primary.x,
883 theme.dungeon_selection_primary.y,
884 theme.dungeon_selection_primary.z, 0.75f);
885 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(highlight),
886 0.0f, 0, 2.5f);
887 } else {
888 ImVec4 ghost(1.0f, 1.0f, 1.0f, 0.25f);
889 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
890 0, 1.0f);
891 }
892 }
893}
894
895} // 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.
bool PreflightDoors(const std::vector< zelda3::Room::Door > &candidate)
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.
std::function< void(const absl::Status &) mutation_error_callback_)
bool UpdateDoor(size_t index, zelda3::DoorType type, zelda3::DoorDirection direction, uint8_t position)
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
zelda3::Room * GetIfLoaded(int room_id)
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:413
const AgentUITheme & GetTheme()
PlacementCapacityState ToPlacementCapacityState(DoorInteractionHandler::GhostCapacityState state)
Editors are the view controllers for the application.
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
absl::StatusOr< int > DungeonConnectionTargetRoom(int source_room_id, const Door &door)
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)
absl::StatusOr< DungeonConnectionPlan > PlanDungeonDoorConnection(const zelda3::Room &source, const zelda3::Room &target, const DungeonConnectionRequest &request)
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ NormalDoor
Normal door (upper layer)
@ NormalDoorLower
Normal door (lower layer)
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:339
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:344
DoorType type
Door type (determines appearance/behavior)
Definition room.h:341
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:389
DoorDirection direction
Which wall the door is on.
Definition room.h:342
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:340
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:345