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