Jelajahi Sumber

feat: Enhance map generation with tile definitions and hexagon data

- Added loading of tile definitions based on the selected template in HomeController.
- Included hexagon data retrieval to provide detailed information about each hexagon's tile.
- Updated home view to pass tile definitions and hexagon data to the frontend.
- Modified tools sidebar to dynamically display tile options based on loaded definitions.
- Introduced SVG assets for various tile types, including grass, forest, and buildings.
- Enhanced map editor to utilize tile definitions for rendering hexagons with appropriate backgrounds.
- Created README for tile assets to document available tiles and their usage.
- Added new JSON map file structure to support hexagon data and timestamps.
Ynats 1 bulan lalu
induk
melakukan
94dcc63a7c

+ 24 - 1
app/Controllers/HomeController.php

@@ -31,6 +31,27 @@ if (!class_exists('HomeController')) {
                     $mapJsonData = json_decode($mapRow['data'], true) ?: [];
                     $template = $mapJsonData['template'] ?? 'neutral';
                     
+                    // Charger les définitions de tuiles du template
+                    require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+                    $templateClass = TemplateFactory::get($template);
+                    $tileDefinitions = $templateClass ? $templateClass::tileDefinitions() : [];
+                    
+                    // Obtenir les données des hexagones
+                    $hexes = [];
+                    foreach ($map->getAllHexagons() as $q => $row) {
+                        foreach ($row as $r => $hexagon) {
+                            $tile = $hexagon->getTile();
+                            $hexes[] = [
+                                'q' => $hexagon->getQ(),
+                                'r' => $hexagon->getR(),
+                                'tile' => $tile ? [
+                                    'type' => $tile->getType(),
+                                    'properties' => $tile->getAllProperties()
+                                ] : null
+                            ];
+                        }
+                    }
+                    
                     $data['loaded_map'] = $map;
                     $data['map_data'] = [
                         'id' => $mapId,
@@ -40,7 +61,9 @@ if (!class_exists('HomeController')) {
                         'created_at' => $map->getCreatedAt(),
                         'updated_at' => $map->getUpdatedAt(),
                         'statistics' => $map->getStatistics(),
-                        'template' => $template
+                        'template' => $template,
+                        'tileDefinitions' => $tileDefinitions,
+                        'hexes' => $hexes
                     ];
                     $data['title'] = $map->getName() . ' - Map Generator';
                 } else {

+ 3 - 0
app/Views/home.php

@@ -189,6 +189,9 @@ include __DIR__ . '/partials/footer.php';
 const mapData = <?php echo json_encode($map_data); ?>;
 const mapWidth = mapData.width;
 const mapHeight = mapData.height;
+const templateData = {
+    tileDefinitions: <?php echo json_encode($map_data['tileDefinitions'] ?? []); ?>
+};
 </script>
 <script src="/assets/js/editor/map-editor.js"></script>
 <?php endif; ?>

+ 38 - 29
app/Views/partials/tools-sidebar.php

@@ -26,42 +26,51 @@
             <h6><i class="bi bi-grid-1x2-fill me-1"></i>Type de tuile</h6>
             <div id="tileTypeOptions" class="d-flex flex-column gap-2">
                 <?php
-                // Récupérer le template depuis les données de la vue
-                $template = 'neutral'; // template par défaut
-                if (isset($map_data) && isset($map_data['template'])) {
-                    $template = $map_data['template'];
-                } elseif (isset($GLOBALS['view_data']['map_data']['template'])) {
-                    $template = $GLOBALS['view_data']['map_data']['template'];
+                // Récupérer les définitions de tuiles depuis les données de la vue
+                $tileDefinitions = [];
+                if (isset($map_data) && isset($map_data['tileDefinitions'])) {
+                    $tileDefinitions = $map_data['tileDefinitions'];
+                } elseif (isset($GLOBALS['view_data']['map_data']['tileDefinitions'])) {
+                    $tileDefinitions = $GLOBALS['view_data']['map_data']['tileDefinitions'];
                 }
 
-                // Charger dynamiquement les SVG depuis mapModels/{template}/assets/
-                $assetsDir = __DIR__ . '/../../../mapModels/' . $template . '/assets';
-                $svgFiles = glob($assetsDir . '/*.svg');
-
-                if (empty($svgFiles)) {
-                    // Fallback vers neutral si aucun fichier trouvé
-                    $assetsDir = __DIR__ . '/../../../mapModels/neutral/assets';
-                    $svgFiles = glob($assetsDir . '/*.svg');
+                // Si aucune définition trouvée, utiliser des valeurs par défaut
+                if (empty($tileDefinitions)) {
+                    $tileDefinitions = [
+                        ['id' => 'empty', 'name' => 'Vide', 'color' => '#ffffff'],
+                        ['id' => 'grass', 'name' => 'Herbe', 'color' => '#90EE90']
+                    ];
                 }
 
-                foreach ($svgFiles as $svgFile) {
-                    $filename = basename($svgFile, '.svg');
-                    $tileType = $filename;
-                    $svgContent = file_get_contents($svgFile);
+                foreach ($tileDefinitions as $tileDef) {
+                    $tileId = $tileDef['id'];
+                    $tileName = $tileDef['name'];
+                    $tileColor = $tileDef['color'] ?? '#ffffff';
+                    $tileAsset = $tileDef['asset'] ?? null;
 
-                    // Modifier les attributs du SVG pour l'adapter à la taille des boutons
-                    $svgContent = preg_replace('/width="[^"]*"/', 'width="36"', $svgContent);
-                    $svgContent = preg_replace('/height="[^"]*"/', 'height="24"', $svgContent);
-                    $svgContent = preg_replace('/viewBox="[^"]*"/', 'viewBox="0 0 36 24"', $svgContent);
+                    echo '<div class="form-check tile-type-option">' . "\n";
+                    echo '    <input class="form-check-input tile-type-radio" type="radio" name="tileType" id="tileType_' . htmlspecialchars($tileId) . '" value="' . htmlspecialchars($tileId) . '">' . "\n";
+                    echo '    <label class="form-check-label d-flex align-items-center gap-2" for="tileType_' . htmlspecialchars($tileId) . '">' . "\n";
 
-                    // Capitaliser le nom pour l'affichage
-                    $displayName = ucfirst(str_replace('-', ' ', $tileType));
+                    if ($tileAsset) {
+                        // Utiliser l'asset SVG hexagonal
+                        $svgPath = '/Volumes/SSD-T1/mamp-pro/map-generator/public' . $tileAsset;
+                        if (file_exists($svgPath)) {
+                            $svgContent = file_get_contents($svgPath);
+                            // Adapter la taille pour les boutons (plus petit que les tuiles complètes)
+                            $svgContent = preg_replace('/width="[^"]*"/', 'width="32"', $svgContent);
+                            $svgContent = preg_replace('/height="[^"]*"/', 'height="28"', $svgContent);
+                            echo '        ' . $svgContent . "\n";
+                        } else {
+                            // Fallback vers un cercle coloré si le SVG n'existe pas
+                            echo '        <div style="width: 32px; height: 28px; background-color: ' . htmlspecialchars($tileColor) . '; border: 1px solid #333; border-radius: 4px;"></div>' . "\n";
+                        }
+                    } else {
+                        // Fallback vers un cercle coloré
+                        echo '        <div style="width: 32px; height: 28px; background-color: ' . htmlspecialchars($tileColor) . '; border: 1px solid #333; border-radius: 4px;"></div>' . "\n";
+                    }
 
-                    echo '<div class="form-check tile-type-option">' . "\n";
-                    echo '    <input class="form-check-input tile-type-radio" type="radio" name="tileType" id="tileType_' . htmlspecialchars($tileType) . '" value="' . htmlspecialchars($tileType) . '">' . "\n";
-                    echo '    <label class="form-check-label d-flex align-items-center gap-2" for="tileType_' . htmlspecialchars($tileType) . '">' . "\n";
-                    echo '        ' . $svgContent . "\n";
-                    echo '        <span>' . htmlspecialchars($displayName) . '</span>' . "\n";
+                    echo '        <span>' . htmlspecialchars($tileName) . '</span>' . "\n";
                     echo '    </label>' . "\n";
                     echo '</div>' . "\n";
                 }

+ 7 - 0
archives/hex.template.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(235,235,235);"/>
+    </g>
+</svg>

+ 8 - 0
mapModels/neutral/assets/hex.template.svg

@@ -0,0 +1,8 @@
+<svg width="100" height="87" viewBox="0 0 100 87" xmlns="http://www.w3.org/2000/svg">
+  <polygon
+    points="50,1.15 98.3,24.25 98.3,62.75 50,85.85 1.7,62.75 1.7,24.25"
+    fill="#e0e0e0"
+    stroke="#333333"
+    stroke-width="2"
+  />
+</svg>

+ 1 - 1
mapModels/neutral/template.php

@@ -26,7 +26,7 @@ class NeutralTemplate implements MapTemplateInterface
     {
         return [
             ['id' => 'empty', 'name' => 'Vide', 'color' => '#eee'],
-            ['id' => 'grass', 'name' => 'Herbe', 'color' => '#8fd694']
+            ['id' => 'grass', 'name' => 'Herbe', 'color' => '#90EE90', 'asset' => '/assets/tiles/grass.svg']
         ];
     }
 }

+ 6 - 6
mapModels/rural/template.php

@@ -32,12 +32,12 @@ class RuralTemplate implements MapTemplateInterface
     public static function tileDefinitions(): array
     {
         return [
-            ['id' => 'forest', 'name' => 'Forêt', 'color' => '#2b7a2b', 'asset' => '/mapModels/rural/assets/forest.svg'],
-            ['id' => 'field', 'name' => 'Champ', 'color' => '#f4ecb0', 'asset' => '/mapModels/rural/assets/field.svg'],
-            ['id' => 'road_straight', 'name' => 'Route droite', 'color' => '#d2b48c', 'asset' => '/mapModels/rural/assets/road-straight.svg'],
-            ['id' => 'road_right', 'name' => 'Route tourne à droite', 'color' => '#d2b48c', 'asset' => '/mapModels/rural/assets/road-right.svg'],
-            ['id' => 'road_left', 'name' => 'Route tourne à gauche', 'color' => '#d2b48c', 'asset' => '/mapModels/rural/assets/road-left.svg'],
-            ['id' => 'villa', 'name' => 'Villa', 'color' => '#f1e0d0', 'asset' => '/mapModels/rural/assets/villa.svg']
+            ['id' => 'forest', 'name' => 'Forêt', 'color' => '#228B22', 'asset' => '/assets/tiles/forest.svg'],
+            ['id' => 'field', 'name' => 'Champ', 'color' => '#F4A460', 'asset' => '/assets/tiles/field.svg'],
+            ['id' => 'road_straight', 'name' => 'Route droite', 'color' => '#D2B48C', 'asset' => '/assets/tiles/road-straight.svg'],
+            ['id' => 'road_right', 'name' => 'Route tourne à droite', 'color' => '#D2B48C', 'asset' => '/assets/tiles/road-right.svg'],
+            ['id' => 'road_left', 'name' => 'Route tourne à gauche', 'color' => '#D2B48C', 'asset' => '/assets/tiles/road-left.svg'],
+            ['id' => 'villa', 'name' => 'Villa', 'color' => '#F5DEB3', 'asset' => '/assets/tiles/villa.svg']
         ];
     }
 }

+ 3 - 3
mapModels/urban/template.php

@@ -34,9 +34,9 @@ class UrbanTemplate implements MapTemplateInterface
     public static function tileDefinitions(): array
     {
         return [
-            ['id' => 'building', 'name' => 'Bâtiment', 'color' => '#b0b0b0'],
-            ['id' => 'road', 'name' => 'Route', 'color' => '#d2b48c'],
-            ['id' => 'park', 'name' => 'Parc', 'color' => '#7fc97f']
+            ['id' => 'building', 'name' => 'Bâtiment', 'color' => '#708090', 'asset' => '/assets/tiles/building.svg'],
+            ['id' => 'road', 'name' => 'Route', 'color' => '#D2B48C', 'asset' => '/assets/tiles/road-straight.svg'],
+            ['id' => 'park', 'name' => 'Parc', 'color' => '#90EE90', 'asset' => '/assets/tiles/grass.svg']
         ];
     }
 }

+ 62 - 42
public/assets/js/editor/map-editor.js

@@ -151,9 +151,6 @@
             }
 
             updateHexagonContent(hex, q, r) {
-                // Vider le contenu existant
-                hex.innerHTML = '';
-
                 // Récupérer le type de tuile
                 let type = null;
                 try {
@@ -168,27 +165,40 @@
                     // ignore
                 }
 
-                if (type && type !== 'empty') {
-                    // Récupérer le SVG depuis les options de la sidebar
-                    const radio = document.querySelector(`.tile-type-radio[value="${type}"]`);
-                    if (radio) {
-                        const label = radio.parentElement.querySelector('.form-check-label');
-                        if (label) {
-                            const svg = label.querySelector('svg');
-                            if (svg) {
-                                const svgClone = svg.cloneNode(true);
-                                // Ajuster la taille du SVG pour la tuile
-                                svgClone.setAttribute('width', '100%');
-                                svgClone.setAttribute('height', '100%');
-                                svgClone.style.position = 'absolute';
-                                svgClone.style.top = '0';
-                                svgClone.style.left = '0';
-                                svgClone.style.pointerEvents = 'none';
-                                hex.appendChild(svgClone);
-                            }
+                // Si pas de type spécifique, chercher dans mapData.hexes
+                if (!type) {
+                    if (window.mapData && window.mapData.hexes) {
+                        const hexData = window.mapData.hexes.find(h => h.q === q && h.r === r);
+                        if (hexData && hexData.tile && hexData.tile.type) {
+                            type = hexData.tile.type;
                         }
                     }
                 }
+
+                if (!type || type === 'empty') {
+                    type = 'empty';
+                }
+
+                // Trouver la définition de tuile
+                let tileDef = null;
+                if (window.templateData && window.templateData.tileDefinitions) {
+                    tileDef = window.templateData.tileDefinitions.find(def => def.id === type);
+                }
+
+                if (tileDef && tileDef.asset) {
+                    // Utiliser l'asset SVG comme background
+                    hex.style.backgroundColor = 'transparent';
+                    hex.style.backgroundImage = `url('${tileDef.asset}')`;
+                    hex.style.backgroundSize = 'contain';
+                    hex.style.backgroundPosition = 'center';
+                    hex.style.filter = 'none';
+                } else {
+                    // Utiliser la couleur par défaut
+                    const color = tileDef && tileDef.color ? tileDef.color : '#ffffff';
+                    hex.style.backgroundColor = color;
+                    hex.style.backgroundImage = 'none';
+                    hex.style.filter = 'none';
+                }
             }
 
             selectTile(q, r) {
@@ -358,9 +368,6 @@
 
     // Fonction pour mettre à jour le contenu d'un hexagone selon son type
     function updateHexagonContent(hex, q, r) {
-        // Vider le contenu existant
-        hex.innerHTML = '';
-
         // Récupérer le type de tuile
         let type = null;
         try {
@@ -375,27 +382,40 @@
             // ignore
         }
 
-        if (type && type !== 'empty') {
-            // Récupérer le SVG depuis les options de la sidebar
-            const radio = document.querySelector(`.tile-type-radio[value="${type}"]`);
-            if (radio) {
-                const label = radio.parentElement.querySelector('.form-check-label');
-                if (label) {
-                    const svg = label.querySelector('svg');
-                    if (svg) {
-                        const svgClone = svg.cloneNode(true);
-                        // Ajuster la taille du SVG pour la tuile
-                        svgClone.setAttribute('width', '100%');
-                        svgClone.setAttribute('height', '100%');
-                        svgClone.style.position = 'absolute';
-                        svgClone.style.top = '0';
-                        svgClone.style.left = '0';
-                        svgClone.style.pointerEvents = 'none';
-                        hex.appendChild(svgClone);
-                    }
+        // Si pas de type spécifique, chercher dans mapData.hexes
+        if (!type) {
+            if (window.mapData && window.mapData.hexes) {
+                const hexData = window.mapData.hexes.find(h => h.q === q && h.r === r);
+                if (hexData && hexData.tile && hexData.tile.type) {
+                    type = hexData.tile.type;
                 }
             }
         }
+
+        if (!type || type === 'empty') {
+            type = 'empty';
+        }
+
+        // Trouver la définition de tuile
+        let tileDef = null;
+        if (window.templateData && window.templateData.tileDefinitions) {
+            tileDef = window.templateData.tileDefinitions.find(def => def.id === type);
+        }
+
+        if (tileDef && tileDef.asset) {
+            // Utiliser l'asset SVG comme background
+            hex.style.backgroundColor = 'transparent';
+            hex.style.backgroundImage = `url('${tileDef.asset}')`;
+            hex.style.backgroundSize = 'contain';
+            hex.style.backgroundPosition = 'center';
+            hex.style.filter = 'none';
+        } else {
+            // Utiliser la couleur par défaut
+            const color = tileDef && tileDef.color ? tileDef.color : '#ffffff';
+            hex.style.backgroundColor = color;
+            hex.style.backgroundImage = 'none';
+            hex.style.filter = 'none';
+        }
     }
 
     // Fonction pour mettre à jour les checkboxes selon le type de la tuile sélectionnée

+ 94 - 45
public/assets/js/map-editor.js

@@ -9,6 +9,7 @@ class MapEditor {
         this.lastMousePos = { x: 0, y: 0 };
         this.selectedTile = null;
         this.tileSize = 50; // Rayon de l'hexagone (du centre au sommet)
+        this.tileDefinitions = [];
 
         this.init();
     }
@@ -25,6 +26,7 @@ class MapEditor {
         }
 
         this.setupCanvas();
+        this.loadTileDefinitions();
         this.renderMap();
         this.setupEventListeners();
         this.hideLoading();
@@ -36,6 +38,35 @@ class MapEditor {
         this.canvas.style.transformOrigin = '0 0';
     }
 
+    loadTileDefinitions() {
+        // Charger les définitions de tuiles depuis le template
+        if (typeof templateData !== 'undefined' && templateData.tileDefinitions) {
+            this.tileDefinitions = templateData.tileDefinitions;
+        } else {
+            // Défauts si pas de template
+            this.tileDefinitions = [
+                { id: 'empty', name: 'Vide', color: '#ffffff' },
+                { id: 'grass', name: 'Herbe', color: '#90EE90' }
+            ];
+        }
+    }
+
+    getTileType(q, r) {
+        // Chercher le type de tuile dans mapData
+        if (typeof mapData !== 'undefined' && mapData.hexes) {
+            for (const hex of mapData.hexes) {
+                if (hex.q === q && hex.r === r) {
+                    return hex.tile ? hex.tile.type : 'empty';
+                }
+            }
+        }
+        return 'empty';
+    }
+
+    getTileDefinition(tileType) {
+        return this.tileDefinitions.find(def => def.id === tileType) || this.tileDefinitions[0];
+    }
+
     // Conversion axial -> pixels pour hexagones pointus (pointy-top)
     axialToPixel(q, r) {
         const size = this.tileSize; // rayon (centre -> sommet)
@@ -94,50 +125,68 @@ class MapEditor {
         }
     }
 
-renderHexagon(q, r, x, y, hexWidth, hexHeight) {
-    const hex = document.createElement('div');
-    hex.className = 'hexagon';
-    hex.dataset.q = q;
-    hex.dataset.r = r;
-
-    // Expansion fine juste pour fondre l'anti-aliasing (évite interstice ET chevauchement)
-    const expansion = Math.round(hexWidth * 0.004 * 1000) / 1000;
-    const adjWidth = hexWidth + expansion;
-    const adjHeight = hexHeight + expansion;
-
-    const halfW = adjWidth / 2;
-    const halfH = adjHeight / 2;
-
-    hex.style.width = adjWidth + 'px';
-    hex.style.height = adjHeight + 'px';
-    hex.style.left = (Math.round((x - halfW) * 1000) / 1000) + 'px';
-    hex.style.top = (Math.round((y - halfH) * 1000) / 1000) + 'px';
-
-    hex.style.position = 'absolute';
-    hex.style.cursor = 'pointer';
-    hex.style.transition = 'background-color 0.2s';
-    hex.style.backgroundColor = '#ffffff';
-    hex.style.clipPath = 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)';
-
-    hex.addEventListener('click', (e) => {
-        e.stopPropagation();
-        this.selectTile(q, r);
-    });
-
-    hex.addEventListener('mouseenter', () => {
-        if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
-            hex.style.backgroundColor = '#e3f2fd';
+    renderHexagon(q, r, x, y, hexWidth, hexHeight) {
+        const hex = document.createElement('div');
+        hex.className = 'hexagon';
+        hex.dataset.q = q;
+        hex.dataset.r = r;
+
+        // Expansion fine juste pour fondre l'anti-aliasing (évite interstice ET chevauchement)
+        const expansion = Math.round(hexWidth * 0.004 * 1000) / 1000;
+        const adjWidth = hexWidth + expansion;
+        const adjHeight = hexHeight + expansion;
+
+        const halfW = adjWidth / 2;
+        const halfH = adjHeight / 2;
+
+        hex.style.width = adjWidth + 'px';
+        hex.style.height = adjHeight + 'px';
+        hex.style.left = (Math.round((x - halfW) * 1000) / 1000) + 'px';
+        hex.style.top = (Math.round((y - halfH) * 1000) / 1000) + 'px';
+
+        hex.style.position = 'absolute';
+        hex.style.cursor = 'pointer';
+        hex.style.transition = 'background-color 0.2s';
+        hex.style.clipPath = 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)';
+
+        // Appliquer la tuile
+        const tileType = this.getTileType(q, r);
+        const tileDef = this.getTileDefinition(tileType);
+
+        if (tileDef && tileDef.asset) {
+            // Utiliser l'asset SVG comme background
+            hex.style.backgroundColor = 'transparent';
+            hex.style.backgroundImage = `url('${tileDef.asset}')`;
+            hex.style.backgroundSize = 'contain';
+            hex.style.backgroundPosition = 'center';
+            hex.style.backgroundRepeat = 'no-repeat';
+            hex.style.filter = 'none';
+        } else {
+            // Utiliser la couleur
+            hex.style.backgroundColor = tileDef ? (tileDef.color || '#ffffff') : '#ffffff';
+            hex.style.backgroundImage = 'none';
+            hex.style.filter = 'none';
         }
-    });
 
-    hex.addEventListener('mouseleave', () => {
-        if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
-            hex.style.backgroundColor = '#ffffff';
-        }
-    });
+        hex.addEventListener('click', (e) => {
+            e.stopPropagation();
+            this.selectTile(q, r);
+        });
 
-    this.canvas.appendChild(hex);
-}
+        hex.addEventListener('mouseenter', () => {
+            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
+                hex.style.filter = 'brightness(1.1)';
+            }
+        });
+
+        hex.addEventListener('mouseleave', () => {
+            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
+                hex.style.filter = 'none';
+            }
+        });
+
+        this.canvas.appendChild(hex);
+    }
 
 
     selectTile(q, r) {
@@ -145,8 +194,8 @@ renderHexagon(q, r, x, y, hexWidth, hexHeight) {
         if (this.selectedTile) {
             const prevHex = this.canvas.querySelector(`[data-q="${this.selectedTile.q}"][data-r="${this.selectedTile.r}"]`);
             if (prevHex) {
-                prevHex.style.backgroundColor = '#ffffff';
-                prevHex.style.borderColor = '#666';
+                prevHex.style.boxShadow = 'none';
+                prevHex.style.border = 'none';
             }
         }
 
@@ -154,8 +203,8 @@ renderHexagon(q, r, x, y, hexWidth, hexHeight) {
         this.selectedTile = { q, r };
         const hex = this.canvas.querySelector(`[data-q="${q}"][data-r="${r}"]`);
         if (hex) {
-            hex.style.backgroundColor = '#2196f3';
-            hex.style.borderColor = '#1976d2';
+            hex.style.boxShadow = '0 0 0 3px #2196f3';
+            hex.style.border = '2px solid #1976d2';
         }
 
         // Afficher les informations de la tuile

+ 24 - 0
public/assets/tiles/README.md

@@ -0,0 +1,24 @@
+# Tuiles Hexagonales
+
+Ce dossier contient les tuiles SVG hexagonales utilisées pour représenter différents types de terrain dans le générateur de cartes.
+
+## Tuiles Disponibles
+
+- **grass.svg** : Herbe avec brins stylisés
+- **forest.svg** : Forêt avec arbres stylisés
+- **field.svg** : Champ cultivé avec rangées
+- **road-straight.svg** : Route droite avec lignes centrales
+- **road-right.svg** : Route tournant à droite
+- **road-left.svg** : Route tournant à gauche
+- **villa.svg** : Maison rurale
+- **building.svg** : Bâtiment urbain
+- **water.svg** : Eau avec vagues
+- **mountain.svg** : Montagne avec neige
+
+## Structure
+
+Chaque tuile est un SVG de 100x87 pixels avec une viewBox correspondante, utilisant un polygone hexagonal comme base.
+
+## Utilisation
+
+Ces tuiles peuvent être utilisées dans le système de rendu de cartes pour afficher visuellement les différents types de terrain sur les hexagones.

+ 7 - 0
public/assets/tiles/building.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(128,128,128);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/field.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(255,215,0);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/forest.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(34,139,34);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/grass.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(144,238,144);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/mountain.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(139,69,19);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/road-left.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(105,105,105);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/road-right.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(105,105,105);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/road-straight.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(105,105,105);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/villa.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(255,182,193);"/>
+    </g>
+</svg>

+ 7 - 0
public/assets/tiles/water.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 577" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-38.5,0)">
+        <path d="M288.5,0L538.348,144.25L538.348,432.75L288.5,577L38.652,432.75L38.652,144.25L288.5,0Z" style="fill:rgb(135,206,235);"/>
+    </g>
+</svg>

+ 142 - 0
storage/maps/map_1761899101_6904725d88377.json

@@ -0,0 +1,142 @@
+{
+    "name": "Test 2",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-31 09:25:01",
+    "updated_at": "2025-10-31 09:25:01",
+    "hexagons": [
+        {
+            "q": -2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -2,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -1,
+            "r": -1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -1,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": -1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": -2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": -1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": -2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": -1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": -2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": -1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        }
+    ]
+}