yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_entity_inspector.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <string>
7
8#include "absl/strings/str_format.h"
15#include "app/gui/core/input.h"
17#include "core/features.h"
18#include "imgui/imgui.h"
21
22namespace yaze::editor {
23namespace {
24
25constexpr std::array<const char*, 28> kPotItemNames = {
26 "Nothing", "Green Rupee", "Rock", "Bee", "Heart (4)",
27 "Bomb (4)", "Heart", "Blue Rupee", "Key", "Arrow (5)",
28 "Bomb (1)", "Heart", "Magic (Small)", "Full Magic", "Cucco",
29 "Green Soldier", "Bush Stal", "Blue Soldier", "Landmine", "Heart",
30 "Fairy", "Heart", "Nothing (22)", "Hole", "Warp",
31 "Staircase", "Bombable", "Switch"};
32
33void PropertyLabel(const char* label) {
34 ImGui::TableNextRow();
35 ImGui::TableNextColumn();
36 ImGui::AlignTextToFramePadding();
37 ImGui::TextUnformatted(label);
38 ImGui::TableNextColumn();
39 ImGui::SetNextItemWidth(-1);
40}
41
43 if (!ImGui::BeginTable(
44 "##EntityProperties", 2,
45 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
46 return false;
47 }
48 ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 68.0f);
49 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
50 return true;
51}
52
53// Deferred text entry makes one history entry when committed. Semantic identity
54// prevents an unfinished edit being applied to a different room or selection.
55bool IntegerProperty(const char* label, const char* id, int& value, int maximum,
56 int step, gui::InputScalarTargetIdentity identity,
57 bool hexadecimal = false) {
58 PropertyLabel(label);
59 ImGui::PushID(id);
60 const float button_width = ImGui::GetFrameHeight();
61 const float spacing = ImGui::GetStyle().ItemInnerSpacing.x;
62 ImGui::SetNextItemWidth(std::max(
63 1.0f, ImGui::GetContentRegionAvail().x - 2 * (button_width + spacing)));
64 bool changed = gui::InputScalarDeferred(
65 "##Value", ImGuiDataType_S32, &value, hexadecimal ? "%02X" : "%d",
66 hexadecimal ? ImGuiInputTextFlags_CharsHexadecimal
67 : ImGuiInputTextFlags_CharsDecimal,
68 identity);
69 gui::AutoRegisterLastItem("input_int", id);
70 ImGui::SameLine(0, spacing);
71 ImGui::BeginDisabled(value <= 0);
72 if (ImGui::Button("-", ImVec2(button_width, button_width))) {
73 value -= step;
74 changed = true;
75 }
76 gui::AutoRegisterLastItem("button", std::string(id) + "Decrease");
77 ImGui::EndDisabled();
78 ImGui::SameLine(0, spacing);
79 ImGui::BeginDisabled(value >= maximum);
80 if (ImGui::Button("+", ImVec2(button_width, button_width))) {
81 value += step;
82 changed = true;
83 }
84 gui::AutoRegisterLastItem("button", std::string(id) + "Increase");
85 ImGui::EndDisabled();
86 ImGui::PopID();
87 if (changed) {
88 value = std::clamp(value, 0, maximum);
89 value -= value % step;
90 }
91 return changed;
92}
93
94bool BeginChoice(const char* label, const char* id, const char* preview) {
95 PropertyLabel(label);
96 const bool open =
97 ImGui::BeginCombo((std::string("##") + id).c_str(), preview);
98 // Capture the combo itself before any popup items replace LastItemData.
99 gui::AutoRegisterLastItem("combo", id);
100 return open;
101}
102
103void DrawDoor(DungeonCanvasViewer& viewer, size_t index,
104 const std::function<void(int, size_t)>& jump_to_reciprocal) {
105 const auto& doors = (*viewer.rooms())[viewer.current_room_id()].GetDoors();
106 if (index >= doors.size()) {
107 return;
108 }
109 auto door = doors[index];
110 ImGui::TextWrapped("Door #%zu · %s", index,
111 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
112 bool changed = false;
113 ImGui::BeginDisabled(viewer.header_read_only() ||
115 if (BeginProperties()) {
116 if (BeginChoice("Type", "DoorType",
117 std::string(zelda3::GetDoorTypeName(door.type)).c_str())) {
118 for (auto type : zelda3::GetPlaceableDoorTypes()) {
119 const auto label =
120 absl::StrFormat("%02X %s", static_cast<int>(type),
121 std::string(zelda3::GetDoorTypeName(type)));
122 if (ImGui::Selectable(label.c_str(), door.type == type)) {
123 door.type = type;
124 changed = true;
125 }
126 }
127 ImGui::EndCombo();
128 }
129 if (BeginChoice("Direction", "DoorDirection",
130 std::string(zelda3::GetDoorDirectionName(door.direction))
131 .c_str())) {
132 for (int i = 0; i < 4; ++i) {
133 const auto direction = static_cast<zelda3::DoorDirection>(i);
134 if (ImGui::Selectable(
135 std::string(zelda3::GetDoorDirectionName(direction)).c_str(),
136 door.direction == direction)) {
137 door.direction = direction;
138 changed = true;
139 }
140 }
141 ImGui::EndCombo();
142 }
143 auto position_label = [&](uint8_t position) {
144 const auto [x, y] =
146 position, door.direction);
147 return absl::StrFormat("%02X · tile %d, %d", position, x, y);
148 };
149 if (BeginChoice("Slot", "DoorPosition",
150 position_label(door.position).c_str())) {
151 for (int i = 0; i < zelda3::DoorPositionManager::kMaxDoorPositions; ++i) {
152 if (!zelda3::DoorPositionManager::IsValidPosition(i, door.direction)) {
153 continue;
154 }
155 if (ImGui::Selectable(position_label(i).c_str(), door.position == i)) {
156 door.position = static_cast<uint8_t>(i);
157 changed = true;
158 }
159 }
160 ImGui::EndCombo();
161 }
162 ImGui::EndTable();
163 }
164 ImGui::EndDisabled();
165 if (changed) {
167 index, door.type, door.direction, door.position);
168 }
169
170 // Re-read accepted room data inside the shared connection controls; the
171 // property mutation above can reject its local candidate.
172 DrawDungeonConnectionEditor(viewer, index, jump_to_reciprocal);
173}
174
175void DrawSprite(DungeonCanvasViewer& viewer, size_t index,
177 const auto& sprites =
178 (*viewer.rooms())[viewer.current_room_id()].GetSprites();
179 if (index >= sprites.size()) {
180 return;
181 }
182 // Read fields before committing below; do not copy the sprite's graphics
183 // buffers every frame or retain references across the mutation callback.
184 const auto& sprite = sprites[index];
185 const auto labels =
187 const auto name = labels.GetLabel(zelda3::ResourceType::kSprite, sprite.id());
188 ImGui::TextWrapped("Sprite #%zu · %02X %s", index, sprite.id(), name.c_str());
189 if (sprite.IsOverlord()) {
190 ImGui::TextDisabled("Overlord");
191 }
192 int id = sprite.id(), x = sprite.x(), y = sprite.y();
193 int subtype = sprite.subtype(), layer = sprite.layer(),
194 key = sprite.key_drop();
195 const ImGuiID error_id = ImGui::GetID("SpriteEditRejected");
196 bool changed = false;
197 if (BeginProperties()) {
198 changed |=
199 IntegerProperty("ID (hex)", "SpriteId", id, 255, 1, identity, true);
200 changed |= IntegerProperty("X (tile)", "SpriteX", x, 31, 1, identity);
201 changed |= IntegerProperty("Y (tile)", "SpriteY", y, 31, 1, identity);
202 changed |=
203 IntegerProperty("Subtype", "SpriteSubtype", subtype, 31, 1, identity);
204 constexpr const char* kLayers[] = {"Upper (0)", "Lower (1)"};
205 if (BeginChoice("Layer", "SpriteLayer",
206 layer >= 0 && layer <= 1 ? kLayers[layer] : "Unknown")) {
207 for (int i = 0; i < 2; ++i) {
208 if (ImGui::Selectable(kLayers[i], layer == i)) {
209 layer = i;
210 changed = true;
211 }
213 "selectable", i == 0 ? "SpriteLayer/Upper" : "SpriteLayer/Lower");
214 }
215 ImGui::EndCombo();
216 }
217 constexpr const char* kKeys[] = {"None", "Small key", "Big key"};
218 if (BeginChoice("Key drop", "SpriteKeyDrop",
219 key >= 0 && key <= 2 ? kKeys[key] : "Unknown")) {
220 for (int i = 0; i < 3; ++i) {
221 if (ImGui::Selectable(kKeys[i], key == i)) {
222 key = i;
223 changed = true;
224 }
225 }
226 ImGui::EndCombo();
227 }
228 ImGui::EndTable();
229 }
230 if (changed) {
232 static_cast<uint8_t>(id), x, y, subtype, layer, key);
233 ImGui::GetStateStorage()->SetBool(error_id, !validation.ok());
234 if (validation.ok()) {
235 viewer.object_interaction()
238 .UpdateSprite(index, static_cast<uint8_t>(id), x, y, subtype, layer,
239 key);
240 }
241 }
242 if (ImGui::GetStateStorage()->GetBool(error_id)) {
243 ImGui::TextWrapped(
244 "Edit not applied: those fields encode a reserved sprite-list marker. "
245 "Change the position, subtype, or layer before trying again.");
246 }
247 ImGui::TextWrapped(
248 "Position uses 16-pixel tiles. Drag in the canvas to move.");
249}
250
251void DrawItem(DungeonCanvasViewer& viewer, size_t index,
253 const auto& items = (*viewer.rooms())[viewer.current_room_id()].GetPotItems();
254 if (index >= items.size()) {
255 return;
256 }
257 const auto item = items[index];
258 auto type = item.item;
259 int x = item.GetPixelX(), y = item.GetPixelY();
260 const char* name =
261 type < kPotItemNames.size() ? kPotItemNames[type] : "Unknown";
262 ImGui::TextWrapped("Pot item #%zu · %02X %s", index, type, name);
264 bool changed = false;
265 bool position_changed = false;
266 if (BeginProperties()) {
267 if (BeginChoice("Type", "ItemType",
268 absl::StrFormat("%02X %s", type, name).c_str())) {
269 for (size_t i = 0; i < kPotItemNames.size(); ++i) {
270 const auto label = absl::StrFormat("%02X %s", i, kPotItemNames[i]);
271 if (ImGui::Selectable(label.c_str(), type == i)) {
272 type = static_cast<uint8_t>(i);
273 changed = true;
274 }
275 gui::AutoRegisterLastItem("selectable",
276 absl::StrFormat("ItemType/%zu", i));
277 }
278 ImGui::EndCombo();
279 }
280 position_changed |=
281 IntegerProperty("X (pixel)", "ItemX", x, 504, 8, identity);
282 position_changed |=
283 IntegerProperty("Y (pixel)", "ItemY", y, 504, 8, identity);
284 ImGui::EndTable();
285 }
286 if (position_changed) {
288 index, type, x, y);
289 } else if (changed) {
290 viewer.object_interaction()
292 .item_handler()
293 .MutateItemType(index, type);
294 }
295 ImGui::TextWrapped(
296 "Position snaps to an 8-pixel grid; layer and control flags are "
297 "preserved.");
298}
299
300} // namespace
301
303 if (core::FeatureFlags::get().dungeon.kSavePotItems) {
304 return;
305 }
306 gui::StyleColorGuard notice_color(ImGuiCol_Text,
307 AgentUI::GetTheme().status_warning);
308 ImGui::TextWrapped(
309 "Pot-item saving is off. Enable Save Pot Items in File > Save Scope > "
310 "Dungeon before saving.");
311}
312
314 DungeonCanvasViewer& viewer,
315 const std::function<void(int, size_t)>& jump_to_reciprocal) {
316 if (!viewer.rooms() || viewer.current_room_id() < 0 ||
317 viewer.current_room_id() >= static_cast<int>(viewer.rooms()->size())) {
318 return;
319 }
320 const auto* context =
322 // Connected-room navigation can display a new room before the editing
323 // context moves to it. Never show fields from one room and write another.
324 if (!context || context->current_room_id != viewer.current_room_id()) {
325 ImGui::TextWrapped(
326 "Open this room in the room canvas to edit its entities.");
327 return;
328 }
329 const auto selection = BuildDungeonSelectionSnapshot(
330 viewer.object_interaction(), viewer.rooms(), viewer.current_room_id());
331 if (selection.HasMixedSelection()) {
332 ImGui::TextWrapped("%s. Select one entity to edit its properties.",
333 GetDungeonSelectionSummaryText(selection).c_str());
334 return;
335 }
336 const auto entity = selection.entity;
337 gui::AutoWidgetScope scope("Dungeon/EntityInspector");
338 ImGui::PushID("DungeonEntityInspector");
339 ImGui::PushID(viewer.current_room_id());
340 ImGui::PushID(static_cast<int>(entity.index));
341 const gui::InputScalarTargetIdentity identity{
342 reinterpret_cast<uintptr_t>(&viewer),
343 static_cast<uint64_t>(viewer.current_room_id()), entity.index,
344 static_cast<uint64_t>(entity.type)};
345 switch (entity.type) {
346 case EntityType::Door:
347 DrawDoor(viewer, entity.index, jump_to_reciprocal);
348 break;
349 case EntityType::Sprite:
350 ImGui::BeginDisabled(viewer.header_read_only() ||
352 DrawSprite(viewer, entity.index, identity);
353 ImGui::EndDisabled();
354 break;
355 case EntityType::Item:
356 ImGui::BeginDisabled(viewer.header_read_only() ||
358 DrawItem(viewer, entity.index, identity);
359 ImGui::EndDisabled();
360 break;
361 default:
362 break;
363 }
364 ImGui::PopID();
365 ImGui::PopID();
366 ImGui::PopID();
367}
368
369} // namespace yaze::editor
static Flags & get()
Definition features.h:119
InteractionContext * context() const
Get the interaction context.
bool UpdateDoor(size_t index, zelda3::DoorType type, zelda3::DoorDirection direction, uint8_t position)
DungeonObjectInteraction & object_interaction()
const project::YazeProject * project() const
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
SpriteInteractionHandler & sprite_handler()
bool MutateItemType(size_t index, uint8_t new_type)
Change one pot item's raw type byte in place.
bool UpdateItem(size_t index, uint8_t type, int pixel_x, int pixel_y)
bool UpdateSprite(size_t index, uint8_t id, int x, int y, int subtype, int layer, int key_drop)
static absl::Status ValidateSpriteProperties(uint8_t id, int x, int y, int subtype, int layer, int key_drop)
RAII scope that enables automatic widget registration.
RAII guard for ImGui style colors.
Definition style_guard.h:27
static constexpr int kMaxDoorPositions
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
static std::pair< int, int > PositionToRenderTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to the top-left tile of the visible door.
const AgentUITheme & GetTheme()
bool BeginChoice(const char *label, const char *id, const char *preview)
void DrawItem(DungeonCanvasViewer &viewer, size_t index, gui::InputScalarTargetIdentity identity)
bool IntegerProperty(const char *label, const char *id, int &value, int maximum, int step, gui::InputScalarTargetIdentity identity, bool hexadecimal=false)
void DrawDoor(DungeonCanvasViewer &viewer, size_t index, const std::function< void(int, size_t)> &jump_to_reciprocal)
void DrawSprite(DungeonCanvasViewer &viewer, size_t index, gui::InputScalarTargetIdentity identity)
zelda3::ResourceLabelProvider LabelsForProject(const project::YazeProject *project)
Editors are the view controllers for the application.
void DrawDungeonEntityInspector(DungeonCanvasViewer &viewer, const std::function< void(int, size_t)> &jump_to_reciprocal)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
void DrawDungeonConnectionEditor(DungeonCanvasViewer &viewer, size_t door_index, const std::function< void(int, size_t)> &jump_to_reciprocal)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:392
constexpr std::array< DoorType, 32 > GetPlaceableDoorTypes()
Get supported door types suitable for editor placement controls.
Definition door_types.h:364
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
DoorDirection
Door direction on room walls.
Definition door_types.h:18
Draw a scalar editor that commits the model value atomically.
Definition input.h:56
Automatic widget registration helpers for ImGui Test Engine integration.