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
4// Third-party library headers
5#include "imgui/imgui.h"
6
7// Project headers
10
11namespace yaze::editor {
12
17
23
24bool DoorInteractionHandler::HandleClick(int canvas_x, int canvas_y) {
25 if (!HasValidContext()) return false;
26
28 PlaceDoorAtSnappedPosition(canvas_x, canvas_y);
29 return true;
30 }
31
32 // Try to select door at position
33 auto door_index = GetEntityAtPosition(canvas_x, canvas_y);
34 if (door_index.has_value()) {
35 SelectDoor(*door_index);
36 is_dragging_ = true;
37 drag_start_pos_ = ImVec2(static_cast<float>(canvas_x),
38 static_cast<float>(canvas_y));
40 return true;
41 }
42
44 return false;
45}
46
47void DoorInteractionHandler::HandleDrag(ImVec2 current_pos, ImVec2 delta) {
48 if (!is_dragging_ || !selected_door_index_.has_value()) return;
49
50 drag_current_pos_ = current_pos;
51}
52
54 if (!is_dragging_ || !selected_door_index_.has_value()) {
55 is_dragging_ = false;
56 return;
57 }
58
59 auto* room = GetCurrentRoom();
60 if (!room) {
61 is_dragging_ = false;
62 return;
63 }
64
65 int drag_x = static_cast<int>(drag_current_pos_.x);
66 int drag_y = static_cast<int>(drag_current_pos_.y);
67
68 // Detect wall from final position
69 zelda3::DoorDirection direction;
71 direction)) {
73 drag_x, drag_y, direction);
74
75 if (zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
77
78 auto& doors = room->GetDoors();
79 if (*selected_door_index_ < doors.size()) {
80 doors[*selected_door_index_].position = position;
81 doors[*selected_door_index_].direction = direction;
82
83 // Re-encode bytes for ROM storage
84 auto [b1, b2] = doors[*selected_door_index_].EncodeBytes();
85 doors[*selected_door_index_].byte1 = b1;
86 doors[*selected_door_index_].byte2 = b2;
87
88 room->MarkObjectsDirty();
90 }
91 }
92 }
93
94 is_dragging_ = false;
95}
96
98 if (!door_placement_mode_ || !HasValidContext()) return;
99
100 auto* canvas = ctx_->canvas;
101 if (!canvas->IsMouseHovering()) return;
102
103 const ImGuiIO& io = ImGui::GetIO();
104 ImVec2 canvas_pos = canvas->zero_point();
105 int canvas_x = static_cast<int>(io.MousePos.x - canvas_pos.x);
106 int canvas_y = static_cast<int>(io.MousePos.y - canvas_pos.y);
107
108 // Try to update snapped position
109 if (!UpdateSnappedPosition(canvas_x, canvas_y)) {
110 // Placement guidance: make invalid hover state explicit instead of showing
111 // no preview.
112 if (canvas->IsMouseHovering()) {
113 const auto& theme = AgentUI::GetTheme();
114 ImDrawList* draw_list = ImGui::GetWindowDrawList();
115 ImVec2 hint_pos(io.MousePos.x + 14.0f, io.MousePos.y + 10.0f);
116 draw_list->AddText(hint_pos, ImGui::GetColorU32(theme.status_warning),
117 "Move cursor near a wall to place door");
118 ImGui::SetTooltip("Door placement requires a wall-adjacent position.");
119 }
120 return; // Not near a wall
121 }
122
123 // Get door position in tile coordinates
126
127 // Get door dimensions
129 int door_width_px = dims.width_tiles * 8;
130 int door_height_px = dims.height_tiles * 8;
131
132 // Convert to canvas pixel coordinates
133 auto [snap_canvas_x, snap_canvas_y] = RoomToCanvas(tile_x, tile_y);
134
135 // Draw ghost preview
136 ImDrawList* draw_list = ImGui::GetWindowDrawList();
137 float scale = GetCanvasScale();
138
139 ImVec2 preview_start(canvas_pos.x + snap_canvas_x * scale,
140 canvas_pos.y + snap_canvas_y * scale);
141 ImVec2 preview_end(preview_start.x + door_width_px * scale,
142 preview_start.y + door_height_px * scale);
143
144 const auto& theme = AgentUI::GetTheme();
145
146 // Capacity-aware colors: normal / near-limit (>=14/16) / blocked (>=16/16).
147
148 auto* room = GetCurrentRoom();
149 size_t current_door_count = room ? room->GetDoors().size() : 0;
150 const bool at_door_limit = (current_door_count >= zelda3::kMaxDoors);
151 const bool near_door_limit = (current_door_count >= zelda3::kMaxDoors - 2);
152
153 ImVec4 base_color = theme.dungeon_selection_primary;
154 if (at_door_limit) {
155 base_color = theme.status_error;
156 } else if (near_door_limit) {
157 base_color = theme.status_warning;
158 }
159
160 // Draw semi-transparent filled rectangle
161 ImVec4 fill_vec(base_color.x, base_color.y, base_color.z, 0.31f);
162 draw_list->AddRectFilled(preview_start, preview_end,
163 ImGui::GetColorU32(fill_vec));
164
165 // Draw outline
166 ImVec4 outline_color(base_color.x, base_color.y, base_color.z, 0.9f);
167 draw_list->AddRect(preview_start, preview_end,
168 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
169
170 // Draw door type label
171 std::string type_name(zelda3::GetDoorTypeName(preview_door_type_));
173 std::string label = type_name + " (" + dir_name + ")";
174
175 ImVec2 text_pos(preview_start.x, preview_start.y - 16 * scale);
176 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 200), label.c_str());
177
178 // Capacity tooltip when at/near limit
179 if ((at_door_limit || near_door_limit) &&
180 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
181 ImGui::SetTooltip("Doors: %zu/%zu%s", current_door_count, zelda3::kMaxDoors,
182 at_door_limit ? "\nPlacement blocked" : "\nNear limit");
183 }
184
185}
186
188 if (!selected_door_index_.has_value() || !HasValidContext()) return;
189
190 auto* room = GetCurrentRoom();
191 if (!room) return;
192
193 const auto& doors = room->GetDoors();
194 if (*selected_door_index_ >= doors.size()) return;
195
196 const auto& door = doors[*selected_door_index_];
197 auto [tile_x, tile_y] = door.GetTileCoords();
198 auto dims = zelda3::GetDoorDimensions(door.direction);
199
200 // If dragging, use current drag position for door preview
201 if (is_dragging_) {
202 int drag_x = static_cast<int>(drag_current_pos_.x);
203 int drag_y = static_cast<int>(drag_current_pos_.y);
204
206 bool is_inner = false;
208 is_inner)) {
209 uint8_t snap_pos =
211 auto [snap_x, snap_y] =
213 tile_x = snap_x;
214 tile_y = snap_y;
215 dims = zelda3::GetDoorDimensions(dir);
216 }
217 }
218
219 ImDrawList* draw_list = ImGui::GetWindowDrawList();
220 ImVec2 canvas_pos = GetCanvasZeroPoint();
221 float scale = GetCanvasScale();
222
223 ImVec2 pos(canvas_pos.x + tile_x * 8 * scale,
224 canvas_pos.y + tile_y * 8 * scale);
225 ImVec2 size(dims.width_tiles * 8 * scale, dims.height_tiles * 8 * scale);
226
227 // Animated selection
228 static float pulse = 0.0f;
229 pulse += ImGui::GetIO().DeltaTime * 3.0f;
230 float alpha = 0.5f + 0.3f * sinf(pulse);
231
232 ImU32 color = IM_COL32(255, 165, 0, 180); // Orange
233 ImU32 fill_color = (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha * 100) << 24);
234
235 draw_list->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y),
236 fill_color);
237 draw_list->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, 0.0f,
238 0, 2.0f);
239
240 // Draw label
241 ImVec2 text_pos(pos.x, pos.y - 14 * scale);
242 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 220), "Door");
243
244 // Draw snap indicators when dragging
245 if (is_dragging_) {
247 }
248}
249
251 int canvas_x, int canvas_y) const {
252 if (!HasValidContext()) return std::nullopt;
253
254 auto* room = ctx_->GetCurrentRoomConst();
255 if (!room) return std::nullopt;
256
257 // Convert screen coordinates to room coordinates
258 float scale = GetCanvasScale();
259 int room_x = static_cast<int>(canvas_x / scale);
260 int room_y = static_cast<int>(canvas_y / scale);
261
262 const auto& doors = room->GetDoors();
263 for (size_t i = 0; i < doors.size(); ++i) {
264 const auto& door = doors[i];
265
266 auto [tile_x, tile_y] = door.GetTileCoords();
267 auto dims = zelda3::GetDoorDimensions(door.direction);
268
269 int door_x = tile_x * 8;
270 int door_y = tile_y * 8;
271 int door_w = dims.width_tiles * 8;
272 int door_h = dims.height_tiles * 8;
273
274 if (room_x >= door_x && room_x < door_x + door_w && room_y >= door_y &&
275 room_y < door_y + door_h) {
276 return i;
277 }
278 }
279
280 return std::nullopt;
281}
282
287
289 selected_door_index_ = std::nullopt;
290 is_dragging_ = false;
291}
292
294 if (!selected_door_index_.has_value() || !HasValidContext()) return;
295
296 auto* room = GetCurrentRoom();
297 if (!room) return;
298
299 auto& doors = room->GetDoors();
300 if (*selected_door_index_ >= doors.size()) return;
301
303 doors.erase(doors.begin() + static_cast<ptrdiff_t>(*selected_door_index_));
304 room->MarkObjectsDirty();
307}
308
310 int canvas_y) {
311 if (!HasValidContext()) {
313 return;
314 }
315
316 auto* room = GetCurrentRoom();
317 if (!room) {
319 return;
320 }
321
322 // Enforce door limit at placement time (matches DungeonValidator::zelda3::kMaxDoors)
323
324 if (room->GetDoors().size() >= zelda3::kMaxDoors) {
326 return;
327 }
328
329 // Detect wall from position
330 zelda3::DoorDirection direction;
332 direction)) {
334 return;
335 }
336
337 // Snap to nearest valid position
338 uint8_t position =
339 zelda3::DoorPositionManager::SnapToNearestPosition(canvas_x, canvas_y, direction);
340
341 // Validate position
342 if (!zelda3::DoorPositionManager::IsValidPosition(position, direction)) {
344 return;
345 }
346
348
350
351 // Create the door
352 zelda3::Room::Door new_door;
353 new_door.position = position;
354 new_door.type = preview_door_type_;
355 new_door.direction = direction;
356
357 // Encode bytes for ROM storage
358 auto [byte1, byte2] = new_door.EncodeBytes();
359 new_door.byte1 = byte1;
360 new_door.byte2 = byte2;
361
362 // Add door to room
363 room->AddDoor(new_door);
365
367}
368
369bool DoorInteractionHandler::UpdateSnappedPosition(int canvas_x, int canvas_y) {
370 zelda3::DoorDirection direction;
372 direction)) {
373 return false;
374 }
375
376 detected_door_direction_ = direction;
378 zelda3::DoorPositionManager::SnapToNearestPosition(canvas_x, canvas_y, direction);
379 return true;
380}
381
383 if (!is_dragging_ || !HasValidContext()) return;
384
385 int drag_x = static_cast<int>(drag_current_pos_.x);
386 int drag_y = static_cast<int>(drag_current_pos_.y);
387
388 zelda3::DoorDirection direction;
389 bool is_inner = false;
390 if (!zelda3::DoorPositionManager::DetectWallSection(drag_x, drag_y, direction,
391 is_inner)) {
392 return;
393 }
394
395 uint8_t start_pos =
397 uint8_t nearest_snap =
399
400 ImDrawList* draw_list = ImGui::GetWindowDrawList();
401 ImVec2 canvas_pos = GetCanvasZeroPoint();
402 float scale = GetCanvasScale();
403 const auto& theme = AgentUI::GetTheme();
404 auto dims = zelda3::GetDoorDimensions(direction);
405
406 // Capacity-aware indicator colors: inherit the same thresholds used in
407 // DrawGhostPreview so dragging mirrors the placement ghost feedback.
408
409 auto* snap_room = GetCurrentRoom();
410 size_t door_count = snap_room ? snap_room->GetDoors().size() : 0;
411 const bool snap_at_limit = (door_count >= zelda3::kMaxDoors);
412 const bool snap_near_limit = (door_count >= zelda3::kMaxDoors - 2);
413
414 // Draw indicators for 6 positions in this section
415 for (uint8_t i = 0; i < 6; ++i) {
416 uint8_t pos = start_pos + i;
417 auto [tile_x, tile_y] =
419 float pixel_x = tile_x * 8.0f;
420 float pixel_y = tile_y * 8.0f;
421
422 ImVec2 snap_start(canvas_pos.x + pixel_x * scale,
423 canvas_pos.y + pixel_y * scale);
424 ImVec2 snap_end(snap_start.x + dims.width_pixels() * scale,
425 snap_start.y + dims.height_pixels() * scale);
426
427 if (pos == nearest_snap) {
428 // Highlighted nearest snap position — capacity-aware color.
429 ImVec4 base;
430 if (snap_at_limit) {
431 base = theme.status_error;
432 } else if (snap_near_limit) {
433 base = theme.status_warning;
434 } else {
435 base = theme.dungeon_selection_primary;
436 }
437 ImVec4 highlight(base.x, base.y, base.z, 0.75f);
438 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(highlight),
439 0.0f, 0, 2.5f);
440 } else {
441 // Ghosted other positions — tint red when at limit.
442 ImVec4 ghost = snap_at_limit
443 ? ImVec4(theme.status_error.x, theme.status_error.y,
444 theme.status_error.z, 0.20f)
445 : ImVec4(1.0f, 1.0f, 1.0f, 0.25f);
446 draw_list->AddRect(snap_start, snap_end, ImGui::GetColorU32(ghost), 0.0f,
447 0, 1.0f);
448 }
449 }
450}
451
452} // namespace yaze::editor
float GetCanvasScale() const
Get canvas global scale.
zelda3::Room * GetCurrentRoom() const
Get current room (convenience method)
bool HasValidContext() const
Check if context is valid.
std::pair< int, int > RoomToCanvas(int room_x, int room_y) const
Convert room tile coordinates to canvas pixel coordinates.
ImVec2 GetCanvasZeroPoint() const
Get canvas zero point (for screen coordinate conversion)
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.
void DrawGhostPreview() override
Draw ghost preview during placement.
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
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.
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.
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
static uint8_t GetSectionStartPosition(DoorDirection direction, bool is_inner)
Get the starting position index for outer/inner section.
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 AgentUITheme & GetTheme()
Editors are the view controllers for the application.
constexpr DoorDimensions GetDoorDimensions(DoorDirection dir)
Get door dimensions based on direction.
Definition door_types.h:192
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:161
constexpr size_t kMaxDoors
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:106
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ North
Top wall (horizontal door, 4x3 tiles)
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:231
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:236
DoorType type
Door type (determines appearance/behavior)
Definition room.h:233
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:270
DoorDirection direction
Which wall the door is on.
Definition room.h:234
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:232
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:237