yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
minecart_canvas_tool.cc
Go to the documentation of this file.
3
4#include <algorithm>
5#include <cmath>
6
7#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11#include "zelda3/dungeon/room.h"
12
13namespace yaze::editor {
14
15std::optional<std::pair<int, int>> MinecartLocalPosition(
16 const MinecartTrack& track) {
17 if (track.room_id < 0 || track.room_id >= zelda3::kNumberOfRooms)
18 return {};
19 const int x = track.start_x - (track.room_id & 15) * 512;
20 const int y = track.start_y - (track.room_id >> 4) * 512;
21 if (x < 0 || x >= 512 || y < 0 || y >= 512)
22 return {};
23 return std::pair{x, y};
24}
25
26absl::StatusOr<size_t> FindCartStartSprite(const zelda3::Room& room,
27 const MinecartTrack& start) {
28 const auto local = MinecartLocalPosition(start);
29 if (!local)
30 return absl::InvalidArgumentError("Start is outside its room");
31 std::optional<size_t> match;
32 const auto& sprites = room.GetSprites();
33 for (size_t i = 0; i < sprites.size(); ++i) {
34 const auto& sprite = sprites[i];
35 if (sprite.deleted() || sprite.id() != 0xA3 ||
36 sprite.subtype() != start.id || sprite.x() * 16 != local->first ||
37 sprite.y() * 16 != local->second)
38 continue;
39 if (match)
40 return absl::FailedPreconditionError("Multiple carts share this start");
41 if (sprite.layer() != 0)
42 return absl::FailedPreconditionError(
43 "Lower-layer stop collision cannot yet be verified");
44 match = i;
45 }
46 if (!match)
47 return absl::FailedPreconditionError("No matching cart at this start");
48 return *match;
49}
50
52 const MinecartTrack& start) {
53 const auto local = MinecartLocalPosition(start);
54 if (!local || local->first % 16 || local->second % 16)
55 return absl::InvalidArgumentError(
56 "Place the cart on the 16 pixel sprite grid");
57 if (!room.has_custom_collision())
58 return absl::FailedPreconditionError(
59 "This room has no verified custom stop collision");
60 const auto tile = room.custom_collision()
61 .tiles[(local->second / 8) * 64 + local->first / 8];
62 if (tile < 0xB7 || tile > 0xBA)
63 return absl::FailedPreconditionError(
64 "Place the cart on a stop, not a rail or floor tile");
65 for (const auto& sprite : room.GetSprites()) {
66 if (!sprite.deleted() && sprite.id() == 0xA3 &&
67 sprite.subtype() != start.id && sprite.layer() == 0 &&
68 sprite.x() * 16 == local->first && sprite.y() * 16 == local->second)
69 return absl::FailedPreconditionError("Another cart occupies this stop");
70 }
71 return absl::OkStatus();
72}
73
75 before_.reset();
76 preview_.reset();
77}
78
80 const zelda3::Room& room, int room_id,
81 bool editable, bool shortcut_focus,
82 const MinecartCanvasBinding& binding) {
83 if (room_id_ != room_id || revision_ != binding.revision || !editable ||
84 !binding.enabled) {
85 Cancel();
86 room_id_ = room_id;
87 revision_ = binding.revision;
88 }
89 if (!binding.enabled)
90 return false;
91 const DungeonCanvasTransform transform(rt.canvas_p0, rt.scrolling, rt.scale);
92 // CanvasRuntime::canvas_sz is unscaled; match the canvas InvisibleButton.
93 const ImVec2 clip_max(rt.canvas_p0.x + rt.canvas_sz.x * transform.scale(),
94 rt.canvas_p0.y + rt.canvas_sz.y * transform.scale());
95 const auto& theme = AgentUI::GetTheme();
96 const auto mouse = ImGui::GetMousePos();
97 const auto room_point = transform.ScreenToRoomPixels(mouse);
98 const bool inside = rt.hovered && room_point.x >= 0 && room_point.x < 512 &&
99 room_point.y >= 0 && room_point.y < 512;
100 int hovered = -1;
101 if (rt.draw_list) {
102 rt.draw_list->PushClipRect(rt.canvas_p0, clip_max, true);
103 for (const auto& start : binding.tracks) {
104 if (start.room_id != room_id)
105 continue;
106 const auto local = MinecartLocalPosition(start);
107 if (!local)
108 continue;
109 const auto center = transform.RoomPixelsToScreen(
110 ImVec2(local->first + 8, local->second + 8));
111 const float radius = std::max(8.0f, 8.0f * rt.scale);
112 const float dx = mouse.x - center.x, dy = mouse.y - center.y;
113 if (inside && dx * dx + dy * dy <= radius * radius)
114 hovered = start.id;
115 const auto color = ImGui::GetColorU32(start.id == binding.selected
116 ? theme.status_warning
117 : theme.selection_primary);
118 rt.draw_list->AddCircleFilled(center, radius, color);
119 rt.draw_list->AddText(ImVec2(center.x + radius + 3, center.y - 7),
120 ImGui::GetColorU32(theme.text_primary),
121 absl::StrFormat("Cart %d", start.id).c_str());
122 }
123 rt.draw_list->PopClipRect();
124 }
125 if (!editable)
126 return true;
127 // A pan or context-menu gesture cannot also commit a left-button move.
128 if (ImGui::IsMouseDown(ImGuiMouseButton_Right) ||
129 ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
130 Cancel();
131 return true;
132 }
133 auto selected = [&](int id) -> const MinecartTrack* {
134 for (const auto& start : binding.tracks)
135 if (start.id == id && start.room_id == room_id)
136 return &start;
137 return nullptr;
138 };
139 if ((shortcut_focus || dragging()) && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
140 Cancel();
141 if (binding.cancel_pick)
142 binding.cancel_pick();
143 message_ = "Move canceled";
144 return true;
145 }
146 if (inside && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
147 const int id = binding.picking >= 0 ? binding.picking : hovered;
148 if (const auto* start = selected(id)) {
149 if (binding.select)
150 binding.select(id);
151 auto sprite = FindCartStartSprite(room, *start);
152 if (sprite.ok()) {
153 before_ = *start;
154 preview_ = *start;
155 message_.clear();
156 } else {
157 message_ = std::string(sprite.status().message());
158 }
159 }
160 }
161 if (before_) {
162 // The marker's visible center is eight pixels from the sprite anchor.
163 const int x = static_cast<int>(std::round((room_point.x - 8) / 16)) * 16;
164 const int y = static_cast<int>(std::round((room_point.y - 8) / 16)) * 16;
165 preview_ = *before_;
166 preview_->start_x = (room_id & 15) * 512 + x;
167 preview_->start_y = (room_id >> 4) * 512 + y;
168 const auto validity =
170 : absl::InvalidArgumentError("Keep the start inside this room");
171 message_ = validity.ok() ? "Release to move cart and start"
172 : std::string(validity.message());
173 if (rt.draw_list) {
174 rt.draw_list->PushClipRect(rt.canvas_p0, clip_max, true);
175 const auto a = transform.RoomPixelsToScreen(ImVec2(x, y));
176 const auto b = transform.RoomPixelsToScreen(ImVec2(x + 16, y + 16));
177 rt.draw_list->AddRect(
178 a, b,
179 ImGui::GetColorU32(validity.ok() ? theme.status_success
180 : theme.status_error),
181 0, 0, 3);
182 rt.draw_list->PopClipRect();
183 }
184 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
185 if (validity.ok() && binding.move) {
186 const auto from = *before_;
187 const auto to = *preview_;
188 Cancel();
189 const auto status = binding.move(from, to);
190 message_ = status.ok()
191 ? "Cart start moved; source and room edits are pending"
192 : std::string(status.message());
193 }
194 Cancel();
195 if (binding.cancel_pick)
196 binding.cancel_pick();
197 }
198 } else if (shortcut_focus && !ImGui::GetIO().WantTextInput &&
199 !ImGui::GetIO().KeyCtrl && !ImGui::GetIO().KeySuper) {
200 int dx = 0, dy = 0;
201 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
202 dx = -16;
203 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
204 dx = 16;
205 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
206 dy = -16;
207 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
208 dy = 16;
209 if (const auto* start = selected(binding.selected); start && (dx || dy)) {
210 auto next = *start;
211 next.start_x += dx;
212 next.start_y += dy;
213 auto status = ValidateCartStartDestination(room, next);
214 if (status.ok() && binding.move)
215 status = binding.move(*start, next);
216 message_ =
217 status.ok() ? "Cart start moved" : std::string(status.message());
218 }
219 }
220 if (inside && !message_.empty())
221 ImGui::SetTooltip("%s", message_.c_str());
222 return true;
223}
224} // namespace yaze::editor
ImVec2 ScreenToRoomPixels(ImVec2 screen) const
std::optional< MinecartTrack > preview_
std::optional< MinecartTrack > before_
bool Draw(const gui::CanvasRuntime &rt, const zelda3::Room &room, int room_id, bool editable, bool shortcut_focus, const MinecartCanvasBinding &binding)
const CustomCollisionMap & custom_collision() const
Definition room.h:559
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
bool has_custom_collision() const
Definition room.h:563
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
std::optional< std::pair< int, int > > MinecartLocalPosition(const MinecartTrack &track)
absl::StatusOr< size_t > FindCartStartSprite(const zelda3::Room &room, const MinecartTrack &start)
absl::Status ValidateCartStartDestination(const zelda3::Room &room, const MinecartTrack &start)
constexpr int kNumberOfRooms
std::function< absl::Status(const MinecartTrack &, const MinecartTrack &) move)
std::span< const MinecartTrack > tracks
std::array< uint8_t, 64 *64 > tiles