yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
minecart_collision_tool.cc
Go to the documentation of this file.
2
6#include "imgui/imgui.h"
7
8namespace yaze::editor {
9namespace {
10bool IsStop(uint8_t value) {
11 return value >= 0xB7 && value <= 0xBA;
12}
13bool IsSwitch(uint8_t value) {
14 return value >= 0xD0 && value <= 0xD3;
15}
16bool IsMovableNode(uint8_t value) {
17 return IsStop(value) || (value >= 0xBB && value <= 0xBE) || IsSwitch(value);
18}
19bool IsRail(uint8_t value) {
20 return (value >= 0xB0 && value <= 0xBE) || IsSwitch(value);
21}
22bool IsInside(int x, int y, int size) {
23 return (size == 1 || size == 2) && x >= 0 && y >= 0 && x <= 64 - size &&
24 y <= 64 - size;
25}
26bool MatchesNode(const zelda3::CustomCollisionMap& map, int x, int y, int size,
27 uint8_t value) {
28 if (!IsMovableNode(value) || !IsInside(x, y, size))
29 return false;
30 for (int dy = 0; dy < size; ++dy) {
31 for (int dx = 0; dx < size; ++dx) {
32 const int offset = (y + dy) * 64 + x + dx;
33 if (!map.HasOverride(offset) || map.tiles[offset] != value)
34 return false;
35 }
36 }
37 return true;
38}
39} // namespace
40
41absl::StatusOr<zelda3::CustomCollisionMap> PreviewMinecartCollisionStamp(
42 const zelda3::CustomCollisionMap& before,
43 const MinecartCollisionStamp& stamp) {
44 if (!IsInside(stamp.x, stamp.y, stamp.size) ||
45 (stamp.value != 0 && !IsRail(stamp.value))) {
46 return absl::InvalidArgumentError("Choose a rail stamp inside the room");
47 }
48 if (stamp.source && !MatchesNode(before, stamp.source->x, stamp.source->y,
49 stamp.size, stamp.value)) {
50 return absl::FailedPreconditionError(
51 "Move requires a matching stop, T junction, or switch override");
52 }
53 auto after = before;
54 if (stamp.source) {
55 for (int y = stamp.source->y; y < stamp.source->y + stamp.size; ++y) {
56 for (int x = stamp.source->x; x < stamp.source->x + stamp.size; ++x) {
57 after.ClearOverride(y * 64 + x);
58 }
59 }
60 }
61 for (int y = stamp.y; y < stamp.y + stamp.size; ++y) {
62 for (int x = stamp.x; x < stamp.x + stamp.size; ++x) {
63 const auto old = before.tiles[y * 64 + x];
64 if (before.HasOverride(y * 64 + x) && !IsRail(old)) {
65 return absl::FailedPreconditionError(
66 "Other collision occupies this cell; use the collision inspector");
67 }
68 if (stamp.value != 0 && ((IsStop(old) && !IsStop(stamp.value)) ||
69 (IsSwitch(old) && !IsSwitch(stamp.value)))) {
70 return absl::FailedPreconditionError(
71 "Stop or switch protected; choose the matching behavior tool");
72 }
73 if (stamp.value == 0) {
74 // The rail tool's erase stamp removes an override, not floor physics.
75 after.ClearOverride(y * 64 + x);
76 } else {
77 after.SetOverride(y * 64 + x, stamp.value);
78 }
79 }
80 }
81 after.has_data = before.has_data || stamp.value != 0;
82 return after;
83}
84
86 drag_.reset();
87 message_.clear();
88}
89
91 const zelda3::CustomCollisionMap& collision,
92 int room_id, uint8_t value, int size,
93 const MinecartCollisionApply& apply) {
94 const int frame = ImGui::GetFrameCount();
95 // An unvisited frame means stamping was disabled or another canvas took over.
96 if (drag_ && (drag_->room_id != room_id || drag_->stamp.value != value ||
97 drag_->stamp.size != size || last_frame_ < frame - 1)) {
98 drag_.reset();
99 }
100 last_frame_ = frame;
101 if (size != 1 && size != 2) {
102 drag_.reset();
103 message_ = "Choose a one-cell or two-cell collision stamp";
104 return;
105 }
106 if (ImGui::IsKeyPressed(ImGuiKey_Escape) ||
107 ImGui::IsMouseDown(ImGuiMouseButton_Right) ||
108 ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
109 drag_.reset();
110 message_ = "Collision move canceled";
111 return;
112 }
113 const DungeonCanvasTransform transform(rt.canvas_p0, rt.scrolling, rt.scale);
114 auto [x, y] = ImGui::IsMousePosValid()
115 ? transform.ScreenToRoomTiles(ImGui::GetMousePos(), 8)
116 : std::pair{-1, -1};
117 const bool inside = rt.hovered && x >= 0 && y >= 0 && x < 64 && y < 64;
118 // Two-cell stamps align with the 16 pixel art / sprite grid.
119 if (size == 2) {
120 x &= ~1;
121 y &= ~1;
122 }
123 MinecartCollisionStamp stamp{x, y, size, value};
124 if (drag_)
125 stamp.source = drag_->stamp.source;
126 const auto& before = drag_ ? drag_->before : collision;
127 const auto preview = PreviewMinecartCollisionStamp(before, stamp);
128 const auto delta = ImGui::GetIO().MouseDelta;
129 if (inside && (drag_ || message_.empty() || delta.x != 0 || delta.y != 0 ||
130 !preview.ok())) {
131 message_ = !preview.ok() ? std::string(preview.status().message())
132 : drag_ ? "Release to move. Escape / right / middle cancels."
133 : IsMovableNode(value)
134 ? "Drag a matching node to move. Click elsewhere to stamp."
135 : "Click to stamp. Escape leaves stamping.";
136 }
137 const auto& theme = AgentUI::GetTheme();
138 if (rt.draw_list) {
139 const ImVec2 max(rt.canvas_p0.x + rt.canvas_sz.x * transform.scale(),
140 rt.canvas_p0.y + rt.canvas_sz.y * transform.scale());
141 rt.draw_list->PushClipRect(rt.canvas_p0, max, true);
142 // Existing matching nodes remain visible while the cursor moves elsewhere.
143 for (int my = 0; my < 64; my += size == 2 ? 2 : 1) {
144 for (int mx = 0; mx < 64; mx += size == 2 ? 2 : 1) {
145 if (!MatchesNode(collision, mx, my, size, value))
146 continue;
147 const auto p = transform.RoomPixelsToScreen(ImVec2(mx * 8, my * 8));
148 const auto q = transform.RoomPixelsToScreen(
149 ImVec2((mx + size) * 8, (my + size) * 8));
150 rt.draw_list->AddRect(p, q, ImGui::GetColorU32(theme.selection_primary),
151 0.0f, 2.0f);
152 }
153 }
154 const auto color = ImGui::GetColorU32(preview.ok() ? theme.status_warning
155 : theme.status_error);
156 for (int dy = 0; inside && dy < size; ++dy) {
157 for (int dx = 0; dx < size; ++dx) {
158 const auto p =
159 transform.RoomPixelsToScreen(ImVec2((x + dx) * 8, (y + dy) * 8));
160 const auto q = transform.RoomPixelsToScreen(
161 ImVec2((x + dx + 1) * 8, (y + dy + 1) * 8));
162 rt.draw_list->AddRectFilled(p, q, (color & 0x00FFFFFF) | 0x55000000);
163 rt.draw_list->AddRect(p, q, color);
164 }
165 }
166 rt.draw_list->PopClipRect();
167 }
168 if (drag_) {
169 const auto mouse = ImGui::GetMousePos();
170 const float dx = mouse.x - drag_->mouse_start_x;
171 const float dy = mouse.y - drag_->mouse_start_y;
172 const float threshold = ImGui::GetIO().MouseDragThreshold;
173 drag_->moved |= dx * dx + dy * dy >= threshold * threshold;
174 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
175 if (inside && preview.ok() && apply && drag_->moved &&
176 (x != stamp.source->x || y != stamp.source->y)) {
177 const auto status = apply(room_id, drag_->before, stamp);
178 if (!status.ok())
179 message_ = std::string(status.message());
180 }
181 drag_.reset();
182 } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
183 drag_.reset();
184 }
185 } else if (inside && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
186 preview.ok() && apply) {
187 if (MatchesNode(collision, x, y, size, value)) {
188 stamp.source = MinecartCollisionSource{x, y};
189 const auto mouse = ImGui::GetMousePos();
190 drag_ = Drag{stamp, collision, mouse.x, mouse.y, room_id};
191 } else {
192 const auto status = apply(room_id, collision, stamp);
193 if (!status.ok())
194 message_ = std::string(status.message());
195 }
196 }
197 if (!inside)
198 return;
199 ImGui::BeginTooltip();
200 ImGui::Text("Upper layer: %d x %d cells at %d, %d", size, size, x, y);
201 if (drag_)
202 ImGui::Text("Source override: %d, %d", drag_->stamp.source->x,
203 drag_->stamp.source->y);
204 if (drag_ || IsMovableNode(value))
205 ImGui::TextUnformatted(
206 "Move clears the old override; underlying room collision returns.");
207 ImGui::TextUnformatted(message_.c_str());
208 ImGui::EndTooltip();
209}
210
211} // namespace yaze::editor
std::pair< int, int > ScreenToRoomTiles(ImVec2 screen, int tile_size) const
void Draw(const gui::CanvasRuntime &rt, const zelda3::CustomCollisionMap &collision, int room_id, uint8_t value, int size, const MinecartCollisionApply &apply)
const AgentUITheme & GetTheme()
bool MatchesNode(const zelda3::CustomCollisionMap &map, int x, int y, int size, uint8_t value)
Editors are the view controllers for the application.
std::function< absl::Status( int, const zelda3::CustomCollisionMap &, const MinecartCollisionStamp &)> MinecartCollisionApply
absl::StatusOr< zelda3::CustomCollisionMap > PreviewMinecartCollisionStamp(const zelda3::CustomCollisionMap &before, const MinecartCollisionStamp &stamp)
std::optional< MinecartCollisionSource > source
std::array< uint8_t, 64 *64 > tiles
bool HasOverride(size_t offset) const