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