feat(editor): add entity operations for overworld editing

- Introduced new `entity_operations.cc` and `entity_operations.h` files to handle the insertion of entrances, exits, sprites, and items in the overworld editor.
- Updated `map_properties.cc` to include a context menu for entity insertion, allowing users to add various entities directly from the canvas.
- Enhanced `overworld_editor.cc` to manage entity insertion callbacks and streamline the editing process.

Benefits:
- Improves the functionality of the overworld editor by enabling direct manipulation of entities.
- Provides a more intuitive user experience with context-sensitive menus for entity operations.
This commit is contained in:
scawful
2025-10-16 11:41:51 -04:00
parent 5aa265230d
commit 69f94323c0
20 changed files with 694 additions and 167 deletions

View File

@@ -23,6 +23,7 @@ set(
app/editor/message/message_preview.cc
app/editor/music/music_editor.cc
app/editor/overworld/entity.cc
app/editor/overworld/entity_operations.cc
app/editor/overworld/map_properties.cc
app/editor/overworld/overworld_editor.cc
app/editor/overworld/overworld_entity_renderer.cc

View File

@@ -0,0 +1,235 @@
#include "entity_operations.h"
#include "absl/strings/str_format.h"
#include "util/log.h"
namespace yaze {
namespace editor {
absl::StatusOr<zelda3::OverworldEntrance*> InsertEntrance(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
bool is_hole) {
if (!overworld || !overworld->is_loaded()) {
return absl::FailedPreconditionError("Overworld not loaded");
}
// Snap to 16x16 grid and clamp to bounds (ZScream: EntranceMode.cs:86-87)
ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
// Get parent map ID (ZScream: EntranceMode.cs:78-82)
auto* current_ow_map = overworld->overworld_map(current_map);
uint8_t map_id = GetParentMapId(current_ow_map, current_map);
if (is_hole) {
// Search for first deleted hole slot (ZScream: EntranceMode.cs:74-100)
auto& holes = overworld->holes();
for (size_t i = 0; i < holes.size(); ++i) {
if (holes[i].deleted) {
// Reuse deleted slot
holes[i].deleted = false;
holes[i].map_id_ = map_id;
holes[i].x_ = static_cast<int>(snapped_pos.x);
holes[i].y_ = static_cast<int>(snapped_pos.y);
holes[i].entrance_id_ = 0; // Default, user configures in popup
holes[i].is_hole_ = true;
// Update map properties (ZScream: EntranceMode.cs:90)
holes[i].UpdateMapProperties(map_id);
LOG_DEBUG("EntityOps", "Inserted hole at slot %zu: pos=(%d,%d) map=0x%02X",
i, holes[i].x_, holes[i].y_, map_id);
return &holes[i];
}
}
return absl::ResourceExhaustedError(
"No space available for new hole. Delete one first.");
} else {
// Search for first deleted entrance slot (ZScream: EntranceMode.cs:104-130)
auto* entrances = overworld->mutable_entrances();
for (size_t i = 0; i < entrances->size(); ++i) {
if (entrances->at(i).deleted) {
// Reuse deleted slot
entrances->at(i).deleted = false;
entrances->at(i).map_id_ = map_id;
entrances->at(i).x_ = static_cast<int>(snapped_pos.x);
entrances->at(i).y_ = static_cast<int>(snapped_pos.y);
entrances->at(i).entrance_id_ = 0; // Default, user configures in popup
entrances->at(i).is_hole_ = false;
// Update map properties (ZScream: EntranceMode.cs:120)
entrances->at(i).UpdateMapProperties(map_id);
LOG_DEBUG("EntityOps", "Inserted entrance at slot %zu: pos=(%d,%d) map=0x%02X",
i, entrances->at(i).x_, entrances->at(i).y_, map_id);
return &entrances->at(i);
}
}
return absl::ResourceExhaustedError(
"No space available for new entrance. Delete one first.");
}
}
absl::StatusOr<zelda3::OverworldExit*> InsertExit(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map) {
if (!overworld || !overworld->is_loaded()) {
return absl::FailedPreconditionError("Overworld not loaded");
}
// Snap to 16x16 grid and clamp to bounds (ZScream: ExitMode.cs:71-72)
ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
// Get parent map ID (ZScream: ExitMode.cs:63-67)
auto* current_ow_map = overworld->overworld_map(current_map);
uint8_t map_id = GetParentMapId(current_ow_map, current_map);
// Search for first deleted exit slot (ZScream: ExitMode.cs:59-124)
auto& exits = *overworld->mutable_exits();
for (size_t i = 0; i < exits.size(); ++i) {
if (exits[i].deleted_) {
// Reuse deleted slot
exits[i].deleted_ = false;
exits[i].map_id_ = map_id;
exits[i].x_ = static_cast<int>(snapped_pos.x);
exits[i].y_ = static_cast<int>(snapped_pos.y);
// Initialize with default values (ZScream: ExitMode.cs:95-112)
// User will configure room_id, scroll, camera in popup
exits[i].room_id_ = 0;
exits[i].x_scroll_ = 0;
exits[i].y_scroll_ = 0;
exits[i].x_camera_ = 0;
exits[i].y_camera_ = 0;
exits[i].x_player_ = static_cast<uint16_t>(snapped_pos.x);
exits[i].y_player_ = static_cast<uint16_t>(snapped_pos.y);
exits[i].scroll_mod_x_ = 0;
exits[i].scroll_mod_y_ = 0;
exits[i].door_type_1_ = 0;
exits[i].door_type_2_ = 0;
// Update map properties
exits[i].UpdateMapProperties(map_id);
LOG_DEBUG("EntityOps", "Inserted exit at slot %zu: pos=(%d,%d) map=0x%02X",
i, exits[i].x_, exits[i].y_, map_id);
return &exits[i];
}
}
return absl::ResourceExhaustedError(
"No space available for new exit. Delete one first.");
}
absl::StatusOr<zelda3::Sprite*> InsertSprite(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
int game_state, uint8_t sprite_id) {
if (!overworld || !overworld->is_loaded()) {
return absl::FailedPreconditionError("Overworld not loaded");
}
if (game_state < 0 || game_state > 2) {
return absl::InvalidArgumentError("Invalid game state (must be 0-2)");
}
// Snap to 16x16 grid and clamp to bounds (ZScream: SpriteMode.cs similar logic)
ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
// Get parent map ID (ZScream: SpriteMode.cs:90-95)
auto* current_ow_map = overworld->overworld_map(current_map);
uint8_t map_id = GetParentMapId(current_ow_map, current_map);
// Calculate map position (ZScream uses mapHover for parent tracking)
// For sprites, we need the actual map coordinates within the 512x512 map
int map_local_x = static_cast<int>(snapped_pos.x) % 512;
int map_local_y = static_cast<int>(snapped_pos.y) % 512;
// Convert to game coordinates (0-63 for X/Y within map)
uint8_t game_x = static_cast<uint8_t>(map_local_x / 16);
uint8_t game_y = static_cast<uint8_t>(map_local_y / 16);
// Add new sprite to the game state array (ZScream: SpriteMode.cs:34-35)
auto& sprites = *overworld->mutable_sprites(game_state);
// Create new sprite
zelda3::Sprite new_sprite(
current_ow_map->current_graphics(),
static_cast<uint8_t>(map_id),
sprite_id, // Sprite ID (user will configure in popup)
game_x, // X position in map coordinates
game_y, // Y position in map coordinates
static_cast<int>(snapped_pos.x), // Real X (world coordinates)
static_cast<int>(snapped_pos.y) // Real Y (world coordinates)
);
sprites.push_back(new_sprite);
// Return pointer to the newly added sprite
zelda3::Sprite* inserted_sprite = &sprites.back();
LOG_DEBUG("EntityOps", "Inserted sprite at game_state=%d: pos=(%d,%d) map=0x%02X id=0x%02X",
game_state, inserted_sprite->x_, inserted_sprite->y_, map_id, sprite_id);
return inserted_sprite;
}
absl::StatusOr<zelda3::OverworldItem*> InsertItem(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
uint8_t item_id) {
if (!overworld || !overworld->is_loaded()) {
return absl::FailedPreconditionError("Overworld not loaded");
}
// Snap to 16x16 grid and clamp to bounds (ZScream: ItemMode.cs similar logic)
ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
// Get parent map ID (ZScream: ItemMode.cs:60-64)
auto* current_ow_map = overworld->overworld_map(current_map);
uint8_t map_id = GetParentMapId(current_ow_map, current_map);
// Calculate game coordinates (0-63 for X/Y within map)
// Following LoadItems logic in overworld.cc:840-854
int fake_id = current_map % 0x40;
int sy = fake_id / 8;
int sx = fake_id - (sy * 8);
// Calculate map-local coordinates
int map_local_x = static_cast<int>(snapped_pos.x) % 512;
int map_local_y = static_cast<int>(snapped_pos.y) % 512;
// Game coordinates (0-63 range)
uint8_t game_x = static_cast<uint8_t>(map_local_x / 16);
uint8_t game_y = static_cast<uint8_t>(map_local_y / 16);
// Add new item to the all_items array (ZScream: ItemMode.cs:92-108)
auto& items = *overworld->mutable_all_items();
// Create new item with calculated coordinates
items.emplace_back(
item_id, // Item ID
static_cast<uint16_t>(map_id), // Room map ID
static_cast<int>(snapped_pos.x), // X (world coordinates)
static_cast<int>(snapped_pos.y), // Y (world coordinates)
false // Not deleted
);
// Set game coordinates
zelda3::OverworldItem* inserted_item = &items.back();
inserted_item->game_x_ = game_x;
inserted_item->game_y_ = game_y;
LOG_DEBUG("EntityOps", "Inserted item: pos=(%d,%d) game=(%d,%d) map=0x%02X id=0x%02X",
inserted_item->x_, inserted_item->y_, game_x, game_y, map_id, item_id);
return inserted_item;
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,136 @@
#ifndef YAZE_APP_EDITOR_OVERWORLD_ENTITY_OPERATIONS_H
#define YAZE_APP_EDITOR_OVERWORLD_ENTITY_OPERATIONS_H
#include "absl/status/statusor.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_entrance.h"
#include "zelda3/overworld/overworld_exit.h"
#include "zelda3/overworld/overworld_item.h"
#include "zelda3/sprite/sprite.h"
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
/**
* @brief Flat helper functions for entity insertion/manipulation
*
* Following ZScream's entity management pattern (EntranceMode.cs, ExitMode.cs, etc.)
* but implemented as free functions to minimize state management.
*
* Key concepts from ZScream:
* - Find first deleted slot for insertion
* - Calculate map position from mouse coordinates
* - Use parent map ID for multi-area maps
* - Call UpdateMapProperties to sync position data
*/
/**
* @brief Insert a new entrance at the specified position
*
* Follows ZScream's EntranceMode.AddEntrance() logic (EntranceMode.cs:53-148):
* - Finds first deleted entrance slot
* - Snaps position to 16x16 grid
* - Uses parent map ID for multi-area maps
* - Calls UpdateMapProperties to calculate game coordinates
*
* @param overworld Overworld data containing entrance arrays
* @param mouse_pos Mouse position in canvas coordinates (world space)
* @param current_map Current map index being edited
* @param is_hole True to insert a hole instead of entrance
* @return Pointer to newly inserted entrance, or error if no slots available
*/
absl::StatusOr<zelda3::OverworldEntrance*> InsertEntrance(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
bool is_hole = false);
/**
* @brief Insert a new exit at the specified position
*
* Follows ZScream's ExitMode.AddExit() logic (ExitMode.cs:59-124):
* - Finds first deleted exit slot
* - Snaps position to 16x16 grid
* - Initializes exit with default scroll/camera values
* - Sets room ID to 0 (needs to be configured by user)
*
* @param overworld Overworld data containing exit arrays
* @param mouse_pos Mouse position in canvas coordinates
* @param current_map Current map index being edited
* @return Pointer to newly inserted exit, or error if no slots available
*/
absl::StatusOr<zelda3::OverworldExit*> InsertExit(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map);
/**
* @brief Insert a new sprite at the specified position
*
* Follows ZScream's SpriteMode sprite insertion (SpriteMode.cs:27-100):
* - Adds new sprite to game state array
* - Calculates map position and game coordinates
* - Sets sprite ID (default 0, user configures in popup)
*
* @param overworld Overworld data containing sprite arrays
* @param mouse_pos Mouse position in canvas coordinates
* @param current_map Current map index being edited
* @param game_state Current game state (0=beginning, 1=zelda, 2=agahnim)
* @param sprite_id Sprite ID to insert (default 0)
* @return Pointer to newly inserted sprite
*/
absl::StatusOr<zelda3::Sprite*> InsertSprite(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
int game_state, uint8_t sprite_id = 0);
/**
* @brief Insert a new item at the specified position
*
* Follows ZScream's ItemMode item insertion (ItemMode.cs:54-113):
* - Adds new item to all_items array
* - Calculates map position and game coordinates
* - Sets item ID (default 0, user configures in popup)
*
* @param overworld Overworld data containing item arrays
* @param mouse_pos Mouse position in canvas coordinates
* @param current_map Current map index being edited
* @param item_id Item ID to insert (default 0x00 - Nothing)
* @return Pointer to newly inserted item
*/
absl::StatusOr<zelda3::OverworldItem*> InsertItem(
zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
uint8_t item_id = 0);
/**
* @brief Helper to get parent map ID for multi-area maps
*
* Returns the parent map ID, handling the case where a map is its own parent.
* Matches ZScream logic where ParentID == 255 means use current map.
*/
inline uint8_t GetParentMapId(const zelda3::OverworldMap* map, int current_map) {
uint8_t parent = map->parent();
return (parent == 0xFF) ? static_cast<uint8_t>(current_map) : parent;
}
/**
* @brief Snap position to 16x16 grid (standard entity positioning)
*/
inline ImVec2 SnapToEntityGrid(ImVec2 pos) {
return ImVec2(
static_cast<float>(static_cast<int>(pos.x / 16) * 16),
static_cast<float>(static_cast<int>(pos.y / 16) * 16)
);
}
/**
* @brief Clamp position to valid overworld bounds
*/
inline ImVec2 ClampToOverworldBounds(ImVec2 pos) {
return ImVec2(
std::clamp(pos.x, 0.0f, 4080.0f), // 4096 - 16
std::clamp(pos.y, 0.0f, 4080.0f)
);
}
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_OVERWORLD_ENTITY_OPERATIONS_H

View File

@@ -7,6 +7,7 @@
#include "app/gui/core/color.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/input.h"
#include "app/gui/core/layout_helpers.h"
#include "zelda3/overworld/overworld_map.h"
#include "imgui/imgui.h"
@@ -410,10 +411,68 @@ void MapPropertiesSystem::DrawOverlayEditor(int current_map,
void MapPropertiesSystem::SetupCanvasContextMenu(
gui::Canvas& canvas, int current_map, bool current_map_lock,
bool& show_map_properties_panel, bool& show_custom_bg_color_editor,
bool& show_overlay_editor) {
bool& show_overlay_editor, int current_mode) {
(void)current_map; // Used for future context-sensitive menu items
// Clear any existing context menu items
canvas.ClearContextMenuItems();
// Add entity insertion submenu (only in MOUSE mode)
if (current_mode == 0 && entity_insert_callback_) { // 0 = EditingMode::MOUSE
gui::Canvas::ContextMenuItem entity_menu;
entity_menu.label = ICON_MD_ADD_LOCATION " Insert Entity";
// Entrance submenu item
gui::Canvas::ContextMenuItem entrance_item;
entrance_item.label = ICON_MD_DOOR_FRONT " Entrance";
entrance_item.callback = [this]() {
if (entity_insert_callback_) {
entity_insert_callback_("entrance");
}
};
entity_menu.subitems.push_back(entrance_item);
// Hole submenu item
gui::Canvas::ContextMenuItem hole_item;
hole_item.label = ICON_MD_CYCLONE " Hole";
hole_item.callback = [this]() {
if (entity_insert_callback_) {
entity_insert_callback_("hole");
}
};
entity_menu.subitems.push_back(hole_item);
// Exit submenu item
gui::Canvas::ContextMenuItem exit_item;
exit_item.label = ICON_MD_DOOR_BACK " Exit";
exit_item.callback = [this]() {
if (entity_insert_callback_) {
entity_insert_callback_("exit");
}
};
entity_menu.subitems.push_back(exit_item);
// Item submenu item
gui::Canvas::ContextMenuItem item_item;
item_item.label = ICON_MD_GRASS " Item";
item_item.callback = [this]() {
if (entity_insert_callback_) {
entity_insert_callback_("item");
}
};
entity_menu.subitems.push_back(item_item);
// Sprite submenu item
gui::Canvas::ContextMenuItem sprite_item;
sprite_item.label = ICON_MD_PEST_CONTROL_RODENT " Sprite";
sprite_item.callback = [this]() {
if (entity_insert_callback_) {
entity_insert_callback_("sprite");
}
};
entity_menu.subitems.push_back(sprite_item);
canvas.AddContextMenuItem(entity_menu);
}
// Add overworld-specific context menu items
gui::Canvas::ContextMenuItem lock_item;
@@ -476,18 +535,16 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
canvas.set_global_scale(scale);
};
canvas.AddContextMenuItem(zoom_out_item);
// Entity Operations submenu will be added in future iteration
// For now, users can use keyboard shortcuts (3-8) to activate entity editing
}
// Private method implementations
void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) {
if (ImGui::BeginPopup("GraphicsPopup")) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
// Use theme-aware spacing instead of hardcoded constants
float spacing = gui::LayoutHelpers::GetStandardSpacing();
float padding = gui::LayoutHelpers::GetButtonPadding();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding, padding));
ImGui::Text("Graphics Settings");
ImGui::Separator();
@@ -589,10 +646,11 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) {
void MapPropertiesSystem::DrawPalettesPopup(int current_map, int game_state,
bool& show_custom_bg_color_editor) {
if (ImGui::BeginPopup("PalettesPopup")) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
// Use theme-aware spacing instead of hardcoded constants
float spacing = gui::LayoutHelpers::GetStandardSpacing();
float padding = gui::LayoutHelpers::GetButtonPadding();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding, padding));
ImGui::Text("Palette Settings");
ImGui::Separator();
@@ -651,10 +709,11 @@ void MapPropertiesSystem::DrawPropertiesPopup(int current_map,
bool& show_overlay_preview,
int& game_state) {
if (ImGui::BeginPopup("ConfigPopup")) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
// Use theme-aware spacing instead of hardcoded constants
float spacing = gui::LayoutHelpers::GetStandardSpacing();
float padding = gui::LayoutHelpers::GetButtonPadding();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding, padding));
ImGui::Text(ICON_MD_TUNE " Area Configuration");
ImGui::Separator();
@@ -1590,10 +1649,11 @@ void MapPropertiesSystem::DrawOverlayPreviewOnMap(int current_map,
void MapPropertiesSystem::DrawViewPopup() {
if (ImGui::BeginPopup("ViewPopup")) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
// Use theme-aware spacing instead of hardcoded constants
float spacing = gui::LayoutHelpers::GetStandardSpacing();
float padding = gui::LayoutHelpers::GetButtonPadding();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding, padding));
ImGui::Text("View Controls");
ImGui::Separator();
@@ -1624,10 +1684,11 @@ void MapPropertiesSystem::DrawViewPopup() {
void MapPropertiesSystem::DrawQuickAccessPopup() {
if (ImGui::BeginPopup("QuickPopup")) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(kCompactItemSpacing, kCompactFramePadding));
// Use theme-aware spacing instead of hardcoded constants
float spacing = gui::LayoutHelpers::GetStandardSpacing();
float padding = gui::LayoutHelpers::GetButtonPadding();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(padding, padding));
ImGui::Text("Quick Access");
ImGui::Separator();

View File

@@ -41,6 +41,12 @@ class MapPropertiesSystem {
refresh_tile16_blockset_ = std::move(refresh_tile16_blockset);
force_refresh_graphics_ = std::move(force_refresh_graphics);
}
// Set callbacks for entity operations
void SetEntityCallbacks(
std::function<void(const std::string&)> insert_callback) {
entity_insert_callback_ = std::move(insert_callback);
}
// Main interface methods
void DrawSimplifiedMapSettings(int& current_world, int& current_map,
@@ -60,7 +66,7 @@ class MapPropertiesSystem {
// Context menu integration
void SetupCanvasContextMenu(gui::Canvas& canvas, int current_map, bool current_map_lock,
bool& show_map_properties_panel, bool& show_custom_bg_color_editor,
bool& show_overlay_editor);
bool& show_overlay_editor, int current_mode = 0);
private:
// Property category drawers
@@ -108,6 +114,9 @@ class MapPropertiesSystem {
RefreshPaletteCallback refresh_tile16_blockset_;
ForceRefreshGraphicsCallback force_refresh_graphics_;
// Callback for entity insertion (generic, editor handles entity types)
std::function<void(const std::string&)> entity_insert_callback_;
// Using centralized UI constants from ui_constants.h
};

View File

@@ -18,6 +18,7 @@
#include "core/features.h"
#include "app/editor/overworld/map_properties.h"
#include "app/editor/overworld/entity.h"
#include "app/editor/overworld/entity_operations.h"
#include "app/editor/overworld/tile16_editor.h"
#include "app/gfx/resource/arena.h"
#include "app/gfx/core/bitmap.h"
@@ -189,6 +190,14 @@ absl::Status OverworldEditor::Load() {
LOG_DEBUG("OverworldEditor", "Overworld editor refreshed after Tile16 changes");
return absl::OkStatus();
});
// Set up entity insertion callback for MapPropertiesSystem
if (map_properties_system_) {
map_properties_system_->SetEntityCallbacks(
[this](const std::string& entity_type) {
HandleEntityInsertion(entity_type);
});
}
ASSIGN_OR_RETURN(entrance_tiletypes_, zelda3::LoadEntranceTileTypes(rom_));
all_gfx_loaded_ = true;
@@ -476,11 +485,17 @@ void OverworldEditor::DrawToolset() {
toolbar.BeginModeGroup();
if (toolbar.ModeButton(ICON_MD_MOUSE, current_mode == EditingMode::MOUSE, "Mouse Mode (1)\nNavigate, pan, and manage entities")) {
current_mode = EditingMode::MOUSE;
if (current_mode != EditingMode::MOUSE) {
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
}
}
if (toolbar.ModeButton(ICON_MD_DRAW, current_mode == EditingMode::DRAW_TILE, "Tile Paint Mode (2)\nDraw tiles on the map")) {
current_mode = EditingMode::DRAW_TILE;
if (current_mode != EditingMode::DRAW_TILE) {
current_mode = EditingMode::DRAW_TILE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kTilePainting);
}
}
toolbar.EndModeGroup();
@@ -647,6 +662,8 @@ void OverworldEditor::DrawToolset() {
// Keyboard shortcuts for the Overworld Editor
if (!ImGui::IsAnyItemActive()) {
using enum EditingMode;
EditingMode old_mode = current_mode;
// Tool shortcuts (simplified)
if (ImGui::IsKeyDown(ImGuiKey_1)) {
@@ -655,25 +672,40 @@ void OverworldEditor::DrawToolset() {
current_mode = EditingMode::DRAW_TILE;
}
// Update canvas usage mode when mode changes
if (old_mode != current_mode) {
if (current_mode == EditingMode::MOUSE) {
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (current_mode == EditingMode::DRAW_TILE) {
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kTilePainting);
}
}
// Entity editing shortcuts (3-8)
if (ImGui::IsKeyDown(ImGuiKey_3)) {
entity_edit_mode_ = EntityEditMode::ENTRANCES;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (ImGui::IsKeyDown(ImGuiKey_4)) {
entity_edit_mode_ = EntityEditMode::EXITS;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (ImGui::IsKeyDown(ImGuiKey_5)) {
entity_edit_mode_ = EntityEditMode::ITEMS;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (ImGui::IsKeyDown(ImGuiKey_6)) {
entity_edit_mode_ = EntityEditMode::SPRITES;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (ImGui::IsKeyDown(ImGuiKey_7)) {
entity_edit_mode_ = EntityEditMode::TRANSPORTS;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
} else if (ImGui::IsKeyDown(ImGuiKey_8)) {
entity_edit_mode_ = EntityEditMode::MUSIC;
current_mode = EditingMode::MOUSE;
ow_map_canvas_.SetUsageMode(gui::CanvasUsage::kEntityManipulation);
}
// View shortcuts
@@ -1455,7 +1487,7 @@ void OverworldEditor::DrawOverworldCanvas() {
map_properties_system_->SetupCanvasContextMenu(
ow_map_canvas_, current_map_, current_map_lock_,
show_map_properties_panel_, show_custom_bg_color_editor_,
show_overlay_editor_);
show_overlay_editor_, static_cast<int>(current_mode));
}
// Handle pan and zoom (works in all modes)
@@ -2971,4 +3003,81 @@ int OverworldEditor::AutomationGetTile(int x, int y) {
return overworld_.GetTile(x, y);
}
void OverworldEditor::HandleEntityInsertion(const std::string& entity_type) {
if (!overworld_.is_loaded()) {
LOG_ERROR("OverworldEditor", "Cannot insert entity: overworld not loaded");
return;
}
// Get mouse position from canvas (in world coordinates)
ImVec2 mouse_pos = ow_map_canvas_.hover_mouse_pos();
LOG_DEBUG("OverworldEditor", "Inserting entity type='%s' at pos=(%f,%f) map=%d",
entity_type.c_str(), mouse_pos.x, mouse_pos.y, current_map_);
if (entity_type == "entrance") {
auto result = InsertEntrance(&overworld_, mouse_pos, current_map_, false);
if (result.ok()) {
current_entrance_ = **result;
current_entity_ = *result;
ImGui::OpenPopup("Entrance Editor");
rom_->set_dirty(true);
} else {
LOG_ERROR("OverworldEditor", "Failed to insert entrance: %s",
result.status().message().data());
}
} else if (entity_type == "hole") {
auto result = InsertEntrance(&overworld_, mouse_pos, current_map_, true);
if (result.ok()) {
current_entrance_ = **result;
current_entity_ = *result;
ImGui::OpenPopup("Entrance Editor");
rom_->set_dirty(true);
} else {
LOG_ERROR("OverworldEditor", "Failed to insert hole: %s",
result.status().message().data());
}
} else if (entity_type == "exit") {
auto result = InsertExit(&overworld_, mouse_pos, current_map_);
if (result.ok()) {
current_exit_ = **result;
current_entity_ = *result;
ImGui::OpenPopup("Exit editor");
rom_->set_dirty(true);
} else {
LOG_ERROR("OverworldEditor", "Failed to insert exit: %s",
result.status().message().data());
}
} else if (entity_type == "item") {
auto result = InsertItem(&overworld_, mouse_pos, current_map_, 0x00);
if (result.ok()) {
current_item_ = **result;
current_entity_ = *result;
ImGui::OpenPopup("Item editor");
rom_->set_dirty(true);
} else {
LOG_ERROR("OverworldEditor", "Failed to insert item: %s",
result.status().message().data());
}
} else if (entity_type == "sprite") {
auto result = InsertSprite(&overworld_, mouse_pos, current_map_, game_state_, 0x00);
if (result.ok()) {
current_sprite_ = **result;
current_entity_ = *result;
ImGui::OpenPopup("Sprite editor");
rom_->set_dirty(true);
} else {
LOG_ERROR("OverworldEditor", "Failed to insert sprite: %s",
result.status().message().data());
}
} else {
LOG_WARN("OverworldEditor", "Unknown entity type: %s", entity_type.c_str());
}
}
} // namespace yaze::editor

View File

@@ -126,6 +126,16 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
* assembling the OverworldMap Bitmap objects.
*/
absl::Status LoadGraphics();
/**
* @brief Handle entity insertion from context menu
*
* Delegates to flat helper functions in entity_operations.cc
* following ZScream's pattern for entity management.
*
* @param entity_type Type of entity to insert ("entrance", "hole", "exit", "item", "sprite")
*/
void HandleEntityInsertion(const std::string& entity_type);
private:
void DrawFullscreenCanvas();