yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_connected_view.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <cstdio>
7#include <functional>
8#include <limits>
9#include <map>
10#include <optional>
11#include <queue>
12#include <set>
13#include <string>
14#include <tuple>
15#include <utility>
16#include <vector>
17
18#include "absl/strings/str_format.h"
21
22namespace yaze::editor {
23
24namespace {
25
42
60
61std::pair<int, int> ConnectedDoorDelta(zelda3::DoorDirection dir) {
62 switch (dir) {
64 return {0, -1};
66 return {0, 1};
68 return {-1, 0};
70 return {1, 0};
71 }
72 return {0, 0};
73}
74
76 const std::map<std::pair<int, int>, int>& occupied_slots, int col,
77 int row) {
78 return occupied_slots.contains({col, row});
79}
80
81std::pair<int, int> FindConnectedDoorPlacement(
82 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
83 int source_row, zelda3::DoorDirection dir) {
84 const auto [dx, dy] = ConnectedDoorDelta(dir);
85 int best_score = std::numeric_limits<int>::max();
86 std::pair<int, int> best = {source_col + dx, source_row + dy};
87
88 for (int radius = 1; radius <= 24; ++radius) {
89 for (int row = source_row - radius; row <= source_row + radius; ++row) {
90 for (int col = source_col - radius; col <= source_col + radius; ++col) {
91 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
92 continue;
93 }
94
95 const int rel_col = col - source_col;
96 const int rel_row = row - source_row;
97 const int forward = (rel_col * dx) + (rel_row * dy);
98 if (forward <= 0) {
99 continue;
100 }
101
102 const int lateral = std::abs((rel_col * dy) - (rel_row * dx));
103 const int distance_from_ideal =
104 std::abs(rel_col - dx) + std::abs(rel_row - dy);
105 const int overshoot = std::max(0, forward - 1);
106 const int score =
107 (distance_from_ideal * 16) + (lateral * 20) + (overshoot * 6);
108 if (score < best_score) {
109 best_score = score;
110 best = {col, row};
111 }
112 }
113 }
114 if (best_score != std::numeric_limits<int>::max()) {
115 return best;
116 }
117 }
118
119 return best;
120}
121
123 const std::map<std::pair<int, int>, int>& occupied_slots, int source_col,
124 int source_row) {
125 int best_score = std::numeric_limits<int>::max();
126 std::pair<int, int> best = {source_col + 1, source_row + 1};
127
128 for (int radius = 1; radius <= 24; ++radius) {
129 for (int row = source_row - radius; row <= source_row + radius; ++row) {
130 for (int col = source_col - radius; col <= source_col + radius; ++col) {
131 if (IsConnectedSlotOccupied(occupied_slots, col, row)) {
132 continue;
133 }
134
135 const int rel_col = col - source_col;
136 const int rel_row = row - source_row;
137 const int distance = std::abs(rel_col) + std::abs(rel_row);
138 if (distance == 0) {
139 continue;
140 }
141
142 const bool diagonal = rel_col != 0 && rel_row != 0;
143 const bool axis_aligned = rel_col == 0 || rel_row == 0;
144 const int distance_bias = std::abs(distance - 2);
145 const int score = (distance_bias * 10) +
146 (diagonal ? 0
147 : axis_aligned ? 8
148 : 4) +
149 ((std::abs(rel_col) + std::abs(rel_row)) > 3 ? 4 : 0);
150 if (score < best_score) {
151 best_score = score;
152 best = {col, row};
153 }
154 }
155 }
156 if (best_score != std::numeric_limits<int>::max()) {
157 return best;
158 }
159 }
160
161 return best;
162}
163
164// Dedup key for the connected-mode graph.
165//
166// Door / Holewarp links are undirected — emitting from either side produces
167// the same logical edge — so we order the room pair (minmax) and treat both
168// emissions as one. There can only be one of either type per room pair.
169//
170// Staircase links carry per-instance provenance (slot_index + object_id) and
171// are *directed*: a stair object lives in exactly one room and points to
172// another. Two stair objects in the same source room targeting the same
173// destination, or two reciprocal stairs (A->B at slot 0 + B->A at slot 1),
174// are distinct edges that must each be visible in the matrix. Including
175// from_room/slot_index/object_id in the key preserves them.
176std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>
179 return std::make_tuple(link.from_room_id, link.to_room_id, link.type,
180 link.slot_index, link.object_id);
181 }
182 const auto ordered_rooms = std::minmax(link.from_room_id, link.to_room_id);
183 return std::make_tuple(ordered_rooms.first, ordered_rooms.second, link.type,
184 -1, static_cast<int16_t>(-1));
185}
186
187} // namespace
188
190 int room_id, const zelda3::Room& room,
191 const std::function<bool(int, zelda3::DoorDirection)>&
192 has_reciprocal_door) {
194
195 const auto& doors = room.GetDoors();
196 for (size_t door_index = 0; door_index < doors.size(); ++door_index) {
197 const auto& door = doors[door_index];
198 if (IsExitDoorType(door.type)) {
199 continue;
200 }
201
202 const int neighbor = NeighborRoomId(room_id, door.direction);
203 if (neighbor < 0) {
204 continue;
205 }
206 if (has_reciprocal_door &&
207 !has_reciprocal_door(neighbor, OppositeDir(door.direction))) {
208 continue;
209 }
210
212 link.from_room_id = room_id;
213 link.to_room_id = neighbor;
215 link.direction = door.direction;
216 link.door_index = static_cast<int>(door_index);
217 link.door_type = door.type;
218 result.links.push_back(link);
219 }
220
221 // Walk placed header-backed staircase objects in placement order. The Nth
222 // such object consumes header slot N (room.staircase_room(N)). Mismatches
223 // are surfaced as diagnostic entries instead of being silently skipped:
224 //
225 // - Slot consumed + valid header → real Staircase link.
226 // - Slot consumed + zero/invalid header → MissingDestination diagnostic
227 // (placed object would be a dead-end stair at runtime).
228 // - >4 placed objects → ExtraPlacedObject diagnostic per surplus object.
229 // - Header non-zero but no consuming object → UnusedHeader diagnostic.
230 //
231 // ASSUMPTION (load-bearing for diagnostics): the placement-order → header-
232 // slot-index mapping mirrors how the runtime walks Room_LoadDungeonState.
233 // ZScream's `Dungeon_LoadStaircaseRooms` and the usdasm dungeon-load path
234 // both consume `Object_Tile_Staircase*` writers in placement order and
235 // index `staircase_rooms[]` with an internal counter. If any custom
236 // sub-routine ever indexes the slot table by a parameter byte instead of
237 // placement order, this mapping will misreport which placed object
238 // collides with which header destination. Verify against the ROM with
239 // `staircase_room_position_select.asm` (usdasm bank-01) when adding
240 // ROM-backed parity tests; the synthetic AddObject fixtures here do not
241 // exercise the real load path.
242 std::array<bool, 4> slot_consumed{false, false, false, false};
243 std::array<int16_t, 4> slot_object_id{
244 static_cast<int16_t>(-1), static_cast<int16_t>(-1),
245 static_cast<int16_t>(-1), static_cast<int16_t>(-1)};
246 int next_slot = 0;
247 for (const auto& object : room.GetTileObjects()) {
248 if (!IsHeaderBackedInterroomStaircaseObject(object.id_)) {
249 continue;
250 }
251 if (next_slot >= 4) {
253 extra.from_room_id = room_id;
255 extra.object_id = object.id_;
256 result.staircase_issues.push_back(extra);
257 continue;
258 }
259 slot_consumed[next_slot] = true;
260 slot_object_id[next_slot] = object.id_;
261 ++next_slot;
262 }
263
264 for (int slot = 0; slot < 4; ++slot) {
265 const int stair_room = static_cast<int>(room.staircase_room(slot));
266 const bool header_valid =
267 stair_room > 0 && stair_room < zelda3::kNumberOfRooms;
268 if (slot_consumed[slot]) {
269 if (header_valid) {
271 link.from_room_id = room_id;
272 link.to_room_id = stair_room;
275 link.slot_index = slot;
276 link.object_id = slot_object_id[slot];
277 result.links.push_back(link);
278 } else {
280 issue.from_room_id = room_id;
282 issue.slot_index = slot;
283 issue.header_room_id = stair_room;
284 issue.object_id = slot_object_id[slot];
285 result.staircase_issues.push_back(issue);
286 }
287 } else if (header_valid) {
289 issue.from_room_id = room_id;
291 issue.slot_index = slot;
292 issue.header_room_id = stair_room;
293 result.staircase_issues.push_back(issue);
294 }
295 }
296
297 const int holewarp_room = static_cast<int>(room.holewarp());
298 if (holewarp_room > 0 && holewarp_room < zelda3::kNumberOfRooms) {
300 link.from_room_id = room_id;
301 link.to_room_id = holewarp_room;
304 result.links.push_back(link);
305 }
306
307 return result;
308}
309
310std::vector<DungeonConnectedRoomLink> CollectDungeonConnectedRoomLinks(
311 int room_id, const zelda3::Room& room,
312 const std::function<bool(int, zelda3::DoorDirection)>&
313 has_reciprocal_door) {
315 has_reciprocal_door)
316 .links;
317}
318
319namespace {
320
322 switch (dir) {
324 return "North";
326 return "South";
328 return "West";
330 return "East";
331 }
332 return "?";
333}
334
335} // namespace
336
338 const DungeonConnectedRoomLink& link) {
339 switch (link.type) {
341 return absl::StrFormat("Door (%s, type 0x%02X) -> [%03X]",
342 DoorDirectionLabel(link.direction),
343 static_cast<unsigned>(link.door_type),
344 static_cast<unsigned>(link.to_room_id));
346 std::string object_str =
347 (link.object_id >= 0)
348 ? absl::StrFormat("0x%03X", static_cast<unsigned>(link.object_id))
349 : std::string("(unknown)");
350 const int slot_index = link.slot_index >= 0 ? link.slot_index : -1;
351 if (slot_index < 0) {
352 return absl::StrFormat("Staircase obj %s -> [%03X]", object_str,
353 static_cast<unsigned>(link.to_room_id));
354 }
355 return absl::StrFormat("Staircase slot %d obj %s -> [%03X]", slot_index,
356 object_str,
357 static_cast<unsigned>(link.to_room_id));
358 }
360 return absl::StrFormat("Holewarp -> [%03X]",
361 static_cast<unsigned>(link.to_room_id));
362 }
363 return absl::StrFormat("Link -> [%03X]",
364 static_cast<unsigned>(link.to_room_id));
365}
366
368 const DungeonStaircaseIssue& issue) {
369 switch (issue.kind) {
371 return absl::StrFormat(
372 "Stale staircase slot %d -> [%03X] (no placed interroom-stair "
373 "object consumes this slot)",
374 issue.slot_index, static_cast<unsigned>(issue.header_room_id));
376 const std::string object_str =
377 (issue.object_id >= 0)
378 ? absl::StrFormat("0x%03X",
379 static_cast<unsigned>(issue.object_id))
380 : std::string("(unknown)");
381 const std::string header_str =
382 (issue.header_room_id == 0)
383 ? std::string("0 (unset)")
384 : absl::StrFormat("0x%03X (out of range)",
385 static_cast<unsigned>(issue.header_room_id));
386 return absl::StrFormat(
387 "Missing staircase destination at slot %d (placed object %s, "
388 "header value %s)",
389 issue.slot_index, object_str, header_str);
390 }
392 const std::string object_str =
393 (issue.object_id >= 0)
394 ? absl::StrFormat("0x%03X",
395 static_cast<unsigned>(issue.object_id))
396 : std::string("(unknown)");
397 return absl::StrFormat(
398 "Extra staircase object %s beyond the 4 header slots (runtime "
399 "cannot reach this stair)",
400 object_str);
401 }
402 }
403 return std::string("Unknown staircase issue");
404}
405
407 int room_id) {
408 if (!rooms_ || !rom_ || !rom_->is_loaded() || room_id < 0 ||
409 room_id >= zelda3::kNumberOfRooms) {
410 return nullptr;
411 }
412
413 auto* room_ptr = rooms_->TryEnsureRoom(room_id);
414 if (!room_ptr) {
415 return nullptr;
416 }
417 auto& room = *room_ptr;
418 room.SetRom(rom_);
420 if (!room.IsLoaded()) {
421 room = zelda3::LoadRoomFromRom(rom_, room_id);
423 }
424 return &room;
425}
426
428 int room_id, zelda3::DoorDirection dir) {
430 if (!room) {
431 return false;
432 }
433
434 for (const auto& door : room->GetDoors()) {
435 if (door.direction == dir && !IsExitDoorType(door.type)) {
436 return true;
437 }
438 }
439 return false;
440}
441
445 if (start_room_id < 0 || start_room_id >= zelda3::kNumberOfRooms) {
446 return graph;
447 }
448
449 zelda3::Room* start_room = EnsureRoomLoadedForConnectedView(start_room_id);
450 if (!start_room) {
451 return graph;
452 }
453
454 // Project-aware scoping: when the project registry maps the start room to
455 // a dungeon entry, restrict BFS to that dungeon's rooms by default.
456 // Cross-blockset / cross-dungeon resolved links are still surfaced via
457 // out_of_scope_links so the diagnostic remains visible without polluting
458 // the visual matrix with rooms from other dungeons.
459 std::set<int> scoped_room_ids;
460 std::map<int, const core::DungeonRoom*> scoped_rooms_by_id;
461 if (const auto* dungeon =
463 graph.dungeon_scope_active = true;
464 for (const auto& dungeon_room : dungeon->rooms) {
465 if (dungeon_room.id < 0 || dungeon_room.id >= zelda3::kNumberOfRooms) {
466 continue;
467 }
468 scoped_room_ids.insert(dungeon_room.id);
469 scoped_rooms_by_id[dungeon_room.id] = &dungeon_room;
470 graph.room_floor_labels[static_cast<size_t>(dungeon_room.id)] =
471 dungeon_room.floor;
472 if (!dungeon_room.floor.empty() &&
473 std::find(graph.floor_order.begin(), graph.floor_order.end(),
474 dungeon_room.floor) == graph.floor_order.end()) {
475 graph.floor_order.push_back(dungeon_room.floor);
476 }
477 }
478 }
479
480 std::map<std::pair<int, int>, int> occupied_slots;
481 std::queue<int> to_visit;
482 std::set<std::tuple<int, int, DungeonConnectedLinkType, int, int16_t>>
483 seen_links;
484
485 auto track_room_bounds = [&](int room_id) {
486 const auto& placement = graph.room_positions[static_cast<size_t>(room_id)];
487 graph.min_col = std::min(graph.min_col, placement.col);
488 graph.max_col = std::max(graph.max_col, placement.col);
489 graph.min_row = std::min(graph.min_row, placement.row);
490 graph.max_row = std::max(graph.max_row, placement.row);
491 };
492
493 auto registry_placement_for =
494 [&](int room_id) -> std::optional<std::pair<int, int>> {
495 const auto it = scoped_rooms_by_id.find(room_id);
496 if (it == scoped_rooms_by_id.end() || !it->second->has_grid_position) {
497 return std::nullopt;
498 }
499 return std::make_pair(it->second->grid_col, it->second->grid_row);
500 };
501
502 auto place_room = [&](int room_id, std::pair<int, int> desired_placement,
503 bool connected_to_start) {
504 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
505 return;
506 }
507 const size_t index = static_cast<size_t>(room_id);
508 if (graph.room_mask[index]) {
509 if (connected_to_start &&
510 !graph.room_positions[index].connected_to_start) {
511 graph.room_positions[index].connected_to_start = true;
512 graph.unlinked_room_count = std::max(0, graph.unlinked_room_count - 1);
513 }
514 return;
515 }
516
517 std::pair<int, int> placement = desired_placement;
518 const auto occupied = occupied_slots.find(placement);
519 if (occupied != occupied_slots.end() && occupied->second != room_id) {
520 placement = FindConnectedTransportPlacement(
521 occupied_slots, desired_placement.first, desired_placement.second);
522 }
523
524 graph.room_mask[index] = true;
525 graph.room_positions[index] = {placement.first, placement.second, true,
526 connected_to_start};
527 occupied_slots[placement] = room_id;
528 ++graph.room_count;
529 if (!connected_to_start) {
530 ++graph.unlinked_room_count;
531 }
532 if (graph.room_count == 1) {
533 graph.min_col = graph.max_col = placement.first;
534 graph.min_row = graph.max_row = placement.second;
535 } else {
536 track_room_bounds(room_id);
537 }
538 };
539
540 auto in_dungeon_scope = [&](int candidate_room_id) {
541 if (!graph.dungeon_scope_active) {
542 return true;
543 }
544 return scoped_room_ids.find(candidate_room_id) != scoped_room_ids.end();
545 };
546
547 place_room(
548 start_room_id,
549 registry_placement_for(start_room_id).value_or(std::make_pair(0, 0)),
550 true);
551 to_visit.push(start_room_id);
552
553 while (!to_visit.empty()) {
554 const int room_id = to_visit.front();
555 to_visit.pop();
556
558 if (!room) {
559 continue;
560 }
561
562 const auto outgoing_diagnostics =
564 room_id, *room,
565 [this](int neighbor_room_id, zelda3::DoorDirection dir) {
566 return RoomHasNonExitDoorInDirection(neighbor_room_id, dir);
567 });
568 for (const auto& issue : outgoing_diagnostics.staircase_issues) {
569 graph.staircase_issues.push_back(issue);
570 }
571 const auto& outgoing_links = outgoing_diagnostics.links;
572
573 for (const auto& link : outgoing_links) {
574 if (link.to_room_id < 0 || link.to_room_id >= zelda3::kNumberOfRooms) {
575 continue;
576 }
577
578 // Out-of-scope link (target room is not in the current dungeon group):
579 // record as a diagnostic, do not recurse, do not add the target to the
580 // visual graph. We still de-dup against `seen_links` so a reciprocal
581 // edge from the other side won't double-list it later.
582 if (!in_dungeon_scope(link.to_room_id)) {
583 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
584 graph.out_of_scope_links.push_back(link);
585 }
586 continue;
587 }
588
589 if (!EnsureRoomLoadedForConnectedView(link.to_room_id)) {
590 continue;
591 }
592
593 if (seen_links.insert(MakeConnectedLinkKey(link)).second) {
594 graph.links.push_back(link);
595 }
596
597 if (!graph.room_mask[static_cast<size_t>(link.to_room_id)]) {
598 const auto& source_placement =
599 graph.room_positions[static_cast<size_t>(room_id)];
600 const std::pair<int, int> placement =
601 registry_placement_for(link.to_room_id)
602 .value_or((link.type == DungeonConnectedLinkType::Door)
603 ? FindConnectedDoorPlacement(
604 occupied_slots, source_placement.col,
605 source_placement.row, link.direction)
606 : FindConnectedTransportPlacement(
607 occupied_slots, source_placement.col,
608 source_placement.row));
609 place_room(link.to_room_id, placement, true);
610 to_visit.push(link.to_room_id);
611 }
612 }
613 }
614
615 // In project-scoped mode, connected view should show the whole dungeon group,
616 // not only the currently reachable component. Unlinked rooms are drawn in the
617 // same canvas with floor badges so bad headers/stairs are visible and fixable
618 // instead of hiding the rest of the floor.
619 if (graph.dungeon_scope_active) {
620 const auto start_placement =
621 graph.room_positions[static_cast<size_t>(start_room_id)];
622 for (int room_id : scoped_room_ids) {
623 if (graph.room_mask[static_cast<size_t>(room_id)]) {
624 continue;
625 }
626 const std::pair<int, int> placement =
627 registry_placement_for(room_id).value_or(
628 FindConnectedTransportPlacement(
629 occupied_slots, start_placement.col, start_placement.row));
630 place_room(room_id, placement, false);
631 }
632 }
633
634 return graph;
635}
636
638 int center_room_id) {
639 if (center_room_id < 0 || center_room_id >= zelda3::kNumberOfRooms) {
640 connected_action_status_message_ = "No current room for connected fixes.";
642 return 0;
643 }
644
645 if (connected_graph_cache_start_room_id_ != center_room_id) {
648 }
649
650 int fixed_count = 0;
651 for (const auto& issue : connected_graph_cache_.staircase_issues) {
652 if (issue.kind != DungeonStaircaseIssueKind::UnusedHeader ||
653 issue.slot_index < 0 || issue.slot_index >= 4 ||
654 issue.header_room_id <= 0) {
655 continue;
656 }
657 zelda3::Room* room = EnsureRoomLoadedForConnectedView(issue.from_room_id);
658 if (!room || room->staircase_room(issue.slot_index) !=
659 static_cast<uint8_t>(issue.header_room_id)) {
660 continue;
661 }
662 room->SetStaircaseRoom(issue.slot_index, 0);
663 ++fixed_count;
664 }
665
666 if (fixed_count > 0) {
668 absl::StrFormat("Cleared %d stale staircase header slot%s.",
669 fixed_count, fixed_count == 1 ? "" : "s");
673 } else {
675 "No stale staircase header slots can be auto-cleared.";
677 }
678 return fixed_count;
679}
680
681} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:144
bool RoomHasNonExitDoorInDirection(int room_id, zelda3::DoorDirection dir)
int ApplyConnectedStaircaseIssueAutoFixes(int center_room_id)
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
ConnectedRoomGraphData connected_graph_cache_
ConnectedRoomGraphData BuildConnectedRoomGraph(int start_room_id)
const project::YazeProject * project_
zelda3::Room * TryEnsureRoom(int room_id)
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
uint8_t holewarp() const
Definition room.h:913
void SetStaircaseRoom(int index, uint8_t room)
Definition room.h:736
uint8_t staircase_room(int index) const
Definition room.h:895
const std::vector< Door > & GetDoors() const
Definition room.h:350
bool IsLoaded() const
Definition room.h:871
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:382
void SetRom(Rom *rom)
Definition room.h:961
void SetGameData(GameData *data)
Definition room.h:963
bool IsConnectedSlotOccupied(const std::map< std::pair< int, int >, int > &occupied_slots, int col, int row)
std::pair< int, int > FindConnectedDoorPlacement(const std::map< std::pair< int, int >, int > &occupied_slots, int source_col, int source_row, zelda3::DoorDirection dir)
std::tuple< int, int, DungeonConnectedLinkType, int, int16_t > MakeConnectedLinkKey(const DungeonConnectedRoomLink &link)
std::pair< int, int > FindConnectedTransportPlacement(const std::map< std::pair< int, int >, int > &occupied_slots, int source_col, int source_row)
const core::DungeonEntry * FindDungeonForRoom(const project::YazeProject *project, int room_id, size_t *dungeon_index=nullptr)
Editors are the view controllers for the application.
std::string FormatDungeonConnectedLinkDescription(const DungeonConnectedRoomLink &link)
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
DungeonConnectedRoomLinkDiagnostics CollectDungeonConnectedRoomLinkDiagnostics(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
std::vector< DungeonConnectedRoomLink > CollectDungeonConnectedRoomLinks(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ FancyDungeonExitLower
Fancy dungeon exit (lower layer)
@ FancyDungeonExit
Fancy dungeon exit.
@ ExitLower
Exit (lower layer)
@ BombableCaveExit
Bombable cave exit.
@ UnusedCaveExit
Unused cave exit (lower layer)
@ LitCaveExitLower
Lit cave exit (lower layer)
@ WaterfallDoor
Waterfall door.
@ ExitMarker
Exit marker.
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:518
constexpr int kNumberOfRooms
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
std::array< RoomPlacement, zelda3::kNumberOfRooms > room_positions
std::array< std::string, zelda3::kNumberOfRooms > room_floor_labels
std::vector< DungeonConnectedRoomLink > links
std::vector< DungeonStaircaseIssue > staircase_issues