mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat(editor): erosion brush, NPC load, auto-save
- Erode brush mode: simulates water erosion by moving height downhill based on slope, creating natural drainage patterns and gullies - NPC JSON loader: File > Load NPCs parses saved creatures.json back into the spawn list (round-trip save/load now works) - Auto-save: every 5 minutes when unsaved changes exist, exports the full zone (ADT + WDT + creatures) to the output directory - Sculpt mode now has 6 brush types: Raise/Lower/Smooth/Flatten/Level/Erode
This commit is contained in:
parent
42749e9b58
commit
a91233a6ec
7 changed files with 164 additions and 7 deletions
|
|
@ -81,6 +81,16 @@ void EditorApp::run() {
|
||||||
// Handle pending UI actions
|
// Handle pending UI actions
|
||||||
ui_.processActions(*this);
|
ui_.processActions(*this);
|
||||||
|
|
||||||
|
// Auto-save
|
||||||
|
if (autoSaveEnabled_ && terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) {
|
||||||
|
autoSaveTimer_ += dt;
|
||||||
|
if (autoSaveTimer_ >= autoSaveInterval_) {
|
||||||
|
autoSaveTimer_ = 0.0f;
|
||||||
|
quickSave();
|
||||||
|
LOG_INFO("Auto-saved zone");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh dirty terrain chunks
|
// Refresh dirty terrain chunks
|
||||||
refreshDirtyChunks();
|
refreshDirtyChunks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,9 @@ private:
|
||||||
bool openContextMenu_ = false;
|
bool openContextMenu_ = false;
|
||||||
std::string lastSavePath_;
|
std::string lastSavePath_;
|
||||||
std::vector<CameraBookmark> bookmarks_;
|
std::vector<CameraBookmark> bookmarks_;
|
||||||
|
float autoSaveTimer_ = 0.0f;
|
||||||
|
float autoSaveInterval_ = 300.0f; // 5 minutes
|
||||||
|
bool autoSaveEnabled_ = true;
|
||||||
size_t lastObjectCount_ = 0;
|
size_t lastObjectCount_ = 0;
|
||||||
EditorMode mode_ = EditorMode::Sculpt;
|
EditorMode mode_ = EditorMode::Sculpt;
|
||||||
float waterHeight_ = 100.0f;
|
float waterHeight_ = 100.0f;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ enum class BrushMode {
|
||||||
Lower,
|
Lower,
|
||||||
Smooth,
|
Smooth,
|
||||||
Flatten,
|
Flatten,
|
||||||
Level
|
Level,
|
||||||
|
Erode
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BrushSettings {
|
struct BrushSettings {
|
||||||
|
|
|
||||||
|
|
@ -222,9 +222,9 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
|
||||||
ImGui::End(); return;
|
ImGui::End(); return;
|
||||||
}
|
}
|
||||||
auto& s = app.getTerrainEditor().brush().settings();
|
auto& s = app.getTerrainEditor().brush().settings();
|
||||||
const char* modes[] = {"Raise", "Lower", "Smooth", "Flatten", "Level"};
|
const char* modes[] = {"Raise", "Lower", "Smooth", "Flatten", "Level", "Erode"};
|
||||||
int idx = static_cast<int>(s.mode);
|
int idx = static_cast<int>(s.mode);
|
||||||
if (ImGui::Combo("Mode", &idx, modes, 5)) s.mode = static_cast<BrushMode>(idx);
|
if (ImGui::Combo("Mode", &idx, modes, 6)) s.mode = static_cast<BrushMode>(idx);
|
||||||
ImGui::SliderFloat("Radius", &s.radius, 5.0f, 200.0f, "%.0f");
|
ImGui::SliderFloat("Radius", &s.radius, 5.0f, 200.0f, "%.0f");
|
||||||
ImGui::SliderFloat("Strength", &s.strength, 0.5f, 50.0f, "%.1f");
|
ImGui::SliderFloat("Strength", &s.strength, 0.5f, 50.0f, "%.1f");
|
||||||
ImGui::SliderFloat("Falloff", &s.falloff, 0.0f, 1.0f, "%.2f");
|
ImGui::SliderFloat("Falloff", &s.falloff, 0.0f, 1.0f, "%.2f");
|
||||||
|
|
@ -701,7 +701,12 @@ void EditorUI::renderNpcPanel(EditorApp& app) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
static char npcPath[256] = "output/creatures.json";
|
static char npcPath[256] = "output/creatures.json";
|
||||||
ImGui::InputText("File##npc", npcPath, sizeof(npcPath));
|
ImGui::InputText("File##npc", npcPath, sizeof(npcPath));
|
||||||
if (ImGui::Button("Save NPCs")) spawner.saveToFile(npcPath);
|
if (ImGui::Button("Save NPCs", ImVec2(100, 0))) spawner.saveToFile(npcPath);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Load NPCs", ImVec2(100, 0))) {
|
||||||
|
spawner.loadFromFile(npcPath);
|
||||||
|
app.markObjectsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Click terrain to place selected creature");
|
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Click terrain to place selected creature");
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,101 @@ void NpcSpawner::scatter(const CreatureSpawn& base, const glm::vec3& center,
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NpcSpawner::loadFromFile(const std::string& path) {
|
bool NpcSpawner::loadFromFile(const std::string& path) {
|
||||||
// Simple JSON-ish parser for our format — full JSON parsing would need a library
|
std::ifstream f(path);
|
||||||
LOG_INFO("NPC spawn loading not yet implemented for: ", path);
|
if (!f) { LOG_ERROR("Failed to open NPC file: ", path); return false; }
|
||||||
return false;
|
|
||||||
|
std::string content((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
|
||||||
|
// Minimal JSON parser — extract fields from our known format
|
||||||
|
spawns_.clear();
|
||||||
|
selectedIdx_ = -1;
|
||||||
|
|
||||||
|
auto findStr = [&](const std::string& block, const std::string& key) -> std::string {
|
||||||
|
auto pos = block.find("\"" + key + "\"");
|
||||||
|
if (pos == std::string::npos) return "";
|
||||||
|
pos = block.find(':', pos);
|
||||||
|
if (pos == std::string::npos) return "";
|
||||||
|
pos = block.find('"', pos + 1);
|
||||||
|
if (pos == std::string::npos) return "";
|
||||||
|
auto end = block.find('"', pos + 1);
|
||||||
|
if (end == std::string::npos) return "";
|
||||||
|
return block.substr(pos + 1, end - pos - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto findNum = [&](const std::string& block, const std::string& key) -> float {
|
||||||
|
auto pos = block.find("\"" + key + "\"");
|
||||||
|
if (pos == std::string::npos) return 0;
|
||||||
|
pos = block.find(':', pos);
|
||||||
|
if (pos == std::string::npos) return 0;
|
||||||
|
return std::stof(block.substr(pos + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto findBool = [&](const std::string& block, const std::string& key) -> bool {
|
||||||
|
auto pos = block.find("\"" + key + "\"");
|
||||||
|
if (pos == std::string::npos) return false;
|
||||||
|
return block.find("true", pos) < block.find('\n', pos);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split by object boundaries
|
||||||
|
size_t start = 0;
|
||||||
|
while ((start = content.find('{', start)) != std::string::npos) {
|
||||||
|
auto end = content.find('}', start);
|
||||||
|
if (end == std::string::npos) break;
|
||||||
|
std::string block = content.substr(start, end - start + 1);
|
||||||
|
|
||||||
|
CreatureSpawn s;
|
||||||
|
s.name = findStr(block, "name");
|
||||||
|
s.modelPath = findStr(block, "model");
|
||||||
|
s.displayId = static_cast<uint32_t>(findNum(block, "displayId"));
|
||||||
|
s.orientation = findNum(block, "orientation");
|
||||||
|
s.scale = findNum(block, "scale");
|
||||||
|
if (s.scale < 0.1f) s.scale = 1.0f;
|
||||||
|
s.level = static_cast<uint32_t>(std::max(1.0f, findNum(block, "level")));
|
||||||
|
s.health = static_cast<uint32_t>(std::max(1.0f, findNum(block, "health")));
|
||||||
|
s.mana = static_cast<uint32_t>(findNum(block, "mana"));
|
||||||
|
s.minDamage = static_cast<uint32_t>(findNum(block, "minDamage"));
|
||||||
|
s.maxDamage = static_cast<uint32_t>(findNum(block, "maxDamage"));
|
||||||
|
s.armor = static_cast<uint32_t>(findNum(block, "armor"));
|
||||||
|
s.faction = static_cast<uint32_t>(findNum(block, "faction"));
|
||||||
|
s.behavior = static_cast<CreatureBehavior>(static_cast<int>(findNum(block, "behavior")));
|
||||||
|
s.wanderRadius = findNum(block, "wanderRadius");
|
||||||
|
s.aggroRadius = findNum(block, "aggroRadius");
|
||||||
|
s.leashRadius = findNum(block, "leashRadius");
|
||||||
|
s.respawnTimeMs = static_cast<uint32_t>(findNum(block, "respawnTimeMs"));
|
||||||
|
s.hostile = findBool(block, "hostile");
|
||||||
|
s.questgiver = findBool(block, "questgiver");
|
||||||
|
s.vendor = findBool(block, "vendor");
|
||||||
|
s.flightmaster = findBool(block, "flightmaster");
|
||||||
|
s.innkeeper = findBool(block, "innkeeper");
|
||||||
|
|
||||||
|
// Parse position array
|
||||||
|
auto posStart = block.find("\"position\"");
|
||||||
|
if (posStart != std::string::npos) {
|
||||||
|
auto bk = block.find('[', posStart);
|
||||||
|
if (bk != std::string::npos) {
|
||||||
|
float vals[3] = {};
|
||||||
|
int vi = 0;
|
||||||
|
auto p = bk + 1;
|
||||||
|
while (vi < 3 && p < block.size()) {
|
||||||
|
vals[vi++] = std::stof(block.substr(p));
|
||||||
|
p = block.find(',', p);
|
||||||
|
if (p == std::string::npos) break;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
s.position = glm::vec3(vals[0], vals[1], vals[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s.name.empty()) {
|
||||||
|
s.id = nextId();
|
||||||
|
spawns_.push_back(s);
|
||||||
|
}
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("NPC spawns loaded: ", path, " (", spawns_.size(), " creatures)");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace editor
|
} // namespace editor
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ void TerrainEditor::applyBrush(float deltaTime) {
|
||||||
case BrushMode::Smooth: applySmooth(deltaTime); break;
|
case BrushMode::Smooth: applySmooth(deltaTime); break;
|
||||||
case BrushMode::Flatten:
|
case BrushMode::Flatten:
|
||||||
case BrushMode::Level: applyFlatten(deltaTime); break;
|
case BrushMode::Level: applyFlatten(deltaTime); break;
|
||||||
|
case BrushMode::Erode: applyErode(deltaTime); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -586,6 +587,50 @@ void TerrainEditor::removeWater(const glm::vec3& center, float radius) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TerrainEditor::applyErode(float dt) {
|
||||||
|
float factor = std::min(1.0f, brush_.settings().strength * dt * 0.3f);
|
||||||
|
|
||||||
|
auto affected = getAffectedChunks(brush_.getPosition(), brush_.settings().radius);
|
||||||
|
for (int chunkIdx : affected) {
|
||||||
|
bool modified = false;
|
||||||
|
auto& chunk = terrain_->chunks[chunkIdx];
|
||||||
|
for (int v = 0; v < 145; v++) {
|
||||||
|
glm::vec3 pos = chunkVertexWorldPos(chunkIdx, v);
|
||||||
|
float dist = glm::length(glm::vec2(pos.x - brush_.getPosition().x,
|
||||||
|
pos.y - brush_.getPosition().y));
|
||||||
|
float influence = brush_.getInfluence(dist);
|
||||||
|
if (influence <= 0.0f) continue;
|
||||||
|
|
||||||
|
float h = chunk.heightMap.heights[v];
|
||||||
|
int row = v / 17, col = v % 17;
|
||||||
|
|
||||||
|
// Find lowest neighbor (same chunk)
|
||||||
|
float lowestH = h;
|
||||||
|
if (col <= 8) {
|
||||||
|
int neighbors[] = {v - 17, v + 17, v - 1, v + 1};
|
||||||
|
for (int n : neighbors) {
|
||||||
|
if (n >= 0 && n < 145)
|
||||||
|
lowestH = std::min(lowestH, chunk.heightMap.heights[n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move height toward lowest neighbor (erosion)
|
||||||
|
float slope = h - lowestH;
|
||||||
|
if (slope > 0.1f) {
|
||||||
|
float erosion = slope * factor * influence * 0.3f;
|
||||||
|
chunk.heightMap.heights[v] -= erosion;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modified) {
|
||||||
|
stitchEdges(chunkIdx);
|
||||||
|
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
|
||||||
|
dirtyChunks_.push_back(chunkIdx);
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, uint32_t seed) {
|
void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, uint32_t seed) {
|
||||||
if (!terrain_) return;
|
if (!terrain_) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ private:
|
||||||
void applyRaise(float dt);
|
void applyRaise(float dt);
|
||||||
void applySmooth(float dt);
|
void applySmooth(float dt);
|
||||||
void applyFlatten(float dt);
|
void applyFlatten(float dt);
|
||||||
|
void applyErode(float dt);
|
||||||
void stitchEdges(int chunkIdx);
|
void stitchEdges(int chunkIdx);
|
||||||
|
|
||||||
std::vector<int> getAffectedChunks(const glm::vec3& center, float radius) const;
|
std::vector<int> getAffectedChunks(const glm::vec3& center, float radius) const;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue