6#include "imgui/imgui.h"
13#include "absl/strings/str_format.h"
32 return ImVec2(std::max(1.0f,
static_cast<float>(label.size()) * 7.0f), 14.0f);
61 if (door_index.has_value()) {
63 if (!room || *door_index >= room->GetDoors().size()) {
66 const auto [anchor_x, anchor_y] =
67 room->
GetDoors()[*door_index].GetPixelCoords();
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));
125 drag_x, drag_y, direction);
128 auto candidate = room->GetDoors();
130 if (door.position != position || door.direction != direction) {
131 door.position = position;
132 door.direction = direction;
135 auto [b1, b2] = door.EncodeBytes();
145 room->GetDoors() = std::move(candidate);
146 room->MarkObjectStreamDirty();
165 const auto& doors = room->GetDoors();
171 const auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
173 const float scale = transform.
scale();
175 ImVec2(
static_cast<float>(door_x),
static_cast<float>(door_y)));
177 ImVec2(
static_cast<float>(door_w),
static_cast<float>(door_h)));
179 if (!badge.has_value() || badge->target_room_id < 0) {
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) {
200 const size_t current_door_count = room ? room->GetDoors().size() : 0;
210 if (!pointer_screen_pos.has_value())
214 const auto [canvas_x, canvas_y] =
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."));
231 const auto [snap_canvas_x, snap_canvas_y, door_width_px, door_height_px] =
236 ImDrawList* draw_list = ImGui::GetWindowDrawList();
237 const float scale = transform.
scale();
239 static_cast<float>(snap_canvas_x),
static_cast<float>(snap_canvas_y)));
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);
248 const auto placement_state = ToPlacementCapacityState(capacity_state);
250 const size_t current_door_count = room ? room->GetDoors().size() : 0;
253 theme, placement_state, theme.dungeon_selection_primary);
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));
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);
268 std::string label = type_name +
" (" + dir_name +
")";
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());
273 const std::string badge_text =
278 badge_min = ImVec2(preview_start.x, preview_end.y + 6.0f);
281 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
282 const ImVec2 status_size = ImGui::CalcTextSize(
284 const float badge_height = badge_size.y + status_size.y + 14.0f;
286 ImVec2(preview_start.x, preview_start.y - badge_height - 6.0f);
290 badge_min = ImVec2(preview_end.x + 6.0f, preview_start.y);
293 const ImVec2 badge_size = ImGui::CalcTextSize(badge_text.c_str());
294 const ImVec2 status_size = ImGui::CalcTextSize(
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);
306 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
321 const auto& doors = room->GetDoors();
326 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
333 bool is_inner =
false;
337 drag_x, drag_y, dir);
338 std::tie(door_x, door_y, door_w, door_h) =
344 ImDrawList* draw_list = ImGui::GetWindowDrawList();
346 const float scale = transform.
scale();
348 ImVec2(
static_cast<float>(door_x),
static_cast<float>(door_y)));
350 ImVec2(
static_cast<float>(door_w),
static_cast<float>(door_h)));
353 static float pulse = 0.0f;
354 pulse += ImGui::GetIO().DeltaTime * 3.0f;
355 float alpha = 0.5f + 0.3f * sinf(pulse);
357 ImU32 color = IM_COL32(255, 165, 0, 180);
359 (color & 0x00FFFFFF) | (
static_cast<ImU32
>(alpha * 100) << 24);
361 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
363 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
366 ImVec2 label_pos(pos.x, pos.y - 14 * scale);
367 draw_list->AddText(label_pos, IM_COL32(255, 255, 255, 220),
"Door");
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 =
375 interactive && ImGui::IsMouseHoveringRect(badge->screen_pos, badge_max);
377 draw_list->AddText(badge->screen_pos, badge->color, badge->label.c_str());
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,
384 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
385 ImGui::SetTooltip(tr(
"Open room 0x%03X"), badge->target_room_id);
395std::optional<DoorInteractionHandler::PairBadgeOverlay>
397 ImVec2 door_pos, ImVec2 door_size,
413 const int neighbor = *target;
416 const uint8_t return_position =
425 if (!source_room || !target_room || !source_room->AreObjectsLoaded() ||
427 badge.
label = absl::StrFormat(
"inspect 0x%03X", neighbor);
428 badge.
color = IM_COL32(170, 170, 170, 220);
434 *source_room, *target_room,
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);
444 if (plan->changed()) {
445 badge.
label = absl::StrFormat(
"layer mismatch 0x%03X", neighbor);
446 badge.
color = IM_COL32(255, 200, 90, 235);
448 badge.
label = absl::StrFormat(
"pair 0x%03X", neighbor);
449 badge.
color = IM_COL32(120, 220, 150, 235);
453 }
else if (
const int neighbor =
456 badge.
label =
"edge";
457 badge.
color = IM_COL32(170, 170, 170, 220);
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 ||
470 any_on_opposite =
true;
482 const auto [target_tile_x, target_tile_y] = target_door.GetTileCoords();
486 const auto [target_tile_x, target_tile_y] =
495 badge.
label = absl::StrFormat(
"nearby 0x%03X", neighbor);
496 badge.
color = IM_COL32(255, 200, 90, 235);
497 }
else if (any_on_opposite) {
498 badge.
label = absl::StrFormat(
"~0x%03X", neighbor);
499 badge.
color = IM_COL32(255, 200, 90, 235);
501 badge.
label = absl::StrFormat(
"no pair 0x%03X", neighbor);
502 badge.
color = IM_COL32(255, 130, 90, 235);
508 badge.
screen_pos = ImVec2(door_pos.x + 40.0f, door_pos.y - 14 * scale);
511 badge.
screen_pos = ImVec2(door_pos.x, door_pos.y + door_size.y + 2.0f);
515 ImVec2(door_pos.x - 88.0f, door_pos.y + door_size.y * 0.5f - 7.0f);
518 badge.
screen_pos = ImVec2(door_pos.x + door_size.x + 6.0f,
519 door_pos.y + door_size.y * 0.5f - 7.0f);
536 int canvas_x,
int canvas_y)
const {
544 const auto& doors = room->
GetDoors();
545 for (
size_t i = 0; i < doors.size(); ++i) {
546 const auto& door = doors[i];
548 auto [door_x, door_y, door_w, door_h] = door.GetEditorBounds();
550 if (canvas_x >= door_x && canvas_x < door_x + door_w &&
551 canvas_y >= door_y && canvas_y < door_y + door_h) {
570 const std::vector<zelda3::Room::Door>& candidate) {
575 const auto* source_context =
ctx_;
583 "Door room changed during edit preflight; repeat the edit");
600 auto candidate = room->GetDoors();
604 candidate.erase(candidate.begin() +
611 room->GetDoors() = std::move(candidate);
612 room->MarkObjectStreamDirty();
625 if (!room || room->GetDoors().empty()) {
634 room->GetDoors().clear();
635 room->MarkObjectStreamDirty();
652 auto candidate = room->GetDoors();
658 int position_delta = 0;
659 switch (door.direction) {
662 position_delta = delta_x;
666 position_delta = delta_y;
670 if (position_delta == 0) {
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)) {
683 door.position =
static_cast<uint8_t
>(next_position);
684 auto [b1, b2] = door.EncodeBytes();
692 room->GetDoors() = std::move(candidate);
693 room->MarkObjectStreamDirty();
703 if (!room || index >= room->GetDoors().size()) {
706 const auto& door = room->GetDoors()[index];
707 return UpdateDoor(index, new_type, door.direction, door.position);
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 ||
724 if (!room || index >= room->GetDoors().size()) {
727 auto candidate = room->GetDoors();
728 auto& door = candidate[index];
729 if (door.type == type && door.direction == direction &&
730 door.position == position) {
734 door.direction = direction;
735 door.position = position;
736 const auto [b1, b2] = door.EncodeBytes();
744 room->GetDoors() = std::move(candidate);
745 room->MarkObjectStreamDirty();
782 canvas_x, canvas_y, direction);
800 new_door.
byte1 = byte1;
801 new_door.
byte2 = byte2;
803 auto candidate = room->GetDoors();
804 candidate.push_back(new_door);
813 room->AddDoor(new_door);
828 canvas_x, canvas_y, direction);
847 bool is_inner =
false;
856 drag_x, drag_y, direction);
858 ImDrawList* draw_list = ImGui::GetWindowDrawList();
864 *selected_door_index_ < room->GetDoors().size()) {
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] =
875 ImVec2(
static_cast<float>(pixel_x),
static_cast<float>(pixel_y)));
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);
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),
888 ImVec4 ghost(1.0f, 1.0f, 1.0f, 0.25f);
889 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
std::optional< ImVec2 > GetPointerScreenPosition() const
DungeonCanvasTransform GetCanvasTransform() const
void TriggerSuccessToast()
InteractionContext * ctx_
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)
bool door_placement_mode_
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.
PlacementBlockReason placement_block_reason_
void SelectDoor(size_t index)
Select door at index.
std::optional< size_t > selected_door_index_
bool PreflightDoors(const std::vector< zelda3::Room::Door > &candidate)
zelda3::DoorType preview_door_type_
void DrawGhostPreview() override
Draw ghost preview during placement.
void ClearSelection()
Clear door selection.
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_)
void DeleteSelected()
Delete selected door.
bool UpdateDoor(size_t index, zelda3::DoorType type, zelda3::DoorDirection direction, uint8_t position)
uint8_t snapped_door_position_
void CancelPlacement() override
Cancel current placement.
zelda3::DoorDirection detected_door_direction_
void BeginPlacement() override
Begin placement mode.
void HandleRelease() override
Handle mouse release.
absl::Status mutation_status_
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.
ImVec2 drag_grab_offset_from_anchor_
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
DoorMutationPreflight door_mutation_preflight_
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
const AgentUITheme & GetTheme()
PlacementCapacityState ToPlacementCapacityState(DoorInteractionHandler::GhostCapacityState state)
ImVec2 EstimateBadgeTextSize(const std::string &label)
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.
@ 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.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
constexpr size_t kMaxDoors
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
DoorDirection
Door direction on room walls.
@ 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::optional< size_t > target_door_index
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.
uint8_t byte1
Original ROM byte 1 (position data)
DoorType type
Door type (determines appearance/behavior)
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
DoorDirection direction
Which wall the door is on.
uint8_t position
Encoded position (5-bit, 0-31)
uint8_t byte2
Original ROM byte 2 (type + direction)