From 4dd29d59d7b1e24e9b8925efddcb66fa53808a5d Mon Sep 17 00:00:00 2001 From: Stephane MAURO Date: Sat, 18 Apr 2026 09:24:59 +0000 Subject: [PATCH] sync_salles --- Patchs/sync_salles/README_SYNC_SALLES.md | 147 +++ .../GestionnaireMultiSalles.cs | 888 ++++++++++++++++++ 2 files changed, 1035 insertions(+) create mode 100644 Patchs/sync_salles/README_SYNC_SALLES.md create mode 100644 Patchs/sync_salles/Scripts_Modifies/GestionnaireMultiSalles.cs diff --git a/Patchs/sync_salles/README_SYNC_SALLES.md b/Patchs/sync_salles/README_SYNC_SALLES.md new file mode 100644 index 0000000..92d741c --- /dev/null +++ b/Patchs/sync_salles/README_SYNC_SALLES.md @@ -0,0 +1,147 @@ +# Sync multi des nouvelles salles (GestionnaireMultiSalles v2.0) + +## Contexte + +Ton dernier test a révélé que j'avais synchronisé le mauvais système : +- Tu utilises **`GestionnaireMultiSalles`** (achat "Nouvelle salle Ouest/Est/Nord/Sud" au Terminal) +- J'avais synchronisé **`AgrandissementSalle`** (panneau mural, qui n'est plus utilisé) + +Ce pack corrige ça. + +## Ce qui change + +Un seul fichier patché : **`GestionnaireMultiSalles.cs` v2.0** + +Changements : +1. La classe passe de `MonoBehaviour` à **`NetworkBehaviour`** +2. Ajout d'une **`SyncList`** qui contient les créations de salles (ex: `"1_Ouest"`, `"2_Nord"`) +3. Ajout d'un **`[ClientRpc] RpcLancerConstructionSalle`** qui broadcast l'ordre aux clients +4. `CreerNouvelleSalle` côté serveur : + - Enregistre dans la SyncList (pour rejoin) + - Broadcast le Rpc aux clients +5. Chaque instance (serveur + clients) exécute l'animation **localement** via `AnimationCreationSalleLocal` +6. Nouvelle méthode `OnStartClient` : les clients qui rejoignent en cours de partie relisent la SyncList et rejouent toutes les salles en mode **instantané** (pas d'animation) +7. Le code de génération est refactorisé : la partie "construire la géométrie de la salle + couloir" est extraite dans `CreerSalleGeometrie` pour être réutilisée par le flux normal ET le rejoin +8. Gardes `DedicatedServerMode.IsDedicatedServer` ajoutées partout où du Material/Light est créé (pas de shaders côté serveur) + +## Setup Inspector + +**Important** : `GestionnaireMultiSalles` est maintenant un `NetworkBehaviour`, donc il a besoin d'un `NetworkIdentity` sur son GameObject. + +### Étapes + +1. Dans la scène `Datacenter_01`, trouver le GameObject qui porte le composant `GestionnaireMultiSalles` +2. Si ce GameObject n'a **pas** de `NetworkIdentity` : + - `Add Component` → `NetworkIdentity` + - Dans l'Inspector du `NetworkIdentity`, **ne coche rien** (ni "Server Only" ni "Local Player Authority" — c'est un objet serveur autoritaire qui synchronise via la SyncList) +3. Sauver la scène (`Ctrl+S`) + +### Note sur les autres composants + +Tu peux **retirer** maintenant : +- `SynchronisationAgrandissement` (de la scène) +- `PlayerAgrandissementBridge` (du prefab joueur) +- Les 2 fichiers `.cs` correspondants (si tu veux nettoyer) + +Puisque `AgrandissementSalle` n'est plus utilisé dans ta version actuelle, les 2 scripts que j'avais livrés hier ne servent plus à rien. Tu peux les garder dans le code mort s'il te semble possible d'y revenir plus tard. + +## Actions à faire + +1. **Remplacer** `Assets/Scripts/GestionnaireMultiSalles.cs` par la nouvelle version +2. **Vérifier / ajouter** `NetworkIdentity` sur le GameObject qui porte ce composant +3. **Sauver la scène** +4. **Build client Windows** (pour pouvoir tester) +5. **Build Dedicated Server Linux** et déployer sur la Debian + +## Test à faire + +### Test 1 — Solo (éditeur Play) +1. Lancer l'éditeur en Play (pas host) +2. Aller au Terminal, acheter "Nouvelle salle Ouest" +3. **Attendu** : le mur explose, couloir apparaît, salle Ouest se construit comme avant + +### Test 2 — Host + Client +1. Éditeur en Host, un build Windows connecté en client +2. Le **host** achète "Nouvelle salle Est" +3. **Attendu** : les DEUX voient la même animation + couloir + salle créée + +### Test 3 — Client déclencheur +1. Host + Client +2. Le **client** achète "Nouvelle salle Nord" +3. **Attendu** : les DEUX voient la même animation + +### Test 4 — Dedicated + 2 clients +1. Dedicated sur Debian + 2 clients Windows +2. Un client achète "Nouvelle salle Sud" +3. **Attendu** : les 2 clients voient l'animation synchrone, le serveur logue la création + +### Test 5 — Rejoin +1. Dedicated + 1 client +2. Le client achète "Nouvelle salle Ouest" +3. Un **2e client** se connecte APRÈS +4. **Attendu** : le 2e client voit la nouvelle salle + le couloir déjà construits (rejeu instantané, pas d'anim) + +## Diagnostic + +Dans les logs, tu devrais voir : + +**Côté serveur** : +``` +[MultiSalles] Salle enregistree dans SyncList : 1_Ouest (total : 1) +[MultiSalles] Debut construction locale salle Ouest depuis Salle 1 (instantane=False) +[MultiSalles] Mur DC_MurEst percé avec morceaux latéraux +[MultiSalles] Nouvelle salle creee localement : Salle 2 (15x10m) direction Ouest +``` + +**Côté client** : +``` +[MultiSalles] Rpc recu : construction salle Ouest depuis parent id=1 +[MultiSalles] Debut construction locale salle Ouest depuis Salle 1 (instantane=False) +[MultiSalles] Mur DC_MurEst percé avec morceaux latéraux +[MultiSalles] Nouvelle salle creee localement : Salle 2 (15x10m) direction Ouest +``` + +**Si rien ne se passe côté client** malgré le Rpc envoyé : +- Vérifier que le GameObject qui porte `GestionnaireMultiSalles` a bien un `NetworkIdentity` +- Vérifier dans Mirror Inspector que le NetworkBehaviour est bien enregistré + +**Si "Salle parente id=X introuvable localement, skip"** : +- Problème de timing : le client n'a pas encore sa salle 1 enregistrée +- Pas grave dans 99% des cas (la salle 1 s'enregistre au démarrage via `SalleDatacenter.Start()`), mais si ça arrive à un client qui rejoint tardivement, le rejeu va skipper proprement + +## Ce qui n'est PAS encore synchronisé + +- **Les équipements placés dans les nouvelles salles** : quand un joueur pose une baie dans la salle 2, ça passe par `BoutiqueReseau.TraiterPlacementBaieServeur` qui utilise bien `NetworkServer.Spawn`, donc **c'est déjà synchronisé**. Pas de travail. +- **L'état "mur percé" dans la SyncList** : je ne track pas séparément les percements de mur, je track la création de salle. Le percement est fait automatiquement quand la salle est créée (dans `AnimationCreationSalleLocal`). Donc pas de divergence possible. +- **L'ordre des salles créées** : la SyncList conserve l'ordre d'insertion. Si un client rejoint, il rejoue dans l'ordre → pas de conflit. + +## Architecture en un schéma + +``` +Client achete "Nouvelle salle Ouest" au Terminal + │ + ▼ +BoutiqueReseau.DemanderCommandeSalle + │ + ▼ +PlayerBoutiqueCommande.CmdCommanderSalle (deja en place, aucun changement) + │ + ▼ +[SERVEUR] BoutiqueReseau.TraiterCommandeSalleServeur + │ + ▼ +[SERVEUR] GestionnaireMultiSalles.CreerNouvelleSalle + │ + ├── Ajoute "1_Ouest" a _sallesSyncList (→ sync auto aux clients) + │ + └── RpcLancerConstructionSalle(1, "Ouest") + │ + ├── [CLIENT 1] StartCoroutine(AnimationCreationSalleLocal(1, "Ouest", false)) + ├── [CLIENT 2] StartCoroutine(AnimationCreationSalleLocal(1, "Ouest", false)) + └── [HOST si applicable] skip (deja joue cote serveur) + +[NOUVEAU CLIENT qui rejoint plus tard] + │ + ▼ +OnStartClient : lit _sallesSyncList → rejoue chaque entree en mode instantane=true +``` diff --git a/Patchs/sync_salles/Scripts_Modifies/GestionnaireMultiSalles.cs b/Patchs/sync_salles/Scripts_Modifies/GestionnaireMultiSalles.cs new file mode 100644 index 0000000..89eabc3 --- /dev/null +++ b/Patchs/sync_salles/Scripts_Modifies/GestionnaireMultiSalles.cs @@ -0,0 +1,888 @@ +// GestionnaireMultiSalles.cs - v2.0 Multi-compatible + +using UnityEngine; +using Mirror; +using System.Collections; +using System.Collections.Generic; + +/// +/// Gestionnaire central des salles du datacenter. +/// NetworkBehaviour singleton — a placer sur un GameObject persistant dans la scene +/// AVEC un NetworkIdentity. +/// +/// v2.0 : Synchronisation multijoueur via SyncList + ClientRpc +/// - Le serveur est autoritaire : il valide l'achat, enregistre la salle creee +/// dans _sallesSyncList (synchronise automatiquement aux clients) +/// - Chaque client execute l'animation en local grace a RpcLancerConstructionSalle +/// - Un client qui rejoint en cours de partie recoit la SyncList et rejoue +/// toutes les salles deja construites en mode instantane (pas d'animation) +/// +/// Gere : +/// - Le registre de toutes les salles (salle d'origine + extensions) +/// - La creation de nouvelles salles (percement mur, couloir, nouvelle salle) +/// - L'attribution d'IDs et noms uniques +/// - La synchronisation reseau en multi +/// +/// Workflow (multi) : +/// 1. Client achete au Terminal → DemanderCommandeSalle → CmdCommanderSalle (deja en place) +/// 2. Serveur : TraiterCommandeSalleServeur → CreerNouvelleSalle → valide, enregistre +/// dans la SyncList, emet RpcLancerConstructionSalle(parentId, direction) +/// 3. Chaque instance (serveur + clients) execute AnimationCreationSalleLocal +/// avec les memes parametres → meme geometrie partout +/// 4. Un client qui rejoint : OnStartClient rejoue la SyncList en mode instantane +/// +public class GestionnaireMultiSalles : NetworkBehaviour +{ + public static GestionnaireMultiSalles Instance { get; private set; } + + [Header("Configuration couloirs")] + [Tooltip("Longueur du couloir entre deux salles (en mètres)")] + public float longueurCouloir = 3f; + [Tooltip("Largeur du couloir (ouverture dans le mur)")] + public float largeurCouloir = 4f; + + [Header("Configuration nouvelles salles")] + [Tooltip("Dimensions par défaut d'une nouvelle salle")] + public float nouvelleSalleLongueur = 15f; + public float nouvelleSalleLargeur = 10f; + [Tooltip("Prix d'une nouvelle salle")] + public float prixNouvelleSalle = 50000f; + + [Header("Animation")] + public int nombreDebris = 20; + public float forceExplosion = 4f; + + [Header("État")] + public List salles = new List(); + + private int _nextId = 1; + + // ==================== SYNC MULTI ==================== + + /// + /// Liste synchronisee des creations de salles effectuees cote serveur. + /// Format des entrees : "parentId_direction" (ex: "1_Ouest", "2_Nord"). + /// Les clients qui rejoignent en cours de partie rejouent ces entrees en + /// mode instantane pour reconstruire l'etat du monde. + /// + private readonly SyncList _sallesSyncList = new SyncList(); + + // ==================== SALLE INFO ==================== + + [System.Serializable] + public class SalleInfo + { + public int id; + public string nom; + public SalleDatacenter salle; + public Vector3 positionMonde; + public int salleParenteId; // -1 pour la salle d'origine + public string directionDepuisParent; // "Nord", "Sud", "Est", "Ouest" + + public SalleInfo(int id, string nom, SalleDatacenter salle, Vector3 pos, int parentId, string direction) + { + this.id = id; + this.nom = nom; + this.salle = salle; + this.positionMonde = pos; + this.salleParenteId = parentId; + this.directionDepuisParent = direction; + } + } + + // ==================== LIFECYCLE ==================== + + void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + } + + public override void OnStartClient() + { + base.OnStartClient(); + + // Ne pas rejouer sur le host (il est deja a jour, c'est lui le serveur) + if (NetworkServer.active) return; + + // Rejouer immediatement les entrees deja presentes dans la SyncList + if (_sallesSyncList.Count > 0) + { + Debug.Log($"[MultiSalles] Client rejoint : rejeu de {_sallesSyncList.Count} salle(s) deja construite(s)"); + foreach (string id in _sallesSyncList) + { + RejouerCreationSalleInstantane(id); + } + } + } + + // ==================== ENREGISTREMENT ==================== + + /// + /// Enregistre une salle existante (appelé par SalleDatacenter.Start() pour la salle d'origine). + /// + public SalleInfo EnregistrerSalle(SalleDatacenter salle, string nom = null, int parentId = -1, string direction = "") + { + // Vérifie si déjà enregistrée + foreach (var s in salles) + if (s.salle == salle) return s; + + int id = _nextId++; + string nomSalle = nom ?? ("Salle " + id); + SalleInfo info = new SalleInfo(id, nomSalle, salle, salle.transform.position, parentId, direction); + salles.Add(info); + Debug.Log($"[MultiSalles] Salle enregistrée : {nomSalle} (ID={id}) @ {salle.transform.position}"); + return info; + } + + // ==================== ACCESSEURS ==================== + + public SalleInfo GetSalleParId(int id) + { + foreach (var s in salles) + if (s.id == id) return s; + return null; + } + + public SalleInfo GetSalleParRef(SalleDatacenter salle) + { + foreach (var s in salles) + if (s.salle == salle) return s; + return null; + } + + public List GetToutesSalles() => salles; + + public int GetNombreSalles() => salles.Count; + + // ==================== VÉRIFICATIONS ==================== + + /// + /// Vérifie si une direction est disponible pour une salle donnée. + /// + public bool DirectionDisponible(SalleDatacenter salleParente, string direction) + { + SalleInfo info = GetSalleParRef(salleParente); + if (info == null) return false; + + // Vérifie qu'aucune salle n'est déjà connectée dans cette direction + foreach (var s in salles) + if (s.salleParenteId == info.id && s.directionDepuisParent == direction) + return false; + + return true; + } + + /// + /// Retourne les directions disponibles pour une salle. + /// + public List GetDirectionsDisponibles(SalleDatacenter salle) + { + List dispo = new List(); + string[] toutes = { "Nord", "Sud", "Est", "Ouest" }; + foreach (string dir in toutes) + if (DirectionDisponible(salle, dir)) + dispo.Add(dir); + return dispo; + } + + // ==================== CRÉATION NOUVELLE SALLE ==================== + + /// + /// v2.0 : Point d'entree serveur pour creer une salle. + /// Cette methode est appelee cote serveur par BoutiqueReseau.TraiterCommandeSalleServeur + /// (qui est dans un flux Cmd venant du client). Elle valide, enregistre dans la SyncList + /// pour le rejoin, puis broadcast a tous les clients pour qu'ils executent l'animation + /// en local. + /// + /// En mode solo (pas de Mirror actif), execute directement en local. + /// + public bool CreerNouvelleSalle(SalleDatacenter salleParente, string direction) + { + SalleInfo parentInfo = GetSalleParRef(salleParente); + if (parentInfo == null) + { + Debug.LogError("[MultiSalles] Salle parente non enregistrée !"); + return false; + } + + if (!DirectionDisponible(salleParente, direction)) + { + Debug.LogWarning($"[MultiSalles] Direction {direction} déjà occupée !"); + return false; + } + + // Mode solo : execution directe + if (!NetworkServer.active && !NetworkClient.active) + { + Debug.Log($"[MultiSalles] Mode solo : creation directe de la salle {direction} depuis {parentInfo.nom}"); + StartCoroutine(AnimationCreationSalleLocal(parentInfo.id, direction, false)); + return true; + } + + // Mode multi : SEUL LE SERVEUR PEUT CREER. Si on n'est pas serveur, refuser. + // (cette methode n'est censee etre appelee que depuis TraiterCommandeSalleServeur + // qui tourne sur le serveur apres une Cmd client) + if (!NetworkServer.active) + { + Debug.LogError("[MultiSalles] CreerNouvelleSalle appelee cote client ! Cela doit passer par une Cmd."); + return false; + } + + // Enregistrer dans la SyncList pour les futurs joueurs qui rejoignent + string id = ConstruireId(parentInfo.id, direction); + if (!_sallesSyncList.Contains(id)) + { + _sallesSyncList.Add(id); + Debug.Log($"[MultiSalles] Salle enregistree dans SyncList : {id} (total : {_sallesSyncList.Count})"); + } + + // Broadcast aux clients : chacun va executer l'animation en local + RpcLancerConstructionSalle(parentInfo.id, direction); + return true; + } + + /// + /// Version avec achat intégré — appelée par la boutique. + /// + public bool AcheterEtCreerSalle(SalleDatacenter salleParente, string direction) + { + if (GameEconomy.Instance != null && !GameEconomy.Instance.modeSandbox) + { + if (!GameEconomy.Instance.TenterAchat(prixNouvelleSalle)) + { + Debug.Log("[MultiSalles] Fonds insuffisants !"); + return false; + } + } + return CreerNouvelleSalle(salleParente, direction); + } + + /// + /// Crée une nouvelle salle IMMÉDIATEMENT (sans animation, sans coroutine). + /// Utilisé par le SaveManager lors du chargement d'une sauvegarde. + /// Retourne la SalleDatacenter créée, ou null en cas d'erreur. + /// + public SalleDatacenter CreerNouvelleSalleImmediat(SalleDatacenter salleParente, string direction) + { + SalleInfo parentInfo = GetSalleParRef(salleParente); + if (parentInfo == null) + { + Debug.LogError("[MultiSalles] Salle parente non enregistrée pour création immédiate !"); + return null; + } + + // Execution immediate locale (pas de reseau, juste la geometrie) + return CreerSalleGeometrie(parentInfo, direction, instantane: true); + } + + // ==================== RPC CLIENTS ==================== + + /// + /// v2.0 : Diffuse a tous les clients (+ le host) pour lancer l'animation de construction + /// en local. Chaque client execute sa propre copie avec les memes parametres. + /// + [ClientRpc] + private void RpcLancerConstructionSalle(int parentId, string direction) + { + // Sur le host, le serveur a deja joue l'animation (c'est lui qui a valide). + // On evite de la rejouer. + if (NetworkServer.active && isClient) return; + + Debug.Log($"[MultiSalles] Rpc recu : construction salle {direction} depuis parent id={parentId}"); + StartCoroutine(AnimationCreationSalleLocal(parentId, direction, false)); + } + + /// + /// Execute l'animation ET la construction geometrique en local. + /// Cote serveur OU cote client : chacun genere sa propre copie. + /// + private IEnumerator AnimationCreationSalleLocal(int parentId, string direction, bool instantane) + { + SalleInfo parentInfo = GetSalleParId(parentId); + if (parentInfo == null) + { + Debug.LogWarning($"[MultiSalles] Salle parente id={parentId} introuvable localement, skip"); + yield break; + } + + if (parentInfo.salle == null) + { + Debug.LogWarning($"[MultiSalles] SalleDatacenter de la salle parente id={parentId} est null, skip"); + yield break; + } + + // Verifier qu'on n'a pas deja construit cette salle localement (protection idempotence) + foreach (var s in salles) + { + if (s.salleParenteId == parentId && s.directionDepuisParent == direction) + { + Debug.Log($"[MultiSalles] Salle {direction} depuis parent {parentId} deja construite localement, skip"); + yield break; + } + } + + SalleDatacenter salleParente = parentInfo.salle; + Debug.Log($"[MultiSalles] Debut construction locale salle {direction} depuis {parentInfo.nom} (instantane={instantane})"); + + // === PHASE 1 : Percement du mur (anime ou instantane) === + if (instantane || DedicatedServerMode.IsDedicatedServer) + { + SupprimerMur(salleParente, direction); + } + else + { + yield return StartCoroutine(PercerMur(salleParente, direction)); + } + + // === PHASE 2 : Construction geometrique de la nouvelle salle === + // Attente courte pour laisser les debris jouer (anime seulement) + if (!instantane && !DedicatedServerMode.IsDedicatedServer) + yield return new WaitForSeconds(0.5f); + + CreerSalleGeometrie(parentInfo, direction, instantane); + } + + /// + /// v2.0 : Methode centrale qui cree la geometrie (couloir + salle + percement mur nouvelle salle). + /// Pas de coroutine ici : appel direct, retourne immediatement la SalleDatacenter creee. + /// Utilisee par : + /// - AnimationCreationSalleLocal (apres l'animation de percement) + /// - CreerNouvelleSalleImmediat (SaveManager) + /// + private SalleDatacenter CreerSalleGeometrie(SalleInfo parentInfo, string direction, bool instantane) + { + SalleDatacenter salleParente = parentInfo.salle; + Vector3 origineParente = salleParente.transform.position; + float pL = salleParente.longueur; + float pW = salleParente.largeur; + float ep = salleParente.epaisseurMur; + + Vector3 origineNouvelle = CalculerPositionNouvelleSalle(origineParente, pL, pW, ep, direction); + float nouvL = nouvelleSalleLongueur; + float nouvW = nouvelleSalleLargeur; + + // Pour Est/Ouest la nouvelle salle garde la même largeur (= même Z) + // Pour Nord/Sud la nouvelle salle garde la même longueur (= même X) + if (direction == "Est" || direction == "Ouest") + nouvW = pW; + else + nouvL = pL; + + // Couloir + Vector3 debutCouloir, finCouloir; + CalculerPositionsCouloir(origineParente, pL, pW, ep, direction, out debutCouloir, out finCouloir); + GenererCouloir(debutCouloir, finCouloir, direction, salleParente); + + // Nouvelle salle (nom base sur parentId+direction pour garantir meme nom partout) + GameObject nouvelleSalleObj = new GameObject($"SalleDatacenter_p{parentInfo.id}_{direction}"); + nouvelleSalleObj.transform.position = origineNouvelle; + + SalleDatacenter nouvelleSalle = nouvelleSalleObj.AddComponent(); + CopierParametresSalle(salleParente, nouvelleSalle); + nouvelleSalle.longueur = nouvL; + nouvelleSalle.largeur = nouvW; + + // Note : GenererSalle() va appeler Start() de SalleDatacenter qui va + // tenter de s'auto-enregistrer. L'enregistrement se fait la si c'est la premiere fois. + nouvelleSalle.GenererSalle(); + + // Percer le mur de la nouvelle salle cote couloir + string dirOpposee = GetDirectionOpposee(direction); + SupprimerMur(nouvelleSalle, dirOpposee); + + // Enregistrement local + SalleInfo newInfo = EnregistrerSalle(nouvelleSalle, null, parentInfo.id, direction); + Debug.Log($"[MultiSalles] Nouvelle salle creee localement : {newInfo.nom} ({nouvL}x{nouvW}m) direction {direction}"); + + // Audio (cote client seulement) + if (!DedicatedServerMode.IsDedicatedServer && AudioManager.Instance != null) + AudioManager.Instance.JouerNotification(); + + // Notifier la boutique que la construction est terminee + if (OnConstructionTerminee != null) OnConstructionTerminee(direction, newInfo.nom); + + return nouvelleSalle; + } + + /// + /// Rejouer une entree de la SyncList en mode instantane (pour un client qui rejoint). + /// + private void RejouerCreationSalleInstantane(string id) + { + int parentId; + string direction; + if (!DecomposerId(id, out parentId, out direction)) + { + Debug.LogWarning($"[MultiSalles] Id mal forme : {id}"); + return; + } + + // Lancer la coroutine en mode instantane + StartCoroutine(AnimationCreationSalleLocal(parentId, direction, true)); + } + + /// + /// Événement déclenché quand une salle est construite. + /// Paramètres : direction, nom de la salle. + /// + public System.Action OnConstructionTerminee; + + // ==================== CALCULS POSITION ==================== + + Vector3 CalculerPositionNouvelleSalle(Vector3 origineParente, float pL, float pW, float ep, string direction) + { + float corridor = longueurCouloir + ep * 2; // mur parent + couloir + mur nouvelle salle + + switch (direction) + { + case "Nord": + return new Vector3(origineParente.x, origineParente.y, origineParente.z + pW + corridor); + case "Sud": + float nouvW_sud = nouvelleSalleLargeur; + // Pour Nord/Sud, on utilise la même longueur X que le parent + return new Vector3(origineParente.x, origineParente.y, origineParente.z - nouvW_sud - corridor); + case "Est": + return new Vector3(origineParente.x + pL + corridor, origineParente.y, origineParente.z); + case "Ouest": + float nouvL_ouest = nouvelleSalleLongueur; + return new Vector3(origineParente.x - nouvL_ouest - corridor, origineParente.y, origineParente.z); + default: + return origineParente; + } + } + + void CalculerPositionsCouloir(Vector3 origineParente, float pL, float pW, float ep, string direction, + out Vector3 debut, out Vector3 fin) + { + // Le couloir commence à la face EXTÉRIEURE du mur de la salle parente + // et se termine à la face EXTÉRIEURE du mur de la nouvelle salle. + // Les murs sont percés, donc le couloir doit couvrir l'épaisseur des deux murs + l'espace entre. + // Position = face extérieure du mur parent → face extérieure du mur nouvelle salle + switch (direction) + { + case "Nord": + debut = origineParente + new Vector3(pL / 2f, 0, pW); + fin = debut + new Vector3(0, 0, longueurCouloir + ep * 2); + break; + case "Sud": + debut = origineParente + new Vector3(pL / 2f, 0, 0); + fin = debut + new Vector3(0, 0, -(longueurCouloir + ep * 2)); + break; + case "Est": + debut = origineParente + new Vector3(pL, 0, pW / 2f); + fin = debut + new Vector3(longueurCouloir + ep * 2, 0, 0); + break; + case "Ouest": + debut = origineParente + new Vector3(0, 0, pW / 2f); + fin = debut + new Vector3(-(longueurCouloir + ep * 2), 0, 0); + break; + default: + debut = fin = origineParente; + break; + } + } + + // ==================== PERCEMENT MUR ==================== + + /// + /// Perce un mur en le remplaçant par deux morceaux latéraux de chaque côté de l'ouverture. + /// L'ouverture fait largeurCouloir de large, centrée sur le mur. + /// + IEnumerator PercerMur(SalleDatacenter salle, string direction) + { + string nomMur = "DC_Mur" + direction; + Transform murT = null; + + foreach (Transform child in salle.GetComponentsInChildren()) + { + if (child.name == nomMur) { murT = child; break; } + } + + if (murT == null) + { + Debug.LogWarning($"[MultiSalles] Mur {nomMur} introuvable — peut-être déjà percé"); + yield break; + } + + // Sauvegarder les infos du mur avant destruction + Vector3 murPos = murT.position; + Vector3 murScale = murT.localScale; + Transform murParent = murT.parent; + + // Cote serveur : pas de debris animes (pas de shader) + Material matMur = null; + if (!DedicatedServerMode.IsDedicatedServer) + { + matMur = new Material(Shader.Find("Standard")); + matMur.color = salle.couleurMur; + } + + // Animation debris (cote client uniquement) + List debris = null; + if (!DedicatedServerMode.IsDedicatedServer) + { + Vector3 dirVec = GetDirectionVector(direction); + debris = new List(); + + for (int i = 0; i < nombreDebris; i++) + { + GameObject d = GameObject.CreatePrimitive(PrimitiveType.Cube); + d.name = "Debris_" + i; + float rx = Random.Range(-murScale.x / 3f, murScale.x / 3f); + float ry = Random.Range(0f, salle.hauteurMurs); + d.transform.position = murPos + murT.right * rx + Vector3.up * ry + Random.insideUnitSphere * 0.1f; + float sz = Random.Range(0.08f, 0.2f); + d.transform.localScale = new Vector3(sz, sz * Random.Range(0.5f, 1.5f), sz * Random.Range(0.3f, 0.8f)); + d.transform.rotation = Random.rotation; + d.GetComponent().material = matMur; + Rigidbody rb = d.AddComponent(); + rb.mass = Random.Range(0.5f, 1.5f); + rb.AddForce((dirVec + Vector3.up * Random.Range(0.2f, 0.5f) + Random.insideUnitSphere * 0.3f) * forceExplosion, ForceMode.Impulse); + rb.AddTorque(Random.insideUnitSphere * 2f, ForceMode.Impulse); + debris.Add(d); + } + } + + // Supprimer le mur original + Destroy(murT.gameObject); + + // Attente + nettoyage debris (cote client uniquement) + if (!DedicatedServerMode.IsDedicatedServer) + { + yield return new WaitForSeconds(2f); + if (debris != null) + { + foreach (var d in debris) + if (d != null) Destroy(d); + } + } + + // Créer les deux morceaux de mur latéraux + CreerMorceauxMurLateraux(salle, direction, murPos, murScale, murParent, matMur); + } + + /// + /// Remplace un mur supprimé par deux morceaux de chaque côté de l'ouverture. + /// Utilisé à la fois pour la salle parente (après animation) et la nouvelle salle (immédiat). + /// + public void SupprimerMur(SalleDatacenter salle, string direction) + { + string nomMur = "DC_Mur" + direction; + Transform murT = null; + + foreach (Transform child in salle.GetComponentsInChildren()) + { + if (child.name == nomMur) { murT = child; break; } + } + + if (murT == null) return; + + Vector3 murPos = murT.position; + Vector3 murScale = murT.localScale; + Transform murParent = murT.parent; + + Material matMur = null; + if (!DedicatedServerMode.IsDedicatedServer) + { + matMur = new Material(Shader.Find("Standard")); + matMur.color = salle.couleurMur; + } + + // DestroyImmediate car on est souvent dans la même frame que GenererSalle + DestroyImmediate(murT.gameObject); + + CreerMorceauxMurLateraux(salle, direction, murPos, murScale, murParent, matMur); + Debug.Log($"[MultiSalles] Mur {nomMur} percé avec morceaux latéraux"); + } + + /// + /// Crée deux morceaux de mur de chaque côté de l'ouverture du couloir. + /// + void CreerMorceauxMurLateraux(SalleDatacenter salle, string direction, + Vector3 murPos, Vector3 murScale, Transform murParent, Material matMur) + { + float hauteurMurs = salle.hauteurMurs; + float ep = salle.epaisseurMur; + + // Déterminer la longueur totale du mur et l'axe + bool estNordSud = (direction == "Nord" || direction == "Sud"); + float longueurMur = estNordSud ? murScale.x : murScale.z; // longueur du mur original + float demiOuverture = largeurCouloir / 2f; + float longueurMorceau = (longueurMur - largeurCouloir) / 2f; + + if (longueurMorceau <= 0.01f) return; // pas de place pour les morceaux + + for (int cote = 0; cote < 2; cote++) + { + float signe = (cote == 0) ? -1f : 1f; + float offset = signe * (demiOuverture + longueurMorceau / 2f); + + Vector3 morceauPos = murPos; + Vector3 morceauScale = murScale; + + if (estNordSud) + { + morceauPos.x += offset; + morceauScale.x = longueurMorceau; + } + else + { + morceauPos.z += offset; + morceauScale.z = longueurMorceau; + } + + GameObject morceau = GameObject.CreatePrimitive(PrimitiveType.Cube); + morceau.name = $"DC_Mur{direction}_Morceau_{cote}"; + morceau.transform.SetParent(murParent); + morceau.transform.position = morceauPos; + morceau.transform.localScale = morceauScale; + if (morceau.GetComponent() != null && matMur != null) + morceau.GetComponent().sharedMaterial = matMur; + morceau.isStatic = true; + // Le BoxCollider est déjà ajouté par CreatePrimitive + } + } + + // ==================== COULOIR ==================== + + GameObject GenererCouloir(Vector3 debut, Vector3 fin, string direction, SalleDatacenter salleRef) + { + GameObject couloir = new GameObject($"Couloir_{direction}_{salles.Count + 1}"); + + float hauteurSol = salleRef.hauteurSol; + float hauteurMurs = salleRef.hauteurMurs; + float hauteurPlafond = salleRef.hauteurPlafond; + float epaisseurDalle = salleRef.epaisseurDalle; + + // Materiels : skip cote serveur + Material matSol = null, matMur = null, matPlafond = null, matNeon = null; + if (!DedicatedServerMode.IsDedicatedServer) + { + matSol = new Material(Shader.Find("Standard")); + matSol.color = salleRef.couleurDalleSol; + matSol.SetFloat("_Metallic", 0.1f); + matSol.SetFloat("_Glossiness", 0.3f); + + matMur = new Material(Shader.Find("Standard")); + matMur.color = salleRef.couleurMur; + + matPlafond = new Material(Shader.Find("Standard")); + matPlafond.color = salleRef.couleurPlafondDalle; + + matNeon = new Material(Shader.Find("Standard")); + matNeon.color = salleRef.couleurNeon; + matNeon.EnableKeyword("_EMISSION"); + matNeon.SetColor("_EmissionColor", salleRef.couleurNeon * 2f); + } + + // Calcul dimensions + Vector3 centre = (debut + fin) / 2f; + bool estNordSud = (direction == "Nord" || direction == "Sud"); + + float couloirLong = longueurCouloir; + float couloirLarg = largeurCouloir; + + float solX, solZ, murLong; + if (estNordSud) + { + solX = couloirLarg; + solZ = couloirLong; + murLong = couloirLong; + } + else + { + solX = couloirLong; + solZ = couloirLarg; + murLong = couloirLong; + } + + // Sol + GameObject sol = GameObject.CreatePrimitive(PrimitiveType.Cube); + sol.name = "Couloir_Sol"; + sol.transform.SetParent(couloir.transform); + sol.transform.position = centre + Vector3.up * hauteurSol; + sol.transform.localScale = new Vector3(solX, epaisseurDalle, solZ); + if (sol.GetComponent() != null && matSol != null) + sol.GetComponent().sharedMaterial = matSol; + sol.isStatic = true; + + // Sol collider — sommet au niveau de la surface de marche (TOUJOURS cree, serveur et client) + GameObject solCol = new GameObject("Couloir_SolCollider"); + solCol.transform.SetParent(couloir.transform); + float solSommet = hauteurSol + epaisseurDalle / 2f + 0.01f; + float solBase = -2f; + float solEp = solSommet - solBase; + solCol.transform.position = centre + Vector3.up * ((solSommet + solBase) / 2f); + BoxCollider sc = solCol.AddComponent(); + sc.size = new Vector3(solX + 0.5f, solEp, solZ + 0.5f); + int layerSol = LayerMask.NameToLayer("Sol"); + if (layerSol >= 0) solCol.layer = layerSol; + + // Murs latéraux + float mY = hauteurMurs / 2f; + float ep = salleRef.epaisseurMur; + + if (estNordSud) + { + // Murs gauche et droit (le long de X) + GameObject murG = GameObject.CreatePrimitive(PrimitiveType.Cube); + murG.name = "Couloir_MurG"; + murG.transform.SetParent(couloir.transform); + murG.transform.position = centre + new Vector3(-couloirLarg / 2f - ep / 2f, mY, 0); + murG.transform.localScale = new Vector3(ep, hauteurMurs, solZ); + if (murG.GetComponent() != null && matMur != null) + murG.GetComponent().sharedMaterial = matMur; + murG.isStatic = true; + murG.AddComponent(); + + GameObject murD = GameObject.CreatePrimitive(PrimitiveType.Cube); + murD.name = "Couloir_MurD"; + murD.transform.SetParent(couloir.transform); + murD.transform.position = centre + new Vector3(couloirLarg / 2f + ep / 2f, mY, 0); + murD.transform.localScale = new Vector3(ep, hauteurMurs, solZ); + if (murD.GetComponent() != null && matMur != null) + murD.GetComponent().sharedMaterial = matMur; + murD.isStatic = true; + murD.AddComponent(); + } + else + { + // Murs avant et arrière (le long de Z) + GameObject murAv = GameObject.CreatePrimitive(PrimitiveType.Cube); + murAv.name = "Couloir_MurAv"; + murAv.transform.SetParent(couloir.transform); + murAv.transform.position = centre + new Vector3(0, mY, -couloirLarg / 2f - ep / 2f); + murAv.transform.localScale = new Vector3(solX, hauteurMurs, ep); + if (murAv.GetComponent() != null && matMur != null) + murAv.GetComponent().sharedMaterial = matMur; + murAv.isStatic = true; + murAv.AddComponent(); + + GameObject murAr = GameObject.CreatePrimitive(PrimitiveType.Cube); + murAr.name = "Couloir_MurAr"; + murAr.transform.SetParent(couloir.transform); + murAr.transform.position = centre + new Vector3(0, mY, couloirLarg / 2f + ep / 2f); + murAr.transform.localScale = new Vector3(solX, hauteurMurs, ep); + if (murAr.GetComponent() != null && matMur != null) + murAr.GetComponent().sharedMaterial = matMur; + murAr.isStatic = true; + murAr.AddComponent(); + } + + // Plafond + GameObject plafond = GameObject.CreatePrimitive(PrimitiveType.Cube); + plafond.name = "Couloir_Plafond"; + plafond.transform.SetParent(couloir.transform); + plafond.transform.position = centre + Vector3.up * hauteurMurs; + plafond.transform.localScale = new Vector3(solX + ep * 2, 0.1f, solZ + ep * 2); + if (plafond.GetComponent() != null && matPlafond != null) + plafond.GetComponent().sharedMaterial = matPlafond; + plafond.isStatic = true; + + // Neon central + Light : SEULEMENT cote client (serveur n'a pas besoin de lumiere) + if (!DedicatedServerMode.IsDedicatedServer) + { + GameObject neon = GameObject.CreatePrimitive(PrimitiveType.Cube); + neon.name = "Couloir_Neon"; + neon.transform.SetParent(couloir.transform); + neon.transform.position = centre + Vector3.up * (hauteurPlafond); + float neonL = estNordSud ? 0.5f : murLong * 0.6f; + float neonW = estNordSud ? murLong * 0.6f : 0.5f; + neon.transform.localScale = new Vector3(neonL, 0.03f, neonW); + if (neon.GetComponent() != null && matNeon != null) + neon.GetComponent().sharedMaterial = matNeon; + neon.isStatic = true; + Collider nc = neon.GetComponent(); + if (nc != null) Destroy(nc); + + GameObject lightObj = new GameObject("Couloir_Light"); + lightObj.transform.SetParent(couloir.transform); + lightObj.transform.position = centre + Vector3.up * (hauteurPlafond - 0.1f); + Light light = lightObj.AddComponent(); + light.type = LightType.Point; + light.color = salleRef.couleurLumiere; + light.intensity = salleRef.intensiteLumiere; + light.range = 6f; + light.shadows = LightShadows.Hard; + lightObj.isStatic = true; + } + + return couloir; + } + + // ==================== HELPERS ==================== + + void CopierParametresSalle(SalleDatacenter source, SalleDatacenter dest) + { + dest.hauteurSol = source.hauteurSol; + dest.hauteurPlafond = source.hauteurPlafond; + dest.hauteurMurs = source.hauteurMurs; + dest.tailleDalle = source.tailleDalle; + dest.epaisseurDalle = source.epaisseurDalle; + dest.espacementDalles = source.espacementDalles; + dest.frequenceDalleVentilee = source.frequenceDalleVentilee; + dest.tailleDallePlafond = source.tailleDallePlafond; + dest.epaisseurPlafond = source.epaisseurPlafond; + dest.frequenceNeon = source.frequenceNeon; + dest.intensiteLumiere = source.intensiteLumiere; + dest.couleurLumiere = source.couleurLumiere; + dest.epaisseurMur = source.epaisseurMur; + dest.nbRangees = source.nbRangees; + dest.emplacementsParRangee = source.emplacementsParRangee; + dest.espaceAlleeFroide = source.espaceAlleeFroide; + dest.espaceAlleeChaude = source.espaceAlleeChaude; + dest.largeurEmplacement = source.largeurEmplacement; + dest.profondeurEmplacement = source.profondeurEmplacement; + dest.margeDebutRangee = source.margeDebutRangee; + dest.espaceBaiesDansRangee = source.espaceBaiesDansRangee; + dest.decalageZRangees = source.decalageZRangees; + dest.couleurDalleSol = source.couleurDalleSol; + dest.couleurDalleVentilee = source.couleurDalleVentilee; + dest.couleurPiedestal = source.couleurPiedestal; + dest.couleurSousSol = source.couleurSousSol; + dest.couleurPlafondDalle = source.couleurPlafondDalle; + dest.couleurNeon = source.couleurNeon; + dest.couleurMur = source.couleurMur; + } + + string GetDirectionOpposee(string direction) + { + switch (direction) + { + case "Nord": return "Sud"; + case "Sud": return "Nord"; + case "Est": return "Ouest"; + case "Ouest": return "Est"; + default: return ""; + } + } + + Vector3 GetDirectionVector(string direction) + { + switch (direction) + { + case "Nord": return Vector3.forward; + case "Sud": return Vector3.back; + case "Est": return Vector3.right; + case "Ouest": return Vector3.left; + default: return Vector3.forward; + } + } + + // ==================== HELPERS ID ==================== + + private static string ConstruireId(int parentId, string direction) => $"{parentId}_{direction}"; + + private static bool DecomposerId(string id, out int parentId, out string direction) + { + parentId = -1; + direction = ""; + if (string.IsNullOrEmpty(id)) return false; + int sep = id.IndexOf('_'); + if (sep <= 0 || sep >= id.Length - 1) return false; + if (!int.TryParse(id.Substring(0, sep), out parentId)) return false; + direction = id.Substring(sep + 1); + return true; + } +}