Refactor DungeonEditor and introduce new components for enhanced dungeon editing

- Integrated new components: DungeonToolset, DungeonObjectInteraction, DungeonRenderer, DungeonRoomLoader, and DungeonUsageTracker to streamline dungeon editing functionalities.
- Updated DungeonEditor to utilize the new components for room loading, object interaction, and rendering, improving code organization and maintainability.
- Enhanced object selection and placement features, including drag-and-drop functionality and improved UI interactions.
- Removed legacy methods and refactored existing code to delegate responsibilities to the new components, ensuring a cleaner architecture.
- Added support for usage tracking of blocksets, spritesets, and palettes across dungeon rooms, providing insights for optimization.
This commit is contained in:
scawful
2025-09-25 19:56:39 -04:00
parent 8389989ab3
commit bfcf54e271
13 changed files with 1303 additions and 941 deletions

View File

@@ -48,44 +48,17 @@ void DungeonEditor::Initialize() {
absl::Status DungeonEditor::Load() {
auto dungeon_man_pal_group = rom()->palette_group().dungeon_main;
for (int i = 0; i < 0x100 + 40; i++) {
rooms_[i] = zelda3::LoadRoomFromRom(rom_, i);
auto room_size = zelda3::CalculateRoomSize(rom_, i);
room_size_pointers_.push_back(room_size.room_size_pointer);
room_sizes_.push_back(room_size.room_size);
if (room_size.room_size_pointer != 0x0A8000) {
room_size_addresses_[i] = room_size.room_size_pointer;
}
rooms_[i].LoadObjects();
auto dungeon_palette_ptr = rom()->paletteset_ids[rooms_[i].palette][0];
auto palette_id = rom()->ReadWord(0xDEC4B + dungeon_palette_ptr);
if (palette_id.status() != absl::OkStatus()) {
continue;
}
int p_id = palette_id.value() / 180;
auto color = dungeon_man_pal_group[p_id][3];
room_palette_[rooms_[i].palette] = color.rgb();
}
LoadDungeonRoomSize();
// LoadRoomEntrances
for (int i = 0; i < 0x07; ++i) {
entrances_[i] = zelda3::RoomEntrance(rom(), i, true);
}
for (int i = 0; i < 0x85; ++i) {
entrances_[i + 0x07] = zelda3::RoomEntrance(rom(), i, false);
}
// Use room loader component for loading rooms
RETURN_IF_ERROR(room_loader_.LoadAllRooms(rooms_));
RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(entrances_));
// Load the palette group and palette for the dungeon
full_palette_ = dungeon_man_pal_group[current_palette_group_id_];
ASSIGN_OR_RETURN(current_palette_group_,
gfx::CreatePaletteGroupFromLargePalette(full_palette_));
CalculateUsageStats();
// Calculate usage statistics
usage_tracker_.CalculateUsageStats(rooms_);
// Initialize the new editor system
if (dungeon_editor_system_) {
@@ -120,9 +93,23 @@ absl::Status DungeonEditor::Load() {
[this](const zelda3::RoomObject& object) {
preview_object_ = object;
object_loaded_ = true;
placement_type_ =
kObject; // Automatically switch to object placement mode
toolset_.set_placement_type(DungeonToolset::kObject);
object_interaction_.SetPreviewObject(object, true);
});
// Set up component callbacks
object_interaction_.SetCurrentRoom(&rooms_, current_room_id_);
object_interaction_.SetObjectPlacedCallback([this](const zelda3::RoomObject& object) {
renderer_.ClearObjectCache();
});
object_interaction_.SetCacheInvalidationCallback([this]() {
renderer_.ClearObjectCache();
});
// Set up toolset callbacks
toolset_.SetUndoCallback([this]() { PRINT_IF_ERROR(Undo()); });
toolset_.SetRedoCallback([this]() { PRINT_IF_ERROR(Redo()); });
toolset_.SetPaletteToggleCallback([this]() { palette_showing_ = !palette_showing_; });
is_loaded_ = true;
return absl::OkStatus();
@@ -179,49 +166,10 @@ absl::Status DungeonEditor::RefreshGraphics() {
return absl::OkStatus();
}
void DungeonEditor::LoadDungeonRoomSize() {
std::map<int, std::vector<int>> rooms_by_bank;
for (const auto& room : room_size_addresses_) {
int bank = room.second >> 16;
rooms_by_bank[bank].push_back(room.second);
}
// Process and calculate room sizes within each bank
for (auto& bank_rooms : rooms_by_bank) {
// Sort the rooms within this bank
std::ranges::sort(bank_rooms.second);
for (size_t i = 0; i < bank_rooms.second.size(); ++i) {
int room_ptr = bank_rooms.second[i];
// Identify the room ID for the current room pointer
int room_id =
std::ranges::find_if(room_size_addresses_, [room_ptr](
const auto& entry) {
return entry.second == room_ptr;
})->first;
if (room_ptr != 0x0A8000) {
if (i < bank_rooms.second.size() - 1) {
// Calculate size as difference between current room and next room
// in the same bank
room_sizes_[room_id] = bank_rooms.second[i + 1] - room_ptr;
} else {
// Calculate size for the last room in this bank
int bank_end_address = (bank_rooms.first << 16) | 0xFFFF;
room_sizes_[room_id] = bank_end_address - room_ptr + 1;
}
total_room_size_ += room_sizes_[room_id];
} else {
// Room with address 0x0A8000
room_sizes_[room_id] = 0x00;
}
}
}
}
// LoadDungeonRoomSize moved to DungeonRoomLoader component
absl::Status DungeonEditor::UpdateDungeonRoomView() {
DrawToolset();
toolset_.Draw();
if (palette_showing_) {
ImGui::Begin("Palette Editor", &palette_showing_, 0);
@@ -285,150 +233,7 @@ void DungeonEditor::OnRoomSelected(int room_id) {
room_selector_.set_active_rooms(active_rooms_);
}
void DungeonEditor::DrawToolset() {
if (BeginTable("DWToolset", 16, ImGuiTableFlags_SizingFixedFit,
ImVec2(0, 0))) {
static std::array<const char*, 16> tool_names = {
"Undo", "Redo", "Separator", "Any", "BG1", "BG2",
"BG3", "Separator", "Object", "Sprite", "Item", "Entrance",
"Door", "Chest", "Block", "Palette"};
std::ranges::for_each(tool_names,
[](const char* name) { TableSetupColumn(name); });
TableNextColumn();
if (Button(ICON_MD_UNDO)) {
PRINT_IF_ERROR(Undo());
}
TableNextColumn();
if (Button(ICON_MD_REDO)) {
PRINT_IF_ERROR(Redo());
}
TableNextColumn();
Text(ICON_MD_MORE_VERT);
TableNextColumn();
if (RadioButton("All", background_type_ == kBackgroundAny)) {
background_type_ = kBackgroundAny;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show all background layers");
}
TableNextColumn();
if (RadioButton("BG1", background_type_ == kBackground1)) {
background_type_ = kBackground1;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 1 only");
}
TableNextColumn();
if (RadioButton("BG2", background_type_ == kBackground2)) {
background_type_ = kBackground2;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 2 only");
}
TableNextColumn();
if (RadioButton("BG3", background_type_ == kBackground3)) {
background_type_ = kBackground3;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 3 only");
}
TableNextColumn();
Text(ICON_MD_MORE_VERT);
TableNextColumn();
if (RadioButton(ICON_MD_SQUARE, placement_type_ == kObject)) {
placement_type_ = kObject;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Objects");
}
TableNextColumn();
if (RadioButton(ICON_MD_PEST_CONTROL, placement_type_ == kSprite)) {
placement_type_ = kSprite;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Sprites");
}
TableNextColumn();
if (RadioButton(ICON_MD_GRASS, placement_type_ == kItem)) {
placement_type_ = kItem;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Items");
}
TableNextColumn();
if (RadioButton(ICON_MD_NAVIGATION, placement_type_ == kEntrance)) {
placement_type_ = kEntrance;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Entrances");
}
TableNextColumn();
if (RadioButton(ICON_MD_SENSOR_DOOR, placement_type_ == kDoor)) {
placement_type_ = kDoor;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Doors");
}
TableNextColumn();
if (RadioButton(ICON_MD_INVENTORY, placement_type_ == kChest)) {
placement_type_ = kChest;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Chests");
}
TableNextColumn();
if (RadioButton(ICON_MD_VIEW_MODULE, placement_type_ == kBlock)) {
placement_type_ = kBlock;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Blocks");
}
TableNextColumn();
if (Button(ICON_MD_PALETTE)) {
palette_showing_ = !palette_showing_;
}
ImGui::EndTable();
}
ImGui::Separator();
ImGui::Text(
"Instructions: Click to place objects, Ctrl+Click to select, drag to "
"move");
ImGui::SameLine();
// Usage statistics as a popup to save space
if (ImGui::Button("Usage Statistics")) {
ImGui::OpenPopup("UsageStatsPopup");
}
if (ImGui::BeginPopupModal("UsageStatsPopup", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
if (is_loaded_) {
DrawUsageStats();
}
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
// DrawToolset() method moved to DungeonToolset component
void DungeonEditor::DrawCanvasAndPropertiesPanel() {
if (ImGui::BeginTabBar("CanvasPropertiesTabBar")) {
@@ -466,12 +271,12 @@ void DungeonEditor::DrawCanvasAndPropertiesPanel() {
ImGui::Text("Chests: %zu", room.GetChests().size());
// Selection info
if (!selected_object_indices_.empty()) {
const auto& selected_indices = object_interaction_.GetSelectedObjectIndices();
if (!selected_indices.empty()) {
ImGui::Separator();
ImGui::Text("Selected Objects: %zu", selected_object_indices_.size());
ImGui::Text("Selected Objects: %zu", selected_indices.size());
if (ImGui::Button("Clear Selection")) {
selected_object_indices_.clear();
object_select_active_ = false;
object_interaction_.ClearSelection();
}
}
@@ -584,9 +389,9 @@ void DungeonEditor::DrawRoomPropertiesDebugPopup() {
room.LoadObjects();
}
ImGui::SameLine();
if (ImGui::Button("Clear Cache")) {
object_render_cache_.clear();
}
if (ImGui::Button("Clear Cache")) {
renderer_.ClearObjectCache();
}
ImGui::SameLine();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
@@ -702,11 +507,11 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
ImGui::Checkbox("Show Layout Objects", &show_layout_objects);
if (ImGui::Button("Clear Object Cache")) {
object_render_cache_.clear();
renderer_.ClearObjectCache();
}
ImGui::SameLine();
ImGui::Text("Cache: %zu objects", object_render_cache_.size());
ImGui::Text("Cache: %zu objects", renderer_.GetCacheSize());
// Object statistics and metadata
ImGui::Separator();
@@ -721,7 +526,7 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
// Palette information
ImGui::Text("Current Palette Group: %llu",
static_cast<unsigned long long>(current_palette_group_id_));
ImGui::Text("Palette Hash: %#016llx", last_palette_hash_);
ImGui::Text("Cache Size: %zu objects", renderer_.GetCacheSize());
// Object type breakdown
ImGui::Separator();
@@ -791,11 +596,11 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
canvas_.DrawBackground();
canvas_.DrawContextMenu();
// Handle object selection and placement
CheckForObjectSelection();
// Handle object selection and placement using component
object_interaction_.CheckForObjectSelection();
// Handle mouse input for drag and select functionality
HandleCanvasMouseInput();
// Handle mouse input for drag and select functionality
object_interaction_.HandleCanvasMouseInput();
// Update preview object position based on mouse cursor
if (object_loaded_ && preview_object_.id_ >= 0 && canvas_.IsMouseHovering()) {
@@ -805,8 +610,8 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
ImVec2 canvas_mouse_pos =
ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
auto [room_x, room_y] =
CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
object_interaction_.CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
preview_object_.x_ = room_x;
preview_object_.y_ = room_y;
}
@@ -823,621 +628,40 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
}
// Render background layers with proper positioning
RenderRoomBackgroundLayers(room_id);
renderer_.RenderRoomBackgroundLayers(room_id);
// Render room objects on top of background using the room's palette
if (current_palette_id_ < current_palette_group_.size()) {
auto room_palette = current_palette_group_[current_palette_id_];
for (const auto& object : rooms_[room_id].GetTileObjects()) {
RenderObjectInCanvas(object, room_palette);
renderer_.RenderObjectInCanvas(object, room_palette);
}
}
}
// Draw selection box and drag preview
DrawSelectBox();
DrawDragPreview();
// Draw selection box and drag preview using component
object_interaction_.DrawSelectBox();
object_interaction_.DrawDragPreview();
canvas_.DrawGrid();
canvas_.DrawOverlay();
}
void DungeonEditor::RenderObjectInCanvas(const zelda3::RoomObject& object,
const gfx::SnesPalette& palette) {
// Validate ROM is loaded
if (!rom_ || !rom_->is_loaded()) {
return;
}
// Create a mutable copy of the object to ensure tiles are loaded
auto mutable_object = object;
mutable_object.set_rom(rom_);
mutable_object.EnsureTilesLoaded();
// Check if tiles were loaded successfully
if (mutable_object.tiles().empty()) {
return; // Skip objects without tiles
}
// Calculate palette hash for caching
uint64_t palette_hash = 0;
for (size_t i = 0; i < palette.size() && i < 16; ++i) {
palette_hash ^= std::hash<uint16_t>{}(palette[i].snes()) + 0x9e3779b9 +
(palette_hash << 6) + (palette_hash >> 2);
}
// Convert room coordinates to canvas coordinates using helper function
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
// Check if object is within canvas bounds (accounting for scrolling)
if (!IsWithinCanvasBounds(canvas_x, canvas_y, 32)) {
return; // Skip objects outside visible area
}
// Check cache first
for (auto& cached : object_render_cache_) {
if (cached.object_id == object.id_ && cached.object_x == object.x_ &&
cached.object_y == object.y_ && cached.object_size == object.size_ &&
cached.palette_hash == palette_hash && cached.is_valid) {
// Use cached bitmap - canvas handles scrolling internally
canvas_.DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255);
return;
}
}
// Render the object to a bitmap
auto render_result = object_renderer_.RenderObject(mutable_object, palette);
if (!render_result.ok()) {
return; // Skip if rendering failed
}
auto object_bitmap = std::move(render_result.value());
// Set the palette for the bitmap
object_bitmap.SetPalette(palette);
// Render the bitmap to a texture so it can be drawn
core::Renderer::Get().RenderBitmap(&object_bitmap);
// Draw the object bitmap to the canvas
// Canvas will handle scrolling and coordinate transformation
canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255);
// Cache the rendered bitmap (create a copy for caching)
ObjectRenderCache cache_entry;
cache_entry.object_id = object.id_;
cache_entry.object_x = object.x_;
cache_entry.object_y = object.y_;
cache_entry.object_size = object.size_;
cache_entry.palette_hash = palette_hash;
cache_entry.rendered_bitmap = object_bitmap; // Copy instead of move
cache_entry.is_valid = true;
// Add to cache (limit cache size)
if (object_render_cache_.size() >= 100) {
object_render_cache_.erase(object_render_cache_.begin());
}
object_render_cache_.push_back(std::move(cache_entry));
}
void DungeonEditor::DisplayObjectInfo(const zelda3::RoomObject& object,
int canvas_x, int canvas_y) {
// Display object information as text overlay
std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_,
object.x_, object.y_, object.size_);
// Draw text at the object position
canvas_.DrawText(info_text, canvas_x, canvas_y - 12);
}
void DungeonEditor::RenderLayoutObjects(const zelda3::RoomLayout& layout,
const gfx::SnesPalette& palette) {
// Render layout objects (walls, floors, etc.) as simple colored rectangles
// This provides a visual representation of the room's structure
for (const auto& layout_obj : layout.GetObjects()) {
// Convert room coordinates to canvas coordinates using helper function
auto [canvas_x, canvas_y] =
RoomToCanvasCoordinates(layout_obj.x(), layout_obj.y());
// Check if layout object is within canvas bounds
if (!IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
continue; // Skip objects outside visible area
}
// Choose color based on object type
gfx::SnesColor color;
switch (layout_obj.type()) {
case zelda3::RoomLayoutObject::Type::kWall:
color = gfx::SnesColor(0x7FFF); // Gray
break;
case zelda3::RoomLayoutObject::Type::kFloor:
color = gfx::SnesColor(0x4210); // Dark brown
break;
case zelda3::RoomLayoutObject::Type::kCeiling:
color = gfx::SnesColor(0x739C); // Light gray
break;
case zelda3::RoomLayoutObject::Type::kPit:
color = gfx::SnesColor(0x0000); // Black
break;
case zelda3::RoomLayoutObject::Type::kWater:
color = gfx::SnesColor(0x001F); // Blue
break;
case zelda3::RoomLayoutObject::Type::kStairs:
color = gfx::SnesColor(0x7E0F); // Yellow
break;
case zelda3::RoomLayoutObject::Type::kDoor:
color = gfx::SnesColor(0xF800); // Red
break;
default:
color = gfx::SnesColor(0x7C1F); // Magenta for unknown
break;
}
// Draw a simple rectangle for the layout object
// This is a placeholder - in a real implementation, you'd render the actual
// tile
canvas_.DrawRect(canvas_x, canvas_y, 16, 16,
gui::ConvertSnesColorToImVec4(color));
}
}
// Coordinate conversion helper functions
std::pair<int, int> DungeonEditor::RoomToCanvasCoordinates(int room_x,
int room_y) const {
// Convert room coordinates (16x16 tile units) to canvas coordinates (pixels)
// Note: The canvas applies global_scale_ and scrolling internally, so we
// return the base coordinates and let the canvas handle the transformation
return {room_x * 16, room_y * 16};
}
std::pair<int, int> DungeonEditor::CanvasToRoomCoordinates(int canvas_x,
int canvas_y) const {
// Convert canvas coordinates (pixels) to room coordinates (16x16 tile units)
// Note: This assumes the canvas coordinates are already adjusted for scaling
// and scrolling
return {canvas_x / 16, canvas_y / 16};
}
bool DungeonEditor::IsWithinCanvasBounds(int canvas_x, int canvas_y,
int margin) const {
// Check if coordinates are within canvas bounds with optional margin
// Get the actual canvas size (accounting for scaling)
auto canvas_size = canvas_.canvas_size();
auto global_scale = canvas_.global_scale();
int scaled_width = static_cast<int>(canvas_size.x * global_scale);
int scaled_height = static_cast<int>(canvas_size.y * global_scale);
return (canvas_x >= -margin && canvas_y >= -margin &&
canvas_x <= scaled_width + margin &&
canvas_y <= scaled_height + margin);
}
// Room graphics management methods
// Legacy method implementations that delegate to components
absl::Status DungeonEditor::LoadAndRenderRoomGraphics(int room_id) {
if (room_id < 0 || room_id >= rooms_.size()) {
return absl::InvalidArgumentError("Invalid room ID");
}
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
auto& room = rooms_[room_id];
// Load room graphics with proper blockset
(void)room.LoadRoomGraphics(room.blockset);
// Load the room's palette with bounds checking
if (room.palette < rom()->paletteset_ids.size() &&
!rom()->paletteset_ids[room.palette].empty()) {
auto dungeon_palette_ptr = rom()->paletteset_ids[room.palette][0];
auto palette_id = rom()->ReadWord(0xDEC4B + dungeon_palette_ptr);
if (palette_id.ok()) {
current_palette_group_id_ = palette_id.value() / 180;
if (current_palette_group_id_ <
rom()->palette_group().dungeon_main.size()) {
full_palette_ =
rom()->palette_group().dungeon_main[current_palette_group_id_];
ASSIGN_OR_RETURN(
current_palette_group_,
gfx::CreatePaletteGroupFromLargePalette(full_palette_));
}
}
}
// Render the room graphics to the graphics arena
(void)room.RenderRoomGraphics();
return absl::OkStatus();
return room_loader_.LoadAndRenderRoomGraphics(room_id, rooms_[room_id]);
}
absl::Status DungeonEditor::ReloadAllRoomGraphics() {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Clear existing graphics cache
object_render_cache_.clear();
// Reload graphics for all rooms
for (int i = 0; i < rooms_.size(); i++) {
auto status = LoadAndRenderRoomGraphics(i);
if (!status.ok()) {
// Log error but continue with other rooms
continue;
}
}
return room_loader_.ReloadAllRoomGraphics(rooms_);
}
absl::Status DungeonEditor::UpdateRoomBackgroundLayers(int room_id) {
// This method is deprecated - rendering is handled by DungeonRenderer component
return absl::OkStatus();
}
void DungeonEditor::RenderRoomBackgroundLayers(int room_id) {
if (room_id < 0 || room_id >= rooms_.size()) {
return;
}
auto& room = rooms_[room_id];
// Get canvas dimensions
int canvas_width = canvas_.width();
int canvas_height = canvas_.height();
// BG1 (background layer 1) - room graphics
auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap();
if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 &&
bg1_bitmap.height() > 0) {
// Scale the background to fit the canvas
float scale_x = static_cast<float>(canvas_width) / bg1_bitmap.width();
float scale_y = static_cast<float>(canvas_height) / bg1_bitmap.height();
float scale = std::min(scale_x, scale_y);
int scaled_width = static_cast<int>(bg1_bitmap.width() * scale);
int scaled_height = static_cast<int>(bg1_bitmap.height() * scale);
int offset_x = (canvas_width - scaled_width) / 2;
int offset_y = (canvas_height - scaled_height) / 2;
canvas_.DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255);
}
// BG2 (background layer 2) - sprite graphics (overlay)
auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap();
if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 &&
bg2_bitmap.height() > 0) {
// Scale the background to fit the canvas
float scale_x = static_cast<float>(canvas_width) / bg2_bitmap.width();
float scale_y = static_cast<float>(canvas_height) / bg2_bitmap.height();
float scale = std::min(scale_x, scale_y);
int scaled_width = static_cast<int>(bg2_bitmap.width() * scale);
int scaled_height = static_cast<int>(bg2_bitmap.height() * scale);
int offset_x = (canvas_width - scaled_width) / 2;
int offset_y = (canvas_height - scaled_height) / 2;
canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale,
200); // Semi-transparent overlay
}
}
void DungeonEditor::CalculateUsageStats() {
for (const auto& room : rooms_) {
if (blockset_usage_.find(room.blockset) == blockset_usage_.end()) {
blockset_usage_[room.blockset] = 1;
} else {
blockset_usage_[room.blockset] += 1;
}
if (spriteset_usage_.find(room.spriteset) == spriteset_usage_.end()) {
spriteset_usage_[room.spriteset] = 1;
} else {
spriteset_usage_[room.spriteset] += 1;
}
if (palette_usage_.find(room.palette) == palette_usage_.end()) {
palette_usage_[room.palette] = 1;
} else {
palette_usage_[room.palette] += 1;
}
}
}
void DungeonEditor::DrawUsageStats() {
if (ImGui::Button("Refresh")) {
selected_blockset_ = 0xFFFF;
selected_spriteset_ = 0xFFFF;
selected_palette_ = 0xFFFF;
spriteset_usage_.clear();
blockset_usage_.clear();
palette_usage_.clear();
CalculateUsageStats();
}
ImGui::Text("Usage Statistics");
ImGui::Separator();
ImGui::Text("Blocksets: %zu used", blockset_usage_.size());
ImGui::Text("Spritesets: %zu used", spriteset_usage_.size());
ImGui::Text("Palettes: %zu used", palette_usage_.size());
}
// Drag and select box functionality
void DungeonEditor::HandleCanvasMouseInput() {
const ImGuiIO& io = ImGui::GetIO();
// Check if mouse is over the canvas
if (!canvas_.IsMouseHovering()) {
return;
}
// Get mouse position relative to canvas
ImVec2 mouse_pos = io.MousePos;
ImVec2 canvas_pos = canvas_.zero_point();
ImVec2 canvas_size = canvas_.canvas_size();
// Convert to canvas coordinates
ImVec2 canvas_mouse_pos =
ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
// Handle mouse clicks
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
// Start selection box
is_selecting_ = true;
select_start_pos_ = canvas_mouse_pos;
select_current_pos_ = canvas_mouse_pos;
selected_objects_.clear();
} else {
// Start dragging or place object
if (object_loaded_) {
// Convert canvas coordinates to room coordinates
auto [room_x, room_y] =
CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
PlaceObjectAtPosition(room_x, room_y);
} else {
// Start dragging existing objects
is_dragging_ = true;
drag_start_pos_ = canvas_mouse_pos;
drag_current_pos_ = canvas_mouse_pos;
}
}
}
// Handle mouse drag
if (is_selecting_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
select_current_pos_ = canvas_mouse_pos;
UpdateSelectedObjects();
}
if (is_dragging_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
drag_current_pos_ = canvas_mouse_pos;
DrawDragPreview();
}
// Handle mouse release
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
if (is_selecting_) {
is_selecting_ = false;
UpdateSelectedObjects();
}
if (is_dragging_) {
is_dragging_ = false;
// TODO: Apply drag transformation to selected objects
}
}
}
void DungeonEditor::DrawSelectBox() {
if (!is_selecting_) return;
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_.zero_point();
// Calculate select box bounds
ImVec2 start = ImVec2(
canvas_pos.x + std::min(select_start_pos_.x, select_current_pos_.x),
canvas_pos.y + std::min(select_start_pos_.y, select_current_pos_.y));
ImVec2 end = ImVec2(
canvas_pos.x + std::max(select_start_pos_.x, select_current_pos_.x),
canvas_pos.y + std::max(select_start_pos_.y, select_current_pos_.y));
// Draw selection box
draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32));
}
void DungeonEditor::DrawDragPreview() {
if (!is_dragging_) return;
// Draw drag preview for selected objects
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_.zero_point();
ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x,
drag_current_pos_.y - drag_start_pos_.y);
// Draw preview of where objects would be moved
for (int obj_id : selected_objects_) {
// TODO: Draw preview of object at new position
// This would require getting the object's current position and drawing it
// offset by drag_delta
}
}
void DungeonEditor::UpdateSelectedObjects() {
if (!is_selecting_) return;
selected_objects_.clear();
// Get current room
int current_room = current_room_id_;
if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) {
current_room = active_rooms_[current_active_room_tab_];
}
if (current_room < 0 || current_room >= rooms_.size()) return;
auto& room = rooms_[current_room];
// Check each object in the room
for (const auto& object : room.GetTileObjects()) {
if (IsObjectInSelectBox(object)) {
selected_objects_.push_back(object.id_);
}
}
}
bool DungeonEditor::IsObjectInSelectBox(
const zelda3::RoomObject& object) const {
if (!is_selecting_) return false;
// Convert object position to canvas coordinates
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
// Calculate select box bounds
float min_x = std::min(select_start_pos_.x, select_current_pos_.x);
float max_x = std::max(select_start_pos_.x, select_current_pos_.x);
float min_y = std::min(select_start_pos_.y, select_current_pos_.y);
float max_y = std::max(select_start_pos_.y, select_current_pos_.y);
// Check if object is within select box
return (canvas_x >= min_x && canvas_x <= max_x && canvas_y >= min_y &&
canvas_y <= max_y);
}
void DungeonEditor::PlaceObjectAtPosition(int room_x, int room_y) {
if (!object_loaded_ || preview_object_.id_ < 0) return;
// Get current room
int current_room = current_room_id_;
if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) {
current_room = active_rooms_[current_active_room_tab_];
}
if (current_room < 0 || current_room >= rooms_.size()) return;
// Create new object at the specified position
auto new_object = preview_object_;
new_object.x_ = room_x;
new_object.y_ = room_y;
new_object.set_rom(rom_);
new_object.EnsureTilesLoaded();
// Add object to room
auto& room = rooms_[current_room];
room.AddTileObject(new_object);
// Clear the object render cache to force redraw
object_render_cache_.clear();
// Update the object selector with the new object count
object_selector_.set_current_room_id(current_room);
}
void DungeonEditor::CheckForObjectSelection() {
// Draw object selection rectangle similar to OverworldEditor
DrawObjectSelectRect();
// Handle object selection when rectangle is active
if (object_select_active_) {
SelectObjectsInRect();
}
}
void DungeonEditor::DrawObjectSelectRect() {
if (!canvas_.IsMouseHovering()) return;
const ImGuiIO& io = ImGui::GetIO();
const ImVec2 canvas_pos = canvas_.zero_point();
const ImVec2 mouse_pos =
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
static bool dragging = false;
static ImVec2 drag_start_pos;
// Right click to start object selection
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !object_loaded_) {
drag_start_pos = mouse_pos;
object_select_start_ = mouse_pos;
selected_object_indices_.clear();
object_select_active_ = false;
dragging = false;
}
// Right drag to create selection rectangle
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && !object_loaded_) {
object_select_end_ = mouse_pos;
dragging = true;
// Draw selection rectangle
ImVec2 start =
ImVec2(canvas_pos.x + std::min(drag_start_pos.x, mouse_pos.x),
canvas_pos.y + std::min(drag_start_pos.y, mouse_pos.y));
ImVec2 end = ImVec2(canvas_pos.x + std::max(drag_start_pos.x, mouse_pos.x),
canvas_pos.y + std::max(drag_start_pos.y, mouse_pos.y));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32));
}
// Complete selection on mouse release
if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
dragging = false;
object_select_active_ = true;
SelectObjectsInRect();
}
}
void DungeonEditor::SelectObjectsInRect() {
int current_room = current_room_id_;
if (!active_rooms_.empty() && current_active_room_tab_ < active_rooms_.Size) {
current_room = active_rooms_[current_active_room_tab_];
}
if (current_room < 0 || current_room >= rooms_.size()) return;
auto& room = rooms_[current_room];
selected_object_indices_.clear();
// Calculate selection bounds in room coordinates
auto [start_room_x, start_room_y] = CanvasToRoomCoordinates(
static_cast<int>(std::min(object_select_start_.x, object_select_end_.x)),
static_cast<int>(std::min(object_select_start_.y, object_select_end_.y)));
auto [end_room_x, end_room_y] = CanvasToRoomCoordinates(
static_cast<int>(std::max(object_select_start_.x, object_select_end_.x)),
static_cast<int>(std::max(object_select_start_.y, object_select_end_.y)));
// Find objects within selection rectangle
const auto& objects = room.GetTileObjects();
for (size_t i = 0; i < objects.size(); ++i) {
const auto& object = objects[i];
if (object.x_ >= start_room_x && object.x_ <= end_room_x &&
object.y_ >= start_room_y && object.y_ <= end_room_y) {
selected_object_indices_.push_back(i);
}
}
// Highlight selected objects
if (!selected_object_indices_.empty()) {
for (size_t index : selected_object_indices_) {
if (index < objects.size()) {
const auto& object = objects[index];
auto [canvas_x, canvas_y] =
RoomToCanvasCoordinates(object.x_, object.y_);
// Draw selection highlight
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_.zero_point();
ImVec2 obj_start(canvas_pos.x + canvas_x - 2,
canvas_pos.y + canvas_y - 2);
ImVec2 obj_end(canvas_pos.x + canvas_x + 18,
canvas_pos.y + canvas_y + 18);
draw_list->AddRect(obj_start, obj_end, IM_COL32(0, 255, 255, 255), 0.0f,
0, 2.0f);
}
}
}
}
} // namespace yaze::editor

View File

@@ -17,6 +17,11 @@
#include "dungeon_room_selector.h"
#include "dungeon_canvas_viewer.h"
#include "dungeon_object_selector.h"
#include "dungeon_toolset.h"
#include "dungeon_object_interaction.h"
#include "dungeon_renderer.h"
#include "dungeon_room_loader.h"
#include "dungeon_usage_tracker.h"
namespace yaze {
namespace editor {
@@ -46,7 +51,8 @@ class DungeonEditor : public Editor {
public:
explicit DungeonEditor(Rom* rom = nullptr)
: rom_(rom), object_renderer_(rom), preview_object_(0, 0, 0, 0, 0),
room_selector_(rom), canvas_viewer_(rom), object_selector_(rom) {
room_selector_(rom), canvas_viewer_(rom), object_selector_(rom),
object_interaction_(&canvas_), renderer_(&canvas_, rom), room_loader_(rom) {
type_ = EditorType::kDungeon;
// Initialize the new dungeon editor system
if (rom) {
@@ -84,8 +90,6 @@ class DungeonEditor : public Editor {
absl::Status UpdateDungeonRoomView();
void DrawToolset();
void DrawDungeonTabView();
void DrawDungeonCanvas(int room_id);
@@ -100,113 +104,20 @@ class DungeonEditor : public Editor {
void DrawTileSelector();
void DrawObjectRenderer();
// Object rendering methods
void RenderObjectInCanvas(const zelda3::RoomObject& object,
const gfx::SnesPalette& palette);
void DisplayObjectInfo(const zelda3::RoomObject& object, int canvas_x,
int canvas_y);
void RenderLayoutObjects(const zelda3::RoomLayout& layout,
const gfx::SnesPalette& palette);
// New editing mode interfaces
void DrawObjectEditor();
void DrawSpriteEditor();
void DrawItemEditor();
void DrawEntranceEditor();
void DrawDoorEditor();
void DrawChestEditor();
void DrawPropertiesEditor();
// Coordinate conversion helpers
std::pair<int, int> RoomToCanvasCoordinates(int room_x, int room_y) const;
std::pair<int, int> CanvasToRoomCoordinates(int canvas_x, int canvas_y) const;
bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin = 32) const;
// Drag and select box functionality
void HandleCanvasMouseInput();
void DrawSelectBox();
void DrawDragPreview();
void UpdateSelectedObjects();
bool IsObjectInSelectBox(const zelda3::RoomObject& object) const;
void PlaceObjectAtPosition(int room_x, int room_y);
// Object selection rectangle (like OverworldEditor)
void CheckForObjectSelection();
void DrawObjectSelectRect();
void SelectObjectsInRect();
// Room graphics management
// Legacy methods (delegated to components)
absl::Status LoadAndRenderRoomGraphics(int room_id);
absl::Status ReloadAllRoomGraphics();
absl::Status UpdateRoomBackgroundLayers(int room_id);
void RenderRoomBackgroundLayers(int room_id);
// Object rendering cache to avoid re-rendering the same objects
struct ObjectRenderCache {
int object_id;
int object_x, object_y, object_size;
uint64_t palette_hash;
gfx::Bitmap rendered_bitmap;
bool is_valid;
};
std::vector<ObjectRenderCache> object_render_cache_;
uint64_t last_palette_hash_ = 0;
// Object preview system
zelda3::RoomObject preview_object_;
gfx::SnesPalette preview_palette_;
void CalculateUsageStats();
void DrawUsageStats();
void DrawUsageGrid();
void RenderSetUsage(const absl::flat_hash_map<uint16_t, int>& usage_map,
uint16_t& selected_set, int spriteset_offset = 0x00);
enum BackgroundType {
kNoBackground,
kBackground1,
kBackground2,
kBackground3,
kBackgroundAny,
};
// Updated placement types to match new editor system
enum PlacementType {
kNoType,
kObject, // Object editing mode
kSprite, // Sprite editing mode
kItem, // Item placement mode
kEntrance, // Entrance/exit editing mode
kDoor, // Door configuration mode
kChest, // Chest management mode
kBlock // Legacy block mode
};
int background_type_ = kNoBackground;
int placement_type_ = kNoType;
bool is_loaded_ = false;
bool object_loaded_ = false;
bool palette_showing_ = false;
bool refresh_graphics_ = false;
// Drag and select box infrastructure
bool is_dragging_ = false;
bool is_selecting_ = false;
ImVec2 drag_start_pos_;
ImVec2 drag_current_pos_;
ImVec2 select_start_pos_;
ImVec2 select_current_pos_;
std::vector<int> selected_objects_;
int current_layer_ = 0; // 0 = BG1, 1 = BG2, 2 = Both
// Object selection rectangle (like OverworldEditor)
bool object_select_active_ = false;
ImVec2 object_select_start_;
ImVec2 object_select_end_;
std::vector<size_t> selected_object_indices_;
// New editor system integration
std::unique_ptr<zelda3::DungeonEditorSystem> dungeon_editor_system_;
std::shared_ptr<zelda3::DungeonObjectEditor> object_editor_;
@@ -243,26 +154,17 @@ class DungeonEditor : public Editor {
std::array<zelda3::RoomEntrance, 0x8C> entrances_ = {};
zelda3::ObjectRenderer object_renderer_;
// New UI components
// UI components
DungeonRoomSelector room_selector_;
DungeonCanvasViewer canvas_viewer_;
DungeonObjectSelector object_selector_;
absl::flat_hash_map<uint16_t, int> spriteset_usage_;
absl::flat_hash_map<uint16_t, int> blockset_usage_;
absl::flat_hash_map<uint16_t, int> palette_usage_;
std::vector<int64_t> room_size_pointers_;
std::vector<int64_t> room_sizes_;
uint16_t selected_blockset_ = 0xFFFF; // 0xFFFF indicates no selection
uint16_t selected_spriteset_ = 0xFFFF;
uint16_t selected_palette_ = 0xFFFF;
uint64_t total_room_size_ = 0;
std::unordered_map<int, int> room_size_addresses_;
std::unordered_map<int, ImVec4> room_palette_;
// Refactored components
DungeonToolset toolset_;
DungeonObjectInteraction object_interaction_;
DungeonRenderer renderer_;
DungeonRoomLoader room_loader_;
DungeonUsageTracker usage_tracker_;
absl::Status status_;

View File

@@ -0,0 +1,308 @@
#include "dungeon_object_interaction.h"
#include "app/gui/color.h"
#include "imgui/imgui.h"
namespace yaze::editor {
void DungeonObjectInteraction::HandleCanvasMouseInput() {
const ImGuiIO& io = ImGui::GetIO();
// Check if mouse is over the canvas
if (!canvas_->IsMouseHovering()) {
return;
}
// Get mouse position relative to canvas
ImVec2 mouse_pos = io.MousePos;
ImVec2 canvas_pos = canvas_->zero_point();
ImVec2 canvas_size = canvas_->canvas_size();
// Convert to canvas coordinates
ImVec2 canvas_mouse_pos =
ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
// Handle mouse clicks
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) ||
ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
// Start selection box
is_selecting_ = true;
select_start_pos_ = canvas_mouse_pos;
select_current_pos_ = canvas_mouse_pos;
selected_objects_.clear();
} else {
// Start dragging or place object
if (object_loaded_) {
// Convert canvas coordinates to room coordinates
auto [room_x, room_y] =
CanvasToRoomCoordinates(static_cast<int>(canvas_mouse_pos.x),
static_cast<int>(canvas_mouse_pos.y));
PlaceObjectAtPosition(room_x, room_y);
} else {
// Start dragging existing objects
is_dragging_ = true;
drag_start_pos_ = canvas_mouse_pos;
drag_current_pos_ = canvas_mouse_pos;
}
}
}
// Handle mouse drag
if (is_selecting_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
select_current_pos_ = canvas_mouse_pos;
UpdateSelectedObjects();
}
if (is_dragging_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
drag_current_pos_ = canvas_mouse_pos;
DrawDragPreview();
}
// Handle mouse release
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
if (is_selecting_) {
is_selecting_ = false;
UpdateSelectedObjects();
}
if (is_dragging_) {
is_dragging_ = false;
// TODO: Apply drag transformation to selected objects
}
}
}
void DungeonObjectInteraction::CheckForObjectSelection() {
// Draw object selection rectangle similar to OverworldEditor
DrawObjectSelectRect();
// Handle object selection when rectangle is active
if (object_select_active_) {
SelectObjectsInRect();
}
}
void DungeonObjectInteraction::DrawObjectSelectRect() {
if (!canvas_->IsMouseHovering()) return;
const ImGuiIO& io = ImGui::GetIO();
const ImVec2 canvas_pos = canvas_->zero_point();
const ImVec2 mouse_pos =
ImVec2(io.MousePos.x - canvas_pos.x, io.MousePos.y - canvas_pos.y);
static bool dragging = false;
static ImVec2 drag_start_pos;
// Right click to start object selection
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !object_loaded_) {
drag_start_pos = mouse_pos;
object_select_start_ = mouse_pos;
selected_object_indices_.clear();
object_select_active_ = false;
dragging = false;
}
// Right drag to create selection rectangle
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && !object_loaded_) {
object_select_end_ = mouse_pos;
dragging = true;
// Draw selection rectangle
ImVec2 start =
ImVec2(canvas_pos.x + std::min(drag_start_pos.x, mouse_pos.x),
canvas_pos.y + std::min(drag_start_pos.y, mouse_pos.y));
ImVec2 end = ImVec2(canvas_pos.x + std::max(drag_start_pos.x, mouse_pos.x),
canvas_pos.y + std::max(drag_start_pos.y, mouse_pos.y));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32));
}
// Complete selection on mouse release
if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
dragging = false;
object_select_active_ = true;
SelectObjectsInRect();
}
}
void DungeonObjectInteraction::SelectObjectsInRect() {
if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296) return;
auto& room = (*rooms_)[current_room_id_];
selected_object_indices_.clear();
// Calculate selection bounds in room coordinates
auto [start_room_x, start_room_y] = CanvasToRoomCoordinates(
static_cast<int>(std::min(object_select_start_.x, object_select_end_.x)),
static_cast<int>(std::min(object_select_start_.y, object_select_end_.y)));
auto [end_room_x, end_room_y] = CanvasToRoomCoordinates(
static_cast<int>(std::max(object_select_start_.x, object_select_end_.x)),
static_cast<int>(std::max(object_select_start_.y, object_select_end_.y)));
// Find objects within selection rectangle
const auto& objects = room.GetTileObjects();
for (size_t i = 0; i < objects.size(); ++i) {
const auto& object = objects[i];
if (object.x_ >= start_room_x && object.x_ <= end_room_x &&
object.y_ >= start_room_y && object.y_ <= end_room_y) {
selected_object_indices_.push_back(i);
}
}
// Highlight selected objects
if (!selected_object_indices_.empty()) {
for (size_t index : selected_object_indices_) {
if (index < objects.size()) {
const auto& object = objects[index];
auto [canvas_x, canvas_y] =
RoomToCanvasCoordinates(object.x_, object.y_);
// Draw selection highlight
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_->zero_point();
ImVec2 obj_start(canvas_pos.x + canvas_x - 2,
canvas_pos.y + canvas_y - 2);
ImVec2 obj_end(canvas_pos.x + canvas_x + 18,
canvas_pos.y + canvas_y + 18);
draw_list->AddRect(obj_start, obj_end, IM_COL32(0, 255, 255, 255), 0.0f,
0, 2.0f);
}
}
}
}
void DungeonObjectInteraction::PlaceObjectAtPosition(int room_x, int room_y) {
if (!object_loaded_ || preview_object_.id_ < 0 || !rooms_) return;
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
// Create new object at the specified position
auto new_object = preview_object_;
new_object.x_ = room_x;
new_object.y_ = room_y;
// Add object to room
auto& room = (*rooms_)[current_room_id_];
room.AddTileObject(new_object);
// Notify callback if set
if (object_placed_callback_) {
object_placed_callback_(new_object);
}
// Trigger cache invalidation
if (cache_invalidation_callback_) {
cache_invalidation_callback_();
}
}
void DungeonObjectInteraction::DrawSelectBox() {
if (!is_selecting_) return;
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_->zero_point();
// Calculate select box bounds
ImVec2 start = ImVec2(
canvas_pos.x + std::min(select_start_pos_.x, select_current_pos_.x),
canvas_pos.y + std::min(select_start_pos_.y, select_current_pos_.y));
ImVec2 end = ImVec2(
canvas_pos.x + std::max(select_start_pos_.x, select_current_pos_.x),
canvas_pos.y + std::max(select_start_pos_.y, select_current_pos_.y));
// Draw selection box
draw_list->AddRect(start, end, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
draw_list->AddRectFilled(start, end, IM_COL32(255, 255, 0, 32));
}
void DungeonObjectInteraction::DrawDragPreview() {
if (!is_dragging_) return;
// Draw drag preview for selected objects
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = canvas_->zero_point();
ImVec2 drag_delta = ImVec2(drag_current_pos_.x - drag_start_pos_.x,
drag_current_pos_.y - drag_start_pos_.y);
// Draw preview of where objects would be moved
for (int obj_id : selected_objects_) {
// TODO: Draw preview of object at new position
// This would require getting the object's current position and drawing it
// offset by drag_delta
}
}
void DungeonObjectInteraction::UpdateSelectedObjects() {
if (!is_selecting_ || !rooms_) return;
selected_objects_.clear();
if (current_room_id_ < 0 || current_room_id_ >= 296) return;
auto& room = (*rooms_)[current_room_id_];
// Check each object in the room
for (const auto& object : room.GetTileObjects()) {
if (IsObjectInSelectBox(object)) {
selected_objects_.push_back(object.id_);
}
}
}
bool DungeonObjectInteraction::IsObjectInSelectBox(
const zelda3::RoomObject& object) const {
if (!is_selecting_) return false;
// Convert object position to canvas coordinates
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
// Calculate select box bounds
float min_x = std::min(select_start_pos_.x, select_current_pos_.x);
float max_x = std::max(select_start_pos_.x, select_current_pos_.x);
float min_y = std::min(select_start_pos_.y, select_current_pos_.y);
float max_y = std::max(select_start_pos_.y, select_current_pos_.y);
// Check if object is within select box
return (canvas_x >= min_x && canvas_x <= max_x && canvas_y >= min_y &&
canvas_y <= max_y);
}
std::pair<int, int> DungeonObjectInteraction::RoomToCanvasCoordinates(int room_x, int room_y) const {
return {room_x * 16, room_y * 16};
}
std::pair<int, int> DungeonObjectInteraction::CanvasToRoomCoordinates(int canvas_x, int canvas_y) const {
return {canvas_x / 16, canvas_y / 16};
}
bool DungeonObjectInteraction::IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin) const {
auto canvas_size = canvas_->canvas_size();
auto global_scale = canvas_->global_scale();
int scaled_width = static_cast<int>(canvas_size.x * global_scale);
int scaled_height = static_cast<int>(canvas_size.y * global_scale);
return (canvas_x >= -margin && canvas_y >= -margin &&
canvas_x <= scaled_width + margin &&
canvas_y <= scaled_height + margin);
}
void DungeonObjectInteraction::SetCurrentRoom(std::array<zelda3::Room, 0x128>* rooms, int room_id) {
rooms_ = rooms;
current_room_id_ = room_id;
}
void DungeonObjectInteraction::SetPreviewObject(const zelda3::RoomObject& object, bool loaded) {
preview_object_ = object;
object_loaded_ = loaded;
}
void DungeonObjectInteraction::ClearSelection() {
selected_object_indices_.clear();
object_select_active_ = false;
is_selecting_ = false;
is_dragging_ = false;
}
} // namespace yaze::editor

View File

@@ -0,0 +1,94 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H
#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H
#include <vector>
#include <functional>
#include "imgui/imgui.h"
#include "app/gui/canvas.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
namespace yaze {
namespace editor {
/**
* @brief Handles object selection, placement, and interaction within the dungeon canvas
*
* This component manages mouse interactions for object selection (similar to OverworldEditor),
* object placement, drag operations, and multi-object selection.
*/
class DungeonObjectInteraction {
public:
explicit DungeonObjectInteraction(gui::Canvas* canvas) : canvas_(canvas) {}
// Main interaction handling
void HandleCanvasMouseInput();
void CheckForObjectSelection();
void PlaceObjectAtPosition(int room_x, int room_y);
// Selection rectangle (like OverworldEditor)
void DrawObjectSelectRect();
void SelectObjectsInRect();
// Drag and select box functionality
void DrawSelectBox();
void DrawDragPreview();
void UpdateSelectedObjects();
bool IsObjectInSelectBox(const zelda3::RoomObject& object) const;
// Coordinate conversion
std::pair<int, int> RoomToCanvasCoordinates(int room_x, int room_y) const;
std::pair<int, int> CanvasToRoomCoordinates(int canvas_x, int canvas_y) const;
bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin = 32) const;
// State management
void SetCurrentRoom(std::array<zelda3::Room, 0x128>* rooms, int room_id);
void SetPreviewObject(const zelda3::RoomObject& object, bool loaded);
// Selection state
const std::vector<size_t>& GetSelectedObjectIndices() const { return selected_object_indices_; }
bool IsObjectSelectActive() const { return object_select_active_; }
void ClearSelection();
// Callbacks
void SetObjectPlacedCallback(std::function<void(const zelda3::RoomObject&)> callback) {
object_placed_callback_ = callback;
}
void SetCacheInvalidationCallback(std::function<void()> callback) {
cache_invalidation_callback_ = callback;
}
private:
gui::Canvas* canvas_;
std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
int current_room_id_ = 0;
// Preview object state
zelda3::RoomObject preview_object_{0, 0, 0, 0, 0};
bool object_loaded_ = false;
// Drag and select infrastructure
bool is_dragging_ = false;
bool is_selecting_ = false;
ImVec2 drag_start_pos_;
ImVec2 drag_current_pos_;
ImVec2 select_start_pos_;
ImVec2 select_current_pos_;
std::vector<int> selected_objects_;
// Object selection rectangle (like OverworldEditor)
bool object_select_active_ = false;
ImVec2 object_select_start_;
ImVec2 object_select_end_;
std::vector<size_t> selected_object_indices_;
// Callbacks
std::function<void(const zelda3::RoomObject&)> object_placed_callback_;
std::function<void()> cache_invalidation_callback_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_OBJECT_INTERACTION_H

View File

@@ -0,0 +1,206 @@
#include "dungeon_renderer.h"
#include "absl/strings/str_format.h"
#include "app/core/window.h"
#include "app/gfx/arena.h"
#include "app/gui/color.h"
namespace yaze::editor {
using core::Renderer;
void DungeonRenderer::RenderObjectInCanvas(const zelda3::RoomObject& object,
const gfx::SnesPalette& palette) {
// Validate ROM is loaded
if (!rom_ || !rom_->is_loaded()) {
return;
}
// Create a mutable copy of the object to ensure tiles are loaded
auto mutable_object = object;
mutable_object.set_rom(rom_);
mutable_object.EnsureTilesLoaded();
// Check if tiles were loaded successfully
if (mutable_object.tiles().empty()) {
return; // Skip objects without tiles
}
// Calculate palette hash for caching
uint64_t palette_hash = 0;
for (size_t i = 0; i < palette.size() && i < 16; ++i) {
palette_hash ^= std::hash<uint16_t>{}(palette[i].snes()) + 0x9e3779b9 +
(palette_hash << 6) + (palette_hash >> 2);
}
// Convert room coordinates to canvas coordinates
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_);
// Check if object is within canvas bounds
if (!IsWithinCanvasBounds(canvas_x, canvas_y, 32)) {
return; // Skip objects outside visible area
}
// Check cache first
for (auto& cached : object_render_cache_) {
if (cached.object_id == object.id_ && cached.object_x == object.x_ &&
cached.object_y == object.y_ && cached.object_size == object.size_ &&
cached.palette_hash == palette_hash && cached.is_valid) {
canvas_->DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255);
return;
}
}
// Render the object to a bitmap
auto render_result = object_renderer_.RenderObject(mutable_object, palette);
if (!render_result.ok()) {
return; // Skip if rendering failed
}
auto object_bitmap = std::move(render_result.value());
object_bitmap.SetPalette(palette);
core::Renderer::Get().RenderBitmap(&object_bitmap);
// Draw the object bitmap to the canvas
canvas_->DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255);
// Cache the rendered bitmap
ObjectRenderCache cache_entry;
cache_entry.object_id = object.id_;
cache_entry.object_x = object.x_;
cache_entry.object_y = object.y_;
cache_entry.object_size = object.size_;
cache_entry.palette_hash = palette_hash;
cache_entry.rendered_bitmap = object_bitmap;
cache_entry.is_valid = true;
// Add to cache (limit cache size)
if (object_render_cache_.size() >= 100) {
object_render_cache_.erase(object_render_cache_.begin());
}
object_render_cache_.push_back(std::move(cache_entry));
}
void DungeonRenderer::DisplayObjectInfo(const zelda3::RoomObject& object,
int canvas_x, int canvas_y) {
std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_,
object.x_, object.y_, object.size_);
canvas_->DrawText(info_text, canvas_x, canvas_y - 12);
}
void DungeonRenderer::RenderLayoutObjects(const zelda3::RoomLayout& layout,
const gfx::SnesPalette& palette) {
for (const auto& layout_obj : layout.GetObjects()) {
auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(layout_obj.x(), layout_obj.y());
if (!IsWithinCanvasBounds(canvas_x, canvas_y, 16)) {
continue;
}
// Choose color based on object type
gfx::SnesColor color;
switch (layout_obj.type()) {
case zelda3::RoomLayoutObject::Type::kWall:
color = gfx::SnesColor(0x7FFF); // Gray
break;
case zelda3::RoomLayoutObject::Type::kFloor:
color = gfx::SnesColor(0x4210); // Dark brown
break;
case zelda3::RoomLayoutObject::Type::kCeiling:
color = gfx::SnesColor(0x739C); // Light gray
break;
case zelda3::RoomLayoutObject::Type::kPit:
color = gfx::SnesColor(0x0000); // Black
break;
case zelda3::RoomLayoutObject::Type::kWater:
color = gfx::SnesColor(0x001F); // Blue
break;
case zelda3::RoomLayoutObject::Type::kStairs:
color = gfx::SnesColor(0x7E0F); // Yellow
break;
case zelda3::RoomLayoutObject::Type::kDoor:
color = gfx::SnesColor(0xF800); // Red
break;
default:
color = gfx::SnesColor(0x7C1F); // Magenta for unknown
break;
}
canvas_->DrawRect(canvas_x, canvas_y, 16, 16,
gui::ConvertSnesColorToImVec4(color));
}
}
void DungeonRenderer::RenderRoomBackgroundLayers(int room_id) {
// Get canvas dimensions to limit rendering
int canvas_width = canvas_->width();
int canvas_height = canvas_->height();
// Validate canvas dimensions
if (canvas_width <= 0 || canvas_height <= 0) {
return;
}
// BG1 (background layer 1) - main room graphics
auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap();
if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 &&
bg1_bitmap.height() > 0) {
float scale_x = static_cast<float>(canvas_width) / bg1_bitmap.width();
float scale_y = static_cast<float>(canvas_height) / bg1_bitmap.height();
float scale = std::min(scale_x, scale_y);
int scaled_width = static_cast<int>(bg1_bitmap.width() * scale);
int scaled_height = static_cast<int>(bg1_bitmap.height() * scale);
int offset_x = (canvas_width - scaled_width) / 2;
int offset_y = (canvas_height - scaled_height) / 2;
canvas_->DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255);
}
// BG2 (background layer 2) - sprite graphics (overlay)
auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap();
if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 &&
bg2_bitmap.height() > 0) {
float scale_x = static_cast<float>(canvas_width) / bg2_bitmap.width();
float scale_y = static_cast<float>(canvas_height) / bg2_bitmap.height();
float scale = std::min(scale_x, scale_y);
int scaled_width = static_cast<int>(bg2_bitmap.width() * scale);
int scaled_height = static_cast<int>(bg2_bitmap.height() * scale);
int offset_x = (canvas_width - scaled_width) / 2;
int offset_y = (canvas_height - scaled_height) / 2;
canvas_->DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200);
}
}
absl::Status DungeonRenderer::RefreshGraphics(int room_id, uint64_t palette_id,
const gfx::PaletteGroup& palette_group) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// This would need access to room data - will be called from main editor
return absl::OkStatus();
}
std::pair<int, int> DungeonRenderer::RoomToCanvasCoordinates(int room_x, int room_y) const {
return {room_x * 16, room_y * 16};
}
std::pair<int, int> DungeonRenderer::CanvasToRoomCoordinates(int canvas_x, int canvas_y) const {
return {canvas_x / 16, canvas_y / 16};
}
bool DungeonRenderer::IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin) const {
auto canvas_size = canvas_->canvas_size();
auto global_scale = canvas_->global_scale();
int scaled_width = static_cast<int>(canvas_size.x * global_scale);
int scaled_height = static_cast<int>(canvas_size.y * global_scale);
return (canvas_x >= -margin && canvas_y >= -margin &&
canvas_x <= scaled_width + margin &&
canvas_y <= scaled_height + margin);
}
} // namespace yaze::editor

View File

@@ -0,0 +1,76 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H
#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H
#include <vector>
#include "absl/status/status.h"
#include "app/gfx/snes_palette.h"
#include "app/gui/canvas.h"
#include "app/rom.h"
#include "app/zelda3/dungeon/object_renderer.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_layout.h"
#include "app/zelda3/dungeon/room_object.h"
namespace yaze {
namespace editor {
/**
* @brief Handles rendering of dungeon objects, layouts, and backgrounds
*
* This component manages all rendering operations for the dungeon editor,
* including object caching, background layers, and layout visualization.
*/
class DungeonRenderer {
public:
explicit DungeonRenderer(gui::Canvas* canvas, Rom* rom)
: canvas_(canvas), rom_(rom), object_renderer_(rom) {}
// Object rendering
void RenderObjectInCanvas(const zelda3::RoomObject& object,
const gfx::SnesPalette& palette);
void DisplayObjectInfo(const zelda3::RoomObject& object, int canvas_x, int canvas_y);
void RenderLayoutObjects(const zelda3::RoomLayout& layout,
const gfx::SnesPalette& palette);
// Background rendering
void RenderRoomBackgroundLayers(int room_id);
absl::Status RefreshGraphics(int room_id, uint64_t palette_id,
const gfx::PaletteGroup& palette_group);
// Graphics management
absl::Status LoadAndRenderRoomGraphics(int room_id,
std::array<zelda3::Room, 0x128>& rooms);
absl::Status ReloadAllRoomGraphics(std::array<zelda3::Room, 0x128>& rooms);
// Cache management
void ClearObjectCache() { object_render_cache_.clear(); }
size_t GetCacheSize() const { return object_render_cache_.size(); }
// Coordinate conversion helpers
std::pair<int, int> RoomToCanvasCoordinates(int room_x, int room_y) const;
std::pair<int, int> CanvasToRoomCoordinates(int canvas_x, int canvas_y) const;
bool IsWithinCanvasBounds(int canvas_x, int canvas_y, int margin = 32) const;
private:
gui::Canvas* canvas_;
Rom* rom_;
zelda3::ObjectRenderer object_renderer_;
// Object rendering cache
struct ObjectRenderCache {
int object_id;
int object_x, object_y, object_size;
uint64_t palette_hash;
gfx::Bitmap rendered_bitmap;
bool is_valid;
};
std::vector<ObjectRenderCache> object_render_cache_;
uint64_t last_palette_hash_ = 0;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_RENDERER_H

View File

@@ -0,0 +1,127 @@
#include "dungeon_room_loader.h"
#include <algorithm>
#include <map>
#include "app/gfx/snes_palette.h"
#include "app/zelda3/dungeon/room.h"
namespace yaze::editor {
absl::Status DungeonRoomLoader::LoadAllRooms(std::array<zelda3::Room, 0x128>& rooms) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
auto dungeon_man_pal_group = rom_->palette_group().dungeon_main;
for (int i = 0; i < 0x100 + 40; i++) {
rooms[i] = zelda3::LoadRoomFromRom(rom_, i);
auto room_size = zelda3::CalculateRoomSize(rom_, i);
room_size_pointers_.push_back(room_size.room_size_pointer);
room_sizes_.push_back(room_size.room_size);
if (room_size.room_size_pointer != 0x0A8000) {
room_size_addresses_[i] = room_size.room_size_pointer;
}
rooms[i].LoadObjects();
auto dungeon_palette_ptr = rom_->paletteset_ids[rooms[i].palette][0];
auto palette_id = rom_->ReadWord(0xDEC4B + dungeon_palette_ptr);
if (palette_id.status() != absl::OkStatus()) {
continue;
}
int p_id = palette_id.value() / 180;
auto color = dungeon_man_pal_group[p_id][3];
room_palette_[rooms[i].palette] = color.rgb();
}
LoadDungeonRoomSize();
return absl::OkStatus();
}
absl::Status DungeonRoomLoader::LoadRoomEntrances(std::array<zelda3::RoomEntrance, 0x8C>& entrances) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Load entrances
for (int i = 0; i < 0x07; ++i) {
entrances[i] = zelda3::RoomEntrance(rom_, i, true);
}
for (int i = 0; i < 0x85; ++i) {
entrances[i + 0x07] = zelda3::RoomEntrance(rom_, i, false);
}
return absl::OkStatus();
}
void DungeonRoomLoader::LoadDungeonRoomSize() {
std::map<int, std::vector<int>> rooms_by_bank;
for (const auto& room : room_size_addresses_) {
int bank = room.second >> 16;
rooms_by_bank[bank].push_back(room.second);
}
// Process and calculate room sizes within each bank
for (auto& bank_rooms : rooms_by_bank) {
std::ranges::sort(bank_rooms.second);
for (size_t i = 0; i < bank_rooms.second.size(); ++i) {
int room_ptr = bank_rooms.second[i];
// Identify the room ID for the current room pointer
int room_id =
std::ranges::find_if(room_size_addresses_, [room_ptr](
const auto& entry) {
return entry.second == room_ptr;
})->first;
if (room_ptr != 0x0A8000) {
if (i < bank_rooms.second.size() - 1) {
room_sizes_[room_id] = bank_rooms.second[i + 1] - room_ptr;
} else {
int bank_end_address = (bank_rooms.first << 16) | 0xFFFF;
room_sizes_[room_id] = bank_end_address - room_ptr + 1;
}
total_room_size_ += room_sizes_[room_id];
} else {
room_sizes_[room_id] = 0x00;
}
}
}
}
absl::Status DungeonRoomLoader::LoadAndRenderRoomGraphics(int room_id, zelda3::Room& room) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Load room graphics with proper blockset
room.LoadRoomGraphics(room.blockset);
// Render the room graphics to the graphics arena
room.RenderRoomGraphics();
return absl::OkStatus();
}
absl::Status DungeonRoomLoader::ReloadAllRoomGraphics(std::array<zelda3::Room, 0x128>& rooms) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Reload graphics for all rooms
for (size_t i = 0; i < rooms.size(); ++i) {
auto status = LoadAndRenderRoomGraphics(static_cast<int>(i), rooms[i]);
if (!status.ok()) {
continue; // Log error but continue with other rooms
}
}
return absl::OkStatus();
}
} // namespace yaze::editor

View File

@@ -0,0 +1,56 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H
#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H
#include <vector>
#include <unordered_map>
#include "absl/status/status.h"
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_entrance.h"
namespace yaze {
namespace editor {
/**
* @brief Manages loading and saving of dungeon room data
*
* This component handles all ROM-related operations for loading room data,
* calculating room sizes, and managing room graphics.
*/
class DungeonRoomLoader {
public:
explicit DungeonRoomLoader(Rom* rom) : rom_(rom) {}
// Room loading
absl::Status LoadAllRooms(std::array<zelda3::Room, 0x128>& rooms);
absl::Status LoadRoomEntrances(std::array<zelda3::RoomEntrance, 0x8C>& entrances);
// Room size management
void LoadDungeonRoomSize();
uint64_t GetTotalRoomSize() const { return total_room_size_; }
// Room graphics
absl::Status LoadAndRenderRoomGraphics(int room_id, zelda3::Room& room);
absl::Status ReloadAllRoomGraphics(std::array<zelda3::Room, 0x128>& rooms);
// Data access
const std::vector<int64_t>& GetRoomSizePointers() const { return room_size_pointers_; }
const std::vector<int64_t>& GetRoomSizes() const { return room_sizes_; }
const std::unordered_map<int, int>& GetRoomSizeAddresses() const { return room_size_addresses_; }
const std::unordered_map<int, ImVec4>& GetRoomPalette() const { return room_palette_; }
private:
Rom* rom_;
std::vector<int64_t> room_size_pointers_;
std::vector<int64_t> room_sizes_;
std::unordered_map<int, int> room_size_addresses_;
std::unordered_map<int, ImVec4> room_palette_;
uint64_t total_room_size_ = 0;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_ROOM_LOADER_H

View File

@@ -0,0 +1,151 @@
#include "dungeon_toolset.h"
#include <algorithm>
#include <array>
#include "app/gui/icons.h"
#include "imgui/imgui.h"
namespace yaze::editor {
using ImGui::BeginTable;
using ImGui::Button;
using ImGui::EndTable;
using ImGui::RadioButton;
using ImGui::TableNextColumn;
using ImGui::TableSetupColumn;
using ImGui::Text;
void DungeonToolset::Draw() {
if (BeginTable("DWToolset", 16, ImGuiTableFlags_SizingFixedFit, ImVec2(0, 0))) {
static std::array<const char*, 16> tool_names = {
"Undo", "Redo", "Separator", "All", "BG1", "BG2",
"BG3", "Separator", "Object", "Sprite", "Item", "Entrance",
"Door", "Chest", "Block", "Palette"};
std::ranges::for_each(tool_names,
[](const char* name) { TableSetupColumn(name); });
// Undo button
TableNextColumn();
if (Button(ICON_MD_UNDO)) {
if (undo_callback_) undo_callback_();
}
// Redo button
TableNextColumn();
if (Button(ICON_MD_REDO)) {
if (redo_callback_) redo_callback_();
}
// Separator
TableNextColumn();
Text(ICON_MD_MORE_VERT);
// Background layer selection
TableNextColumn();
if (RadioButton("All", background_type_ == kBackgroundAny)) {
background_type_ = kBackgroundAny;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show all background layers");
}
TableNextColumn();
if (RadioButton("BG1", background_type_ == kBackground1)) {
background_type_ = kBackground1;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 1 only");
}
TableNextColumn();
if (RadioButton("BG2", background_type_ == kBackground2)) {
background_type_ = kBackground2;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 2 only");
}
TableNextColumn();
if (RadioButton("BG3", background_type_ == kBackground3)) {
background_type_ = kBackground3;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show background layer 3 only");
}
// Separator
TableNextColumn();
Text(ICON_MD_MORE_VERT);
// Placement mode selection
TableNextColumn();
if (RadioButton(ICON_MD_SQUARE, placement_type_ == kObject)) {
placement_type_ = kObject;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Objects");
}
TableNextColumn();
if (RadioButton(ICON_MD_PEST_CONTROL, placement_type_ == kSprite)) {
placement_type_ = kSprite;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Sprites");
}
TableNextColumn();
if (RadioButton(ICON_MD_GRASS, placement_type_ == kItem)) {
placement_type_ = kItem;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Items");
}
TableNextColumn();
if (RadioButton(ICON_MD_NAVIGATION, placement_type_ == kEntrance)) {
placement_type_ = kEntrance;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Entrances");
}
TableNextColumn();
if (RadioButton(ICON_MD_SENSOR_DOOR, placement_type_ == kDoor)) {
placement_type_ = kDoor;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Doors");
}
TableNextColumn();
if (RadioButton(ICON_MD_INVENTORY, placement_type_ == kChest)) {
placement_type_ = kChest;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Chests");
}
TableNextColumn();
if (RadioButton(ICON_MD_VIEW_MODULE, placement_type_ == kBlock)) {
placement_type_ = kBlock;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Blocks");
}
// Palette button
TableNextColumn();
if (Button(ICON_MD_PALETTE)) {
if (palette_toggle_callback_) palette_toggle_callback_();
}
ImGui::EndTable();
}
ImGui::Separator();
ImGui::Text("Instructions: Click to place objects, Ctrl+Click to select, drag to move");
}
} // namespace yaze::editor

View File

@@ -0,0 +1,69 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H
#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H
#include <functional>
#include <array>
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
/**
* @brief Handles the dungeon editor toolset UI
*
* This component manages the toolbar with placement modes, background layer
* selection, and other editing tools.
*/
class DungeonToolset {
public:
enum BackgroundType {
kNoBackground,
kBackground1,
kBackground2,
kBackground3,
kBackgroundAny,
};
enum PlacementType {
kNoType,
kObject, // Object editing mode
kSprite, // Sprite editing mode
kItem, // Item placement mode
kEntrance, // Entrance/exit editing mode
kDoor, // Door configuration mode
kChest, // Chest management mode
kBlock // Legacy block mode
};
DungeonToolset() = default;
void Draw();
// Getters
BackgroundType background_type() const { return background_type_; }
PlacementType placement_type() const { return placement_type_; }
// Setters
void set_background_type(BackgroundType type) { background_type_ = type; }
void set_placement_type(PlacementType type) { placement_type_ = type; }
// Callbacks
void SetUndoCallback(std::function<void()> callback) { undo_callback_ = callback; }
void SetRedoCallback(std::function<void()> callback) { redo_callback_ = callback; }
void SetPaletteToggleCallback(std::function<void()> callback) { palette_toggle_callback_ = callback; }
private:
BackgroundType background_type_ = kBackgroundAny;
PlacementType placement_type_ = kNoType;
// Callbacks for editor actions
std::function<void()> undo_callback_;
std::function<void()> redo_callback_;
std::function<void()> palette_toggle_callback_;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_TOOLSET_H

View File

@@ -0,0 +1,87 @@
#include "dungeon_usage_tracker.h"
#include "imgui/imgui.h"
namespace yaze::editor {
void DungeonUsageTracker::CalculateUsageStats(const std::array<zelda3::Room, 0x128>& rooms) {
blockset_usage_.clear();
spriteset_usage_.clear();
palette_usage_.clear();
for (const auto& room : rooms) {
if (blockset_usage_.find(room.blockset) == blockset_usage_.end()) {
blockset_usage_[room.blockset] = 1;
} else {
blockset_usage_[room.blockset] += 1;
}
if (spriteset_usage_.find(room.spriteset) == spriteset_usage_.end()) {
spriteset_usage_[room.spriteset] = 1;
} else {
spriteset_usage_[room.spriteset] += 1;
}
if (palette_usage_.find(room.palette) == palette_usage_.end()) {
palette_usage_[room.palette] = 1;
} else {
palette_usage_[room.palette] += 1;
}
}
}
void DungeonUsageTracker::DrawUsageStats() {
if (ImGui::Button("Refresh")) {
ClearUsageStats();
}
ImGui::Text("Usage Statistics");
ImGui::Separator();
ImGui::Text("Blocksets: %zu used", blockset_usage_.size());
ImGui::Text("Spritesets: %zu used", spriteset_usage_.size());
ImGui::Text("Palettes: %zu used", palette_usage_.size());
ImGui::Separator();
// Detailed usage breakdown
if (ImGui::CollapsingHeader("Blockset Usage")) {
for (const auto& [blockset, count] : blockset_usage_) {
ImGui::Text("Blockset 0x%02X: %d rooms", blockset, count);
}
}
if (ImGui::CollapsingHeader("Spriteset Usage")) {
for (const auto& [spriteset, count] : spriteset_usage_) {
ImGui::Text("Spriteset 0x%02X: %d rooms", spriteset, count);
}
}
if (ImGui::CollapsingHeader("Palette Usage")) {
for (const auto& [palette, count] : palette_usage_) {
ImGui::Text("Palette 0x%02X: %d rooms", palette, count);
}
}
}
void DungeonUsageTracker::DrawUsageGrid() {
// TODO: Implement usage grid visualization
ImGui::Text("Usage grid visualization not yet implemented");
}
void DungeonUsageTracker::RenderSetUsage(const absl::flat_hash_map<uint16_t, int>& usage_map,
uint16_t& selected_set, int spriteset_offset) {
// TODO: Implement set usage rendering
ImGui::Text("Set usage rendering not yet implemented");
}
void DungeonUsageTracker::ClearUsageStats() {
selected_blockset_ = 0xFFFF;
selected_spriteset_ = 0xFFFF;
selected_palette_ = 0xFFFF;
spriteset_usage_.clear();
blockset_usage_.clear();
palette_usage_.clear();
}
} // namespace yaze::editor

View File

@@ -0,0 +1,57 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H
#define YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H
#include "absl/container/flat_hash_map.h"
#include "app/zelda3/dungeon/room.h"
namespace yaze {
namespace editor {
/**
* @brief Tracks and analyzes usage statistics for dungeon resources
*
* This component manages blockset, spriteset, and palette usage statistics
* across all dungeon rooms, providing insights for optimization.
*/
class DungeonUsageTracker {
public:
DungeonUsageTracker() = default;
// Statistics calculation
void CalculateUsageStats(const std::array<zelda3::Room, 0x128>& rooms);
void DrawUsageStats();
void DrawUsageGrid();
void RenderSetUsage(const absl::flat_hash_map<uint16_t, int>& usage_map,
uint16_t& selected_set, int spriteset_offset = 0x00);
// Data access
const absl::flat_hash_map<uint16_t, int>& GetBlocksetUsage() const { return blockset_usage_; }
const absl::flat_hash_map<uint16_t, int>& GetSpritesetUsage() const { return spriteset_usage_; }
const absl::flat_hash_map<uint16_t, int>& GetPaletteUsage() const { return palette_usage_; }
// Selection state
uint16_t GetSelectedBlockset() const { return selected_blockset_; }
uint16_t GetSelectedSpriteset() const { return selected_spriteset_; }
uint16_t GetSelectedPalette() const { return selected_palette_; }
void SetSelectedBlockset(uint16_t blockset) { selected_blockset_ = blockset; }
void SetSelectedSpriteset(uint16_t spriteset) { selected_spriteset_ = spriteset; }
void SetSelectedPalette(uint16_t palette) { selected_palette_ = palette; }
// Clear data
void ClearUsageStats();
private:
absl::flat_hash_map<uint16_t, int> spriteset_usage_;
absl::flat_hash_map<uint16_t, int> blockset_usage_;
absl::flat_hash_map<uint16_t, int> palette_usage_;
uint16_t selected_blockset_ = 0xFFFF; // 0xFFFF indicates no selection
uint16_t selected_spriteset_ = 0xFFFF;
uint16_t selected_palette_ = 0xFFFF;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_DUNGEON_DUNGEON_USAGE_TRACKER_H

View File

@@ -5,6 +5,11 @@ set(
app/editor/dungeon/dungeon_room_selector.cc
app/editor/dungeon/dungeon_canvas_viewer.cc
app/editor/dungeon/dungeon_object_selector.cc
app/editor/dungeon/dungeon_toolset.cc
app/editor/dungeon/dungeon_object_interaction.cc
app/editor/dungeon/dungeon_renderer.cc
app/editor/dungeon/dungeon_room_loader.cc
app/editor/dungeon/dungeon_usage_tracker.cc
app/editor/overworld/overworld_editor.cc
app/editor/sprite/sprite_editor.cc
app/editor/music/music_editor.cc