Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
TiledToOlympe.cpp
Go to the documentation of this file.
1/*
2 * TiledToOlympe.cpp - TMJ to Olympe Engine Level Converter
3 *
4 * ============================================================================
5 * ISOMETRIC COORDINATE CONVERSION - FINAL WORKING SOLUTION
6 * ============================================================================
7 *
8 * Tiled stores isometric object positions in TMJ using a special coordinate
9 * system where BOTH X and Y are measured in tileHeight pixel units along
10 * the isometric axes.
11 *
12 * CONVERSION FORMULA (verified working):
13 * --------------------------------------
14 * tileX = tmjPixelX / tileHeight
15 * tileY = tmjPixelY / tileHeight
16 * worldX = (tileX - tileY) * (tileWidth / 2)
17 * worldY = (tileX + tileY) * (tileHeight / 2)
18 *
19 * EXAMPLE (184x128 map, 58x27 tiles):
20 * player_1: TMJ(1818.4, 1064.26) -> tile(67.35, 39.42) -> world(810, 1441)
21 *
22 * See IsometricProjection.cpp for detailed documentation.
23 * ============================================================================
24 */
25#include "../include/TiledLevelLoader.h"
26#include "../include/TiledToOlympe.h"
27#include "../include/IsometricProjection.h"
28#include "../include/ParallaxLayerManager.h"
29#include "../../OlympeTilemapEditor/include/LevelManager.h"
30#include "../../system/system_utils.h"
31#include "../../vector.h"
32#include "../../prefabfactory.h"
33#include <algorithm>
34#include <fstream>
35
36namespace Olympe {
37namespace Tiled {
38
40 {
41 mapWidth_ = 0; mapHeight_ = 0;
43 minTileX_ = 0; minTileY_ = 0;
44 maxTileX_ = 0; maxTileY_ = 0;
45 // Set default configuration
46 config_.flipY = true;
47 config_.defaultPrefab = "Blueprints/DefaultEntity.json";
48 config_.collisionLayerPatterns.push_back("collision");
49 config_.collisionLayerPatterns.push_back("walls");
50 config_.sectorLayerPatterns.push_back("sector");
51 config_.sectorLayerPatterns.push_back("zone");
52 }
53
57
58 // Property key constants
59 namespace {
60 const char* PROPERTY_PATROL_WAY = "patrol way";
61 const char* PROPERTY_TARGET = "target";
62 const char* PROPERTY_AUDIO = "audio";
63
64 // Flip flag constants for compact storage
68
69 // Helper function to extract flip flags from Tiled GID
77 }
78
83
84 // ok - NEW: Calculate actual bounds by scanning all tile chunks
86 {
87 MapBounds bounds;
88 bool firstTile = true;
89
90 // Scan all layers
91 for (const auto& layer : tiledMap.layers)
92 {
93 // Only process tile layers
94 if (layer->type != LayerType::TileLayer)
95 continue;
96
97 // Scan all chunks
98 for (const auto& chunk : layer->chunks)
99 {
100 // Chunk position in tiles
101 int chunkMinX = chunk.x;
102 int chunkMinY = chunk.y;
103 int chunkMaxX = chunk.x + chunk.width - 1;
104 int chunkMaxY = chunk.y + chunk.height - 1;
105
106 // Update bounds
107 if (firstTile)
108 {
109 bounds.minTileX = chunkMinX;
110 bounds.minTileY = chunkMinY;
111 bounds.maxTileX = chunkMaxX;
112 bounds.maxTileY = chunkMaxY;
113 firstTile = false;
114 }
115 else
116 {
117 bounds.minTileX = std::min(bounds.minTileX, chunkMinX);
118 bounds.minTileY = std::min(bounds.minTileY, chunkMinY);
119 bounds.maxTileX = std::max(bounds.maxTileX, chunkMaxX);
120 bounds.maxTileY = std::max(bounds.maxTileY, chunkMaxY);
121 }
122 }
123 }
124
125 // Calculate dimensions
126 bounds.widthInTiles = bounds.maxTileX - bounds.minTileX + 1;
127 bounds.heightInTiles = bounds.maxTileY - bounds.minTileY + 1;
128
129 return bounds;
130 }
131
133 {
134 lastError_.clear();
136
137 // Store tileset reference for gid lookups
138 tilesets_ = &tiledMap.tilesets;
139
142
143 SYSTEM_LOG << "\n+===========================================================+\n";
144 SYSTEM_LOG << "| TILED -> OLYMPE CONVERSION - COMPLETE PIPELINE |\n";
145 SYSTEM_LOG << "+===========================================================+\n\n";
146
147 // ok - PHASE 0: Calculate actual map dimensions
148 isInfiniteMap_ = tiledMap.infinite;
149
150 if (isInfiniteMap_)
151 {
152 SYSTEM_LOG << " /!\\ Map is INFINITE - calculating actual bounds...\n";
154
155 mapWidth_ = bounds.widthInTiles;
156 mapHeight_ = bounds.heightInTiles;
157
158 // Store chunk origin offset for coordinate transformations
159 chunkOriginX_ = bounds.minTileX;
160 chunkOriginY_ = bounds.minTileY;
161
162 // Store actual tile coordinate bounds for bounds-aware origin calculation
163 minTileX_ = bounds.minTileX;
164 minTileY_ = bounds.minTileY;
165 maxTileX_ = bounds.maxTileX;
166 maxTileY_ = bounds.maxTileY;
167
168 SYSTEM_LOG << " -> TMJ declared size: " << tiledMap.width << "x" << tiledMap.height << " (INVALID)\n";
169 SYSTEM_LOG << " -> Actual bounds: " << bounds.minTileX << "," << bounds.minTileY
170 << " to " << bounds.maxTileX << "," << bounds.maxTileY << "\n";
171 SYSTEM_LOG << " -> Chunk origin offset: (" << chunkOriginX_ << ", " << chunkOriginY_ << ")\n";
172 SYSTEM_LOG << " -> Actual map size: " << mapWidth_ << "x" << mapHeight_ << " tiles ok - \n\n";
173
174 // Update offset cache (chunk origin offsets are non-zero for infinite maps)
175 hasOffsets_ = (chunkOriginX_ != 0 || chunkOriginY_ != 0 ||
176 globalOffsetX_ != 0.0f || globalOffsetY_ != 0.0f);
177 }
178 else
179 {
180 // Non-infinite maps: use declared dimensions
181 mapWidth_ = tiledMap.width;
182 mapHeight_ = tiledMap.height;
183
184 // No chunk offset for finite maps
185 chunkOriginX_ = 0;
186 chunkOriginY_ = 0;
187
188 // For finite maps, bounds are from 0 to width/height - 1
189 minTileX_ = 0;
190 minTileY_ = 0;
191 maxTileX_ = mapWidth_ - 1;
192 maxTileY_ = mapHeight_ - 1;
193
194 SYSTEM_LOG << " -> Map size (from TMJ): " << mapWidth_ << "x" << mapHeight_ << " tiles\n\n";
195
196 // Update offset cache (only global offsets might be non-zero)
197 hasOffsets_ = (globalOffsetX_ != 0.0f || globalOffsetY_ != 0.0f);
198 }
199
200 // Initialize config with map properties
201 config_.tileWidth = tiledMap.tilewidth;
202 config_.tileHeight = tiledMap.tileheight;
203
204 switch (tiledMap.orientation) {
206 config_.mapOrientation = "orthogonal";
207 break;
209 config_.mapOrientation = "isometric";
210 break;
211 default:
212 config_.mapOrientation = "orthogonal";
213 }
214
215 SYSTEM_LOG << " Map Orientation: " << config_.mapOrientation
216 << " (" << config_.tileWidth << "x" << config_.tileHeight << ")\n";
217
218 // ===================================================================
219 // PHASE 1: MAP CONFIGURATION & METADATA
220 // ===================================================================
221 SYSTEM_LOG << "[Phase 1/6] Extracting Map Configuration & Metadata...\n";
224
225 // ===================================================================
226 // PHASE 2: VISUAL LAYERS (Parallax, Image Layers, Tile Layers)
227 // ===================================================================
228 SYSTEM_LOG << "[Phase 2/6] Processing Visual Layers...\n";
229 int visualLayerCount = 0;
231 SYSTEM_LOG << " ok - Processed " << visualLayerCount << " visual layers\n";
232
233 // ===================================================================
234 // PHASE 3: SPATIAL STRUCTURES (Sectors, Collision, Navigation)
235 // ===================================================================
236 SYSTEM_LOG << "[Phase 3/6] Extracting Spatial Structures...\n";
237 int spatialObjectCount = 0;
239 SYSTEM_LOG << " ok - Extracted " << spatialObjectCount << " spatial objects\n";
240
241 // ===================================================================
242 // PHASE 4: GAME OBJECTS (Categorized by Type)
243 // ===================================================================
244 SYSTEM_LOG << "[Phase 4/6] Converting Game Objects...\n";
245 ConversionStats stats;
247 SYSTEM_LOG << " ok - Static: " << stats.staticObjects
248 << " | Dynamic: " << stats.dynamicObjects
249 << " | Paths: " << stats.patrolPaths
250 << " | Sounds: " << stats.soundObjects << "\n";
251
252 // ===================================================================
253 // POST-CONVERSION: Normalize Entity Types Immediately
254 // ===================================================================
255 SYSTEM_LOG << "[Post-Conversion] Normalizing Entity Types...\n";
257
258 int normalizedCount = 0;
259 for (auto& entity : outLevel.entities)
260 {
261 if (!entity) continue;
262
263 std::string originalType = entity->type;
264 entity->type = factory.NormalizeType(originalType);
265
266 if (originalType != entity->type)
267 {
269 }
270 }
271
272 // Also normalize categorizedObjects (they may be copies)
273 for (auto& entity : outLevel.categorizedObjects.dynamicObjects)
274 {
275 if (entity) entity->type = factory.NormalizeType(entity->type);
276 }
277 for (auto& entity : outLevel.categorizedObjects.staticObjects)
278 {
279 if (entity) entity->type = factory.NormalizeType(entity->type);
280 }
281 for (auto& entity : outLevel.categorizedObjects.patrolPaths)
282 {
283 if (entity) entity->type = factory.NormalizeType(entity->type);
284 }
285 for (auto& entity : outLevel.categorizedObjects.soundObjects)
286 {
287 if (entity) entity->type = factory.NormalizeType(entity->type);
288 }
289
290 SYSTEM_LOG << " ok - Normalized " << normalizedCount << " entity types\n";
291
292 // ===================================================================
293 // PHASE 5: OBJECT RELATIONSHIPS (Links, References)
294 // ===================================================================
295 SYSTEM_LOG << "[Phase 5/6] Extracting Object Relationships...\n";
296 int linkCount = 0;
298 SYSTEM_LOG << " ok - Created " << linkCount << " object links\n";
299
300 // ===================================================================
301 // PHASE 6: RESOURCE CATALOG
302 // ===================================================================
303 SYSTEM_LOG << "[Phase 6/6] Building Resource Catalog...\n";
305 SYSTEM_LOG << " ok - Tilesets: " << outLevel.resources.tilesetPaths.size()
306 << " | Images: " << outLevel.resources.imagePaths.size()
307 << " | Audio: " << outLevel.resources.audioPaths.size() << "\n";
308
309 // ===================================================================
310 // FINAL SUMMARY
311 // ===================================================================
312 SYSTEM_LOG << "\n+===========================================================+\n";
313 SYSTEM_LOG << "| CONVERSION COMPLETE |\n";
314 SYSTEM_LOG << "+===========================================================+\n";
315 SYSTEM_LOG << "| Map: " << outLevel.mapConfig.orientation
316 << " " << outLevel.mapConfig.mapWidth << "x" << outLevel.mapConfig.mapHeight << "\n";
317 SYSTEM_LOG << "| Visual Layers: " << visualLayerCount << "\n";
318 SYSTEM_LOG << "| Entities: " << stats.totalObjects << "\n";
319 SYSTEM_LOG << "| Relationships: " << linkCount << "\n";
320 SYSTEM_LOG << "+===========================================================+\n\n";
321
322 return true;
323 }
324
326 {
327 SYSTEM_LOG << "TiledToOlympe: Converting tile layer '" << layer.name << "'" << std::endl;
328
329 // Check if this is a collision layer
331 // Treat non-zero tiles as collision
332 int index = 0;
333 for (int y = 0; y < layer.height && y < mapHeight_; ++y) {
334 for (int x = 0; x < layer.width && x < mapWidth_; ++x) {
335 if (index < static_cast<int>(layer.data.size())) {
336 uint32_t gid = layer.data[index];
338 if (tileId > 0) {
339 level.collisionMap[y][x] = 0xFF; // Solid collision
340 }
341 }
342 ++index;
343 }
344 }
345 }
346 else {
347 // Regular tile layer - merge into tilemap
348 MergeTileLayer(layer, level.tileMap, mapWidth_, mapHeight_);
349 }
350 }
351
353 {
354 SYSTEM_LOG << "TiledToOlympe: Converting object layer '" << layer.name
355 << "' with " << layer.objects.size() << " objects" << std::endl;
356
357 // Log layer offsets if non-zero
358 if (layer.offsetx != 0.0f || layer.offsety != 0.0f) {
359 SYSTEM_LOG << " -> Layer has offsets: offsetx=" << layer.offsetx
360 << ", offsety=" << layer.offsety << std::endl;
361 }
362
363 for (const auto& obj : layer.objects) {
364 ConvertObject(obj, level, layer.offsetx, layer.offsety);
365 }
366 }
367
369 {
370 SYSTEM_LOG << "TiledToOlympe: Converting image layer '" << layer.name << "'" << std::endl;
371
373 parallax.name = layer.name;
374
375 // Resolve image path
376 if (!layer.image.empty()) {
377 if (!config_.resourceBasePath.empty()) {
378 parallax.imagePath = config_.resourceBasePath + "/" + layer.image;
379 } else {
380 parallax.imagePath = layer.image;
381 }
382 }
383
384 parallax.scrollFactorX = layer.parallaxx;
385 parallax.scrollFactorY = layer.parallaxy;
386 parallax.offsetX = layer.offsetx;
387 parallax.offsetY = layer.offsety;
388 parallax.opacity = layer.opacity;
389 parallax.repeatX = layer.repeatx;
390 parallax.repeatY = layer.repeaty;
391 parallax.visible = layer.visible;
392 parallax.tintColor = layer.tintcolor;
393
395 }
396
398 {
399 SYSTEM_LOG << "TiledToOlympe: Converting group layer '" << layer.name << "'" << std::endl;
400
401 // Recursively process child layers
402 for (const auto& childLayer : layer.layers) {
403 if (!childLayer->visible) {
404 continue;
405 }
406
407 switch (childLayer->type) {
410 break;
413 break;
416 break;
417 case LayerType::Group:
419 break;
420 }
421 }
422 }
423
425 float layerOffsetX, float layerOffsetY)
426 {
427 // Check for collision polygons/polylines first
428 std::string typeLower = obj.type;
429 std::transform(typeLower.begin(), typeLower.end(), typeLower.begin(), ::tolower);
430
431 if ((typeLower == "collision" || typeLower.find("collision") != std::string::npos))
432 {
433 if (obj.objectType == ObjectType::Polygon || obj.objectType == ObjectType::Polyline)
434 {
436 return;
437 }
438 }
439
440 // Check for patrol paths (polyline objects)
441 if (obj.objectType == ObjectType::Polyline) {
443 return;
444 }
445
446 // Check for sector polygons
447 if (obj.objectType == ObjectType::Polygon) {
449 return;
450 }
451
452 // Regular entity
454 if (entityDescriptor) {
455 level.entities.push_back(std::move(entityDescriptor));
456 }
457 }
458
460 {
461 // Convert rectangle to collision tiles
462 int tileSize = 32; // Default tile size
463 int startX = static_cast<int>(obj.x / tileSize);
464 int startY = static_cast<int>(TransformY(obj.y, obj.height) / tileSize);
465 int endX = static_cast<int>((obj.x + obj.width) / tileSize);
466 int endY = static_cast<int>((TransformY(obj.y, obj.height) + obj.height) / tileSize);
467
468 for (int y = startY; y <= endY && y < mapHeight_; ++y) {
469 for (int x = startX; x <= endX && x < mapWidth_; ++x) {
470 if (y >= 0 && x >= 0) {
471 level.collisionMap[y][x] = 0xFF;
472 }
473 }
474 }
475 }
476
478 float layerOffsetX, float layerOffsetY)
479 {
480 // Create a sector entity
481 auto entity = std::make_unique<Olympe::Editor::EntityInstance>();
482
483 // Generate unique ID
484 entity->id = "sector_" + std::to_string(obj.id);
485 entity->name = obj.name.empty() ? ("Sector " + std::to_string(obj.id)) : obj.name;
486 entity->prefabPath = "Blueprints/Sector.json";
487
488 // Transform position based on map orientation
489 entity->position = TransformObjectPosition(obj.x, obj.y, layerOffsetX, layerOffsetY, obj.gid);
490
491 // Store polygon points in overrides
492 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
493 // Only apply flipY for orthogonal maps
494 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
495 nlohmann::json polygon = nlohmann::json::array();
496 for (const auto& pt : obj.polygon) {
497 nlohmann::json point = nlohmann::json::object();
498 point["x"] = pt.x;
499 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
500 polygon.push_back(point);
501 }
502
503 entity->overrides["Sector"] = nlohmann::json::object();
504 entity->overrides["Sector"]["polygon"] = polygon;
505 entity->overrides["Sector"]["type"] = obj.type;
506
507 // Convert properties
508 PropertiesToOverrides(obj.properties, entity->overrides);
509
510 level.entities.push_back(std::move(entity));
511 }
512
514 float layerOffsetX, float layerOffsetY)
515 {
516 // Create a collision polygon entity
517 auto entity = std::make_unique<Olympe::Editor::EntityInstance>();
518
519 entity->id = "collision_poly_" + std::to_string(obj.id);
520 entity->name = obj.name.empty() ? ("CollisionPoly " + std::to_string(obj.id)) : obj.name;
521 entity->type = "CollisionPolygon";
522 entity->prefabPath = "Blueprints/CollisionPolygon.json";
523
524 // Transform position based on map orientation
525 entity->position = TransformObjectPosition(obj.x, obj.y, layerOffsetX, layerOffsetY, obj.gid);
526 entity->rotation = obj.rotation;
527
528 // Store polygon/polyline points
529 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
530 // Only apply flipY for orthogonal maps
531 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
532 nlohmann::json polygon = nlohmann::json::array();
533 const auto& points = (obj.objectType == ObjectType::Polygon) ? obj.polygon : obj.polyline;
534
535 for (const auto& pt : points)
536 {
537 nlohmann::json point = nlohmann::json::object();
538 point["x"] = pt.x;
539 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
540 polygon.push_back(point);
541 }
542
543 entity->overrides["CollisionPolygon_data"] = nlohmann::json::object();
544 entity->overrides["CollisionPolygon_data"]["points"] = polygon;
545 entity->overrides["CollisionPolygon_data"]["closed"] = (obj.objectType == ObjectType::Polygon);
546
547 // Store dimensions for bounding box fallback
548 entity->overrides["width"] = obj.width;
549 entity->overrides["height"] = obj.height;
550
551 // Convert properties
552 PropertiesToOverrides(obj.properties, entity->overrides);
553
554 level.entities.push_back(std::move(entity));
555 }
556
558 float layerOffsetX, float layerOffsetY)
559 {
560 // Create a patrol path entity
561 auto entity = std::make_unique<Olympe::Editor::EntityInstance>();
562
563 entity->id = "patrol_" + std::to_string(obj.id);
564 entity->name = obj.name.empty() ? ("Patrol " + std::to_string(obj.id)) : obj.name;
565 entity->prefabPath = "Blueprints/PatrolPath.json";
566
567 // Transform position based on map orientation
568 entity->position = TransformObjectPosition(obj.x, obj.y, layerOffsetX, layerOffsetY, obj.gid);
569
570 // Store polyline points in overrides
571 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
572 // Only apply flipY for orthogonal maps
573 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
574 nlohmann::json path = nlohmann::json::array();
575 for (const auto& pt : obj.polyline) {
576 nlohmann::json point = nlohmann::json::object();
577 point["x"] = pt.x;
578 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
579 path.push_back(point);
580 }
581
582 // REQUIREMENT C: Duplicate patrol path for compatibility
583 // Store in both AIBlackboard_data.patrolPath (component-scoped)
584 // AND flat patrolPath for World::InstantiatePass5_Relationships
585 entity->overrides["AIBlackboard_data"] = nlohmann::json::object();
586 entity->overrides["AIBlackboard_data"]["patrolPath"] = path;
587 entity->overrides["patrolPath"] = path; // Flat override for relationship system
588
589 // Convert properties
590 PropertiesToOverrides(obj.properties, entity->overrides);
591
592 level.entities.push_back(std::move(entity));
593 }
594
595 std::unique_ptr<Olympe::Editor::EntityInstance> TiledToOlympe::ParseEntityDescriptor(const TiledObject& obj,
596 float layerOffsetX,
597 float layerOffsetY)
598 {
599 auto entityDescriptor = std::make_unique<Olympe::Editor::EntityInstance>();
600
601 // Generate unique ID (memory structure, NOT ECS entity)
602 entityDescriptor->id = "entity_" + std::to_string(obj.id);
603 entityDescriptor->name = obj.name.empty() ? ("Object " + std::to_string(obj.id)) : obj.name;
604
605 // Store entity type (will be normalized later)
606 entityDescriptor->type = obj.type;
607
608 // Get prefab path from type mapping
609 entityDescriptor->prefabPath = GetPrefabPath(obj.type);
610
611 // Transform position based on map orientation (isometric vs orthogonal)
613
614 SYSTEM_LOG << " -> Parsed entity descriptor: '" << entityDescriptor->name
615 << "' (type: " << entityDescriptor->type << ")\n";
616
617 // Store rotation (extract to entity level field)
618 entityDescriptor->rotation = obj.rotation;
619
620 // Convert properties to overrides
621 PropertiesToOverrides(obj.properties, entityDescriptor->overrides);
622
623 // Store TMJ native fields as flat properties (not under "Transform")
624 // These will be mapped to component-scoped overrides by World.cpp based on prefab components
625
626 // Store dimensions (width, height)
627 // IMPORTANT: Always store these, even if 0, because World.cpp needs explicit 0 values
628 // to detect point objects (spawners, waypoints) via width==0 && height==0 check
629 entityDescriptor->overrides["width"] = obj.width;
630 entityDescriptor->overrides["height"] = obj.height;
631
632 // Store rotation if non-zero
633 if (obj.rotation != 0.0f) {
634 entityDescriptor->overrides["rotation"] = obj.rotation;
635 }
636
637 // Store visible flag if false (true is default, so absence implies visible)
638 if (!obj.visible) {
639 entityDescriptor->overrides["visible"] = obj.visible;
640 }
641
642 // Store point flag for point objects (absence implies non-point object)
643 // Point objects are spawners, waypoints, etc. that have no collision size
644 if (obj.objectType == ObjectType::Point) {
645 entityDescriptor->overrides["point"] = true;
646 }
647
648 return entityDescriptor;
649 }
650
652 const std::map<std::string, TiledProperty>& properties,
653 nlohmann::json& overrides)
654 {
655 for (const auto& pair : properties) {
656 const TiledProperty& prop = pair.second;
657
658 // Check if property name contains a component prefix (e.g., "Transform.width")
659 std::string propName = prop.name;
660 size_t dotPos = propName.find('.');
661
663
664 // Convert property value to JSON
665 switch (prop.type) {
669 propValue = prop.stringValue;
670 break;
672 propValue = prop.intValue;
673 break;
675 propValue = prop.floatValue;
676 break;
678 propValue = prop.boolValue;
679 break;
680 default:
681 propValue = prop.stringValue;
682 break;
683 }
684
685 // Validate component-scoped property format
686 // Must have exactly one dot, with content before and after it
687 // Rejects: ".property", "Component.", "..property", "Component..param", etc.
688 if (dotPos != std::string::npos && dotPos > 0 && dotPos < propName.length() - 1)
689 {
690 // Check for multiple dots (nested structures not supported)
691 size_t secondDot = propName.find('.', dotPos + 1);
692 if (secondDot != std::string::npos)
693 {
694 // Multiple dots found - treat as flat property
695 SYSTEM_LOG << "[TiledToOlympe] WARNING: Property '" << propName
696 << "' has multiple dots (nested structures not supported). "
697 << "Treating as flat property." << std::endl;
698 overrides[propName] = propValue;
699 }
700 else
701 {
702 // Valid component-scoped property
703 std::string componentName = propName.substr(0, dotPos);
704 std::string paramName = propName.substr(dotPos + 1);
705
706 // Ensure component object exists
707 if (!overrides.contains(componentName) || !overrides[componentName].is_object()) {
708 overrides[componentName] = nlohmann::json::object();
709 }
710
711 // Store in component-scoped structure
712 overrides[componentName][paramName] = propValue;
713 }
714 }
715 else
716 {
717 // Store as flat property (legacy/backward compatibility)
718 // This includes properties starting with dot, ending with dot, or no dot at all
719 overrides[propName] = propValue;
720 }
721 }
722 }
723
724 std::string TiledToOlympe::GetPrefabPath(const std::string& objectType)
725 {
726 if (objectType.empty()) {
727 return config_.defaultPrefab;
728 }
729
730 // Check type mapping
731 auto it = config_.typeToPrefabMap.find(objectType);
732 if (it != config_.typeToPrefabMap.end()) {
733 return it->second;
734 }
735
736 // Default: try to construct path from type
737 return "Blueprints/" + objectType + ".json";
738 }
739
740 bool TiledToOlympe::MatchesPattern(const std::string& layerName,
741 const std::vector<std::string>& patterns)
742 {
743 std::string lowerName = layerName;
744 std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
745
746 for (const auto& pattern : patterns) {
747 std::string lowerPattern = pattern;
748 std::transform(lowerPattern.begin(), lowerPattern.end(), lowerPattern.begin(), ::tolower);
749
750 if (lowerName.find(lowerPattern) != std::string::npos) {
751 return true;
752 }
753 }
754
755 return false;
756 }
757
758 float TiledToOlympe::TransformY(float y, float height)
759 {
760 if (config_.flipY) {
761 // Flip Y coordinate (Tiled top-left vs bottom-left origin)
762 float worldHeight = mapHeight_ * 32.0f; // Assume 32px tiles
763 return worldHeight - y - height;
764 }
765 return y;
766 }
767
769 {
770 if (!tilesets_ || gid == 0) {
771 return nullptr;
772 }
773
774 // Strip flip flags to get actual tile ID
775 // GetTileId() removes horizontal/vertical/diagonal flip flags (stored in high bits)
776 // and returns the base tile ID that can be used to look up the tileset
778
779 // Tilesets are ordered by firstgid. Find the tileset that owns this gid.
780 const TiledTileset* matchingTileset = nullptr;
781 for (const auto& tileset : *tilesets_) {
782 if (tileId >= static_cast<uint32_t>(tileset.firstgid)) {
783 matchingTileset = &tileset;
784 } else {
785 // Since tilesets are ordered, we've gone past our tileset
786 break;
787 }
788 }
789
790 return matchingTileset;
791 }
792
794 {
795 // Transform object position based on map orientation
796
797 if (config_.mapOrientation == "isometric")
798 {
799 // ISOMETRIC TMJ TO WORLD COORDINATE CONVERSION
800 // --------------------------------------------
801 // TMJ stores positions where BOTH x and y use tileHeight as the unit.
802 // Formula: tileCoord = tmjPixel / tileHeight, then standard iso projection.
803
804 const float tileWidth = static_cast<float>(config_.tileWidth);
805 const float tileHeight = static_cast<float>(config_.tileHeight);
806 const float halfWidth = tileWidth * 0.5f;
807 const float halfHeight = tileHeight * 0.5f;
808
809 // Convert TMJ pixel coords to tile coords (both use tileHeight!)
810 const float tileX = (tileHeight != 0.0f) ? (x / tileHeight) : 0.0f;
811 const float tileY = (tileHeight != 0.0f) ? (y / tileHeight) : 0.0f;
812
813 // Standard isometric projection
814 float worldX = (tileX - tileY) * halfWidth;
815 float worldY = (tileX + tileY) * halfHeight;
816
817 // Apply layer offsets (also in isometric pixel space)
818 if (layerOffsetX != 0.0f || layerOffsetY != 0.0f) {
819 float layerTileX = layerOffsetX / tileHeight;
820 float layerTileY = layerOffsetY / tileHeight;
821 worldX += (layerTileX - layerTileY) * halfWidth;
822 worldY += (layerTileX + layerTileY) * halfHeight;
823 }
824
825 // Apply tileset offsets for tile objects (gid > 0)
826 if (gid > 0) {
827 const TiledTileset* tileset = FindTilesetForGid(gid);
828 if (tileset) {
829 worldX += static_cast<float>(tileset->tileoffsetX);
830 worldY += static_cast<float>(tileset->tileoffsetY);
831 }
832 }
833
834 // Debug logging
835 SYSTEM_LOG << "[TransformObjectPosition] ISO: TMJ(" << x << ", " << y << ")"
836 << " -> tile(" << tileX << ", " << tileY << ")"
837 << " -> world(" << worldX << ", " << worldY << ")\n";
838
839 return Vector(worldX, worldY, 0.0f);
840 }
841
842 // Orthogonal: direct pixel coordinates with layer offset
843 float posX = x + layerOffsetX;
844 float posY = y + layerOffsetY;
845
846 return Vector(posX, posY, 0.0f);
847 }
848
850 int width, int height)
851 {
852 level.collisionMap.resize(height);
853 for (int y = 0; y < height; ++y) {
854 level.collisionMap[y].resize(width, 0);
855 }
856 }
857
859 std::vector<std::vector<int>>& tileMap,
860 int mapWidth, int mapHeight)
861 {
862 if (layer.data.empty()) {
863 return;
864 }
865
866 int index = 0;
867 for (int y = 0; y < layer.height && y < mapHeight; ++y) {
868 for (int x = 0; x < layer.width && x < mapWidth; ++x) {
869 if (index < static_cast<int>(layer.data.size())) {
870 uint32_t gid = layer.data[index];
872
873 // Only overwrite if tile is not empty (0)
874 if (tileId > 0) {
875 tileMap[y][x] = static_cast<int>(tileId);
876 }
877 }
878 ++index;
879 }
880 }
881 }
882
883 // ===================================================================
884 // NEW 6-PHASE PIPELINE IMPLEMENTATION
885 // ===================================================================
886
889 {
890 outLevel.mapConfig.mapWidth = tiledMap.width;
891 outLevel.mapConfig.mapHeight = tiledMap.height;
892 outLevel.mapConfig.tileWidth = tiledMap.tilewidth;
893 outLevel.mapConfig.tileHeight = tiledMap.tileheight;
894 outLevel.mapConfig.infinite = tiledMap.infinite;
895
896 // Convert orientation enum to string
897 switch (tiledMap.orientation) {
898 case MapOrientation::Orthogonal: outLevel.mapConfig.orientation = "orthogonal"; break;
899 case MapOrientation::Isometric: outLevel.mapConfig.orientation = "isometric"; break;
900 case MapOrientation::Staggered: outLevel.mapConfig.orientation = "staggered"; break;
901 case MapOrientation::Hexagonal: outLevel.mapConfig.orientation = "hexagonal"; break;
902 default: outLevel.mapConfig.orientation = "unknown"; break;
903 }
904
905 // Convert render order
906 switch (tiledMap.renderorder) {
907 case RenderOrder::RightDown: outLevel.mapConfig.renderOrder = "right-down"; break;
908 case RenderOrder::RightUp: outLevel.mapConfig.renderOrder = "right-up"; break;
909 case RenderOrder::LeftDown: outLevel.mapConfig.renderOrder = "left-down"; break;
910 case RenderOrder::LeftUp: outLevel.mapConfig.renderOrder = "left-up"; break;
911 }
912
913 // Store render order in conversion config for use in coordinate transformations
914 config_.renderOrder = outLevel.mapConfig.renderOrder;
915
916 // Cache Y-flip requirement for performance (avoids repeated string comparisons)
917 requiresYFlip_ = (config_.renderOrder == "left-up" || config_.renderOrder == "right-up");
918
919 // Set world size
920 outLevel.worldSize.x = (float) tiledMap.width * tiledMap.tilewidth;
921 outLevel.worldSize.y = (float) tiledMap.height * tiledMap.tileheight;
922
923 // Background color
924 if (!tiledMap.backgroundcolor.empty()) {
925 outLevel.ambientColor = tiledMap.backgroundcolor;
926 }
927
928 SYSTEM_LOG << " -> Map: " << outLevel.mapConfig.orientation
929 << " " << outLevel.mapConfig.mapWidth << "x" << outLevel.mapConfig.mapHeight
930 << " (tiles: " << outLevel.mapConfig.tileWidth << "x" << outLevel.mapConfig.tileHeight << ")\n";
931 SYSTEM_LOG << " -> Render order: " << config_.renderOrder << "\n";
932 }
933
936 {
937 // Store map configuration for rendering
938 // FIX: Store orientation as STRING, not INT
939 switch (tiledMap.orientation) {
941 outLevel.metadata.customData["orientation"] = "orthogonal";
942 break;
944 outLevel.metadata.customData["orientation"] = "isometric";
945 break;
947 outLevel.metadata.customData["orientation"] = "staggered";
948 break;
950 outLevel.metadata.customData["orientation"] = "hexagonal";
951 break;
952 default:
953 outLevel.metadata.customData["orientation"] = "unknown";
954 break;
955 }
956
957 outLevel.metadata.customData["tilewidth"] = tiledMap.tilewidth;
958 outLevel.metadata.customData["tileheight"] = tiledMap.tileheight;
959
960 // Store map bounds for isometric origin calculation and chunk origin for entity offset
961 outLevel.metadata.customData["minTileX"] = minTileX_;
962 outLevel.metadata.customData["minTileY"] = minTileY_;
963 outLevel.metadata.customData["maxTileX"] = maxTileX_;
964 outLevel.metadata.customData["maxTileY"] = maxTileY_;
965 outLevel.metadata.customData["chunkOriginX"] = chunkOriginX_;
966 outLevel.metadata.customData["chunkOriginY"] = chunkOriginY_;
967
968 // Convert map custom properties to metadata
969 for (const auto& prop : tiledMap.properties) {
970 outLevel.metadata.customData[prop.first] = PropertyToJSON(prop.second);
971 }
972 }
973
976 int& layerCount)
977 {
978 layerCount = 0;
979 int zOrder = 0;
980
981 // Initialize tile map
982 outLevel.tileMap.resize(mapHeight_);
983 for (int y = 0; y < mapHeight_; ++y) {
984 outLevel.tileMap[y].resize(mapWidth_, 0);
985 }
986
987 for (const auto& layer : tiledMap.layers) {
988 if (!layer->visible) continue;
989
990 switch (layer->type) {
992 // Parallax/Background layers
994 visual.name = layer->name;
995 visual.zOrder = zOrder++;
996 visual.isParallax = (layer->parallaxx != 1.0f || layer->parallaxy != 1.0f);
997 visual.imagePath = ResolveImagePath(layer->image);
998 visual.scrollFactorX = layer->parallaxx;
999 visual.scrollFactorY = layer->parallaxy;
1000 visual.offsetX = layer->offsetx;
1001 visual.offsetY = layer->offsety;
1002 visual.repeatX = layer->repeatx;
1003 visual.repeatY = layer->repeaty;
1004 visual.opacity = layer->opacity;
1005 visual.tintColor = layer->tintcolor;
1006 visual.visible = layer->visible;
1007
1008 outLevel.visualLayers.push_back(visual);
1009 layerCount++;
1010
1011 // Also add to parallax layer manager for backward compatibility
1012 ConvertImageLayer(*layer);
1013
1014 SYSTEM_LOG << " -> Image Layer: '" << visual.name << "' (parallax: "
1015 << visual.scrollFactorX << ", z: " << visual.zOrder << ")\n";
1016 break;
1017 }
1018
1019 case LayerType::TileLayer: {
1020 // Skip collision layers (handled in Phase 3)
1021 if (MatchesPattern(layer->name, config_.collisionLayerPatterns)) {
1022 break;
1023 }
1024
1025 // Visual tile layer
1027 tileDef.name = layer->name;
1028 tileDef.zOrder = zOrder++;
1029 tileDef.opacity = layer->opacity;
1030 tileDef.visible = layer->visible;
1031 tileDef.isInfinite = !layer->chunks.empty();
1032
1033 // Debug: Log startx/starty offsets
1034 if (layer->startx != 0 || layer->starty != 0) {
1035 SYSTEM_LOG << " -> [DEBUG] Tile Layer '" << layer->name
1036 << "' has startx=" << layer->startx
1037 << ", starty=" << layer->starty << "\n";
1038 }
1039
1040 // Handle infinite maps with chunks
1041 if (tileDef.isInfinite) {
1042 for (const auto& chunk : layer->chunks) {
1044 chunkDef.x = chunk.x;
1045 chunkDef.y = chunk.y;
1046 chunkDef.width = chunk.width;
1047 chunkDef.height = chunk.height;
1048
1049 // Extract chunk tile data and flip flags
1050 chunkDef.tiles.resize(chunk.height);
1051 chunkDef.tileFlipFlags.resize(chunk.height);
1052 int index = 0;
1053 for (int y = 0; y < chunk.height; ++y) {
1054 chunkDef.tiles[y].resize(chunk.width, 0);
1055 chunkDef.tileFlipFlags[y].resize(chunk.width, 0);
1056 for (int x = 0; x < chunk.width; ++x) {
1057 if (index < static_cast<int>(chunk.data.size())) {
1058 uint32_t gid = chunk.data[index];
1059 chunkDef.tiles[y][x] = GetTileId(gid);
1060 chunkDef.tileFlipFlags[y][x] = ExtractFlipFlags(gid);
1061 }
1062 ++index;
1063 }
1064 }
1065
1066 tileDef.chunks.push_back(chunkDef);
1067 }
1068
1069 SYSTEM_LOG << " -> Tile Layer (Infinite): '" << tileDef.name << "' ("
1070 << tileDef.chunks.size() << " chunks, z: " << tileDef.zOrder << ")\n";
1071 }
1072 // Handle finite maps with regular data
1073 else {
1074 // Extract tile data and flip flags
1075 tileDef.tiles.resize(layer->height);
1076 tileDef.tileFlipFlags.resize(layer->height);
1077 int index = 0;
1078 for (int y = 0; y < layer->height; ++y) {
1079 tileDef.tiles[y].resize(layer->width, 0);
1080 tileDef.tileFlipFlags[y].resize(layer->width, 0);
1081 for (int x = 0; x < layer->width; ++x) {
1082 if (index < static_cast<int>(layer->data.size())) {
1083 uint32_t gid = layer->data[index];
1084 tileDef.tiles[y][x] = GetTileId(gid);
1085 tileDef.tileFlipFlags[y][x] = ExtractFlipFlags(gid);
1086 }
1087 ++index;
1088 }
1089 }
1090
1091 SYSTEM_LOG << " -> Tile Layer: '" << tileDef.name << "' ("
1092 << layer->width << "x" << layer->height << " tiles, z: " << tileDef.zOrder << ")\n";
1093 }
1094
1095 outLevel.tileLayers.push_back(tileDef);
1096 layerCount++;
1097
1098 // Also merge into legacy tileMap for backward compatibility
1099 MergeTileLayer(*layer, outLevel.tileMap, mapWidth_, mapHeight_);
1100
1101 break;
1102 }
1103
1104 case LayerType::Group: {
1105 // Recursively process group layers
1106 ProcessGroupLayers(*layer, outLevel, zOrder, layerCount);
1107 break;
1108 }
1109
1110 default:
1111 break;
1112 }
1113 }
1114
1115 // Store parallax layers in metadata for backward compatibility
1116 if (ParallaxLayerManager::Get().GetLayerCount() > 0)
1117 {
1118 nlohmann::json parallaxLayersJson = nlohmann::json::array();
1119
1120 for (size_t i = 0; i < ParallaxLayerManager::Get().GetLayerCount(); ++i)
1121 {
1123 if (layer)
1124 {
1125 nlohmann::json layerJson = nlohmann::json::object();
1126 layerJson["name"] = layer->name;
1127 layerJson["imagePath"] = layer->imagePath;
1128 layerJson["scrollFactorX"] = layer->scrollFactorX;
1129 layerJson["scrollFactorY"] = layer->scrollFactorY;
1130 layerJson["repeatX"] = layer->repeatX;
1131 layerJson["repeatY"] = layer->repeatY;
1132 layerJson["offsetX"] = layer->offsetX;
1133 layerJson["offsetY"] = layer->offsetY;
1134 layerJson["opacity"] = layer->opacity;
1135 layerJson["zOrder"] = static_cast<int>(i);
1136 layerJson["visible"] = layer->visible;
1137 layerJson["tintColor"] = layer->tintColor;
1138 parallaxLayersJson.push_back(layerJson);
1139 }
1140 }
1141
1142 outLevel.metadata.customData["parallaxLayers"] = parallaxLayersJson;
1143 }
1144
1145 // Store tile layers in metadata
1146 if (!outLevel.tileLayers.empty())
1147 {
1148 nlohmann::json tileLayersJson = nlohmann::json::array();
1149
1150 for (const auto& tileLayer : outLevel.tileLayers)
1151 {
1152 nlohmann::json layerJson = nlohmann::json::object();
1153 layerJson["name"] = tileLayer.name;
1154 layerJson["type"] = "tilelayer";
1155 layerJson["zOrder"] = tileLayer.zOrder;
1156 layerJson["opacity"] = tileLayer.opacity;
1157 layerJson["visible"] = tileLayer.visible;
1158 layerJson["isInfinite"] = tileLayer.isInfinite;
1159 layerJson["encoding"] = "base64"; // Store encoding for decoding
1160
1161 if (tileLayer.isInfinite && !tileLayer.chunks.empty())
1162 {
1163 nlohmann::json chunksJson = nlohmann::json::array();
1164
1165 for (const auto& chunk : tileLayer.chunks)
1166 {
1167 nlohmann::json chunkJson = nlohmann::json::object();
1168 chunkJson["x"] = chunk.x;
1169 chunkJson["y"] = chunk.y;
1170 chunkJson["width"] = chunk.width;
1171 chunkJson["height"] = chunk.height;
1172
1173 // Flatten tile data to array (with flip flags)
1174 nlohmann::json dataJson = nlohmann::json::array();
1175 for (int y = 0; y < chunk.height; ++y)
1176 {
1177 for (int x = 0; x < chunk.width; ++x)
1178 {
1179 uint32_t gid = 0;
1180 uint8_t flipFlags = 0;
1181
1182 if (y < chunk.tiles.size() && x < chunk.tiles[y].size())
1183 {
1184 gid = chunk.tiles[y][x];
1185 }
1186
1187 if (y < chunk.tileFlipFlags.size() && x < chunk.tileFlipFlags[y].size())
1188 {
1189 flipFlags = chunk.tileFlipFlags[y][x];
1190 }
1191
1192 // Reconstruct GID with flip flags (Tiled format)
1193 uint32_t fullGID = gid;
1194 if (flipFlags & 0x1) fullGID |= 0x80000000; // Horizontal flip
1195 if (flipFlags & 0x2) fullGID |= 0x40000000; // Vertical flip
1196 if (flipFlags & 0x4) fullGID |= 0x20000000; // Diagonal flip
1197
1198 dataJson.push_back((int)fullGID);
1199 }
1200 }
1201
1202 chunkJson["data"] = dataJson;
1203 chunksJson.push_back(chunkJson);
1204 }
1205
1206 layerJson["chunks"] = chunksJson;
1207 }
1208 else if (!tileLayer.tiles.empty())
1209 {
1210 // Finite map
1211 int width = tileLayer.tiles.empty() ? 0 : static_cast<int>(tileLayer.tiles[0].size());
1212 int height = static_cast<int>(tileLayer.tiles.size());
1213
1214 layerJson["width"] = width;
1215 layerJson["height"] = height;
1216
1217 nlohmann::json dataJson = nlohmann::json::array();
1218 for (int y = 0; y < height; ++y)
1219 {
1220 for (int x = 0; x < width; ++x)
1221 {
1222 int gid = 0;
1223 if (y < tileLayer.tiles.size() && x < tileLayer.tiles[y].size())
1224 {
1225 gid = tileLayer.tiles[y][x];
1226 }
1227 dataJson.push_back(static_cast<int>(gid));
1228 }
1229 }
1230 layerJson["data"] = dataJson;
1231 }
1232
1233 tileLayersJson.push_back(layerJson);
1234 }
1235
1236 outLevel.metadata.customData["tileLayers"] = tileLayersJson;
1237
1238 SYSTEM_LOG << " ok - Stored " << tileLayersJson.size()
1239 << " tile layers in metadata\n";
1240 }
1241 }
1242
1245 int& objectCount)
1246 {
1247 objectCount = 0;
1248 int collisionTileCount = 0;
1249
1250 // Initialize collision map
1252
1253 for (const auto& layer : tiledMap.layers) {
1254 if (!layer->visible) continue;
1255
1256 // Process collision tile layers
1257 if (layer->type == LayerType::TileLayer &&
1259
1260 int index = 0;
1261 for (int y = 0; y < layer->height && y < mapHeight_; ++y) {
1262 for (int x = 0; x < layer->width && x < mapWidth_; ++x) {
1263 if (index < static_cast<int>(layer->data.size())) {
1264 uint32_t tileId = GetTileId(layer->data[index]);
1265 if (tileId > 0) {
1266 outLevel.collisionMap[y][x] = 0xFF;
1268 }
1269 }
1270 ++index;
1271 }
1272 }
1273
1274 SYSTEM_LOG << " -> Collision Layer: '" << layer->name << "' (filled tiles: " << collisionTileCount << ")\n";
1275 }
1276
1277 // Process object layers (sectors, collision shapes)
1278 if (layer->type == LayerType::ObjectGroup) {
1279 for (const auto& obj : layer->objects) {
1280 // Sector objects (polygons)
1281 if (obj.objectType == ObjectType::Polygon) {
1283 sector.name = obj.name.empty() ? ("Sector_" + std::to_string(obj.id)) : obj.name;
1284 sector.type = obj.type;
1285 sector.position = Vector(obj.x, TransformY(obj.y, 0), 0.f);
1286
1287 for (const auto& pt : obj.polygon) {
1288 // NOTE: For isometric, renderorder handles Y-flip, not flipY
1289 // Only apply flipY for orthogonal maps
1290 bool shouldFlipY = config_.flipY && (config_.mapOrientation != "isometric");
1291 sector.polygon.push_back(Vector(
1292 pt.x, shouldFlipY ? -pt.y : pt.y, 0.f
1293 ));
1294 }
1295
1296 // Store properties
1297 for (const auto& prop : obj.properties) {
1298 sector.properties[prop.first] = PropertyToJSON(prop.second);
1299 }
1300
1301 outLevel.sectors.push_back(sector);
1302 objectCount++;
1303
1304 SYSTEM_LOG << " -> Sector: '" << sector.name << "' (" << sector.polygon.size() << " points)\n";
1305 }
1306
1307 // Collision shapes (rectangles)
1308 else if (obj.type == "collision" && obj.objectType == ObjectType::Rectangle) {
1310 shape.name = obj.name;
1312 shape.position = Vector(obj.x, TransformY(obj.y, obj.height), 0.f);
1313 shape.size = Vector(obj.width, obj.height, 0.f);
1314
1315 outLevel.collisionShapes.push_back(shape);
1316 objectCount++;
1317
1318 SYSTEM_LOG << " -> Collision Shape: '" << shape.name << "' (rect: "
1319 << shape.size.x << "x" << shape.size.y << ")\n";
1320 }
1321 }
1322 }
1323 }
1324 }
1325
1328 ConversionStats& stats)
1329 {
1330 // Define category rules (synchronized with World.cpp InstantiatePass3_StaticObjects)
1331 const std::set<std::string> staticTypes = {
1332 "item", "collectible", "key", "treasure", "waypoint", "way", "trigger", "portal", "door", "exit",
1333 "pickup", "interactable", "checkpoint", "teleporter", "switch", "spawn"
1334 };
1335
1336 const std::set<std::string> dynamicTypes = {
1337 "player", "npc", "guard", "enemy", "zombie"
1338 };
1339
1340 const std::set<std::string> soundTypes = {
1341 "ambient", "sound", "music"
1342 };
1343
1344 // ok - FIX #1: Track global zOrder across ALL layers for depth sorting
1345 int globalZOrder = 0;
1346
1347 for (const auto& layer : tiledMap.layers) {
1348 // ok - Process object layers and assign zOrder
1349 if (layer->type == LayerType::ObjectGroup) {
1350 if (!layer->visible) {
1351 globalZOrder++; // ok - Increment even for invisible layers to maintain ordering
1352 continue;
1353 }
1354
1355 SYSTEM_LOG << "[CategorizeGameObjects] Processing object layer '"
1356 << layer->name << "' (zOrder: " << globalZOrder << ")\n";
1357
1358 for (const auto& obj : layer->objects) {
1359 // PROCESS collision polylines/polygons BEFORE filtering
1360 std::string typeLower = obj.type;
1361 std::transform(typeLower.begin(), typeLower.end(), typeLower.begin(), ::tolower);
1362
1363 if (typeLower == "collision" || typeLower.find("collision") != std::string::npos) {
1364 if (obj.objectType == ObjectType::Polyline || obj.objectType == ObjectType::Polygon) {
1365 auto collisionDescriptor = ParseCollisionPolylineDescriptor(obj, layer->offsetx, layer->offsety);
1366 if (collisionDescriptor) {
1367 // ok - CRITICAL FIX: Store layer zOrder in position.z
1368 collisionDescriptor->position.z = static_cast<float>(globalZOrder);
1369
1370 // Create a copy for the legacy entities array
1371 auto entityCopy = std::make_unique<Olympe::Editor::EntityInstance>();
1373 entityCopy->prefabPath = collisionDescriptor->prefabPath;
1374 entityCopy->name = collisionDescriptor->name;
1375 entityCopy->type = collisionDescriptor->type;
1376 entityCopy->spritePath = collisionDescriptor->spritePath;
1377 entityCopy->position = collisionDescriptor->position;
1378 entityCopy->overrides = collisionDescriptor->overrides;
1379
1380 outLevel.categorizedObjects.staticObjects.push_back(std::move(collisionDescriptor));
1381 outLevel.entities.push_back(std::move(entityCopy));
1382 stats.staticObjects++;
1383 stats.totalObjects++;
1384 SYSTEM_LOG << " -> Collision Polyline: '" << obj.name << "' (zOrder: " << globalZOrder << ")\n";
1385 }
1386 continue; // Skip to next object
1387 }
1388 }
1389
1390 // Skip polygon objects (sectors already processed in ExtractSpatialStructures)
1391 if (obj.objectType == ObjectType::Polygon) {
1392 continue;
1393 }
1394
1395 // Skip sector/zone objects (already processed)
1396 if (typeLower.find("sector") != std::string::npos ||
1397 typeLower.find("zone") != std::string::npos) {
1398 continue;
1399 }
1400
1401 auto entityDescriptor = ParseEntityDescriptor(obj, layer->offsetx, layer->offsety);
1402 if (!entityDescriptor) continue;
1403
1404 // ok - CRITICAL FIX: Store layer zOrder in position.z
1405 entityDescriptor->position.z = static_cast<float>(globalZOrder);
1406
1407 SYSTEM_LOG << " -> Entity '" << entityDescriptor->name
1408 << "' assigned zOrder: " << globalZOrder << "\n";
1409
1410 // Create a copy for the legacy entities array
1411 // Note: Both categorizedObjects (new system) and entities (legacy) need to be populated
1412 // for backward compatibility. The entity is moved into categorizedObjects and a copy
1413 // is placed in the legacy array.
1414 auto entityCopy = std::make_unique<Olympe::Editor::EntityInstance>();
1415 entityCopy->id = entityDescriptor->id;
1416 entityCopy->prefabPath = entityDescriptor->prefabPath;
1417 entityCopy->name = entityDescriptor->name;
1418 entityCopy->type = entityDescriptor->type;
1419 entityCopy->spritePath = entityDescriptor->spritePath;
1420 entityCopy->position = entityDescriptor->position;
1421 entityCopy->overrides = entityDescriptor->overrides;
1422
1423 // Categorize by type (use typeLower for case-insensitive comparison)
1424 if (obj.objectType == ObjectType::Polyline && typeLower == "way") {
1425 outLevel.categorizedObjects.patrolPaths.push_back(std::move(entityDescriptor));
1426 stats.patrolPaths++;
1427 SYSTEM_LOG << " -> Patrol Path: '" << obj.name << "' (" << obj.polyline.size() << " points)\n";
1428 }
1429 else if (soundTypes.count(typeLower)) {
1430 outLevel.categorizedObjects.soundObjects.push_back(std::move(entityDescriptor));
1431 stats.soundObjects++;
1432 SYSTEM_LOG << " -> Sound Object: '" << obj.name << "' (type: " << obj.type << ")\n";
1433 }
1434 else if (staticTypes.count(typeLower)) {
1435 outLevel.categorizedObjects.staticObjects.push_back(std::move(entityDescriptor));
1436 stats.staticObjects++;
1437 }
1438 else if (dynamicTypes.count(typeLower)) {
1439 outLevel.categorizedObjects.dynamicObjects.push_back(std::move(entityDescriptor));
1440 stats.dynamicObjects++;
1441 }
1442 else {
1443 // Default: static object
1444 outLevel.categorizedObjects.staticObjects.push_back(std::move(entityDescriptor));
1445 stats.staticObjects++;
1446 }
1447
1448 // Add copy to legacy entities array for backward compatibility
1449 outLevel.entities.push_back(std::move(entityCopy));
1450
1451 stats.totalObjects++;
1452 }
1453
1454 // ok - Increment zOrder after processing object layer
1455 globalZOrder++;
1456
1457 } else {
1458 // ok - For non-object layers (tile layers, image layers, etc.), still increment zOrder
1459 // to maintain correct ordering between all layer types
1460 globalZOrder++;
1461 }
1462 }
1463
1464 // Ensure totalObjects is accurate
1465 stats.totalObjects = stats.staticObjects + stats.dynamicObjects + stats.patrolPaths + stats.soundObjects;
1466 }
1467
1470 int& linkCount)
1471 {
1472 linkCount = 0;
1473
1474 // Build object ID -> name mapping
1475 std::map<int, std::string> idToName;
1476 for (const auto& layer : tiledMap.layers) {
1477 if (layer->type != LayerType::ObjectGroup) continue;
1478 for (const auto& obj : layer->objects) {
1479 idToName[obj.id] = obj.name;
1480 }
1481 }
1482
1483 SYSTEM_LOG << "[DEBUG] ExtractObjectRelationships - Processing objects...\n";
1484
1485 // Extract relationships from custom properties
1486 for (const auto& layer : tiledMap.layers) {
1487 if (layer->type != LayerType::ObjectGroup) continue;
1488
1489 for (const auto& obj : layer->objects) {
1490 // Debug: Show properties of guard/npc objects
1491 std::string typeLower = obj.type;
1492 std::transform(typeLower.begin(), typeLower.end(), typeLower.begin(), ::tolower);
1493
1494 if (typeLower == "guard" || typeLower == "npc") {
1495 SYSTEM_LOG << " [DEBUG] Object '" << obj.name << "' (type: " << obj.type << ") properties:\n";
1496 for (const auto& prop : obj.properties) {
1497 SYSTEM_LOG << " - '" << prop.first << "' = ";
1498 if (prop.second.type == PropertyType::Object) {
1499 int targetID = prop.second.intValue;
1500 std::string targetName = idToName.count(targetID) ? idToName[targetID] : "(unknown)";
1501 SYSTEM_LOG << "(Object ID: " << targetID << " -> '" << targetName << "')\n";
1502 } else if (prop.second.type == PropertyType::String || prop.second.type == PropertyType::File) {
1503 SYSTEM_LOG << "\"" << prop.second.stringValue << "\"\n";
1504 } else if (prop.second.type == PropertyType::Int) {
1505 SYSTEM_LOG << prop.second.intValue << "\n";
1506 } else if (prop.second.type == PropertyType::Float) {
1507 SYSTEM_LOG << prop.second.floatValue << "\n";
1508 } else if (prop.second.type == PropertyType::Bool) {
1509 SYSTEM_LOG << (prop.second.boolValue ? "true" : "false") << "\n";
1510 }
1511 }
1512 }
1513
1514 // Check for "patrol way" property (NPC -> patrol path link)
1515 auto patrolProp = obj.properties.find(PROPERTY_PATROL_WAY);
1516 if (patrolProp != obj.properties.end() && patrolProp->second.type == PropertyType::Object) {
1518 link.sourceObjectName = obj.name;
1519 link.sourceObjectId = obj.id;
1520 link.targetObjectId = patrolProp->second.intValue;
1521 link.targetObjectName = idToName[link.targetObjectId];
1522 link.linkType = "patrol_path";
1523
1524 outLevel.objectLinks.push_back(link);
1525 linkCount++;
1526
1527 SYSTEM_LOG << " -> Link: '" << link.sourceObjectName << "' -> '"
1528 << link.targetObjectName << "' (patrol_path)\n";
1529 }
1530
1531 // Check for trigger targets
1532 auto targetProp = obj.properties.find(PROPERTY_TARGET);
1533 if (targetProp != obj.properties.end() && targetProp->second.type == PropertyType::Object) {
1535 link.sourceObjectName = obj.name;
1536 link.sourceObjectId = obj.id;
1537 link.targetObjectId = targetProp->second.intValue;
1538 link.targetObjectName = idToName[link.targetObjectId];
1539 link.linkType = "trigger_target";
1540
1541 outLevel.objectLinks.push_back(link);
1542 linkCount++;
1543
1544 SYSTEM_LOG << " -> Link: '" << link.sourceObjectName << "' -> '"
1545 << link.targetObjectName << "' (trigger_target)\n";
1546 }
1547 }
1548 }
1549
1550 if (linkCount == 0) {
1551 SYSTEM_LOG << " /!\\ No object relationships found. Check:\n";
1552 SYSTEM_LOG << " - Guards should have 'patrol way' property (Object type)\n";
1553 SYSTEM_LOG << " - Property must reference a 'way' object by ID\n";
1554 }
1555 }
1556
1559 {
1560 // Extract tileset paths
1561 for (const auto& tileset : tiledMap.tilesets) {
1562 if (!tileset.source.empty()) {
1563 outLevel.resources.tilesetPaths.push_back(tileset.source);
1564 }
1565 else if (!tileset.image.empty()) {
1566 outLevel.resources.imagePaths.push_back(tileset.image);
1567 }
1568 }
1569
1570 // Extract image layer paths
1571 for (const auto& layer : tiledMap.layers) {
1572 if (layer->type == LayerType::ImageLayer && !layer->image.empty()) {
1573 std::string resolvedPath = ResolveImagePath(layer->image);
1574 outLevel.resources.imagePaths.push_back(resolvedPath);
1575 }
1576 }
1577
1578 // Extract object template paths (from custom properties)
1579 for (const auto& layer : tiledMap.layers) {
1580 if (layer->type != LayerType::ObjectGroup) continue;
1581 for (const auto& obj : layer->objects) {
1582 // Check for audio properties
1583 auto audioProp = obj.properties.find(PROPERTY_AUDIO);
1584 if (audioProp != obj.properties.end() && audioProp->second.type == PropertyType::File) {
1585 outLevel.resources.audioPaths.push_back(audioProp->second.stringValue);
1586 }
1587 }
1588 }
1589
1590 // Remove duplicates
1591 auto removeDuplicates = [](std::vector<std::string>& vec) {
1592 std::sort(vec.begin(), vec.end());
1593 vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
1594 };
1595
1596 removeDuplicates(outLevel.resources.tilesetPaths);
1597 removeDuplicates(outLevel.resources.imagePaths);
1598 removeDuplicates(outLevel.resources.audioPaths);
1599
1600 // Store tileset metadata
1601 if (!tiledMap.tilesets.empty())
1602 {
1603 nlohmann::json tilesetsJson = nlohmann::json::array();
1604
1605 for (const auto& tileset : tiledMap.tilesets)
1606 {
1607 nlohmann::json tilesetJson = nlohmann::json::object();
1608 tilesetJson["firstgid"] = tileset.firstgid;
1609 tilesetJson["name"] = tileset.name;
1610 tilesetJson["tilewidth"] = tileset.tilewidth;
1611 tilesetJson["tileheight"] = tileset.tileheight;
1612 tilesetJson["tilecount"] = tileset.tilecount;
1613 tilesetJson["columns"] = tileset.columns;
1614 tilesetJson["imagewidth"] = tileset.imagewidth;
1615 tilesetJson["imageheight"] = tileset.imageheight;
1616 tilesetJson["margin"] = tileset.margin;
1617 tilesetJson["spacing"] = tileset.spacing;
1618
1619 // Handle image-based tilesets
1620 if (!tileset.image.empty())
1621 {
1622 tilesetJson["image"] = tileset.image;
1623 tilesetJson["type"] = "image";
1624 }
1625 // Handle collection tilesets (individual tiles)
1626 else if (!tileset.tiles.empty())
1627 {
1628 tilesetJson["type"] = "collection";
1629 nlohmann::json tilesJson = nlohmann::json::array();
1630
1631 for (const auto& tile : tileset.tiles)
1632 {
1633 nlohmann::json tileJson = nlohmann::json::object();
1634 tileJson["id"] = tile.id;
1635 tileJson["image"] = tile.image;
1636 tileJson["width"] = tile.imagewidth;
1637 tileJson["height"] = tile.imageheight;
1638 tilesJson.push_back(tileJson);
1639 }
1640
1641 tilesetJson["tiles"] = tilesJson;
1642 }
1643
1644 tilesetJson["source"] = tileset.source;
1645
1646 tilesetsJson.push_back(tilesetJson);
1647 }
1648
1649 outLevel.metadata.customData["tilesets"] = tilesetsJson;
1650
1651 SYSTEM_LOG << " ok - Stored " << tilesetsJson.size() << " tilesets in metadata\n";
1652 }
1653 }
1654
1655 std::string TiledToOlympe::ResolveImagePath(const std::string& imagePath)
1656 {
1657 if (imagePath.empty()) return "";
1658
1659 if (!config_.resourceBasePath.empty()) {
1660 return config_.resourceBasePath + "/" + imagePath;
1661 }
1662 return imagePath;
1663 }
1664
1666 {
1667 switch (prop.type) {
1669 case PropertyType::File:
1671 return prop.stringValue;
1672 case PropertyType::Int:
1673 return prop.intValue;
1675 return prop.floatValue;
1676 case PropertyType::Bool:
1677 return prop.boolValue;
1679 return prop.intValue; // Object reference (ID)
1680 default:
1681 return nullptr;
1682 }
1683 }
1684
1687 int& zOrder,
1688 int& layerCount)
1689 {
1690 for (const auto& childLayer : groupLayer.layers) {
1691 if (!childLayer->visible) continue;
1692
1693 if (childLayer->type == LayerType::ImageLayer) {
1694 // Process as visual layer
1696 visual.name = childLayer->name;
1697 visual.zOrder = zOrder++;
1698 visual.isParallax = (childLayer->parallaxx != 1.0f || childLayer->parallaxy != 1.0f);
1699 visual.imagePath = ResolveImagePath(childLayer->image);
1700 visual.scrollFactorX = childLayer->parallaxx;
1701 visual.scrollFactorY = childLayer->parallaxy;
1702 visual.offsetX = childLayer->offsetx;
1703 visual.offsetY = childLayer->offsety;
1704 visual.repeatX = childLayer->repeatx;
1705 visual.repeatY = childLayer->repeaty;
1706 visual.opacity = childLayer->opacity;
1707 visual.tintColor = childLayer->tintcolor;
1708 visual.visible = childLayer->visible;
1709
1710 outLevel.visualLayers.push_back(visual);
1711 layerCount++;
1712
1713 // Also add to parallax layer manager for backward compatibility
1715 }
1716 else if (childLayer->type == LayerType::TileLayer) {
1717 // Skip collision layers
1719 continue;
1720 }
1721
1722 // Process as tile layer
1724 tileDef.name = childLayer->name;
1725 tileDef.zOrder = zOrder++;
1726 tileDef.opacity = childLayer->opacity;
1727 tileDef.visible = childLayer->visible;
1728 tileDef.isInfinite = !childLayer->chunks.empty();
1729
1730 // Handle infinite maps with chunks
1731 if (tileDef.isInfinite) {
1732 for (const auto& chunk : childLayer->chunks) {
1734 chunkDef.x = chunk.x;
1735 chunkDef.y = chunk.y;
1736 chunkDef.width = chunk.width;
1737 chunkDef.height = chunk.height;
1738
1739 // Extract chunk tile data and flip flags
1740 chunkDef.tiles.resize(chunk.height);
1741 chunkDef.tileFlipFlags.resize(chunk.height);
1742 int index = 0;
1743 for (int y = 0; y < chunk.height; ++y) {
1744 chunkDef.tiles[y].resize(chunk.width, 0);
1745 chunkDef.tileFlipFlags[y].resize(chunk.width, 0);
1746 for (int x = 0; x < chunk.width; ++x) {
1747 if (index < static_cast<int>(chunk.data.size())) {
1748 uint32_t gid = chunk.data[index];
1749 chunkDef.tiles[y][x] = GetTileId(gid);
1750 chunkDef.tileFlipFlags[y][x] = ExtractFlipFlags(gid);
1751 }
1752 ++index;
1753 }
1754 }
1755
1756 tileDef.chunks.push_back(chunkDef);
1757 }
1758 }
1759 // Handle finite maps with regular data
1760 else {
1761 // Extract tile data and flip flags
1762 tileDef.tiles.resize(childLayer->height);
1763 tileDef.tileFlipFlags.resize(childLayer->height);
1764 int index = 0;
1765 for (int y = 0; y < childLayer->height; ++y) {
1766 tileDef.tiles[y].resize(childLayer->width, 0);
1767 tileDef.tileFlipFlags[y].resize(childLayer->width, 0);
1768 for (int x = 0; x < childLayer->width; ++x) {
1769 if (index < static_cast<int>(childLayer->data.size())) {
1770 uint32_t gid = childLayer->data[index];
1771 tileDef.tiles[y][x] = GetTileId(gid);
1772 tileDef.tileFlipFlags[y][x] = ExtractFlipFlags(gid);
1773 }
1774 ++index;
1775 }
1776 }
1777 }
1778
1779 outLevel.tileLayers.push_back(tileDef);
1780 layerCount++;
1781
1782 // Also merge into legacy tileMap
1784 }
1785 else if (childLayer->type == LayerType::Group) {
1787 }
1788 }
1789 }
1790
1792 {
1793 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - Loading from: " << jsonFilePath << std::endl;
1794
1795 // Read file
1796 std::ifstream file(jsonFilePath);
1797 if (!file.is_open())
1798 {
1799 lastError_ = "Failed to open prefab mapping file: " + jsonFilePath;
1800 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - " << lastError_ << std::endl;
1801 return false;
1802 }
1803
1804 // Parse JSON
1806 try
1807 {
1808 file >> j;
1809 }
1810 catch (const std::exception& e)
1811 {
1812 lastError_ = std::string("JSON parse error: ") + e.what();
1813 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - " << lastError_ << std::endl;
1814 return false;
1815 }
1816
1817 // Validate schema
1818 if (!j.contains("schema_version"))
1819 {
1820 lastError_ = "Missing 'schema_version' in prefab mapping file";
1821 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - " << lastError_ << std::endl;
1822 return false;
1823 }
1824
1825 int schemaVersion = j["schema_version"].get<int>();
1826 if (schemaVersion != 1)
1827 {
1828 lastError_ = "Unsupported schema version: " + std::to_string(schemaVersion);
1829 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - " << lastError_ << std::endl;
1830 return false;
1831 }
1832
1833 // Load mappings
1834 if (!j.contains("mapping") || !j["mapping"].is_object())
1835 {
1836 lastError_ = "Missing or invalid 'mapping' object in prefab mapping file";
1837 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - " << lastError_ << std::endl;
1838 return false;
1839 }
1840
1841 const auto& mapping = j["mapping"];
1842 config_.typeToPrefabMap.clear();
1843
1844 for (auto it = mapping.begin(); it != mapping.end(); ++it)
1845 {
1846 std::string objectType = it.key();
1847 std::string prefabPath = it.value().get<std::string>();
1848
1849 config_.typeToPrefabMap[objectType] = prefabPath;
1850
1851 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - Mapped '" << objectType
1852 << "' -> '" << prefabPath << "'" << std::endl;
1853 }
1854
1855 SYSTEM_LOG << "TiledToOlympe::LoadPrefabMapping - Loaded "
1856 << config_.typeToPrefabMap.size() << " prefab mappings" << std::endl;
1857
1858 return true;
1859 }
1860
1862 {
1863 if (colorStr.empty() || colorStr == "none") return 0xFFFFFFFF;
1864
1865 // Fix potential buffer underflow - check string has content after '#'
1866 if (colorStr == "#") return 0xFFFFFFFF;
1867
1868 std::string hex = colorStr;
1869 if (hex.length() > 0 && hex[0] == '#') {
1870 hex = hex.substr(1);
1871 }
1872
1873 // Handle empty string after removing '#'
1874 if (hex.empty()) return 0xFFFFFFFF;
1875
1876 uint32_t color = 0xFFFFFFFF;
1877 try {
1878 color = static_cast<uint32_t>(std::stoul(hex, nullptr, 16));
1879
1880 // If 6-digit hex (RGB), add full alpha
1881 if (hex.length() == 6) {
1882 color = 0xFF000000 | color;
1883 }
1884 } catch (...) {
1885 // Invalid color format, return white
1886 }
1887
1888 return color;
1889 }
1890
1891 std::unique_ptr<Olympe::Editor::EntityInstance> TiledToOlympe::ParseSectorDescriptor(const TiledObject& obj,
1892 float layerOffsetX,
1893 float layerOffsetY)
1894 {
1895 auto entityDescriptor = std::make_unique<Olympe::Editor::EntityInstance>();
1896
1897 entityDescriptor->id = "sector_" + std::to_string(obj.id);
1898 entityDescriptor->name = obj.name.empty() ? ("Sector_" + std::to_string(obj.id)) : obj.name;
1899 entityDescriptor->type = "Sector";
1900 entityDescriptor->prefabPath = "Blueprints/Sector.json";
1901
1902 // Apply layer offsets to position
1903 entityDescriptor->position = Vector(obj.x + layerOffsetX, obj.y + layerOffsetY, 0.0f);
1904 entityDescriptor->rotation = obj.rotation;
1905
1906 // Store polygon in overrides
1907 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
1908 // Only apply flipY for orthogonal maps
1909 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
1910 nlohmann::json polygon = nlohmann::json::array();
1911 for (const auto& pt : obj.polygon) {
1913 point["x"] = pt.x;
1914 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
1915 polygon.push_back(point);
1916 }
1917
1918 entityDescriptor->overrides["Sector"] = nlohmann::json::object();
1919 entityDescriptor->overrides["Sector"]["polygon"] = polygon;
1920 entityDescriptor->overrides["Sector"]["type"] = obj.type;
1921
1922 PropertiesToOverrides(obj.properties, entityDescriptor->overrides);
1923
1924 return entityDescriptor;
1925 }
1926
1927 std::unique_ptr<Olympe::Editor::EntityInstance> TiledToOlympe::ParsePatrolPathDescriptor(const TiledObject& obj,
1928 float layerOffsetX,
1929 float layerOffsetY)
1930 {
1931 auto entityDescriptor = std::make_unique<Olympe::Editor::EntityInstance>();
1932
1933 entityDescriptor->id = "patrol_" + std::to_string(obj.id);
1934 entityDescriptor->name = obj.name.empty() ? ("PatrolPath_" + std::to_string(obj.id)) : obj.name;
1935 entityDescriptor->type = "PatrolPath";
1936 entityDescriptor->prefabPath = "Blueprints/PatrolPath.json";
1937
1938 // Apply layer offsets to position
1939 entityDescriptor->position = Vector(obj.x + layerOffsetX, obj.y + layerOffsetY, 0.0f);
1940 entityDescriptor->rotation = obj.rotation;
1941
1942 // Store polyline in overrides
1943 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
1944 // Only apply flipY for orthogonal maps
1945 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
1946 nlohmann::json path = nlohmann::json::array();
1947 for (const auto& pt : obj.polyline) {
1949 point["x"] = pt.x;
1950 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
1951 path.push_back(point);
1952 }
1953
1954 entityDescriptor->overrides["AIBlackboard_data"] = nlohmann::json::object();
1955 entityDescriptor->overrides["AIBlackboard_data"]["patrolPath"] = path;
1956
1957 PropertiesToOverrides(obj.properties, entityDescriptor->overrides);
1958
1959 return entityDescriptor;
1960 }
1961
1962 std::unique_ptr<Olympe::Editor::EntityInstance> TiledToOlympe::ParseCollisionPolylineDescriptor(const TiledObject& obj,
1963 float layerOffsetX,
1964 float layerOffsetY)
1965 {
1966 auto entityDescriptor = std::make_unique<Olympe::Editor::EntityInstance>();
1967
1968 entityDescriptor->id = "collision_poly_" + std::to_string(obj.id);
1969 entityDescriptor->name = obj.name.empty() ? ("CollisionPoly_" + std::to_string(obj.id)) : obj.name;
1970 entityDescriptor->type = "CollisionPolygon";
1971 entityDescriptor->prefabPath = "Blueprints/CollisionPolygon.json";
1972
1973 // Apply layer offsets to position
1974 entityDescriptor->position = Vector(obj.x + layerOffsetX, obj.y + layerOffsetY, 0.0f);
1975 entityDescriptor->rotation = obj.rotation;
1976
1977 // Store polyline/polygon points
1978 // NOTE: For isometric, renderorder handles Y-flip in position transform, not flipY
1979 // Only apply flipY for orthogonal maps
1980 bool shouldFlipPolyY = config_.flipY && (config_.mapOrientation != "isometric");
1981 nlohmann::json polygon = nlohmann::json::array();
1982 const auto& points = (obj.objectType == ObjectType::Polygon) ? obj.polygon : obj.polyline;
1983
1984 for (const auto& pt : points) {
1986 point["x"] = pt.x;
1987 point["y"] = shouldFlipPolyY ? -pt.y : pt.y;
1988 polygon.push_back(point);
1989 }
1990
1991 entityDescriptor->overrides["CollisionPolygon"] = nlohmann::json::object();
1992 entityDescriptor->overrides["CollisionPolygon"]["points"] = polygon;
1993 entityDescriptor->overrides["CollisionPolygon"]["isClosed"] = (obj.objectType == ObjectType::Polygon);
1994
1995 PropertiesToOverrides(obj.properties, entityDescriptor->overrides);
1996
1997 return entityDescriptor;
1998 }
1999
2000} // namespace Tiled
2001} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
void ExtractFlipFlags(uint32_t gid, bool &flipH, bool &flipV, bool &flipD)
const ParallaxLayer * GetLayer(size_t index) const
void AddLayer(const ParallaxLayer &layer)
static ParallaxLayerManager & Get()
bool Convert(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel)
const TiledTileset * FindTilesetForGid(uint32_t gid) const
void CategorizeGameObjects(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel, ConversionStats &stats)
void ConvertSectorObject(const TiledObject &obj, Olympe::Editor::LevelDefinition &level, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
std::string ResolveImagePath(const std::string &imagePath)
void PropertiesToOverrides(const std::map< std::string, TiledProperty > &properties, nlohmann::json &overrides)
std::unique_ptr< Olympe::Editor::EntityInstance > ParsePatrolPathDescriptor(const TiledObject &obj, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
void ConvertObjectLayer(const TiledLayer &layer, Olympe::Editor::LevelDefinition &level)
void InitializeCollisionMap(Olympe::Editor::LevelDefinition &level, int width, int height)
nlohmann::json PropertyToJSON(const TiledProperty &prop)
void ConvertImageLayer(const TiledLayer &layer)
void ConvertCollisionObject(const TiledObject &obj, Olympe::Editor::LevelDefinition &level)
void MergeTileLayer(const TiledLayer &layer, std::vector< std::vector< int > > &tileMap, int mapWidth, int mapHeight)
void ConvertObject(const TiledObject &obj, Olympe::Editor::LevelDefinition &level, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
float TransformY(float y, float height)
void ExtractSpatialStructures(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel, int &objectCount)
Vector TransformObjectPosition(float x, float y, float layerOffsetX=0.0f, float layerOffsetY=0.0f, uint32_t gid=0)
void ConvertTileLayer(const TiledLayer &layer, Olympe::Editor::LevelDefinition &level)
void SetConfig(const ConversionConfig &config)
std::unique_ptr< Olympe::Editor::EntityInstance > ParseSectorDescriptor(const TiledObject &obj, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
void ExtractMapConfiguration(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel)
TiledToOlympe::MapBounds CalculateActualMapBounds(const TiledMap &tiledMap)
void BuildResourceCatalog(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel)
void ProcessGroupLayers(const TiledLayer &groupLayer, Olympe::Editor::LevelDefinition &outLevel, int &zOrder, int &layerCount)
bool MatchesPattern(const std::string &layerName, const std::vector< std::string > &patterns)
void ProcessVisualLayers(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel, int &layerCount)
void ConvertGroupLayer(const TiledLayer &layer, Olympe::Editor::LevelDefinition &level)
const std::vector< TiledTileset > * tilesets_
void ExtractObjectRelationships(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel, int &linkCount)
std::unique_ptr< Olympe::Editor::EntityInstance > ParseEntityDescriptor(const TiledObject &obj, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
void ConvertPatrolPath(const TiledObject &obj, Olympe::Editor::LevelDefinition &level, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
std::string GetPrefabPath(const std::string &objectType)
void ConvertPolygonCollision(const TiledObject &obj, Olympe::Editor::LevelDefinition &level, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
bool LoadPrefabMapping(const std::string &jsonFilePath)
void ExtractMapMetadata(const TiledMap &tiledMap, Olympe::Editor::LevelDefinition &outLevel)
uint32_t ParseTintColor(const std::string &colorStr)
std::unique_ptr< Olympe::Editor::EntityInstance > ParseCollisionPolylineDescriptor(const TiledObject &obj, float layerOffsetX=0.0f, float layerOffsetY=0.0f)
Factory class for creating entities from prefab blueprints.
static PrefabFactory & Get()
Get singleton instance.
bool IsFlippedDiagonally(uint32_t gid)
bool IsFlippedVertically(uint32_t gid)
bool IsFlippedHorizontally(uint32_t gid)
uint32_t GetTileId(uint32_t gid)
nlohmann::json json
std::vector< std::string > collisionLayerPatterns
std::vector< std::string > sectorLayerPatterns
std::map< std::string, std::string > typeToPrefabMap
std::vector< TiledObject > objects
std::vector< uint32_t > data
std::vector< std::shared_ptr< TiledLayer > > layers
#define SYSTEM_LOG