mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
feat: WOC collision mesh format — 7th novel open format
New format: WOC (Wowee Open Collision) — binary collision mesh for custom zone walkability. Magic WOC1 (0x31434F57). - WoweeCollisionBuilder::fromTerrain() generates collision triangles from terrain heightmap with slope classification (50 deg threshold) - Per-triangle flags: walkable (0x01), water (0x02), steep (0x04) - Respects terrain holes (skips triangles in hole regions) - Binary save/load with bounds, tile coords, triangle data - Auto-exported on zone save alongside WOT/WHM/WOM/WOB - Added to content pack validation (score now 0-7) - FORMAT_SPEC.md v1.1 updated with WOC binary layout - 19 new test assertions: flat terrain generation (32k tris all walkable), save/load round-trip, hole skipping - 328 total assertions across 84 test cases
This commit is contained in:
parent
961d863f82
commit
4d5eef480e
10 changed files with 342 additions and 7 deletions
|
|
@ -592,6 +592,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_terrain_loader.cpp
|
||||
src/pipeline/wowee_model.cpp
|
||||
src/pipeline/wowee_building.cpp
|
||||
src/pipeline/wowee_collision.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1323,6 +1324,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_terrain_loader.cpp
|
||||
src/pipeline/wowee_model.cpp
|
||||
src/pipeline/wowee_building.cpp
|
||||
src/pipeline/wowee_collision.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
54
include/pipeline/wowee_collision.hpp
Normal file
54
include/pipeline/wowee_collision.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
struct ADTTerrain;
|
||||
|
||||
// Wowee Open Collision format (.woc) — walkability mesh for custom zones
|
||||
struct WoweeCollision {
|
||||
struct Triangle {
|
||||
glm::vec3 v0, v1, v2;
|
||||
uint8_t flags; // 0x01=walkable, 0x02=water, 0x04=steep, 0x08=indoor
|
||||
};
|
||||
|
||||
struct BoundingBox {
|
||||
glm::vec3 min{1e30f}, max{-1e30f};
|
||||
void expand(const glm::vec3& p) {
|
||||
min = glm::min(min, p);
|
||||
max = glm::max(max, p);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Triangle> triangles;
|
||||
BoundingBox bounds;
|
||||
uint32_t tileX = 0, tileY = 0;
|
||||
|
||||
bool isValid() const { return !triangles.empty(); }
|
||||
size_t walkableCount() const;
|
||||
size_t steepCount() const;
|
||||
};
|
||||
|
||||
class WoweeCollisionBuilder {
|
||||
public:
|
||||
// Generate collision mesh from terrain heightmap
|
||||
static WoweeCollision fromTerrain(const ADTTerrain& terrain,
|
||||
float steepAngle = 50.0f);
|
||||
|
||||
// Save collision mesh to binary file
|
||||
static bool save(const WoweeCollision& collision, const std::string& path);
|
||||
|
||||
// Load collision mesh from binary file
|
||||
static WoweeCollision load(const std::string& path);
|
||||
|
||||
// Check if a collision file exists
|
||||
static bool exists(const std::string& basePath);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
176
src/pipeline/wowee_collision.cpp
Normal file
176
src/pipeline/wowee_collision.cpp
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
#include "pipeline/wowee_collision.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
static constexpr uint32_t WOC_MAGIC = 0x31434F57; // "WOC1"
|
||||
|
||||
size_t WoweeCollision::walkableCount() const {
|
||||
size_t n = 0;
|
||||
for (const auto& t : triangles)
|
||||
if (t.flags & 0x01) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t WoweeCollision::steepCount() const {
|
||||
size_t n = 0;
|
||||
for (const auto& t : triangles)
|
||||
if (t.flags & 0x04) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
WoweeCollision WoweeCollisionBuilder::fromTerrain(const ADTTerrain& terrain,
|
||||
float steepAngle) {
|
||||
WoweeCollision col;
|
||||
col.tileX = terrain.coord.x;
|
||||
col.tileY = terrain.coord.y;
|
||||
|
||||
float steepCos = std::cos(steepAngle * 3.14159265f / 180.0f);
|
||||
|
||||
float tileSize = 533.33333f;
|
||||
float chunkSize = tileSize / 16.0f;
|
||||
float vertSpacing = chunkSize / 8.0f;
|
||||
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
const auto& chunk = terrain.chunks[ci];
|
||||
if (!chunk.hasHeightMap()) continue;
|
||||
|
||||
int cx = ci % 16;
|
||||
int cy = ci / 16;
|
||||
float chunkBaseX = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize;
|
||||
float chunkBaseY = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize;
|
||||
|
||||
bool isHoleChunk = (chunk.holes != 0);
|
||||
|
||||
// Build outer vertex grid (9x9)
|
||||
for (int row = 0; row < 8; row++) {
|
||||
for (int col2 = 0; col2 < 8; col2++) {
|
||||
// Check hole mask (4x4 grid, each bit covers 2x2 sub-quads)
|
||||
if (isHoleChunk) {
|
||||
int hx = col2 / 2, hy = row / 2;
|
||||
if (chunk.holes & (1 << (hy * 4 + hx))) continue;
|
||||
}
|
||||
|
||||
int i00 = row * 17 + col2;
|
||||
int i10 = row * 17 + col2 + 1;
|
||||
int i01 = (row + 1) * 17 + col2;
|
||||
int i11 = (row + 1) * 17 + col2 + 1;
|
||||
|
||||
auto vtx = [&](int idx) -> glm::vec3 {
|
||||
int r = idx / 17, c = idx % 17;
|
||||
float x = chunkBaseX - r * vertSpacing;
|
||||
float y = chunkBaseY - c * vertSpacing;
|
||||
float z = chunk.position[2] + chunk.heightMap.heights[idx];
|
||||
return glm::vec3(x, y, z);
|
||||
};
|
||||
|
||||
glm::vec3 v00 = vtx(i00), v10 = vtx(i10);
|
||||
glm::vec3 v01 = vtx(i01), v11 = vtx(i11);
|
||||
|
||||
auto classifyTri = [&](const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
|
||||
WoweeCollision::Triangle tri;
|
||||
tri.v0 = a; tri.v1 = b; tri.v2 = c;
|
||||
|
||||
glm::vec3 normal = glm::normalize(glm::cross(b - a, c - a));
|
||||
float nz = std::abs(normal.z);
|
||||
|
||||
tri.flags = 0;
|
||||
if (nz >= steepCos)
|
||||
tri.flags |= 0x01; // walkable
|
||||
else
|
||||
tri.flags |= 0x04; // steep
|
||||
|
||||
col.bounds.expand(a);
|
||||
col.bounds.expand(b);
|
||||
col.bounds.expand(c);
|
||||
col.triangles.push_back(tri);
|
||||
};
|
||||
|
||||
classifyTri(v00, v10, v01);
|
||||
classifyTri(v10, v11, v01);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark water triangles
|
||||
if (terrain.waterData[ci].hasWater()) {
|
||||
float waterH = terrain.waterData[ci].layers[0].maxHeight;
|
||||
for (size_t ti = col.triangles.size() - 128; ti < col.triangles.size(); ti++) {
|
||||
auto& tri = col.triangles[ti];
|
||||
float avgZ = (tri.v0.z + tri.v1.z + tri.v2.z) / 3.0f;
|
||||
if (avgZ < waterH) tri.flags |= 0x02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Collision mesh: ", col.triangles.size(), " triangles (",
|
||||
col.walkableCount(), " walkable, ", col.steepCount(), " steep)");
|
||||
return col;
|
||||
}
|
||||
|
||||
bool WoweeCollisionBuilder::save(const WoweeCollision& collision, const std::string& path) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::create_directories(fs::path(path).parent_path());
|
||||
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f) return false;
|
||||
|
||||
f.write(reinterpret_cast<const char*>(&WOC_MAGIC), 4);
|
||||
uint32_t triCount = static_cast<uint32_t>(collision.triangles.size());
|
||||
f.write(reinterpret_cast<const char*>(&triCount), 4);
|
||||
f.write(reinterpret_cast<const char*>(&collision.tileX), 4);
|
||||
f.write(reinterpret_cast<const char*>(&collision.tileY), 4);
|
||||
f.write(reinterpret_cast<const char*>(&collision.bounds.min), 12);
|
||||
f.write(reinterpret_cast<const char*>(&collision.bounds.max), 12);
|
||||
|
||||
for (const auto& tri : collision.triangles) {
|
||||
f.write(reinterpret_cast<const char*>(&tri.v0), 12);
|
||||
f.write(reinterpret_cast<const char*>(&tri.v1), 12);
|
||||
f.write(reinterpret_cast<const char*>(&tri.v2), 12);
|
||||
f.write(reinterpret_cast<const char*>(&tri.flags), 1);
|
||||
}
|
||||
|
||||
LOG_INFO("WOC saved: ", path, " (", triCount, " triangles)");
|
||||
return true;
|
||||
}
|
||||
|
||||
WoweeCollision WoweeCollisionBuilder::load(const std::string& path) {
|
||||
WoweeCollision col;
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return col;
|
||||
|
||||
uint32_t magic;
|
||||
f.read(reinterpret_cast<char*>(&magic), 4);
|
||||
if (magic != WOC_MAGIC) return col;
|
||||
|
||||
uint32_t triCount;
|
||||
f.read(reinterpret_cast<char*>(&triCount), 4);
|
||||
f.read(reinterpret_cast<char*>(&col.tileX), 4);
|
||||
f.read(reinterpret_cast<char*>(&col.tileY), 4);
|
||||
f.read(reinterpret_cast<char*>(&col.bounds.min), 12);
|
||||
f.read(reinterpret_cast<char*>(&col.bounds.max), 12);
|
||||
|
||||
col.triangles.resize(triCount);
|
||||
for (uint32_t i = 0; i < triCount; i++) {
|
||||
auto& tri = col.triangles[i];
|
||||
f.read(reinterpret_cast<char*>(&tri.v0), 12);
|
||||
f.read(reinterpret_cast<char*>(&tri.v1), 12);
|
||||
f.read(reinterpret_cast<char*>(&tri.v2), 12);
|
||||
f.read(reinterpret_cast<char*>(&tri.flags), 1);
|
||||
}
|
||||
|
||||
LOG_INFO("WOC loaded: ", path, " (", triCount, " triangles)");
|
||||
return col;
|
||||
}
|
||||
|
||||
bool WoweeCollisionBuilder::exists(const std::string& basePath) {
|
||||
return std::filesystem::exists(basePath + ".woc");
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -403,6 +403,7 @@ register_test_target(test_gm_commands)
|
|||
add_executable(test_open_formats
|
||||
test_open_formats.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/wowee_building.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/wowee_collision.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/wowee_terrain_loader.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/wmo_loader.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/core/logger.cpp
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Tests for Wowee open format round-trips (WOM, WOB, WHM/WOT)
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "pipeline/wowee_building.hpp"
|
||||
#include "pipeline/wowee_collision.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
|
|
@ -262,6 +263,83 @@ TEST_CASE("WOT metadata round-trip with placements", "[wot]") {
|
|||
std::filesystem::remove(wotPath);
|
||||
}
|
||||
|
||||
// ============== WOC Tests ==============
|
||||
|
||||
TEST_CASE("WOC collision from flat terrain", "[woc]") {
|
||||
ADTTerrain terrain{};
|
||||
terrain.loaded = true;
|
||||
terrain.coord = {32, 48};
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
auto& chunk = terrain.chunks[ci];
|
||||
chunk.heightMap.loaded = true;
|
||||
chunk.indexX = ci % 16;
|
||||
chunk.indexY = ci / 16;
|
||||
chunk.position[2] = 100.0f;
|
||||
chunk.holes = 0;
|
||||
for (int v = 0; v < 145; v++)
|
||||
chunk.heightMap.heights[v] = 0.0f;
|
||||
}
|
||||
|
||||
auto col = WoweeCollisionBuilder::fromTerrain(terrain);
|
||||
REQUIRE(col.isValid());
|
||||
REQUIRE(col.triangles.size() == 256 * 64 * 2); // 8x8 quads * 2 tris * 256 chunks
|
||||
REQUIRE(col.walkableCount() == col.triangles.size()); // flat = all walkable
|
||||
REQUIRE(col.steepCount() == 0);
|
||||
REQUIRE(col.tileX == 32);
|
||||
REQUIRE(col.tileY == 48);
|
||||
}
|
||||
|
||||
TEST_CASE("WOC save and load round-trip", "[woc]") {
|
||||
ensureTestDir();
|
||||
|
||||
WoweeCollision col;
|
||||
col.tileX = 10; col.tileY = 20;
|
||||
WoweeCollision::Triangle tri;
|
||||
tri.v0 = {0,0,0}; tri.v1 = {1,0,0}; tri.v2 = {0,1,0}; tri.flags = 0x01;
|
||||
col.triangles.push_back(tri);
|
||||
tri.v0 = {5,5,10}; tri.v1 = {6,5,10}; tri.v2 = {5,6,15}; tri.flags = 0x04;
|
||||
col.triangles.push_back(tri);
|
||||
col.bounds.expand({0,0,0}); col.bounds.expand({6,6,15});
|
||||
|
||||
std::string path = TEST_DIR + "/test_collision.woc";
|
||||
REQUIRE(WoweeCollisionBuilder::save(col, path));
|
||||
|
||||
auto loaded = WoweeCollisionBuilder::load(path);
|
||||
REQUIRE(loaded.isValid());
|
||||
REQUIRE(loaded.triangles.size() == 2);
|
||||
REQUIRE(loaded.tileX == 10);
|
||||
REQUIRE(loaded.tileY == 20);
|
||||
REQUIRE(loaded.triangles[0].flags == 0x01);
|
||||
REQUIRE(loaded.triangles[1].flags == 0x04);
|
||||
REQUIRE(loaded.triangles[0].v0.x == Catch::Approx(0.0f));
|
||||
REQUIRE(loaded.triangles[1].v2.z == Catch::Approx(15.0f));
|
||||
REQUIRE(loaded.walkableCount() == 1);
|
||||
REQUIRE(loaded.steepCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("WOC holes skip triangles", "[woc]") {
|
||||
ADTTerrain terrain{};
|
||||
terrain.loaded = true;
|
||||
terrain.coord = {32, 32};
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
auto& chunk = terrain.chunks[ci];
|
||||
chunk.heightMap.loaded = true;
|
||||
chunk.indexX = ci % 16;
|
||||
chunk.indexY = ci / 16;
|
||||
chunk.position[2] = 100.0f;
|
||||
chunk.holes = 0;
|
||||
for (int v = 0; v < 145; v++)
|
||||
chunk.heightMap.heights[v] = 0.0f;
|
||||
}
|
||||
// Punch a hole in chunk 0 (all 16 sub-quads)
|
||||
terrain.chunks[0].holes = 0xFFFF;
|
||||
|
||||
auto col = WoweeCollisionBuilder::fromTerrain(terrain);
|
||||
REQUIRE(col.isValid());
|
||||
// Chunk 0 should produce zero triangles, rest produce 128 each
|
||||
REQUIRE(col.triangles.size() == 255 * 128);
|
||||
}
|
||||
|
||||
TEST_CASE("WOB rejects missing file", "[wob]") {
|
||||
REQUIRE_FALSE(WoweeBuildingLoader::exists("nonexistent_path"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,18 +59,28 @@ Novel file formats for custom WoW zone content. No Blizzard IP.
|
|||
- Standard PNG format, loaded by client's texture override system
|
||||
- Editor auto-converts BLP→PNG on export via stb_image_write
|
||||
|
||||
## WOC — Wowee Open Collision (binary)
|
||||
- Extension: `.woc`
|
||||
- Magic: `WOC1` (0x31434F57)
|
||||
- Layout: magic(4) + triCount(4) + tileX(4) + tileY(4) + boundsMin(12) + boundsMax(12) + triangles
|
||||
- Triangle: v0(12) + v1(12) + v2(12) + flags(1) = 37 bytes
|
||||
- Flags: 0x01=walkable, 0x02=water, 0x04=steep, 0x08=indoor
|
||||
- Generated from terrain heightmap with slope classification (50 deg threshold)
|
||||
- Respects terrain holes (skips triangles in hole regions)
|
||||
|
||||
## Terrain Stamps (.json)
|
||||
- Portable terrain feature snapshots (mountains, craters, etc.)
|
||||
- Format: `{"format": "wowee-stamp-1.0", "vertices": [[dx, dy, height], ...]}`
|
||||
- Can be saved/loaded across zones and sessions
|
||||
|
||||
## Open Format Scoring (0-6)
|
||||
## Open Format Scoring (0-7)
|
||||
1. WOT terrain metadata present
|
||||
2. WHM heightmap with valid magic
|
||||
3. zone.json map definition
|
||||
4. PNG textures present
|
||||
5. WOM models with valid magic
|
||||
6. WOB buildings with valid magic
|
||||
7. WOC collision mesh with valid magic
|
||||
|
||||
## All formats are novel, portable, and open for redistribution.
|
||||
## No Blizzard intellectual property is used in any format definition.
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ ContentPacker::ValidationResult ContentPacker::validateZone(const std::string& z
|
|||
static constexpr uint32_t WHM_MAGIC = 0x314D4857; // "WHM1"
|
||||
static constexpr uint32_t WOM_MAGIC = 0x314D4F57; // "WOM1"
|
||||
static constexpr uint32_t WOB_MAGIC = 0x31424F57; // "WOB1"
|
||||
static constexpr uint32_t WOC_MAGIC = 0x31434F57; // "WOC1"
|
||||
|
||||
for (auto& entry : fs::recursive_directory_iterator(zoneDir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
|
@ -220,6 +221,10 @@ ContentPacker::ValidationResult ContentPacker::validateZone(const std::string& z
|
|||
r.hasWob = true;
|
||||
if (checkMagic(entry.path().string(), WOB_MAGIC)) r.wobValid = true;
|
||||
}
|
||||
if (ext == ".woc") {
|
||||
r.hasWoc = true;
|
||||
if (checkMagic(entry.path().string(), WOC_MAGIC)) r.wocValid = true;
|
||||
}
|
||||
if (ext == ".png") r.hasPng = true;
|
||||
if (fname == "zone.json") r.hasZoneJson = true;
|
||||
if (fname == "creatures.json") r.hasCreatures = true;
|
||||
|
|
@ -237,7 +242,8 @@ int ContentPacker::ValidationResult::openFormatScore() const {
|
|||
if (hasPng) score++;
|
||||
if (hasWom && womValid) score++;
|
||||
if (hasWob && wobValid) score++;
|
||||
return score; // max 6 for fully open
|
||||
if (hasWoc && wocValid) score++;
|
||||
return score; // max 7 for fully open
|
||||
}
|
||||
|
||||
std::string ContentPacker::ValidationResult::summary() const {
|
||||
|
|
@ -252,6 +258,7 @@ std::string ContentPacker::ValidationResult::summary() const {
|
|||
add(hasWhm, whmValid, "WHM");
|
||||
add(hasWom, womValid, "WOM");
|
||||
add(hasWob, wobValid, "WOB");
|
||||
add(hasWoc, wocValid, "WOC");
|
||||
if (hasZoneJson) s += "zone.json ";
|
||||
if (hasPng) s += "PNG ";
|
||||
if (hasCreatures) s += "creatures ";
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ public:
|
|||
// Validate that a zone directory has all required open format files
|
||||
struct ValidationResult {
|
||||
bool hasWot = false, hasWhm = false, hasZoneJson = false;
|
||||
bool hasPng = false, hasWom = false, hasWob = false;
|
||||
bool hasPng = false, hasWom = false, hasWob = false, hasWoc = false;
|
||||
bool hasCreatures = false, hasQuests = false, hasObjects = false;
|
||||
bool whmValid = false, womValid = false, wobValid = false;
|
||||
bool whmValid = false, womValid = false, wobValid = false, wocValid = false;
|
||||
int openFormatScore() const;
|
||||
std::string summary() const;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "dbc_exporter.hpp"
|
||||
#include "pipeline/wowee_model.hpp"
|
||||
#include "pipeline/wowee_building.hpp"
|
||||
#include "pipeline/wowee_collision.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
|
@ -956,6 +957,11 @@ void EditorApp::exportZone(const std::string& outputDir) {
|
|||
std::to_string(loadedTileX_) + "_" + std::to_string(loadedTileY_);
|
||||
WoweeTerrain::exportOpen(terrain_, openBase, loadedTileX_, loadedTileY_);
|
||||
WoweeTerrain::exportNormalMap(terrain_, openBase + "_normals.png");
|
||||
|
||||
// Export collision mesh (.woc)
|
||||
auto collision = pipeline::WoweeCollisionBuilder::fromTerrain(terrain_);
|
||||
if (collision.isValid())
|
||||
pipeline::WoweeCollisionBuilder::save(collision, openBase + ".woc");
|
||||
WoweeTerrain::exportAlphaMaps(terrain_, base + "/alphamaps");
|
||||
WoweeTerrain::exportWaterMask(terrain_, openBase + "_watermask.png");
|
||||
WoweeTerrain::exportHoleMask(terrain_, openBase + "_holemask.png");
|
||||
|
|
@ -1059,11 +1065,11 @@ void EditorApp::exportZone(const std::string& outputDir) {
|
|||
if (objectPlacer_.objectCount() > 0) summary += ", " + std::to_string(objectPlacer_.objectCount()) + " obj";
|
||||
if (npcSpawner_.spawnCount() > 0) summary += ", " + std::to_string(npcSpawner_.spawnCount()) + " NPC";
|
||||
if (questEditor_.questCount() > 0) summary += ", " + std::to_string(questEditor_.questCount()) + " quest";
|
||||
summary += " (score " + std::to_string(score) + "/6)";
|
||||
summary += " (score " + std::to_string(score) + "/7)";
|
||||
showToast(summary, 5.0f);
|
||||
LOG_INFO("=== Zone Export Summary ===");
|
||||
LOG_INFO(" Output: ", base);
|
||||
LOG_INFO(" Open format score: ", score, "/6");
|
||||
LOG_INFO(" Open format score: ", score, "/7");
|
||||
LOG_INFO(" Formats: ", validation.summary());
|
||||
LOG_INFO(" Terrain: WOT/WHM + heightmap/normals PNG");
|
||||
LOG_INFO(" Textures: ", usedTextures.size(), " BLP→PNG");
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ void EditorUI::renderMenuBar(EditorApp& app) {
|
|||
ImVec4 scoreColor = score >= 5 ? ImVec4(0.3f, 1, 0.3f, 1) :
|
||||
score >= 3 ? ImVec4(1, 1, 0.3f, 1) :
|
||||
ImVec4(1, 0.3f, 0.3f, 1);
|
||||
ImGui::TextColored(scoreColor, "Open Format Score: %d/6", score);
|
||||
ImGui::TextColored(scoreColor, "Open Format Score: %d/7", score);
|
||||
ImGui::Separator();
|
||||
auto fmt = [](bool has, bool valid, const char* name, const char* desc) {
|
||||
ImVec4 c = has ? (valid ? ImVec4(0.3f,1,0.3f,1) : ImVec4(1,0.7f,0.3f,1))
|
||||
|
|
@ -380,6 +380,7 @@ void EditorUI::renderMenuBar(EditorApp& app) {
|
|||
fmt(val.hasPng, true, "PNG", "textures");
|
||||
fmt(val.hasWom, val.womValid, "WOM", "models");
|
||||
fmt(val.hasWob, val.wobValid, "WOB", "buildings");
|
||||
fmt(val.hasWoc, val.wocValid, "WOC", "collision mesh");
|
||||
ImGui::Separator();
|
||||
fmt(val.hasCreatures, true, "creatures", "NPC spawns");
|
||||
fmt(val.hasQuests, true, "quests", "quest data");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue