yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_connection_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <string>
5
9#include "imgui/imgui.h"
10
11namespace yaze::editor {
12namespace {
13
14void DrawEndpoint(const char* label, int room_id,
15 const zelda3::Room::Door& proposed,
16 const zelda3::Room::Door* existing) {
17 ImGui::PushID(label);
18 ImGui::TextWrapped("%s · room %03X", label, room_id);
19 if (ImGui::BeginTable(
20 "##Endpoint", 2,
21 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
22 constexpr float kSize = 52.0f;
23 ImGui::TableSetupColumn("Slot preview", ImGuiTableColumnFlags_WidthFixed,
24 kSize);
25 ImGui::TableSetupColumn("Endpoint", ImGuiTableColumnFlags_WidthStretch);
26 ImGui::TableNextRow();
27 ImGui::TableNextColumn();
28 const ImVec2 origin = ImGui::GetCursorScreenPos();
29 auto* draw = ImGui::GetWindowDrawList();
30 const ImVec2 corner(origin.x + kSize, origin.y + kSize);
31 draw->AddRectFilled(origin, corner, ImGui::GetColorU32(ImGuiCol_FrameBg),
32 3.0f);
33 draw->AddRect(origin, corner, ImGui::GetColorU32(ImGuiCol_Border), 3.0f);
34 const auto [x, y, width, height] = proposed.GetEditorBounds();
35 constexpr float kScale = kSize / 512.0f;
36 const ImVec2 slot_min(origin.x + std::clamp(x * kScale, 0.0f, kSize - 3),
37 origin.y + std::clamp(y * kScale, 0.0f, kSize - 3));
38 const ImVec2 slot_max(
39 std::min(corner.x, slot_min.x + std::max(3.0f, width * kScale)),
40 std::min(corner.y, slot_min.y + std::max(3.0f, height * kScale)));
41 draw->AddRectFilled(slot_min, slot_max,
42 ImGui::GetColorU32(ImGuiCol_PlotHistogram));
43 ImGui::Dummy(ImVec2(kSize, kSize));
44 if (ImGui::IsItemHovered()) {
45 ImGui::SetTooltip("Door slot preview in the room's 64 by 64 tile grid");
46 }
47 ImGui::TableNextColumn();
48 ImGui::TextWrapped(
49 "%s · slot %02X",
50 std::string(zelda3::GetDoorDirectionName(proposed.direction)).c_str(),
51 proposed.position);
52 if (existing && existing->type != proposed.type) {
53 ImGui::TextWrapped(
54 "%s → %s",
55 std::string(zelda3::GetDoorTypeName(existing->type)).c_str(),
56 std::string(zelda3::GetDoorTypeName(proposed.type)).c_str());
57 } else {
58 ImGui::TextWrapped(
59 "%s%s", existing ? "" : "New: ",
60 std::string(zelda3::GetDoorTypeName(proposed.type)).c_str());
61 }
62 ImGui::EndTable();
63 }
64 ImGui::PopID();
65}
66
68 const std::function<void(int, size_t)>& jump_to_reciprocal) {
69 if (!plan.creates_return && jump_to_reciprocal) {
70 jump_to_reciprocal(plan.target_room_id, plan.target_door_index);
71 return;
72 }
73 if (!plan.creates_return &&
75 plan.target_door_index)) {
76 return;
77 }
78 viewer.NavigateToRoom(plan.target_room_id);
79 if (viewer.current_room_id() != plan.target_room_id) {
80 return;
81 }
82 const auto& door = plan.target_after[plan.target_door_index];
83 const auto [x, y, width, height] = door.GetEditorBounds();
84 if (!plan.creates_return) {
85 viewer.object_interaction().SelectEntity(EntityType::Door,
87 }
88 viewer.ScrollToTile((x + width / 2) / 8, (y + height / 2) / 8);
89 viewer.TriggerCanvasPingRect(x, y, width, height);
90}
91
92} // namespace
93
95 DungeonCanvasViewer& viewer, size_t door_index,
96 const std::function<void(int, size_t)>& jump_to_reciprocal) {
97 const auto* room = viewer.rooms()
98 ? viewer.rooms()->GetIfLoaded(viewer.current_room_id())
99 : nullptr;
100 if (!room || door_index >= room->GetDoors().size()) {
101 return;
102 }
103 const auto source = room->GetDoors()[door_index];
104 const std::array<uint8_t, 3> source_identity{
105 source.position, static_cast<uint8_t>(source.type),
106 static_cast<uint8_t>(source.direction)};
107 auto& state = viewer.connection_editor_state();
108 if (state.room_id != viewer.current_room_id() ||
109 state.door_index != door_index || state.rom != viewer.rom() ||
110 state.source != source_identity) {
111 state = {};
112 state.room_id = viewer.current_room_id();
113 state.door_index = door_index;
114 state.rom = viewer.rom();
115 state.source = source_identity;
116 state.layer = source.type == zelda3::DoorType::NormalDoorLower
119 }
120
121 gui::AutoWidgetScope scope("Dungeon/ConnectionEditor");
122 ImGui::PushID("DungeonConnectionEditor");
123 ImGui::PushID(state.room_id);
124 ImGui::PushID(static_cast<int>(door_index));
125 ImGui::Spacing();
126 ImGui::Separator();
127 ImGui::TextUnformatted("Room connection");
128 ImGui::TextWrapped("Preview both endpoints before applying the pair.");
129
130 const bool normal = source.type == zelda3::DoorType::NormalDoor ||
132 ImGui::BeginDisabled(viewer.header_read_only() ||
133 !viewer.IsObjectInteractionEnabled() || !normal);
134 ImGui::TextUnformatted("Pair layer");
135 ImGui::SetNextItemWidth(-1);
136 const bool lower = state.layer == DungeonConnectionLayer::kLower;
137 const bool open =
138 ImGui::BeginCombo("##ConnectionLayer", lower ? "Lower" : "Upper");
139 gui::AutoRegisterLastItem("combo", "ConnectionLayer");
140 if (open) {
141 if (ImGui::Selectable("Upper", !lower)) {
142 state.layer = DungeonConnectionLayer::kUpper;
143 state.error.clear();
144 }
145 gui::AutoRegisterLastItem("selectable", "ConnectionUpper");
146 if (ImGui::Selectable("Lower", lower)) {
147 state.layer = DungeonConnectionLayer::kLower;
148 state.error.clear();
149 }
150 gui::AutoRegisterLastItem("selectable", "ConnectionLower");
151 ImGui::EndCombo();
152 }
153 ImGui::EndDisabled();
154
155 const DungeonConnectionRequest request{state.room_id, door_index,
156 state.layer};
157 const auto preview = viewer.PreviewDoorConnection(request);
158 if (!preview.ok()) {
159 DrawEndpoint("Source", state.room_id, source, &source);
160 ImGui::TextWrapped("Connection unavailable: %s",
161 std::string(preview.status().message()).c_str());
162 } else {
163 const auto& plan = *preview;
164 const auto* target_before =
165 plan.creates_return ? nullptr
166 : &plan.target_before[plan.target_door_index];
167 DrawEndpoint("Source", state.room_id, plan.source_after[door_index],
168 &plan.source_before[door_index]);
169 DrawEndpoint("Return", plan.target_room_id,
170 plan.target_after[plan.target_door_index], target_before);
171 ImGui::TextWrapped(
172 "%s", plan.creates_return
173 ? "Missing return door. Apply creates the shown endpoint."
174 : plan.changed()
175 ? "Return door found. Apply updates the shown pair."
176 : "Both doors already match.");
177 if (viewer.header_read_only()) {
178 ImGui::TextWrapped("This view is read-only.");
179 }
180 ImGui::BeginDisabled(viewer.header_read_only() ||
181 !viewer.IsObjectInteractionEnabled() ||
182 !plan.changed());
183 const bool apply = ImGui::Button(
184 plan.creates_return ? "Create Return Door" : "Update Pair",
185 ImVec2(-1, 0));
186 gui::AutoRegisterLastItem("button", "ConnectionApply");
187 ImGui::EndDisabled();
188 if (apply) {
189 const auto status = viewer.ApplyDoorConnection(plan);
190 state.error = status.ok() ? "" : std::string(status.message());
191 }
192 const bool can_open =
193 viewer.CanNavigateRooms() ||
194 (!plan.creates_return &&
195 (jump_to_reciprocal || viewer.CanNavigateDoorConnectionTarget()));
196 ImGui::BeginDisabled(!can_open);
197 const bool open_target = ImGui::Button("Open Target Room", ImVec2(-1, 0));
198 gui::AutoRegisterLastItem("button", "ConnectionOpenTarget");
199 ImGui::EndDisabled();
200 if (open_target) {
201 OpenTarget(viewer, plan, jump_to_reciprocal);
202 }
203 }
204 if (!state.error.empty()) {
205 ImGui::TextWrapped("Edit not applied: %s", state.error.c_str());
206 }
207 ImGui::PopID();
208 ImGui::PopID();
209 ImGui::PopID();
210}
211
212} // namespace yaze::editor
absl::Status ApplyDoorConnection(const DungeonConnectionPlan &plan)
DungeonConnectionEditorState & connection_editor_state()
absl::StatusOr< DungeonConnectionPlan > PreviewDoorConnection(const DungeonConnectionRequest &request)
DungeonObjectInteraction & object_interaction()
void TriggerCanvasPingRect(int pixel_x, int pixel_y, int pixel_w, int pixel_h)
bool NavigateToDoorConnectionTarget(int room_id, size_t door_index)
void ScrollToTile(int tile_x, int tile_y)
void SelectEntity(EntityType type, size_t index)
zelda3::Room * GetIfLoaded(int room_id)
RAII scope that enables automatic widget registration.
const std::vector< Door > & GetDoors() const
Definition room.h:413
void OpenTarget(DungeonCanvasViewer &viewer, const DungeonConnectionPlan &plan, const std::function< void(int, size_t)> &jump_to_reciprocal)
void DrawEndpoint(const char *label, int room_id, const zelda3::Room::Door &proposed, const zelda3::Room::Door *existing)
Editors are the view controllers for the application.
void DrawDungeonConnectionEditor(DungeonCanvasViewer &viewer, size_t door_index, const std::function< void(int, size_t)> &jump_to_reciprocal)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
@ NormalDoor
Normal door (upper layer)
@ NormalDoorLower
Normal door (lower layer)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
std::vector< zelda3::Room::Door > target_after
Represents a door in a dungeon room.
Definition room.h:339
std::tuple< int, int, int, int > GetEditorBounds() const
Get editor interaction bounds (x, y, width, height in pixels)
Definition room.h:373
DoorType type
Door type (determines appearance/behavior)
Definition room.h:341
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
Automatic widget registration helpers for ImGui Test Engine integration.