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
103template <typename PlaceFn, typename DeleteFn>
104bool DrawEntityActionRow(PlaceFn&& on_place_another, DeleteFn&& on_delete) {
105 bool deleted = false;
106 if (ImGui::BeginTable(
107 "##EntityActions", 2,
108 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
109 ImGui::TableNextColumn();
110 if (ImGui::Button("Place another", ImVec2(-1, 0))) {
111 on_place_another();
112 }
113 gui::AutoRegisterLastItem("button", "EntityPlaceAnother");
114 ImGui::TableNextColumn();
115 if (ImGui::Button("Delete", ImVec2(-1, 0))) {
116 on_delete();
117 deleted = true;
118 }
119 gui::AutoRegisterLastItem("button", "EntityDelete");
120 ImGui::EndTable();
121 }
122 return deleted;
123}
124
125void DrawDoor(DungeonCanvasViewer& viewer, size_t index,
126 const std::function<void(int, size_t)>& jump_to_reciprocal) {
127 const auto& doors = (*viewer.rooms())[viewer.current_room_id()].GetDoors();
128 if (index >= doors.size()) {
129 return;
130 }
131 auto door = doors[index];
132 ImGui::TextWrapped("Door #%zu · %s", index,
133 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
134 bool changed = false;
135 bool deleted = false;
136 ImGui::BeginDisabled(viewer.header_read_only() ||
138 if (BeginProperties()) {
139 if (BeginChoice("Type", "DoorType",
140 std::string(zelda3::GetDoorTypeName(door.type)).c_str())) {
141 for (auto type : zelda3::GetPlaceableDoorTypes()) {
142 const auto label =
143 absl::StrFormat("%02X %s", static_cast<int>(type),
144 std::string(zelda3::GetDoorTypeName(type)));
145 if (ImGui::Selectable(label.c_str(), door.type == type)) {
146 door.type = type;
147 changed = true;
148 }
149 }
150 ImGui::EndCombo();
151 }
152 if (BeginChoice("Direction", "DoorDirection",
153 std::string(zelda3::GetDoorDirectionName(door.direction))
154 .c_str())) {
155 for (int i = 0; i < 4; ++i) {
156 const auto direction = static_cast<zelda3::DoorDirection>(i);
157 if (ImGui::Selectable(
158 std::string(zelda3::GetDoorDirectionName(direction)).c_str(),
159 door.direction == direction)) {
160 door.direction = direction;
161 changed = true;
162 }
163 }
164 ImGui::EndCombo();
165 }
166 auto position_label = [&](uint8_t position) {
167 const auto [x, y] =
169 position, door.direction);
170 return absl::StrFormat("%02X · tile %d, %d", position, x, y);
171 };
172 if (BeginChoice("Slot", "DoorPosition",
173 position_label(door.position).c_str())) {
174 for (int i = 0; i < zelda3::DoorPositionManager::kMaxDoorPositions; ++i) {
175 if (!zelda3::DoorPositionManager::IsValidPosition(i, door.direction)) {
176 continue;
177 }
178 if (ImGui::Selectable(position_label(i).c_str(), door.position == i)) {
179 door.position = static_cast<uint8_t>(i);
180 changed = true;
181 }
182 }
183 ImGui::EndCombo();
184 }
185 ImGui::EndTable();
186 }
187 deleted = DrawEntityActionRow(
188 [&]() {
189 viewer.object_interaction().SetDoorPlacementMode(true, door.type);
190 },
191 [&]() {
192 (void)viewer.object_interaction()
195 });
196 ImGui::EndDisabled();
197 if (deleted) {
198 return;
199 }
200 if (changed) {
202 index, door.type, door.direction, door.position);
203 }
204
205 ImGui::Spacing();
206 const bool normal_door =
207 (*viewer.rooms())[viewer.current_room_id()].GetDoors()[index].type ==
209 ImGui::BeginDisabled(viewer.header_read_only() ||
210 !viewer.IsObjectInteractionEnabled() || !normal_door);
211 if (ImGui::Button("Complete stone surround", ImVec2(-1, 0))) {
213 }
214 gui::AutoRegisterLastItem("button", "CompleteDoorTrim");
215 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
216 ImGui::SetTooltip(
217 "%s", normal_door
218 ? "Add missing stone trim on the selected side and select it "
219 "with the door.\n"
220 "Existing matching trim is reused. One Undo step."
221 : "Stone surrounds are available for ordinary doors.");
222 }
223 ImGui::EndDisabled();
224 const auto& trim_status =
226 if (!trim_status.ok()) {
227 ImGui::TextWrapped("%s", std::string(trim_status.message()).c_str());
228 }
229
230 // Re-read accepted room data inside the shared connection controls; the
231 // property mutation above can reject its local candidate.
232 DrawDungeonConnectionEditor(viewer, index, jump_to_reciprocal);
233}
234
235void DrawSprite(DungeonCanvasViewer& viewer, size_t index,
237 const auto& sprites =
238 (*viewer.rooms())[viewer.current_room_id()].GetSprites();
239 if (index >= sprites.size()) {
240 return;
241 }
242 // Read fields before committing below; do not copy the sprite's graphics
243 // buffers every frame or retain references across the mutation callback.
244 const auto& sprite = sprites[index];
245 const auto labels =
247 const auto name = labels.GetLabel(zelda3::ResourceType::kSprite, sprite.id());
248 ImGui::TextWrapped("Sprite #%zu · %02X %s", index, sprite.id(), name.c_str());
249 if (sprite.IsOverlord()) {
250 ImGui::TextDisabled("Overlord");
251 }
252 int id = sprite.id(), x = sprite.x(), y = sprite.y();
253 int subtype = sprite.subtype(), layer = sprite.layer(),
254 key = sprite.key_drop();
255 const ImGuiID error_id = ImGui::GetID("SpriteEditRejected");
256 bool changed = false;
257 if (BeginProperties()) {
258 changed |=
259 IntegerProperty("ID (hex)", "SpriteId", id, 255, 1, identity, true);
260 changed |= IntegerProperty("X (tile)", "SpriteX", x, 31, 1, identity);
261 changed |= IntegerProperty("Y (tile)", "SpriteY", y, 31, 1, identity);
262 changed |=
263 IntegerProperty("Subtype", "SpriteSubtype", subtype, 31, 1, identity);
264 constexpr const char* kLayers[] = {"Upper (0)", "Lower (1)"};
265 if (BeginChoice("Layer", "SpriteLayer",
266 layer >= 0 && layer <= 1 ? kLayers[layer] : "Unknown")) {
267 for (int i = 0; i < 2; ++i) {
268 if (ImGui::Selectable(kLayers[i], layer == i)) {
269 layer = i;
270 changed = true;
271 }
273 "selectable", i == 0 ? "SpriteLayer/Upper" : "SpriteLayer/Lower");
274 }
275 ImGui::EndCombo();
276 }
277 constexpr const char* kKeys[] = {"None", "Small key", "Big key"};
278 if (BeginChoice("Key drop", "SpriteKeyDrop",
279 key >= 0 && key <= 2 ? kKeys[key] : "Unknown")) {
280 for (int i = 0; i < 3; ++i) {
281 if (ImGui::Selectable(kKeys[i], key == i)) {
282 key = i;
283 changed = true;
284 }
285 }
286 ImGui::EndCombo();
287 }
288 ImGui::EndTable();
289 }
290 if (changed) {
292 static_cast<uint8_t>(id), x, y, subtype, layer, key);
293 ImGui::GetStateStorage()->SetBool(error_id, !validation.ok());
294 if (validation.ok()) {
295 viewer.object_interaction()
298 .UpdateSprite(index, static_cast<uint8_t>(id), x, y, subtype, layer,
299 key);
300 }
301 }
302 if (ImGui::GetStateStorage()->GetBool(error_id)) {
303 ImGui::TextWrapped(
304 "Edit not applied: those fields encode a reserved sprite-list marker. "
305 "Change the position, subtype, or layer before trying again.");
306 }
307 ImGui::TextWrapped(
308 "Position uses 16-pixel tiles. Drag in the canvas to move.");
310 [&]() {
312 true, static_cast<uint8_t>(id));
313 },
314 [&]() {
315 (void)viewer.object_interaction()
318 });
319}
320
321void DrawItem(DungeonCanvasViewer& viewer, size_t index,
323 const auto& items = (*viewer.rooms())[viewer.current_room_id()].GetPotItems();
324 if (index >= items.size()) {
325 return;
326 }
327 const auto item = items[index];
328 auto type = item.item;
329 int x = item.GetPixelX(), y = item.GetPixelY();
330 const char* name = GetDungeonPotItemName(type);
331 ImGui::TextWrapped("Pot item #%zu · %02X %s", index, type, name);
333 bool changed = false;
334 bool position_changed = false;
335 if (BeginProperties()) {
336 if (BeginChoice("Type", "ItemType",
337 absl::StrFormat("%02X %s", type, name).c_str())) {
338 for (size_t i = 0; i < kPotItemNames.size(); ++i) {
339 const auto label = absl::StrFormat("%02X %s", i, kPotItemNames[i]);
340 if (ImGui::Selectable(label.c_str(), type == i)) {
341 type = static_cast<uint8_t>(i);
342 changed = true;
343 }
344 gui::AutoRegisterLastItem("selectable",
345 absl::StrFormat("ItemType/%zu", i));
346 }
347 ImGui::EndCombo();
348 }
349 position_changed |=
350 IntegerProperty("X (pixel)", "ItemX", x, 504, 8, identity);
351 position_changed |=
352 IntegerProperty("Y (pixel)", "ItemY", y, 504, 8, identity);
353 ImGui::EndTable();
354 }
355 if (position_changed) {
357 index, type, x, y);
358 } else if (changed) {
359 viewer.object_interaction()
361 .item_handler()
362 .MutateItemType(index, type);
363 }
364 ImGui::TextWrapped(
365 "Position snaps to an 8-pixel grid; layer and control flags are "
366 "preserved.");
368 [&]() { viewer.object_interaction().SetItemPlacementMode(true, type); },
369 [&]() {
370 (void)viewer.object_interaction()
373 });
374}
375
376} // namespace
377
378const char* GetDungeonPotItemName(uint8_t item_id) {
379 return item_id < kPotItemNames.size() ? kPotItemNames[item_id] : "Unknown";
380}
381
383 if (core::FeatureFlags::get().dungeon.kSavePotItems) {
384 return;
385 }
386 gui::StyleColorGuard notice_color(ImGuiCol_Text,
387 AgentUI::GetTheme().status_warning);
388 ImGui::TextWrapped(
389 "Pot-item saving is off. Enable Save Pot Items in File > Save Scope > "
390 "Dungeon before saving.");
391}
392
394 DungeonCanvasViewer& viewer,
395 const std::function<void(int, size_t)>& jump_to_reciprocal) {
396 if (!viewer.rooms() || viewer.current_room_id() < 0 ||
397 viewer.current_room_id() >= static_cast<int>(viewer.rooms()->size())) {
398 return;
399 }
400 const auto* context =
402 // Connected-room navigation can display a new room before the editing
403 // context moves to it. Never show fields from one room and write another.
404 if (!context || context->current_room_id != viewer.current_room_id()) {
405 ImGui::TextWrapped(
406 "Open this room in the room canvas to edit its entities.");
407 return;
408 }
409 const auto selection = BuildDungeonSelectionSnapshot(
410 viewer.object_interaction(), viewer.rooms(), viewer.current_room_id());
411 if (selection.HasMixedSelection()) {
412 ImGui::TextWrapped("%s. Select one entity to edit its properties.",
413 GetDungeonSelectionSummaryText(selection).c_str());
414 return;
415 }
416 const auto entity = selection.entity;
417 gui::AutoWidgetScope scope("Dungeon/EntityInspector");
418 ImGui::PushID("DungeonEntityInspector");
419 ImGui::PushID(viewer.current_room_id());
420 ImGui::PushID(static_cast<int>(entity.index));
421 const gui::InputScalarTargetIdentity identity{
422 reinterpret_cast<uintptr_t>(&viewer),
423 static_cast<uint64_t>(viewer.current_room_id()), entity.index,
424 static_cast<uint64_t>(entity.type)};
425 switch (entity.type) {
426 case EntityType::Door:
427 DrawDoor(viewer, entity.index, jump_to_reciprocal);
428 break;
429 case EntityType::Sprite:
430 ImGui::BeginDisabled(viewer.header_read_only() ||
432 DrawSprite(viewer, entity.index, identity);
433 ImGui::EndDisabled();
434 break;
435 case EntityType::Item:
436 ImGui::BeginDisabled(viewer.header_read_only() ||
438 DrawItem(viewer, entity.index, identity);
439 ImGui::EndDisabled();
440 break;
441 default:
442 break;
443 }
444 ImGui::PopID();
445 ImGui::PopID();
446 ImGui::PopID();
447}
448
449} // namespace yaze::editor
static Flags & get()
Definition features.h:122
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
void SetItemPlacementMode(bool enabled, uint8_t item_id=0)
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
absl::Status CompleteDoorTrim(size_t door_index)
SpriteInteractionHandler & sprite_handler()
const absl::Status & selection_edit_status() const
absl::Status DeleteSelectedEntity()
Delete currently selected entity.
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:29
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 DrawEntityActionRow(PlaceFn &&on_place_another, DeleteFn &&on_delete)
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.
const char * GetDungeonPotItemName(uint8_t item_id)
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:360
@ NormalDoor
Normal door (upper layer)
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:61
Automatic widget registration helpers for ImGui Test Engine integration.