Bladeren bron

feat: Implement project editing functionality with template support

- Added editProject method in HomeController to handle project editing.
- Created updateProject method to process updates via POST.
- Enhanced MapModel to include template and radius metadata during creation and updates.
- Introduced TemplateFactory for dynamic loading of map templates.
- Developed new templates: NeutralTemplate, RuralTemplate, and UrbanTemplate with respective tile definitions.
- Updated views for project creation and editing to include template selection and preview.
- Added JavaScript functionality to handle template previews and tile definitions dynamically.
- Improved error handling and logging for missing files during template loading.
Ynats 2 maanden geleden
bovenliggende
commit
902298b836

File diff suppressed because it is too large
+ 373 - 1
.specstory/history/2025-10-13_08-16Z-adaptation-du-formulaire-pour-éléments-hexagonaux.md


+ 135 - 1
app/Controllers/HomeController.php

@@ -55,6 +55,11 @@ if (!class_exists('HomeController')) {
                 'page' => 'new_project'
             ];
 
+            // Découvrir les templates disponibles
+            require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+            $templates = TemplateFactory::listAvailable();
+            $data['templates'] = $templates;
+
             Renderer::render('projects/new', $data);
         }
 
@@ -100,7 +105,14 @@ if (!class_exists('HomeController')) {
                 $map = new Map($name, $width, $height);
             }
 
-            $created = MapModel::create($map, $description);
+            // Appliquer le template si fourni (ou template neutre par défaut)
+            require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+            $templateClass = TemplateFactory::get($template);
+            if (class_exists($templateClass) && method_exists($templateClass, 'applyTemplate')) {
+                $templateClass::applyTemplate($map);
+            }
+
+            $created = MapModel::create($map, $description, $template, $radius);
 
             // Détecter AJAX
             $isAjax = false;
@@ -146,6 +158,128 @@ if (!class_exists('HomeController')) {
             Renderer::render('projects/index', $data);
         }
 
+        /**
+         * Affiche le formulaire d'édition pour une carte existante
+         * @param int $id
+         */
+        public static function editProject(int $id)
+        {
+            // Charger la map via MapModel
+            $map = MapModel::findMap($id);
+            if (!$map) {
+                http_response_code(404);
+                echo 'Carte non trouvée';
+                return;
+            }
+
+            // Préparer les données de la view
+            require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+            $templates = TemplateFactory::listAvailable();
+
+            // Récupérer la row brute pour extraire les métadonnées JSON
+            $rawRow = null;
+            try {
+                $rawRow = MapModel::getRowById($id);
+            } catch (Throwable $e) {
+                $rawRow = null;
+            }
+
+            $templateId = null;
+            $radiusVal = null;
+            if (!empty($rawRow['data'])) {
+                $decoded = is_string($rawRow['data']) ? json_decode($rawRow['data'], true) : $rawRow['data'];
+                if (is_array($decoded)) {
+                    $templateId = $decoded['template'] ?? null;
+                    $radiusVal = isset($decoded['radius']) ? (int)$decoded['radius'] : null;
+                }
+            }
+
+            $data = [
+                'title' => 'Édition de la carte - ' . $map->getName(),
+                'page' => 'projects_edit',
+                'map' => $map,
+                'templates' => $templates,
+                'templateId' => $templateId,
+                'radiusVal' => $radiusVal,
+                'mapId' => $id,
+                'mapDescription' => $rawRow['description'] ?? ''
+            ];
+
+            Renderer::render('projects/edit', $data);
+        }
+
+        /**
+         * Traite la mise à jour d'une carte (POST)
+         */
+        public static function updateProject()
+        {
+            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+                http_response_code(405);
+                echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
+                return;
+            }
+
+            $input = $_POST;
+            $id = isset($input['id']) ? (int)$input['id'] : null;
+            if (!$id) {
+                http_response_code(400);
+                echo json_encode(['success' => false, 'message' => 'ID manquant']);
+                return;
+            }
+
+            $name = isset($input['name']) ? trim($input['name']) : '';
+            $description = $input['description'] ?? null;
+            $template = $input['template'] ?? null;
+            $radius = isset($input['radius']) ? (int)$input['radius'] : null;
+
+            $existing = MapModel::findMap($id);
+            if (!$existing) {
+                http_response_code(404);
+                echo json_encode(['success' => false, 'message' => 'Carte non trouvée']);
+                return;
+            }
+
+            // Par défaut, préserver le contenu existant
+            $map = $existing;
+
+            // Récupérer la row brute pour comparer le radius existant
+            $existingRow = MapModel::getRowById($id);
+            $existingRadius = null;
+            if (!empty($existingRow['data'])) {
+                $decoded = is_string($existingRow['data']) ? json_decode($existingRow['data'], true) : $existingRow['data'];
+                if (is_array($decoded) && isset($decoded['radius'])) {
+                    $existingRadius = (int)$decoded['radius'];
+                }
+            }
+
+            // Si l'utilisateur a fourni un radius et qu'il est différent de l'existant, reconstruire la map
+            if ($radius && $radius > 0 && $radius !== $existingRadius) {
+                $map = Map::fromRadius($name ?: $existing->getName(), $radius);
+            } else {
+                // Ne pas reconstruire : mettre à jour seulement le nom si fourni
+                if (!empty($name)) {
+                    $map->setName($name);
+                }
+            }
+
+            // Appliquer le template si fourni
+            require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+            $templateClass = TemplateFactory::get($template);
+            if (class_exists($templateClass) && method_exists($templateClass, 'applyTemplate')) {
+                $templateClass::applyTemplate($map);
+            }
+
+            // Mettre à jour
+            $ok = MapModel::update($id, $map, $description, $template, $radius);
+
+            if ($ok) {
+                header('Location: /projects');
+            } else {
+                http_response_code(500);
+                echo 'Erreur lors de la mise à jour';
+            }
+        }
+
         /**
          * Supprime une carte (endpoint POST)
          * Accepte JSON (AJAX) ou form POST standard.

+ 20 - 2
app/Models/MapModel.php

@@ -75,7 +75,7 @@ if (!class_exists('MapModel')) {
          * @param string|null $description Description optionnelle
          * @return int|false L'ID de la carte créée ou false en cas d'erreur
          */
-    public static function create(Map $map, ?string $description = null): int|false
+        public static function create(Map $map, ?string $description = null, ?string $template = null, ?int $radius = null): int|false
     {
             $pdo = self::getPDO();
 
@@ -99,6 +99,10 @@ if (!class_exists('MapModel')) {
                 'statistics' => $map->getStatistics()
             ];
 
+            // Ajouter métadonnées template et radius pour traçabilité
+            if ($template !== null) $data['template'] = $template;
+            if ($radius !== null) $data['radius'] = (int)$radius;
+
             $sql = "
                 INSERT INTO maps (name, description, width, height, data, file_path, created_at, updated_at)
                 VALUES (:name, :description, :width, :height, :data, :file_path, :created_at, :updated_at)
@@ -148,6 +152,16 @@ if (!class_exists('MapModel')) {
             }
         }
 
+        /**
+         * Retourne la row brute (publique) pour un id
+         * @param int $id
+         * @return array|null
+         */
+        public static function getRowById(int $id): ?array
+        {
+            return self::findById($id);
+        }
+
         /**
          * Récupère une carte par son ID
          *
@@ -179,7 +193,7 @@ if (!class_exists('MapModel')) {
          * @param string|null $description Nouvelle description optionnelle
          * @return bool True si la mise à jour a réussi
          */
-        public static function update(int $id, Map $map, ?string $description = null): bool
+        public static function update(int $id, Map $map, ?string $description = null, ?string $template = null, ?int $radius = null): bool
         {
             $pdo = self::getPDO();
 
@@ -207,6 +221,10 @@ if (!class_exists('MapModel')) {
                 'statistics' => $map->getStatistics()
             ];
 
+            // Ajouter métadonnées template et radius si fournies
+            if ($template !== null) $data['template'] = $template;
+            if ($radius !== null) $data['radius'] = (int)$radius;
+
             $sql = "
                 UPDATE maps
                 SET name = :name, description = :description, width = :width, height = :height,

+ 29 - 0
app/Models/Templates/MapTemplateInterface.php

@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * Interface pour les templates de carte
+ */
+if (!interface_exists('MapTemplateInterface')) {
+    interface MapTemplateInterface
+    {
+        /**
+         * Retourne l'identifiant du template
+         * @return string
+         */
+        public static function id(): string;
+
+        /**
+         * Initialise une carte (Map) selon le template
+         * Peut modifier les tuiles de la carte fournie
+         * @param Map $map
+         * @return void
+         */
+        public static function applyTemplate(Map $map): void;
+
+        /**
+         * Retourne la définition des types de tuiles pour ce template
+         * @return array
+         */
+        public static function tileDefinitions(): array;
+    }
+}

+ 24 - 0
app/Models/Templates/NeutralTemplate.php

@@ -0,0 +1,24 @@
+<?php
+
+require_once __DIR__ . '/MapTemplateInterface.php';
+
+if (!class_exists('NeutralTemplate')) {
+    class NeutralTemplate implements MapTemplateInterface
+    {
+        public static function id(): string { return 'neutral'; }
+
+        public static function applyTemplate(Map $map): void
+        {
+            // Ne modifie pas la carte — template neutre
+            // On laisse toutes les tuiles en 'empty'
+        }
+
+        public static function tileDefinitions(): array
+        {
+            return [
+                'empty' => ['label' => 'Vide', 'walkable' => true],
+                'grass' => ['label' => 'Herbe', 'walkable' => true],
+            ];
+        }
+    }
+}

+ 34 - 0
app/Models/Templates/RuralTemplate.php

@@ -0,0 +1,34 @@
+<?php
+
+require_once __DIR__ . '/MapTemplateInterface.php';
+
+if (!class_exists('RuralTemplate')) {
+    class RuralTemplate implements MapTemplateInterface
+    {
+        public static function id(): string { return 'rural'; }
+
+        public static function applyTemplate(Map $map): void
+        {
+            // Simple rural: placer des zones de forêts dispersées
+            $w = $map->getWidth();
+            $h = $map->getHeight();
+
+            for ($i = 0; $i < max(3, (int)floor(($w*$h)/50)); $i++) {
+                $q = rand(0, max(0, $w-1));
+                $r = rand(0, max(0, $h-1));
+                if ($map->isValidCoordinate($q, $r)) {
+                    $map->setTile($q, $r, new Tile(['type' => 'forest']));
+                }
+            }
+        }
+
+        public static function tileDefinitions(): array
+        {
+            return [
+                'empty' => ['label' => 'Vide', 'walkable' => true],
+                'forest' => ['label' => 'Forêt', 'walkable' => false],
+                'field' => ['label' => 'Champ', 'walkable' => true]
+            ];
+        }
+    }
+}

+ 118 - 0
app/Models/Templates/TemplateFactory.php

@@ -0,0 +1,118 @@
+<?php
+
+if (!class_exists('TemplateFactory')) {
+    class TemplateFactory
+    {
+        private static $cache = null;
+
+        /**
+         * Scanne le dossier mapModels pour découvrir les modèles disponibles
+         * Retourne un tableau id => ['id'=>..., 'name'=>..., 'description'=>..., 'class'=>...]
+         * @return array
+         */
+        public static function listAvailable(): array
+        {
+            if (self::$cache !== null) return self::$cache;
+
+            $base = __DIR__ . '/../../mapModels';
+            $results = [];
+
+            if (is_dir($base)) {
+                $dirs = scandir($base);
+                foreach ($dirs as $d) {
+                    if ($d === '.' || $d === '..') continue;
+                    $modelJson = $base . '/' . $d . '/model.json';
+                    if (file_exists($modelJson)) {
+                        $raw = file_get_contents($modelJson);
+                        $meta = json_decode($raw, true) ?: [];
+                        $id = $meta['id'] ?? $d;
+                        $name = $meta['name'] ?? $id;
+                        $desc = $meta['description'] ?? '';
+
+                        // tenter de charger un fichier PHP depuis le dossier du modèle (sécurisé)
+                        $modelDir = realpath($base . '/' . $d);
+                        $entry = $meta['entry'] ?? 'template.php';
+                        $entryPath = $modelDir ? $modelDir . DIRECTORY_SEPARATOR . $entry : null;
+
+                        $loadedClass = null;
+                        $loadedFromModel = false;
+
+                        if ($entryPath && is_file($entryPath) && pathinfo($entryPath, PATHINFO_EXTENSION) === 'php') {
+                            // protection : s'assurer que le fichier est bien dans le dossier mapModels
+                            $realEntry = realpath($entryPath);
+                            if ($realEntry !== false && strpos($realEntry, realpath($base)) === 0) {
+                                // inclure le fichier. utiliser require_once pour éviter double inclusion
+                                try {
+                                    require_once $realEntry;
+                                    $loadedFromModel = true;
+                                    // détecter automatiquement une classe qui implémente MapTemplateInterface
+                                    foreach (get_declared_classes() as $decl) {
+                                        if (is_subclass_of($decl, 'MapTemplateInterface')) {
+                                            $loadedClass = $decl;
+                                            break;
+                                        }
+                                    }
+                                    // fallback : nom conventionnel
+                                    if ($loadedClass === null) {
+                                        $canon = ucfirst($id) . 'Template';
+                                        if (class_exists($canon)) $loadedClass = $canon;
+                                    }
+                                } catch (Throwable $e) {
+                                    // ne pas interrompre la discovery sur erreur de template utilisateur
+                                    $loadedClass = null;
+                                }
+                            }
+                        }
+
+                        // s'il n'y a pas de fichier dans mapModels, tenter la classe interne comme avant
+                        if ($loadedClass === null) {
+                            $classFile = __DIR__ . '/' . ucfirst($id) . 'Template.php';
+                            $className = ucfirst($id) . 'Template';
+                            if (file_exists($classFile)) {
+                                require_once $classFile;
+                                if (class_exists($className)) $loadedClass = $className;
+                            }
+                        }
+
+                        $results[$id] = [
+                            'id' => $id,
+                            'name' => $name,
+                            'description' => $desc,
+                            'class' => $loadedClass,
+                            'entry' => $entryPath,
+                            'loadedFromModel' => $loadedFromModel
+                        ];
+                    }
+                }
+            }
+
+            // Always ensure neutral exists as fallback
+            if (!isset($results['neutral'])) {
+                // try to include NeutralTemplate
+                $neutralFile = __DIR__ . '/NeutralTemplate.php';
+                if (file_exists($neutralFile)) require_once $neutralFile;
+                $results['neutral'] = [
+                    'id' => 'neutral',
+                    'name' => 'Neutre',
+                    'description' => 'Template neutre',
+                    'class' => class_exists('NeutralTemplate') ? 'NeutralTemplate' : null
+                ];
+            }
+
+            self::$cache = $results;
+            return $results;
+        }
+
+        /**
+         * Retourne la classe du template demandé (ou NeutralTemplate)
+         * @param string|null $id
+         * @return string|null
+         */
+        public static function get(string $id = null): ?string
+        {
+            $list = self::listAvailable();
+            if ($id && isset($list[$id]) && !empty($list[$id]['class'])) return $list[$id]['class'];
+            return $list['neutral']['class'] ?? null;
+        }
+    }
+}

+ 54 - 0
app/Models/Templates/UrbanTemplate.php

@@ -0,0 +1,54 @@
+<?php
+
+require_once __DIR__ . '/MapTemplateInterface.php';
+
+if (!class_exists('UrbanTemplate')) {
+    class UrbanTemplate implements MapTemplateInterface
+    {
+        public static function id(): string { return 'urban'; }
+
+        public static function applyTemplate(Map $map): void
+        {
+            // Simple mise en place : construire un bloc central de bâtiments
+            $w = $map->getWidth();
+            $h = $map->getHeight();
+
+            // calculer centre approximatif
+            $centerQ = (int) floor($w / 2);
+            $centerR = (int) floor($h / 2);
+
+            // placer un petit quartier de bâtiments autour du centre
+            for ($dq = -1; $dq <= 1; $dq++) {
+                for ($dr = -1; $dr <= 1; $dr++) {
+                    $q = $centerQ + $dq;
+                    $r = $centerR + $dr;
+                    if ($map->isValidCoordinate($q, $r)) {
+                        $map->setTile($q, $r, new Tile(['type' => 'building']));
+                    }
+                }
+            }
+
+            // tracer deux routes en croix
+            for ($q = 0; $q < $w; $q++) {
+                if ($map->isValidCoordinate($q, $centerR)) {
+                    $map->setTile($q, $centerR, new Tile(['type' => 'road']));
+                }
+            }
+            for ($r = 0; $r < $h; $r++) {
+                if ($map->isValidCoordinate($centerQ, $r)) {
+                    $map->setTile($centerQ, $r, new Tile(['type' => 'road']));
+                }
+            }
+        }
+
+        public static function tileDefinitions(): array
+        {
+            return [
+                'empty' => ['label' => 'Vide', 'walkable' => true],
+                'building' => ['label' => 'Bâtiment', 'walkable' => false],
+                'road' => ['label' => 'Route', 'walkable' => true],
+                'park' => ['label' => 'Parc', 'walkable' => true]
+            ];
+        }
+    }
+}

+ 41 - 0
app/Views/partials/project-card.php

@@ -61,6 +61,47 @@
                 </div>
             </div>
 
+            <!-- Aperçu des tuiles du template (si disponible dans les métadonnées) -->
+            <?php
+            $templateId = null;
+            if (!empty($map['data'])) {
+                $raw = is_string($map['data']) ? json_decode($map['data'], true) : $map['data'];
+                if (is_array($raw)) {
+                    // chercher un champ template à la racine
+                    if (!empty($raw['template'])) $templateId = $raw['template'];
+                    // ou dans statistics
+                    if (!$templateId && !empty($raw['statistics']['template'])) $templateId = $raw['statistics']['template'];
+                }
+            }
+
+            if ($templateId) {
+                require_once __DIR__ . '/../Models/Templates/TemplateFactory.php';
+                $available = TemplateFactory::listAvailable();
+                if (!empty($available[$templateId]['class'])) {
+                    $class = $available[$templateId]['class'];
+                    try {
+                        $defs = method_exists($class, 'tileDefinitions') ? $class::tileDefinitions() : [];
+                    } catch (Throwable $e) {
+                        $defs = [];
+                    }
+                    if (!empty($defs)) {
+                        echo '<div class="mb-3">';
+                        echo '<small class="text-muted d-block">Tuiles (' . htmlspecialchars($available[$templateId]['name']) . ')</small>';
+                        echo '<div class="d-flex gap-2 flex-wrap mt-2">';
+                        foreach ($defs as $td) {
+                            $color = htmlspecialchars($td['color'] ?? '#ccc');
+                            $name = htmlspecialchars($td['name'] ?? $td['id'] ?? '');
+                            echo '<div style="min-width:60px;text-align:center;">';
+                            echo '<div style="width:28px;height:20px;margin:0 auto;border:1px solid rgba(0,0,0,0.08);background:' . $color . ';border-radius:3px"></div>';
+                            echo '<div style="font-size:0.8rem;color:#333">' . $name . '</div>';
+                            echo '</div>';
+                        }
+                        echo '</div></div>';
+                    }
+                }
+            }
+            ?>
+
             <!-- Description -->
             <?php if (!empty($map['description'])): ?>
                 <p class="card-text small text-muted mb-3">

+ 136 - 0
app/Views/projects/edit.php

@@ -0,0 +1,136 @@
+<?php
+/**
+ * Vue d'édition d'un projet existant
+ */
+include __DIR__ . '/../partials/header.php';
+
+// Variables attendues: $map (objet Map), $templates (array), $templateId, $radiusVal
+$mapObj = $map ?? null;
+$mapName = $mapObj ? $mapObj->getName() : '';
+$mapWidth = $mapObj ? $mapObj->getWidth() : 10;
+$mapHeight = $mapObj ? $mapObj->getHeight() : 10;
+$mapDescription = '';
+if (!empty($mapObj) && method_exists($mapObj, 'getDescription')) {
+    $mapDescription = $mapObj->getDescription();
+}
+?>
+
+<main class="main-content">
+    <div class="container-fluid">
+        <div class="row justify-content-center">
+            <div class="col-lg-8 col-xl-6">
+                <div class="d-flex align-items-center mb-4">
+                    <div>
+                        <h1 class="h3 mb-0">
+                            <i class="bi bi-pencil-square me-2"></i>Modifier le projet
+                        </h1>
+                        <p class="text-muted mb-0">Mettez à jour les paramètres de la carte</p>
+                    </div>
+                </div>
+
+                <div class="card">
+                    <div class="card-header">
+                        <h5 class="card-title mb-0">Configuration</h5>
+                    </div>
+                    <div class="card-body">
+                        <form id="editProjectForm" action="/projects/update" method="POST">
+                            <input type="hidden" name="id" value="<?php echo htmlspecialchars($mapId ?? ''); ?>">
+
+                            <div class="mb-3">
+                                <label for="mapName" class="form-label">Nom de la carte</label>
+                                <input type="text" class="form-control" id="mapName" name="name" required value="<?php echo htmlspecialchars($mapName); ?>">
+                            </div>
+
+                            <div class="mb-3">
+                                <label class="form-label">Taille (rayon hexagonal)</label>
+                                <select id="hexRadius" class="form-select">
+                                    <?php for ($r = 1; $r <= 15; $r++):
+                                        $count = 1 + 3 * $r * ($r + 1);
+                                        $sel = ($radiusVal ?? 2) == $r ? ' selected' : '';
+                                    ?>
+                                        <option value="<?php echo $r; ?>"<?php echo $sel; ?>>Rayon <?php echo $r; ?> (<?php echo $count; ?> cases)</option>
+                                    <?php endfor; ?>
+                                </select>
+                                <!-- Valeur cachée envoyée au serveur (le select est purement UX) -->
+                                <input type="hidden" id="radiusInput" name="radius" value="<?php echo htmlspecialchars($radiusVal ?? 2); ?>">
+
+                                <!-- Aperçu SVG (comme sur la page de création) -->
+                                <div id="hexPreviewContainer" class="mb-2 mt-3">
+                                    <label class="form-label small">Aperçu</label>
+                                    <div id="hexPreview" style="width:100%;height:140px;border:1px solid #e9ecef;background:#fafafa;display:flex;align-items:center;justify-content:center;"></div>
+                                </div>
+                            </div>
+
+                            <div class="mb-3">
+                                <label for="mapDescription" class="form-label">Description</label>
+                                <textarea id="mapDescription" name="description" class="form-control" rows="3"><?php echo htmlspecialchars($mapDescription ?? ''); ?></textarea>
+                            </div>
+
+                            <div class="mb-3">
+                                <label for="mapTemplate" class="form-label">Modèle</label>
+                                <select id="mapTemplate" class="form-select" name="template" required>
+                                    <option value="" disabled>Choisissez un modèle</option>
+                                    <?php
+                                    if (!empty($templates) && is_array($templates)) {
+                                        foreach ($templates as $tid => $tmeta) {
+                                            if (!in_array($tid, ['neutral','rural','urban'])) continue;
+                                            $sel = ($templateId ?? '') === $tid ? ' selected' : '';
+                                            echo '<option value="' . htmlspecialchars($tmeta['id']) . '"' . $sel . '>' . htmlspecialchars($tmeta['name']) . '</option>';
+                                        }
+                                    }
+                                    ?>
+                                </select>
+                            </div>
+
+                            <!-- Préparer et injecter les tileDefinitions côté serveur -->
+                            <?php
+                            $tilesExport = [];
+                            if (!empty($templates) && is_array($templates)) {
+                                foreach ($templates as $tid => $tmeta) {
+                                    if (empty($tmeta['class'])) continue;
+                                    $class = $tmeta['class'];
+                                    try {
+                                        if (method_exists($class, 'tileDefinitions')) {
+                                            $defs = $class::tileDefinitions();
+                                            $tilesExport[$tid] = $defs;
+                                        }
+                                    } catch (Throwable $e) {
+                                        $tilesExport[$tid] = [];
+                                    }
+                                }
+                            }
+                            ?>
+
+                            <script id="templateTilesData" type="application/json">
+                                <?= json_encode($tilesExport, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?>
+                            </script>
+
+                            <!-- Aperçu du modèle sélectionné (title/description + tiles) -->
+                            <div id="templatePreview" class="mb-4" style="display: none;">
+                                <div class="card bg-light">
+                                    <div class="card-body text-center">
+                                        <div id="templateImage" class="mb-2"></div>
+                                        <h6 id="templateTitle"></h6>
+                                        <p id="templateDescription" class="text-muted small mb-2"></p>
+                                        <div id="templateTiles" class="d-flex gap-2 justify-content-center flex-wrap"></div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div class="d-flex gap-2 justify-content-end">
+                                <a href="/projects" class="btn btn-outline-secondary">Annuler</a>
+                                <button type="submit" class="btn btn-primary">Enregistrer</button>
+                            </div>
+
+                        </form>
+                    </div>
+                </div>
+
+            </div>
+        </div>
+    </div>
+</main>
+
+<script src="/assets/js/pages/new-project.js"></script>
+
+<?php include __DIR__ . '/../partials/footer.php'; ?>

+ 47 - 29
app/Views/projects/new.php

@@ -92,15 +92,53 @@ include __DIR__ . '/../partials/header.php';
                                 </label>
                                 <select class="form-select" id="mapTemplate" name="template" required>
                                     <option value="" disabled selected>Choisissez un modèle</option>
-                                    <option value="desert">🏜️ Paysage désertique</option>
-                                    <option value="coastal">🏖️ Paysage littoral</option>
-                                    <option value="mountain">🏔️ Paysage de montagne</option>
-                                    <option value="rural">🌾 Paysage rural</option>
-                                    <option value="urban">🏙️ Paysage urbain</option>
+                                    <?php
+                                    // Si le contrôleur a passé la liste des templates, utiliser celle-ci
+                                    if (!empty($templates) && is_array($templates)) {
+                                        foreach ($templates as $tid => $tmeta) {
+                                            // n'afficher que les templates neutral, rural, urban selon demande
+                                            if (!in_array($tid, ['neutral', 'rural', 'urban'])) continue;
+                                            $label = htmlspecialchars($tmeta['name']);
+                                            $desc = htmlspecialchars($tmeta['description'] ?? '');
+                                            echo "<option value=\"{$tmeta['id']}\" title=\"{$desc}\">{$label}</option>\n";
+                                        }
+                                    } else {
+                                        // Fallbacks
+                                        echo "<option value=\"neutral\">Neutre</option>\n";
+                                        echo "<option value=\"rural\">Paysage rural</option>\n";
+                                        echo "<option value=\"urban\">Paysage urbain</option>\n";
+                                    }
+                                    ?>
                                 </select>
                                 <div class="form-text">Le modèle définit l'apparence de base de votre carte</div>
                             </div>
 
+                            <!-- Définitions de tuiles (export JSON caché pour JS) -->
+                            <?php
+                            // Préparer les définitions de tuiles côté serveur pour les templates chargés
+                            $tilesExport = [];
+                            if (!empty($templates) && is_array($templates)) {
+                                foreach ($templates as $tid => $tmeta) {
+                                    if (empty($tmeta['class'])) continue; // pas de code chargé
+                                    $class = $tmeta['class'];
+                                    try {
+                                        if (method_exists($class, 'tileDefinitions')) {
+                                            $defs = $class::tileDefinitions();
+                                            $tilesExport[$tid] = $defs;
+                                        }
+                                    } catch (Throwable $e) {
+                                        // ignore
+                                        $tilesExport[$tid] = [];
+                                    }
+                                }
+                            }
+                            ?>
+
+                            <script id="templateTilesData" type="application/json">
+                                <?= json_encode($tilesExport, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?>
+                            </script>
+                            </div>
+
                             <!-- Aperçu du modèle sélectionné -->
                             <div id="templatePreview" class="mb-4" style="display: none;">
                                 <div class="card bg-light">
@@ -109,7 +147,10 @@ include __DIR__ . '/../partials/header.php';
                                             <!-- L'image du modèle sera chargée ici via JavaScript -->
                                         </div>
                                         <h6 id="templateTitle"></h6>
-                                        <p id="templateDescription" class="text-muted small mb-0"></p>
+                                        <p id="templateDescription" class="text-muted small mb-2"></p>
+                                        <div id="templateTiles" class="d-flex gap-2 justify-content-center flex-wrap">
+                                            <!-- Les tuiles (petits carrés) seront injectées ici par JS -->
+                                        </div>
                                     </div>
                                 </div>
                             </div>
@@ -126,29 +167,6 @@ include __DIR__ . '/../partials/header.php';
                         </form>
                     </div>
                 </div>
-
-                <!-- Informations supplémentaires -->
-                <div class="card mt-4">
-                    <div class="card-body">
-                        <h6 class="card-title">
-                            <i class="bi bi-info-circle me-1"></i>Informations
-                        </h6>
-                        <ul class="list-unstyled small mb-0">
-                            <li class="mb-2">
-                                <i class="bi bi-dot me-2"></i>
-                                <strong>Dimensions :</strong> Les dimensions recommandées sont entre 50x50 et 500x500 parcelles
-                            </li>
-                            <li class="mb-2">
-                                <i class="bi bi-dot me-2"></i>
-                                <strong>Modèles :</strong> Vous pourrez modifier l'apparence de l'arrière plan après la création du projet
-                            </li>
-                            <li class="mb-0">
-                                <i class="bi bi-dot me-2"></i>
-                                <strong>Sauvegarde :</strong> Votre projet sera automatiquement sauvegardé lors de la création
-                            </li>
-                        </ul>
-                    </div>
-                </div>
             </div>
         </div>
     </div>

+ 2 - 0
logs/app.log

@@ -110,3 +110,5 @@
 [2025-10-13 09:25:45] [ERROR] Erreur : include(): Failed opening '/Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/partials/tools-sidebar.php' for inclusion (include_path='.:/Applications/MAMP/bin/php/php8.1.31/lib/php') dans /Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/header.php à la ligne 55
 [2025-10-13 09:25:47] [ERROR] Erreur : include(/Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/partials/tools-sidebar.php): Failed to open stream: No such file or directory dans /Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/header.php à la ligne 55
 [2025-10-13 09:25:47] [ERROR] Erreur : include(): Failed opening '/Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/partials/tools-sidebar.php' for inclusion (include_path='.:/Applications/MAMP/bin/php/php8.1.31/lib/php') dans /Volumes/SSD-T1/mamp-pro/map-generator/app/Views/partials/header.php à la ligne 55
+[2025-10-13 10:56:01] [ERROR] Erreur : include(/Volumes/SSD-T1/mamp-pro/map-generator/app/Controllers/../Views/projects/edit.php): Failed to open stream: No such file or directory dans /Volumes/SSD-T1/mamp-pro/map-generator/app/Controllers/Renderer.php à la ligne 22
+[2025-10-13 10:56:01] [ERROR] Erreur : include(): Failed opening '/Volumes/SSD-T1/mamp-pro/map-generator/app/Controllers/../Views/projects/edit.php' for inclusion (include_path='.:/Applications/MAMP/bin/php/php8.1.31/lib/php') dans /Volumes/SSD-T1/mamp-pro/map-generator/app/Controllers/Renderer.php à la ligne 22

+ 5 - 0
mapModels/neutral/model.json

@@ -0,0 +1,5 @@
+{
+  "id": "neutral",
+  "name": "Neutre",
+  "description": "Template neutre sans règle particulière"
+}

+ 32 - 0
mapModels/neutral/template.php

@@ -0,0 +1,32 @@
+<?php
+
+// Template PHP minimal pour neutral
+if (!interface_exists('MapTemplateInterface')) {
+    interface MapTemplateInterface
+    {
+        public static function id(): string;
+        public static function applyTemplate($map): void;
+        public static function tileDefinitions(): array;
+    }
+}
+
+class NeutralTemplate implements MapTemplateInterface
+{
+    public static function id(): string
+    {
+        return 'neutral';
+    }
+
+    public static function applyTemplate($map): void
+    {
+        // aucun changement, template neutre
+    }
+
+    public static function tileDefinitions(): array
+    {
+        return [
+            ['id' => 'empty', 'name' => 'Vide', 'color' => '#eee'],
+            ['id' => 'grass', 'name' => 'Herbe', 'color' => '#8fd694']
+        ];
+    }
+}

+ 5 - 0
mapModels/rural/model.json

@@ -0,0 +1,5 @@
+{
+  "id": "rural",
+  "name": "Paysage rural",
+  "description": "Template rural avec champs et forêts"
+}

+ 39 - 0
mapModels/rural/template.php

@@ -0,0 +1,39 @@
+<?php
+
+if (!interface_exists('MapTemplateInterface')) {
+    interface MapTemplateInterface
+    {
+        public static function id(): string;
+        public static function applyTemplate($map): void;
+        public static function tileDefinitions(): array;
+    }
+}
+
+class RuralTemplate implements MapTemplateInterface
+{
+    public static function id(): string
+    {
+        return 'rural';
+    }
+
+    public static function applyTemplate($map): void
+    {
+        if (!method_exists($map, 'getHexes')) return;
+        $hexes = $map->getHexes();
+        $i = 0;
+        foreach ($hexes as $k => $h) {
+            // marquer approximativement 10% comme forêt
+            if ($i++ % 10 === 0) {
+                if (method_exists($h, 'setTile')) $h->setTile('forest');
+            }
+        }
+    }
+
+    public static function tileDefinitions(): array
+    {
+        return [
+            ['id' => 'forest', 'name' => 'Forêt', 'color' => '#2b7a2b'],
+            ['id' => 'field', 'name' => 'Champ', 'color' => '#f4ecb0']
+        ];
+    }
+}

+ 5 - 0
mapModels/urban/model.json

@@ -0,0 +1,5 @@
+{
+  "id": "urban",
+  "name": "Paysage urbain",
+  "description": "Template urbain avec bâtiments et routes"
+}

+ 42 - 0
mapModels/urban/template.php

@@ -0,0 +1,42 @@
+<?php
+
+if (!interface_exists('MapTemplateInterface')) {
+    interface MapTemplateInterface
+    {
+        public static function id(): string;
+        public static function applyTemplate($map): void;
+        public static function tileDefinitions(): array;
+    }
+}
+
+class UrbanTemplate implements MapTemplateInterface
+{
+    public static function id(): string
+    {
+        return 'urban';
+    }
+
+    public static function applyTemplate($map): void
+    {
+        // Exemple simple : marquer quelques tuiles centrales comme buildings
+        if (!method_exists($map, 'getHexes')) return;
+        $hexes = $map->getHexes();
+        $count = count($hexes);
+        $i = 0;
+        foreach ($hexes as $k => $h) {
+            if ($i++ % 7 === 0) {
+                if (method_exists($h, 'setTile')) $h->setTile('building');
+            }
+            if ($i > 30) break;
+        }
+    }
+
+    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']
+        ];
+    }
+}

+ 70 - 12
public/assets/js/pages/new-project.js

@@ -1,14 +1,23 @@
 // JS pour la view projects/new
 (function(){
     var templates = {
-        desert: { title: 'Paysage désertique', description: 'Terrain sablonneux avec dunes et oasis occasionnelles', emoji: '🏜️' },
-        coastal: { title: 'Paysage littoral', description: 'Côtes avec plages, falaises et éléments marins', emoji: '🏖️' },
-        mountain: { title: 'Paysage de montagne', description: 'Reliefs escarpés avec sommets enneigés et vallées', emoji: '🏔️' },
-        rural: { title: 'Paysage rural', description: 'Champs, forêts et éléments agricoles traditionnels', emoji: '🌾' },
-        urban: { title: 'Paysage urbain', description: 'Villes avec bâtiments, routes et infrastructures modernes', emoji: '🏙️' }
+        neutral: { title: 'Neutre', description: 'Template neutre', emoji: '🔲' },
+        rural: { title: 'Paysage rural', description: 'Champs et forêts', emoji: '🌾' },
+        urban: { title: 'Paysage urbain', description: 'Villes, bâtiments et routes', emoji: '🏙️' }
     };
 
     var mapTemplateEl = document.getElementById('mapTemplate');
+    // Lire les tileDefinitions injectées côté serveur
+    var tilesDataEl = document.getElementById('templateTilesData');
+    var tilesData = {};
+    if (tilesDataEl) {
+        try {
+            tilesData = JSON.parse(tilesDataEl.textContent || '{}');
+        } catch (e) {
+            tilesData = {};
+        }
+    }
+
     if (mapTemplateEl) {
         mapTemplateEl.addEventListener('change', function() {
             var selectedTemplate = this.value;
@@ -16,16 +25,65 @@
             var templateImage = document.getElementById('templateImage');
             var templateTitle = document.getElementById('templateTitle');
             var templateDescription = document.getElementById('templateDescription');
+            var templateTilesContainer = document.getElementById('templateTiles');
 
-            if (selectedTemplate && templates[selectedTemplate]) {
-                var t = templates[selectedTemplate];
-                templateImage.innerHTML = '<span style="font-size: 3rem;">' + t.emoji + '</span>';
-                templateTitle.textContent = t.title;
-                templateDescription.textContent = t.description;
-                previewDiv.style.display = 'block';
-            } else {
+            if (!selectedTemplate) {
                 previewDiv.style.display = 'none';
+                return;
+            }
+
+            // Render title/description from static map or fallback
+            var staticMeta = templates[selectedTemplate] || null;
+            if (staticMeta) {
+                templateImage.innerHTML = '<span style="font-size: 3rem;">' + (staticMeta.emoji || '🎲') + '</span>';
+                templateTitle.textContent = staticMeta.title || '';
+                templateDescription.textContent = staticMeta.description || '';
+            } else {
+                templateImage.innerHTML = '';
+                templateTitle.textContent = '';
+                templateDescription.textContent = '';
+            }
+
+            // Render tiles from tilesData if present
+            templateTilesContainer.innerHTML = '';
+            var defs = tilesData[selectedTemplate] || [];
+            if (Array.isArray(defs) && defs.length > 0) {
+                defs.forEach(function(td) {
+                    var tileEl = document.createElement('div');
+                    tileEl.className = 'd-flex align-items-center gap-2';
+                    tileEl.style.margin = '0 .25rem';
+
+                    var swatch = document.createElement('div');
+                    swatch.style.width = '28px';
+                    swatch.style.height = '20px';
+                    swatch.style.background = td.color || '#ccc';
+                    swatch.style.border = '1px solid rgba(0,0,0,0.08)';
+                    swatch.style.borderRadius = '3px';
+
+                    var label = document.createElement('div');
+                    label.style.fontSize = '0.85rem';
+                    label.style.color = '#333';
+                    label.textContent = td.name || td.id || '';
+
+                    var wrapper = document.createElement('div');
+                    wrapper.style.display = 'flex';
+                    wrapper.style.flexDirection = 'column';
+                    wrapper.style.alignItems = 'center';
+                    wrapper.style.minWidth = '60px';
+
+                    wrapper.appendChild(swatch);
+                    wrapper.appendChild(label);
+                    templateTilesContainer.appendChild(wrapper);
+                });
+            } else {
+                // pas de définitions => message court
+                var msg = document.createElement('div');
+                msg.className = 'text-muted small';
+                msg.textContent = 'Aucune définition de tuiles disponible pour ce modèle.';
+                templateTilesContainer.appendChild(msg);
             }
+
+            previewDiv.style.display = 'block';
         });
     }
     // Gestion des presets hexagonaux et synchronisation du radius

+ 13 - 0
public/index.php

@@ -43,4 +43,17 @@ if (isset($routes[$uri])) {
         http_response_code(404);
         echo "Page non trouvée";
     }
+
+} else {
+    // Routes simples non-statiques (paramètres)
+    // Edition d'un projet : /projects/{id}/edit
+    if (preg_match('#^/projects/(\d+)/edit$#', $uri, $m)) {
+        require_once __DIR__ . '/../app/Controllers/HomeController.php';
+        HomeController::editProject((int)$m[1]);
+        return;
+    }
+
+    // Page non trouvée
+    http_response_code(404);
+    echo "Page non trouvée";
 }

+ 2 - 0
routes/web.php

@@ -11,4 +11,6 @@ return [
     '/projects/create' => [HomeController::class, 'createProject'],
     '/projects/new' => [HomeController::class, 'newProject'],
     '/projects/delete' => [HomeController::class, 'deleteProject'],
+    // POST mise à jour d'un projet
+    '/projects/update' => [HomeController::class, 'updateProject'],
 ];

Some files were not shown because too many files changed in this diff