From 6a7ea6dcfc7e8e0069eb41a9c1b569db0b293b4d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 15:47:26 -0700 Subject: [PATCH] feat(editor): in-editor "Audit Spawns Against Terrain" menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EditorApp::auditSpawnsAgainstTerrain(threshold) counts every creature + object whose Z is more than `threshold` yards off the sampled terrain. Returns the issue count; non-mutating. Generate menu gets a new "Audit Spawns Against Terrain" item that runs the audit at the default 5y threshold and shows a toast: a clean count if everything's fine, or the issue count + a hint to run "Snap All" if not. Surfaces placement bugs without dropping to the CLI. Pairs with the existing "Snap All Spawns to Ground" so the workflow stays inside the editor: audit → see count → snap → audit again. --- tools/editor/editor_app.cpp | 24 ++++++++++++++++++++++++ tools/editor/editor_app.hpp | 5 +++++ tools/editor/editor_ui.cpp | 13 +++++++++++++ 3 files changed, 42 insertions(+) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index af1208b1..0327e3fc 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -1830,6 +1830,30 @@ void EditorApp::snapAllSpawnsToGround() { std::to_string(snappedO) + " object(s) to ground"); } +int EditorApp::auditSpawnsAgainstTerrain(float threshold) const { + if (!terrain_.isLoaded()) return 0; + auto castDown = [&](const glm::vec3& pos, glm::vec3& hit) { + rendering::Ray ray; + ray.origin = pos + glm::vec3(0, 0, 500); + ray.direction = glm::vec3(0, 0, -1); + return const_cast(terrainEditor_).raycastTerrain(ray, hit); + }; + int issues = 0; + for (const auto& s : npcSpawner_.getSpawns()) { + glm::vec3 hit; + if (castDown(s.position, hit)) { + if (std::fabs(s.position.z - hit.z) > threshold) issues++; + } + } + for (const auto& o : objectPlacer_.getObjects()) { + glm::vec3 hit; + if (castDown(o.position, hit)) { + if (std::fabs(o.position.z - hit.z) > threshold) issues++; + } + } + return issues; +} + void EditorApp::clearAllObjects() { vkDeviceWaitIdle(window_->getVkContext()->getDevice()); objectPlacer_.clearAll(); diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index fc12a82b..adcd0cfe 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -106,6 +106,11 @@ public: // mirror of the --snap-zone-to-ground CLI; useful after terrain // edits or random population to fix floating/buried spawns. void snapAllSpawnsToGround(); + // Count spawns whose Z is more than `threshold` yards off from + // the terrain. Returns the issue count; 0 means clean. Used by + // the in-editor "Audit Spawns" menu to surface placement bugs + // without dropping to CLI. + int auditSpawnsAgainstTerrain(float threshold = 5.0f) const; void centerOnTerrain(); // Multi-tile support diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 112c09b5..0ead3ecb 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -403,6 +403,19 @@ void EditorUI::renderMenuBar(EditorApp& app) { ImGui::SetTooltip( "Re-snap every creature + object's Z to actual terrain height.\n" "Run after terrain edits to fix floating/buried spawns."); + if (ImGui::MenuItem("Audit Spawns Against Terrain", nullptr, false, + app.hasTerrainLoaded())) { + int issues = app.auditSpawnsAgainstTerrain(5.0f); + if (issues == 0) + app.showToast("Audit clean — every spawn within 5y of terrain"); + else + app.showToast(std::to_string(issues) + + " spawn(s) more than 5y off terrain — try Snap All"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Count spawns whose Z is more than 5y off terrain.\n" + "Surfaces placement bugs without modifying anything."); if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) { if (app.getObjectPlacer().objectCount() > 0 || app.getNpcSpawner().spawnCount() > 0) app.clearAllObjects();