yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_map_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_MAP_PANEL_H_
2#define YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_MAP_PANEL_H_
3
4#include <array>
5#include <algorithm>
6#include <cmath>
7#include <functional>
8#include <map>
9#include <string>
10#include <vector>
11
15#include "app/gui/core/icons.h"
16#include "core/hack_manifest.h"
17#include "imgui/imgui.h"
18#include "zelda3/dungeon/room.h"
20
21namespace yaze {
22namespace editor {
23
43 public:
51 DungeonMapPanel(int* current_room_id, ImVector<int>* active_rooms,
52 std::function<void(int)> on_room_selected,
53 std::array<zelda3::Room, 0x128>* rooms = nullptr)
54 : current_room_id_(current_room_id),
55 active_rooms_(active_rooms),
56 rooms_(rooms),
57 on_room_selected_(std::move(on_room_selected)) {}
58
59 // ==========================================================================
60 // EditorPanel Identity
61 // ==========================================================================
62
63 std::string GetId() const override { return "dungeon.dungeon_map"; }
64 std::string GetDisplayName() const override { return "Dungeon Map"; }
65 std::string GetIcon() const override { return ICON_MD_MAP; }
66 std::string GetEditorCategory() const override { return "Dungeon"; }
67 int GetPriority() const override { return 35; }
68
70 std::function<void(int, RoomSelectionIntent)> callback) {
71 on_room_intent_ = std::move(callback);
72 }
73
74 // ==========================================================================
75 // Configuration
76 // ==========================================================================
77
82 void SetDungeonRooms(const std::vector<int>& room_ids) {
83 dungeon_room_ids_ = room_ids;
85 }
86
90 void AddRoom(int room_id) {
91 // Avoid duplicates
92 for (int id : dungeon_room_ids_) {
93 if (id == room_id) return;
94 }
95 dungeon_room_ids_.push_back(room_id);
97 }
98
102 void ClearRooms() {
103 dungeon_room_ids_.clear();
104 room_positions_.clear();
105 }
106
110 void SetRoomPosition(int room_id, int grid_x, int grid_y) {
111 room_positions_[room_id] = ImVec2(static_cast<float>(grid_x),
112 static_cast<float>(grid_y));
113 }
114
115 void SetRooms(std::array<zelda3::Room, 0x128>* rooms) { rooms_ = rooms; }
116
120 void SetHackManifest(const core::HackManifest* manifest) {
121 hack_manifest_ = manifest;
122 }
123
128 ClearRooms();
129 current_dungeon_name_ = dungeon.name;
130 for (const auto& room : dungeon.rooms) {
131 dungeon_room_ids_.push_back(room.id);
132 room_positions_[room.id] =
133 ImVec2(static_cast<float>(room.grid_col),
134 static_cast<float>(room.grid_row));
135 room_types_[room.id] = room.type;
136 }
137 stair_connections_ = dungeon.stairs;
139 }
140
141 // ==========================================================================
142 // EditorPanel Drawing
143 // ==========================================================================
144
145 void Draw(bool* p_open) override {
146 if (!current_room_id_ || !active_rooms_) return;
147
148 const auto& theme = AgentUI::GetTheme();
149
150 // Show dungeon selection/quick presets
152
153 ImGui::Separator();
154
155 // Room size in the map
156 constexpr float kRoomWidth = 64.0f;
157 constexpr float kRoomHeight = 64.0f;
158 constexpr float kRoomSpacing = 8.0f;
159
160 // Calculate canvas size based on room positions
161 float max_x = 0, max_y = 0;
162 for (const auto& [room_id, pos] : room_positions_) {
163 max_x = std::max(max_x, pos.x);
164 max_y = std::max(max_y, pos.y);
165 }
166 float canvas_width = (max_x + 1) * (kRoomWidth + kRoomSpacing) + kRoomSpacing;
167 float canvas_height = (max_y + 1) * (kRoomHeight + kRoomSpacing) + kRoomSpacing;
168
169 // Minimum size
170 canvas_width = std::max(canvas_width, 200.0f);
171 canvas_height = std::max(canvas_height, 200.0f);
172
173 ImVec2 available = ImGui::GetContentRegionAvail();
174 ImVec2 canvas_size(std::min(available.x, canvas_width),
175 std::min(available.y - 40, canvas_height));
176
177 // Begin canvas area
178 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
179 ImDrawList* draw_list = ImGui::GetWindowDrawList();
180
181 // Background
182 ImU32 bg_color = ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker);
183 draw_list->AddRectFilled(canvas_pos,
184 ImVec2(canvas_pos.x + canvas_size.x,
185 canvas_pos.y + canvas_size.y),
186 bg_color);
187
188 // Helper lambda: compute the center pixel position for a room on the canvas
189 auto RoomCenter = [&](int room_id) -> ImVec2 {
190 auto it = room_positions_.find(room_id);
191 if (it == room_positions_.end()) return ImVec2(0, 0);
192 ImVec2 pos = it->second;
193 return ImVec2(
194 canvas_pos.x + kRoomSpacing +
195 pos.x * (kRoomWidth + kRoomSpacing) + kRoomWidth * 0.5f,
196 canvas_pos.y + kRoomSpacing +
197 pos.y * (kRoomHeight + kRoomSpacing) + kRoomHeight * 0.5f);
198 };
199
200 // Draw connections between adjacent rooms (gray lines — doors)
201 ImVec4 connection_color = theme.dungeon_room_border_dark;
202 connection_color.w = 0.45f;
203 for (size_t i = 0; i < dungeon_room_ids_.size(); i++) {
204 for (size_t j = i + 1; j < dungeon_room_ids_.size(); j++) {
205 int room_a = dungeon_room_ids_[i];
206 int room_b = dungeon_room_ids_[j];
207
208 bool adjacent = false;
209 if (std::abs(room_a - room_b) == 16) {
210 adjacent = true;
211 } else if (std::abs(room_a - room_b) == 1) {
212 int col_a = room_a % 16;
213 int col_b = room_b % 16;
214 if (std::abs(col_a - col_b) == 1) {
215 adjacent = true;
216 }
217 }
218
219 if (adjacent) {
220 draw_list->AddLine(RoomCenter(room_a), RoomCenter(room_b),
221 ImGui::ColorConvertFloat4ToU32(connection_color),
222 1.5f);
223 }
224 }
225 }
226
227 // Draw stair connections (blue dashed lines — bidirectional)
228 for (const auto& conn : stair_connections_) {
229 if (room_positions_.count(conn.from_room) &&
230 room_positions_.count(conn.to_room)) {
231 ImVec2 from = RoomCenter(conn.from_room);
232 ImVec2 to = RoomCenter(conn.to_room);
233 DrawDashedLine(draw_list, from, to,
234 IM_COL32(100, 149, 237, 200), 1.5f, 6.0f);
235 }
236 }
237
238 // Draw holewarp connections (red lines with arrow — one-way falls)
239 for (const auto& conn : holewarp_connections_) {
240 if (room_positions_.count(conn.from_room) &&
241 room_positions_.count(conn.to_room)) {
242 ImVec2 from = RoomCenter(conn.from_room);
243 ImVec2 to = RoomCenter(conn.to_room);
244 ImU32 red = IM_COL32(220, 60, 60, 200);
245 draw_list->AddLine(from, to, red, 2.0f);
246 // Arrowhead at destination
247 DrawArrowhead(draw_list, from, to, red, 6.0f);
248 }
249 }
250
251 // Draw each room
252 for (int room_id : dungeon_room_ids_) {
253 auto pos_it = room_positions_.find(room_id);
254 if (pos_it == room_positions_.end()) continue;
255
256 ImVec2 grid_pos = pos_it->second;
257 ImVec2 room_min(
258 canvas_pos.x + kRoomSpacing + grid_pos.x * (kRoomWidth + kRoomSpacing),
259 canvas_pos.y + kRoomSpacing + grid_pos.y * (kRoomHeight + kRoomSpacing));
260 ImVec2 room_max(room_min.x + kRoomWidth, room_min.y + kRoomHeight);
261
262 // Check if room is valid
263 if (room_id < 0 || room_id >= 0x128) continue;
264
265 bool is_current = (*current_room_id_ == room_id);
266 bool is_open = false;
267 for (int i = 0; i < active_rooms_->Size; i++) {
268 if ((*active_rooms_)[i] == room_id) {
269 is_open = true;
270 break;
271 }
272 }
273
274 // Draw room thumbnail or placeholder
275 if (rooms_ && (*rooms_)[room_id].IsLoaded()) {
276 auto& bg1_bitmap = (*rooms_)[room_id].bg1_buffer().bitmap();
277 if (bg1_bitmap.is_active() && bg1_bitmap.texture() != 0) {
278 // Draw room thumbnail
279 draw_list->AddImage(
280 (ImTextureID)(intptr_t)bg1_bitmap.texture(),
281 room_min, room_max);
282 } else {
283 // Placeholder for loaded but no texture
284 draw_list->AddRectFilled(
285 room_min, room_max,
286 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_color));
287 }
288 } else {
289 // Not loaded - gray placeholder
290 draw_list->AddRectFilled(
291 room_min, room_max,
292 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker));
293
294 // Show room ID
295 char label[8];
296 snprintf(label, sizeof(label), "%02X", room_id);
297 ImVec2 text_size = ImGui::CalcTextSize(label);
298 ImVec2 text_pos(room_min.x + (kRoomWidth - text_size.x) * 0.5f,
299 room_min.y + (kRoomHeight - text_size.y) * 0.5f);
300 draw_list->AddText(
301 text_pos,
302 ImGui::ColorConvertFloat4ToU32(theme.text_secondary_gray), label);
303 }
304
305 // Draw border based on state
306 if (is_current) {
307 // Glow effect
308 ImVec4 glow = theme.dungeon_selection_primary;
309 glow.w = 0.4f;
310 ImVec2 glow_min(room_min.x - 2, room_min.y - 2);
311 ImVec2 glow_max(room_max.x + 2, room_max.y + 2);
312 draw_list->AddRect(glow_min, glow_max,
313 ImGui::ColorConvertFloat4ToU32(glow), 0.0f, 0, 4.0f);
314 // Inner border
315 draw_list->AddRect(room_min, room_max,
316 ImGui::ColorConvertFloat4ToU32(
317 theme.dungeon_selection_primary),
318 0.0f, 0, 2.0f);
319 } else if (is_open) {
320 draw_list->AddRect(room_min, room_max,
321 ImGui::ColorConvertFloat4ToU32(
322 theme.dungeon_grid_cell_selected),
323 0.0f, 0, 2.0f);
324 } else {
325 draw_list->AddRect(room_min, room_max,
326 ImGui::ColorConvertFloat4ToU32(
327 theme.dungeon_grid_cell_border),
328 0.0f, 0, 1.0f);
329 }
330
331 // Room type badge (small colored dot in top-left corner)
332 auto type_it = room_types_.find(room_id);
333 if (type_it != room_types_.end()) {
334 ImU32 badge_color = 0;
335 if (type_it->second == "entrance") {
336 badge_color = IM_COL32(76, 175, 80, 220); // Green
337 } else if (type_it->second == "boss") {
338 badge_color = IM_COL32(244, 67, 54, 220); // Red
339 } else if (type_it->second == "mini_boss") {
340 badge_color = IM_COL32(255, 152, 0, 220); // Orange
341 }
342 if (badge_color != 0) {
343 ImVec2 badge_center(room_min.x + 6.0f, room_min.y + 6.0f);
344 draw_list->AddCircleFilled(badge_center, 4.0f, badge_color);
345 }
346 }
347
348 // Handle clicks
349 ImGui::SetCursorScreenPos(room_min);
350 char btn_id[32];
351 snprintf(btn_id, sizeof(btn_id), "##map_room%d", room_id);
352 ImGui::InvisibleButton(btn_id, ImVec2(kRoomWidth, kRoomHeight));
353
354 if (ImGui::IsItemClicked()) {
355 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
356 if (on_room_intent_) {
358 } else if (on_room_selected_) {
359 on_room_selected_(room_id);
360 }
361 } else if (on_room_selected_) {
362 on_room_selected_(room_id);
363 }
364 }
365
366 // Tooltip
367 if (ImGui::IsItemHovered()) {
368 ImGui::BeginTooltip();
369 ImGui::Text("[%03X] %s", room_id,
370 zelda3::GetRoomLabel(room_id).c_str());
371 if (rooms_ && (*rooms_)[room_id].IsLoaded()) {
372 ImGui::TextDisabled("Palette: %d", (*rooms_)[room_id].palette());
373 }
374 ImGui::TextDisabled("Click to select");
375 ImGui::EndTooltip();
376 }
377 }
378
379 // Advance past canvas
380 ImGui::Dummy(canvas_size);
381
382 // Status bar
383 ImGui::TextDisabled("%zu rooms in view", dungeon_room_ids_.size());
384 }
385
386 private:
391 room_positions_.clear();
392
393 int cols = static_cast<int>(std::ceil(std::sqrt(
394 static_cast<double>(dungeon_room_ids_.size()))));
395 cols = std::max(1, cols);
396
397 for (size_t i = 0; i < dungeon_room_ids_.size(); i++) {
398 int room_id = dungeon_room_ids_[i];
399 int grid_x = static_cast<int>(i % cols);
400 int grid_y = static_cast<int>(i / cols);
401 room_positions_[room_id] = ImVec2(static_cast<float>(grid_x),
402 static_cast<float>(grid_y));
403 }
404 }
405
411 bool has_registry = hack_manifest_ && hack_manifest_->HasProjectRegistry();
412
413 if (has_registry) {
415 } else {
417 }
418
419 ImGui::SameLine();
420 if (ImGui::Button(ICON_MD_ADD " Add Current")) {
421 if (current_room_id_ && *current_room_id_ >= 0) {
423 }
424 }
425 if (ImGui::IsItemHovered()) {
426 ImGui::SetTooltip("Add currently selected room to the map");
427 }
428
429 ImGui::SameLine();
430 if (ImGui::Button(ICON_MD_CLEAR " Clear")) {
431 ClearRooms();
432 stair_connections_.clear();
433 holewarp_connections_.clear();
434 room_types_.clear();
435 current_dungeon_name_ = "Select Dungeon...";
436 selected_preset_ = -1;
437 }
438 }
439
444 const auto& registry = hack_manifest_->project_registry();
445
446 if (ImGui::BeginCombo("##DungeonRegistry",
447 current_dungeon_name_.c_str())) {
448 for (size_t i = 0; i < registry.dungeons.size(); i++) {
449 const auto& dungeon = registry.dungeons[i];
450 // Show as "D4: Zora Temple (Thieves' Town)"
451 char label[128];
452 if (!dungeon.vanilla_name.empty()) {
453 snprintf(label, sizeof(label), "%s: %s (%s)",
454 dungeon.id.c_str(), dungeon.name.c_str(),
455 dungeon.vanilla_name.c_str());
456 } else {
457 snprintf(label, sizeof(label), "%s: %s",
458 dungeon.id.c_str(), dungeon.name.c_str());
459 }
460 bool selected = (current_dungeon_name_ == dungeon.name);
461 if (ImGui::Selectable(label, selected)) {
462 LoadFromRegistry(dungeon);
463 selected_preset_ = static_cast<int>(i);
464 }
465 }
466 ImGui::EndCombo();
467 }
468 }
469
474 struct DungeonPreset {
475 const char* name;
476 int start_room;
477 int count;
478 };
479
480 static const DungeonPreset kPresets[] = {
481 {"Eastern Palace", 0xC8, 8},
482 {"Desert Palace", 0x33, 8},
483 {"Tower of Hera", 0x07, 8},
484 {"Palace of Darkness", 0x09, 12},
485 {"Swamp Palace", 0x28, 10},
486 {"Skull Woods", 0x29, 10},
487 {"Thieves' Town", 0x44, 8},
488 {"Ice Palace", 0x0E, 12},
489 {"Misery Mire", 0x61, 10},
490 {"Turtle Rock", 0x04, 12},
491 {"Ganon's Tower", 0x0C, 16},
492 {"Hyrule Castle", 0x01, 12},
493 };
494
495 if (ImGui::BeginCombo("##DungeonPreset",
497 ? kPresets[selected_preset_].name
498 : "Select Dungeon...")) {
499 for (int i = 0; i < IM_ARRAYSIZE(kPresets); i++) {
500 if (ImGui::Selectable(kPresets[i].name, selected_preset_ == i)) {
502 dungeon_room_ids_.clear();
503 for (int j = 0; j < kPresets[i].count; j++) {
504 int room_id = kPresets[i].start_room + j;
505 if (room_id < 0x128) {
506 dungeon_room_ids_.push_back(room_id);
507 }
508 }
510 }
511 }
512 ImGui::EndCombo();
513 }
514 }
515
519 static void DrawDashedLine(ImDrawList* dl, ImVec2 from, ImVec2 to,
520 ImU32 color, float thickness, float dash_len) {
521 float dx = to.x - from.x;
522 float dy = to.y - from.y;
523 float length = std::sqrt(dx * dx + dy * dy);
524 if (length < 1.0f) return;
525 float nx = dx / length;
526 float ny = dy / length;
527
528 float drawn = 0.0f;
529 bool visible = true;
530 while (drawn < length) {
531 float seg = std::min(dash_len, length - drawn);
532 ImVec2 seg_start(from.x + nx * drawn, from.y + ny * drawn);
533 ImVec2 seg_end(from.x + nx * (drawn + seg),
534 from.y + ny * (drawn + seg));
535 if (visible) {
536 dl->AddLine(seg_start, seg_end, color, thickness);
537 }
538 drawn += seg;
539 visible = !visible;
540 }
541 }
542
546 static void DrawArrowhead(ImDrawList* dl, ImVec2 from, ImVec2 to,
547 ImU32 color, float size) {
548 float dx = to.x - from.x;
549 float dy = to.y - from.y;
550 float length = std::sqrt(dx * dx + dy * dy);
551 if (length < 1.0f) return;
552 float nx = dx / length;
553 float ny = dy / length;
554 // Perpendicular
555 float px = -ny;
556 float py = nx;
557
558 ImVec2 tip = to;
559 ImVec2 left(to.x - nx * size + px * size * 0.5f,
560 to.y - ny * size + py * size * 0.5f);
561 ImVec2 right(to.x - nx * size - px * size * 0.5f,
562 to.y - ny * size - py * size * 0.5f);
563 dl->AddTriangleFilled(tip, left, right, color);
564 }
565
566 int* current_room_id_ = nullptr;
567 ImVector<int>* active_rooms_ = nullptr;
568 std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
569 std::function<void(int)> on_room_selected_;
570 std::function<void(int, RoomSelectionIntent)> on_room_intent_;
571
572 // Room data
573 std::vector<int> dungeon_room_ids_;
574 std::map<int, ImVec2> room_positions_;
575 std::map<int, std::string> room_types_;
577
578 // Project registry integration
580 std::vector<core::DungeonConnection> stair_connections_;
581 std::vector<core::DungeonConnection> holewarp_connections_;
582 std::string current_dungeon_name_ = "Select Dungeon...";
583};
584
585} // namespace editor
586} // namespace yaze
587
588#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_MAP_PANEL_H_
Loads and queries the hack manifest JSON for yaze-ASM integration.
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
EditorPanel for displaying multiple rooms in a spatial dungeon layout.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
std::map< int, ImVec2 > room_positions_
void LoadFromRegistry(const core::DungeonEntry &dungeon)
Load rooms and connections from a DungeonEntry in the project registry.
void SetRooms(std::array< zelda3::Room, 0x128 > *rooms)
void ClearRooms()
Clear all rooms from the dungeon map.
void SetRoomIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
const core::HackManifest * hack_manifest_
std::vector< core::DungeonConnection > stair_connections_
std::function< void(int, RoomSelectionIntent)> on_room_intent_
void SetDungeonRooms(const std::vector< int > &room_ids)
Set which rooms to display in this dungeon map.
void DrawVanillaPresetSelector()
Fallback selector using vanilla ALTTP dungeon presets.
DungeonMapPanel(int *current_room_id, ImVector< int > *active_rooms, std::function< void(int)> on_room_selected, std::array< zelda3::Room, 0x128 > *rooms=nullptr)
Construct a dungeon map panel.
void AutoLayoutRooms()
Auto-layout rooms in a grid based on their IDs.
static void DrawDashedLine(ImDrawList *dl, ImVec2 from, ImVec2 to, ImU32 color, float thickness, float dash_len)
Draw a dashed line between two points.
std::function< void(int)> on_room_selected_
void AddRoom(int room_id)
Add a single room to the dungeon map.
void DrawDungeonSelector()
Draw dungeon preset selector — uses project registry if available, falls back to vanilla ALTTP preset...
std::map< int, std::string > room_types_
static void DrawArrowhead(ImDrawList *dl, ImVec2 from, ImVec2 to, ImU32 color, float size)
Draw a small triangle arrowhead at the 'to' end of a line.
void DrawRegistrySelector()
Selector using Oracle project registry dungeon entries.
int GetPriority() const override
Get display priority for menu ordering.
std::array< zelda3::Room, 0x128 > * rooms_
std::string GetIcon() const override
Material Design icon for this panel.
void SetHackManifest(const core::HackManifest *manifest)
Set the hack manifest for project registry access.
void Draw(bool *p_open) override
Draw the panel content.
std::vector< core::DungeonConnection > holewarp_connections_
std::string GetId() const override
Unique identifier for this panel.
void SetRoomPosition(int room_id, int grid_x, int grid_y)
Manually set a room's position in the grid.
Base interface for all logical panel components.
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_CLEAR
Definition icons.h:416
const AgentUITheme & GetTheme()
RoomSelectionIntent
Intent for room selection in the dungeon editor.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
A complete dungeon entry with rooms and connections.
std::vector< DungeonConnection > holewarps
std::vector< DungeonRoom > rooms
std::vector< DungeonConnection > stairs
std::vector< DungeonEntry > dungeons