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 <algorithm>
5#include <array>
6#include <cmath>
7#include <functional>
8#include <map>
9#include <string>
10#include <vector>
11
17#include "app/gui/core/icons.h"
18#include "core/hack_manifest.h"
19#include "imgui/imgui.h"
20#include "zelda3/dungeon/room.h"
23
24namespace yaze {
25namespace editor {
26
46 public:
54 DungeonMapPanel(int* current_room_id, ImVector<int>* active_rooms,
55 std::function<void(int)> on_room_selected,
56 DungeonRoomStore* rooms = nullptr)
57 : current_room_id_(current_room_id),
58 active_rooms_(active_rooms),
59 rooms_(rooms),
60 on_room_selected_(std::move(on_room_selected)) {}
61
62 // ==========================================================================
63 // WindowContent Identity
64 // ==========================================================================
65
66 std::string GetId() const override { return "dungeon.dungeon_map"; }
67 std::string GetDisplayName() const override { return "Dungeon Map"; }
68 std::string GetIcon() const override { return ICON_MD_MAP; }
69 std::string GetEditorCategory() const override { return "Dungeon"; }
70 int GetPriority() const override { return 35; }
71
73 std::function<void(int, RoomSelectionIntent)> callback) {
74 on_room_intent_ = std::move(callback);
75 }
76
77 // ==========================================================================
78 // Configuration
79 // ==========================================================================
80
85 void SetDungeonRooms(const std::vector<int>& room_ids) {
86 dungeon_room_ids_ = room_ids;
88 }
89
93 void AddRoom(int room_id) {
94 // Avoid duplicates
95 for (int id : dungeon_room_ids_) {
96 if (id == room_id)
97 return;
98 }
99 dungeon_room_ids_.push_back(room_id);
101 }
102
106 void ClearRooms() {
107 dungeon_room_ids_.clear();
108 room_positions_.clear();
109 }
110
114 void SetRoomPosition(int room_id, int grid_x, int grid_y) {
115 room_positions_[room_id] =
116 ImVec2(static_cast<float>(grid_x), static_cast<float>(grid_y));
117 }
118
119 void SetRooms(DungeonRoomStore* rooms) { rooms_ = rooms; }
120
124 void SetHackManifest(const core::HackManifest* manifest) {
125 hack_manifest_ = manifest;
126 }
127
132 ClearRooms();
133 current_dungeon_name_ = dungeon.name;
134 for (const auto& room : dungeon.rooms) {
135 dungeon_room_ids_.push_back(room.id);
136 room_positions_[room.id] = ImVec2(static_cast<float>(room.grid_col),
137 static_cast<float>(room.grid_row));
138 room_types_[room.id] = room.type;
139 }
140 stair_connections_ = dungeon.stairs;
142 }
143
144 // ==========================================================================
145 // WindowContent Drawing
146 // ==========================================================================
147
148 void Draw(bool* p_open) override {
150 return;
151
152 const auto& theme = AgentUI::GetTheme();
153
154 // Show dungeon selection/quick presets
156
157 ImGui::Separator();
158
159 // Room size in the map
160 constexpr float kRoomWidth = 64.0f;
161 constexpr float kRoomHeight = 64.0f;
162 constexpr float kRoomSpacing = 8.0f;
163
164 // Calculate canvas size based on room positions
165 float max_x = 0, max_y = 0;
166 for (const auto& [room_id, pos] : room_positions_) {
167 max_x = std::max(max_x, pos.x);
168 max_y = std::max(max_y, pos.y);
169 }
170 float canvas_width =
171 (max_x + 1) * (kRoomWidth + kRoomSpacing) + kRoomSpacing;
172 float canvas_height =
173 (max_y + 1) * (kRoomHeight + kRoomSpacing) + kRoomSpacing;
174
175 // Minimum size
176 canvas_width = std::max(canvas_width, 200.0f);
177 canvas_height = std::max(canvas_height, 200.0f);
178
179 ImVec2 available = ImGui::GetContentRegionAvail();
180 ImVec2 canvas_size(std::min(available.x, canvas_width),
181 std::min(available.y - 40, canvas_height));
182
183 // Begin canvas area
184 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
185 ImDrawList* draw_list = ImGui::GetWindowDrawList();
186
187 // Background
188 ImU32 bg_color = ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker);
189 draw_list->AddRectFilled(
190 canvas_pos,
191 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
192 bg_color);
193
194 // Helper lambda: compute the center pixel position for a room on the canvas
195 auto RoomCenter = [&](int room_id) -> ImVec2 {
196 auto it = room_positions_.find(room_id);
197 if (it == room_positions_.end())
198 return ImVec2(0, 0);
199 ImVec2 pos = it->second;
200 return ImVec2(canvas_pos.x + kRoomSpacing +
201 pos.x * (kRoomWidth + kRoomSpacing) + kRoomWidth * 0.5f,
202 canvas_pos.y + kRoomSpacing +
203 pos.y * (kRoomHeight + kRoomSpacing) +
204 kRoomHeight * 0.5f);
205 };
206
207 // Draw connections between adjacent rooms (gray lines — doors)
208 ImVec4 connection_color = theme.dungeon_room_border_dark;
209 connection_color.w = 0.45f;
210 for (size_t i = 0; i < dungeon_room_ids_.size(); i++) {
211 for (size_t j = i + 1; j < dungeon_room_ids_.size(); j++) {
212 int room_a = dungeon_room_ids_[i];
213 int room_b = dungeon_room_ids_[j];
214
215 bool adjacent = false;
216 if (std::abs(room_a - room_b) == 16) {
217 adjacent = true;
218 } else if (std::abs(room_a - room_b) == 1) {
219 int col_a = room_a % 16;
220 int col_b = room_b % 16;
221 if (std::abs(col_a - col_b) == 1) {
222 adjacent = true;
223 }
224 }
225
226 if (adjacent) {
227 draw_list->AddLine(RoomCenter(room_a), RoomCenter(room_b),
228 ImGui::ColorConvertFloat4ToU32(connection_color),
229 1.5f);
230 }
231 }
232 }
233
234 // Draw stair connections (blue dashed lines — bidirectional)
235 for (const auto& conn : stair_connections_) {
236 if (room_positions_.count(conn.from_room) &&
237 room_positions_.count(conn.to_room)) {
238 ImVec2 from = RoomCenter(conn.from_room);
239 ImVec2 to = RoomCenter(conn.to_room);
240 DrawDashedLine(draw_list, from, to, IM_COL32(100, 149, 237, 200), 1.5f,
241 6.0f);
242 }
243 }
244
245 // Draw holewarp connections (red lines with arrow — one-way falls)
246 for (const auto& conn : holewarp_connections_) {
247 if (room_positions_.count(conn.from_room) &&
248 room_positions_.count(conn.to_room)) {
249 ImVec2 from = RoomCenter(conn.from_room);
250 ImVec2 to = RoomCenter(conn.to_room);
251 ImU32 red = IM_COL32(220, 60, 60, 200);
252 draw_list->AddLine(from, to, red, 2.0f);
253 // Arrowhead at destination
254 DrawArrowhead(draw_list, from, to, red, 6.0f);
255 }
256 }
257
258 // Draw each room
259 for (int room_id : dungeon_room_ids_) {
260 auto pos_it = room_positions_.find(room_id);
261 if (pos_it == room_positions_.end())
262 continue;
263
264 ImVec2 grid_pos = pos_it->second;
265 ImVec2 room_min(canvas_pos.x + kRoomSpacing +
266 grid_pos.x * (kRoomWidth + kRoomSpacing),
267 canvas_pos.y + kRoomSpacing +
268 grid_pos.y * (kRoomHeight + kRoomSpacing));
269 ImVec2 room_max(room_min.x + kRoomWidth, room_min.y + kRoomHeight);
270
271 // Check if room is valid
272 if (room_id < 0 || room_id >= 0x128)
273 continue;
274
275 bool is_current = (*current_room_id_ == room_id);
276 bool is_open = false;
277 for (int i = 0; i < active_rooms_->Size; i++) {
278 if ((*active_rooms_)[i] == room_id) {
279 is_open = true;
280 break;
281 }
282 }
283
284 // Draw room thumbnail or placeholder
285 if (rooms_) {
286 auto* loaded_room = rooms_->GetIfLoaded(room_id);
287 if (loaded_room != nullptr) {
288 zelda3::RoomLayerManager layer_mgr;
289 layer_mgr.ApplyLayerMerging(loaded_room->layer_merging());
290 auto& preview_bitmap = loaded_room->GetCompositeBitmap(layer_mgr);
291 if (preview_bitmap.is_active() && preview_bitmap.width() > 0) {
292 if (!preview_bitmap.texture()) {
296 } else if (preview_bitmap.modified()) {
300 preview_bitmap.set_modified(false);
301 }
302 }
303 if (preview_bitmap.is_active() && preview_bitmap.texture() != 0) {
304 // Draw room thumbnail
305 draw_list->AddImage((ImTextureID)(intptr_t)preview_bitmap.texture(),
306 room_min, room_max);
307 } else {
308 // Placeholder for loaded but no texture
309 draw_list->AddRectFilled(
310 room_min, room_max,
311 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_color));
312 }
313 } else {
314 // Not loaded - gray placeholder
315 draw_list->AddRectFilled(
316 room_min, room_max,
317 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker));
318
319 // Show room ID
320 char label[8];
321 snprintf(label, sizeof(label), "%02X", room_id);
322 ImVec2 text_size = ImGui::CalcTextSize(label);
323 ImVec2 text_pos(room_min.x + (kRoomWidth - text_size.x) * 0.5f,
324 room_min.y + (kRoomHeight - text_size.y) * 0.5f);
325 draw_list->AddText(
326 text_pos,
327 ImGui::ColorConvertFloat4ToU32(theme.text_secondary_gray), label);
328 }
329 } else {
330 // Not loaded - gray placeholder
331 draw_list->AddRectFilled(
332 room_min, room_max,
333 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker));
334
335 // Show room ID
336 char label[8];
337 snprintf(label, sizeof(label), "%02X", room_id);
338 ImVec2 text_size = ImGui::CalcTextSize(label);
339 ImVec2 text_pos(room_min.x + (kRoomWidth - text_size.x) * 0.5f,
340 room_min.y + (kRoomHeight - text_size.y) * 0.5f);
341 draw_list->AddText(
342 text_pos, ImGui::ColorConvertFloat4ToU32(theme.text_secondary_gray),
343 label);
344 }
345
346 // Draw border based on state
347 if (is_current) {
348 // Glow effect
349 ImVec4 glow = theme.dungeon_selection_primary;
350 glow.w = 0.4f;
351 ImVec2 glow_min(room_min.x - 2, room_min.y - 2);
352 ImVec2 glow_max(room_max.x + 2, room_max.y + 2);
353 draw_list->AddRect(glow_min, glow_max,
354 ImGui::ColorConvertFloat4ToU32(glow), 0.0f, 0, 4.0f);
355 // Inner border
356 draw_list->AddRect(
357 room_min, room_max,
358 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary),
359 0.0f, 0, 2.0f);
360 } else if (is_open) {
361 draw_list->AddRect(
362 room_min, room_max,
363 ImGui::ColorConvertFloat4ToU32(theme.dungeon_grid_cell_selected),
364 0.0f, 0, 2.0f);
365 } else {
366 draw_list->AddRect(
367 room_min, room_max,
368 ImGui::ColorConvertFloat4ToU32(theme.dungeon_grid_cell_border),
369 0.0f, 0, 1.0f);
370 }
371
372 // Room type badge (small colored dot in top-left corner)
373 auto type_it = room_types_.find(room_id);
374 if (type_it != room_types_.end()) {
375 ImU32 badge_color = 0;
376 if (type_it->second == "entrance") {
377 badge_color = IM_COL32(76, 175, 80, 220); // Green
378 } else if (type_it->second == "boss") {
379 badge_color = IM_COL32(244, 67, 54, 220); // Red
380 } else if (type_it->second == "mini_boss") {
381 badge_color = IM_COL32(255, 152, 0, 220); // Orange
382 }
383 if (badge_color != 0) {
384 ImVec2 badge_center(room_min.x + 6.0f, room_min.y + 6.0f);
385 draw_list->AddCircleFilled(badge_center, 4.0f, badge_color);
386 }
387 }
388
389 // Handle clicks
390 ImGui::SetCursorScreenPos(room_min);
391 char btn_id[32];
392 snprintf(btn_id, sizeof(btn_id), "##map_room%d", room_id);
393 ImGui::InvisibleButton(btn_id, ImVec2(kRoomWidth, kRoomHeight));
394
395 if (ImGui::IsItemClicked()) {
396 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
397 if (on_room_intent_) {
399 } else if (on_room_selected_) {
400 on_room_selected_(room_id);
401 }
402 } else if (on_room_selected_) {
403 on_room_selected_(room_id);
404 }
405 }
406
407 // Tooltip
408 if (ImGui::IsItemHovered()) {
409 ImGui::BeginTooltip();
410 ImGui::Text("[%03X] %s", room_id,
411 zelda3::GetRoomLabel(room_id).c_str());
412 if (rooms_) {
413 if (auto* loaded_room = rooms_->GetIfLoaded(room_id)) {
414 ImGui::TextDisabled("Palette: %d", loaded_room->palette());
415 }
416 }
417 ImGui::TextDisabled("Click to select");
418 ImGui::EndTooltip();
419 }
420 }
421
422 // Advance past canvas
423 ImGui::Dummy(canvas_size);
424
425 // Status bar
426 ImGui::TextDisabled("%zu rooms in view", dungeon_room_ids_.size());
427 }
428
429 private:
434 room_positions_.clear();
435
436 int cols = static_cast<int>(
437 std::ceil(std::sqrt(static_cast<double>(dungeon_room_ids_.size()))));
438 cols = std::max(1, cols);
439
440 for (size_t i = 0; i < dungeon_room_ids_.size(); i++) {
441 int room_id = dungeon_room_ids_[i];
442 int grid_x = static_cast<int>(i % cols);
443 int grid_y = static_cast<int>(i / cols);
444 room_positions_[room_id] =
445 ImVec2(static_cast<float>(grid_x), static_cast<float>(grid_y));
446 }
447 }
448
454 bool has_registry = hack_manifest_ && hack_manifest_->HasProjectRegistry();
455
456 if (has_registry) {
458 } else {
460 }
461
462 ImGui::SameLine();
463 if (ImGui::Button(ICON_MD_ADD " Add Current")) {
464 if (current_room_id_ && *current_room_id_ >= 0) {
466 }
467 }
468 if (ImGui::IsItemHovered()) {
469 ImGui::SetTooltip("Add currently selected room to the map");
470 }
471
472 ImGui::SameLine();
473 if (ImGui::Button(ICON_MD_CLEAR " Clear")) {
474 ClearRooms();
475 stair_connections_.clear();
476 holewarp_connections_.clear();
477 room_types_.clear();
478 current_dungeon_name_ = "Select Dungeon...";
479 selected_preset_ = -1;
480 }
481 }
482
487 const auto& dungeons = hack_manifest_->project_registry().dungeons;
488
489 if (ImGui::BeginCombo("##DungeonRegistry", current_dungeon_name_.c_str())) {
490 for (size_t i = 0; i < dungeons.size(); i++) {
491 const auto& dungeon = dungeons[i];
492 char label[128];
493 if (!dungeon.vanilla_name.empty()) {
494 snprintf(label, sizeof(label), "%s: %s (%s)", dungeon.id.c_str(),
495 dungeon.name.c_str(), dungeon.vanilla_name.c_str());
496 } else {
497 snprintf(label, sizeof(label), "%s: %s", dungeon.id.c_str(),
498 dungeon.name.c_str());
499 }
500 bool selected = (current_dungeon_name_ == dungeon.name);
501 if (ImGui::Selectable(label, selected)) {
502 LoadFromDungeonEntry(dungeon);
503 selected_preset_ = static_cast<int>(i);
504 }
505 }
506 ImGui::EndCombo();
507 }
508 }
509
514 struct DungeonPreset {
515 const char* name;
516 int start_room;
517 int count;
518 };
519
520 static const DungeonPreset kPresets[] = {
521 {"Eastern Palace", 0xC8, 8}, {"Desert Palace", 0x33, 8},
522 {"Tower of Hera", 0x07, 8}, {"Palace of Darkness", 0x09, 12},
523 {"Swamp Palace", 0x28, 10}, {"Skull Woods", 0x29, 10},
524 {"Thieves' Town", 0x44, 8}, {"Ice Palace", 0x0E, 12},
525 {"Misery Mire", 0x61, 10}, {"Turtle Rock", 0x04, 12},
526 {"Ganon's Tower", 0x0C, 16}, {"Hyrule Castle", 0x01, 12},
527 };
528
529 if (ImGui::BeginCombo("##DungeonPreset",
531 ? kPresets[selected_preset_].name
532 : "Select Dungeon...")) {
533 for (int i = 0; i < IM_ARRAYSIZE(kPresets); i++) {
534 if (ImGui::Selectable(kPresets[i].name, selected_preset_ == i)) {
536 dungeon_room_ids_.clear();
537 for (int j = 0; j < kPresets[i].count; j++) {
538 int room_id = kPresets[i].start_room + j;
539 if (room_id < 0x128) {
540 dungeon_room_ids_.push_back(room_id);
541 }
542 }
544 }
545 }
546 ImGui::EndCombo();
547 }
548 }
549
553 static void DrawDashedLine(ImDrawList* dl, ImVec2 from, ImVec2 to,
554 ImU32 color, float thickness, float dash_len) {
555 float dx = to.x - from.x;
556 float dy = to.y - from.y;
557 float length = std::sqrt(dx * dx + dy * dy);
558 if (length < 1.0f)
559 return;
560 float nx = dx / length;
561 float ny = dy / length;
562
563 float drawn = 0.0f;
564 bool visible = true;
565 while (drawn < length) {
566 float seg = std::min(dash_len, length - drawn);
567 ImVec2 seg_start(from.x + nx * drawn, from.y + ny * drawn);
568 ImVec2 seg_end(from.x + nx * (drawn + seg), from.y + ny * (drawn + seg));
569 if (visible) {
570 dl->AddLine(seg_start, seg_end, color, thickness);
571 }
572 drawn += seg;
573 visible = !visible;
574 }
575 }
576
580 static void DrawArrowhead(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 color,
581 float size) {
582 float dx = to.x - from.x;
583 float dy = to.y - from.y;
584 float length = std::sqrt(dx * dx + dy * dy);
585 if (length < 1.0f)
586 return;
587 float nx = dx / length;
588 float ny = dy / length;
589 // Perpendicular
590 float px = -ny;
591 float py = nx;
592
593 ImVec2 tip = to;
594 ImVec2 left(to.x - nx * size + px * size * 0.5f,
595 to.y - ny * size + py * size * 0.5f);
596 ImVec2 right(to.x - nx * size - px * size * 0.5f,
597 to.y - ny * size - py * size * 0.5f);
598 dl->AddTriangleFilled(tip, left, right, color);
599 }
600
601 int* current_room_id_ = nullptr;
602 ImVector<int>* active_rooms_ = nullptr;
604 std::function<void(int)> on_room_selected_;
605 std::function<void(int, RoomSelectionIntent)> on_room_intent_;
606
607 // Room data
608 std::vector<int> dungeon_room_ids_;
609 std::map<int, ImVec2> room_positions_;
610 std::map<int, std::string> room_types_;
612
613 // Project registry integration
615 std::vector<core::DungeonConnection> stair_connections_;
616 std::vector<core::DungeonConnection> holewarp_connections_;
617 std::string current_dungeon_name_ = "Select Dungeon...";
618};
619
620} // namespace editor
621} // namespace yaze
622
623#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
WindowContent 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 ClearRooms()
Clear all rooms from the dungeon map.
void SetRoomIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void LoadFromDungeonEntry(const core::DungeonEntry &dungeon)
Load rooms and connections from a project registry dungeon entry.
const core::HackManifest * hack_manifest_
void SetRooms(DungeonRoomStore *rooms)
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.
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 project registry area overviews.
int GetPriority() const override
Get display priority for menu ordering.
DungeonMapPanel(int *current_room_id, ImVector< int > *active_rooms, std::function< void(int)> on_room_selected, DungeonRoomStore *rooms=nullptr)
Construct a dungeon map panel.
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.
zelda3::Room * GetIfLoaded(int room_id)
Base interface for all logical window content components.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
RoomLayerManager - Manages layer visibility and compositing.
void ApplyLayerMerging(const LayerMergeType &merge_type)
#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