// 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; } }