// AgrandissementSalle.cs - v7.0 Multi-compatible // // v7.0 : Support multijoueur via SynchronisationAgrandissement // - Nouvelle methode DemanderAgrandir() : appelee par PlayerInteraction, route // vers le singleton SynchronisationAgrandissement qui orchestre la sync // - Nouvelle methode publique AgrandirLocal(bool instantane) : execute l'agrandissement // sur l'instance locale (serveur OU client). Parametre instantane=true pour le rejeu // des clients qui rejoignent en cours de partie (pas d'animation, pas d'explosion) // - Methode Agrandir() originale conservee pour compat editor/test // - Cote dedicated server : skip debris/anim visuelle (pas de gestion de shader/material) // mais execute bien la construction logique pour que les emplacements existent using UnityEngine; using System.Collections; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif public class AgrandissementSalle : MonoBehaviour { public enum DirectionAgrandissement { Nord, Sud, Est, Ouest } [Header("Agrandissement")] public DirectionAgrandissement direction = DirectionAgrandissement.Nord; public float nouvelleLargeur = 10f; public float nouvelleProfondeur = 10f; [Header("Prix")] public float prix = 25000f; [Header("Animation")] public int nombreDebris = 25; public float forceExplosion = 5f; public float dureeDebris = 3f; public float dureeConstruction = 4f; [Header("Panneau")] public float largeurPanneau = 0.6f; public float hauteurPanneau = 0.4f; public Vector3 offsetPanneau = new Vector3(0f, 0f, 0.1f); public Color couleurPanneau = new Color(0.1f, 0.3f, 0.6f, 1f); [Header("=== Ajustements manuels ===")] [Tooltip("Décalage manuel de la grille de dalles en X/Z (local à l'extension)")] public Vector2 offsetGrille = Vector2.zero; [Tooltip("Recul des murs latéraux côté passage (évite chevauchement avec la salle existante)")] public float margeReculMurs = 0.0f; [Tooltip("Largeur de l'ouverture entre salle et extension (0 = toute la largeur du mur cassé)")] public float largeurOuverture = 0f; [Header("=== Debug Gizmos ===")] [Tooltip("Afficher les gizmos de debug dans la Scene view")] public bool afficherGizmos = true; public Color couleurGizmoZone = new Color(0f, 1f, 0.5f, 0.3f); public Color couleurGizmoGrille = new Color(1f, 1f, 0f, 0.5f); [Header("État")] public bool estAgrandi = false; public bool enAnimation = false; private GameObject _panneau; private SalleDatacenter _salleRef; // ==================== PANNEAU ==================== public void CreerPanneau() { if (_panneau != null) { #if UNITY_EDITOR if (!Application.isPlaying) DestroyImmediate(_panneau); else Destroy(_panneau); #else Destroy(_panneau); #endif } _panneau = new GameObject("Panneau_Agrandissement"); // On parente au mur (transform) pour que l'Interactable fonctionne, // mais on compense le scale non-uniforme du mur _panneau.transform.SetParent(transform); _panneau.transform.localPosition = Vector3.zero; _panneau.transform.localRotation = Quaternion.identity; _panneau.transform.localScale = Vector3.one; // Calculer la position WORLD du panneau (centre du mur, décalé vers l'intérieur) SalleDatacenter salle = FindObjectOfType(); float sL = salle != null ? salle.longueur : 15f; float sW = salle != null ? salle.largeur : 10f; float hMur = salle != null ? salle.hauteurMurs / 2f : 1.6f; Vector3 salleOrigin = salle != null ? salle.transform.position : Vector3.zero; Vector3 panneauWorldPos = Vector3.zero; Quaternion panneauWorldRot = Quaternion.identity; switch (direction) { case DirectionAgrandissement.Nord: panneauWorldPos = salleOrigin + new Vector3(sL / 2f, hMur, sW - 0.1f); panneauWorldRot = Quaternion.Euler(0, 180, 0); break; case DirectionAgrandissement.Sud: panneauWorldPos = salleOrigin + new Vector3(sL / 2f, hMur, 0.1f); panneauWorldRot = Quaternion.Euler(0, 0, 0); break; case DirectionAgrandissement.Est: panneauWorldPos = salleOrigin + new Vector3(sL - 0.1f, hMur, sW / 2f); panneauWorldRot = Quaternion.Euler(0, -90, 0); break; case DirectionAgrandissement.Ouest: panneauWorldPos = salleOrigin + new Vector3(0.1f, hMur, sW / 2f); panneauWorldRot = Quaternion.Euler(0, 90, 0); break; } // Détacher temporairement du mur pour positionner en world-space sans déformation _panneau.transform.SetParent(null); _panneau.transform.position = panneauWorldPos; _panneau.transform.rotation = panneauWorldRot; _panneau.transform.localScale = Vector3.one; // Reparenter au mur (nécessaire pour l'Interactable et Agrandir) // worldPositionStays = true pour garder la position/rotation world _panneau.transform.SetParent(transform, true); GameObject fond = GameObject.CreatePrimitive(PrimitiveType.Cube); fond.name = "Panneau_Fond"; fond.transform.SetParent(_panneau.transform); fond.transform.localPosition = Vector3.zero; fond.transform.localScale = new Vector3(largeurPanneau, hauteurPanneau, 0.02f); if (!DedicatedServerMode.IsDedicatedServer) { Material matFond = new Material(Shader.Find("Standard")); matFond.color = couleurPanneau; matFond.EnableKeyword("_EMISSION"); matFond.SetColor("_EmissionColor", couleurPanneau * 0.3f); fond.GetComponent().sharedMaterial = matFond; } GameObject bordure = GameObject.CreatePrimitive(PrimitiveType.Cube); bordure.name = "Panneau_Bordure"; bordure.transform.SetParent(_panneau.transform); bordure.transform.localPosition = new Vector3(0, 0, -0.005f); bordure.transform.localScale = new Vector3(largeurPanneau + 0.03f, hauteurPanneau + 0.03f, 0.01f); if (!DedicatedServerMode.IsDedicatedServer) { Material matBord = new Material(Shader.Find("Standard")); matBord.color = new Color(0.15f, 0.5f, 0.8f); matBord.EnableKeyword("_EMISSION"); matBord.SetColor("_EmissionColor", new Color(0.1f, 0.4f, 0.7f) * 1.5f); bordure.GetComponent().sharedMaterial = matBord; } Collider bc = bordure.GetComponent(); if (bc != null) DestroyImmediate(bc); // Textes sur la face AVANT du panneau (côté joueur = +Z local) if (!DedicatedServerMode.IsDedicatedServer) { CreerTexte(_panneau.transform, "AGRANDIR", new Vector3(0, 0.1f, 0.015f), 200, 0.002f, new Color(0.8f, 0.9f, 1f)); CreerTexte(_panneau.transform, nouvelleLargeur + "m x " + nouvelleProfondeur + "m", new Vector3(0, 0.02f, 0.015f), 160, 0.0015f, new Color(0.6f, 0.8f, 0.9f)); CreerTexte(_panneau.transform, prix.ToString("N0") + " EUR", new Vector3(0, -0.08f, 0.015f), 180, 0.0018f, new Color(1f, 0.85f, 0.2f)); } Interactable inter = fond.AddComponent(); inter.nomEquipement = "Agrandissement " + nouvelleLargeur + "x" + nouvelleProfondeur + "m"; inter.tailleU = 0; } // ==================== AGRANDIR ==================== /// /// v7.0 : Point d'entree multijoueur. Appele par PlayerInteraction quand le joueur /// clique [E] sur un panneau d'agrandissement. Route vers le singleton qui orchestre /// la sync via Cmd/Rpc. En solo, le singleton appelle directement AgrandirLocal(). /// public void DemanderAgrandir() { if (estAgrandi || enAnimation) return; // Validation economique locale (pre-check, affinable cote serveur plus tard) // En sandbox, TenterAchat retourne toujours true. if (GameEconomy.Instance != null && !GameEconomy.Instance.modeSandbox) { if (!GameEconomy.Instance.TenterAchat(prix)) { Debug.Log("[AgrandissementSalle] Fonds insuffisants pour agrandir"); return; } } else if (GameEconomy.Instance != null) { GameEconomy.Instance.TenterAchat(prix); // log + compteur sandbox } // Recuperer le salleId. Si pas encore enregistree, on utilise 1 par defaut // (cas mono-salle au demarrage). int salleId = 1; SalleDatacenter salleParente = FindObjectOfType(); if (salleParente != null && salleParente.salleId > 0) salleId = salleParente.salleId; if (SynchronisationAgrandissement.Instance != null) { SynchronisationAgrandissement.Instance.DemanderAgrandissement(salleId, direction.ToString()); } else { // Fallback : si le singleton n'existe pas (scene sans multi), on execute en local direct Debug.LogWarning("[AgrandissementSalle] SynchronisationAgrandissement.Instance introuvable, fallback solo"); AgrandirLocal(false); } } /// /// Version originale conservee pour compat (editor, boutons de test). /// Equivalent a AgrandirLocal(false). /// public void Agrandir() { AgrandirLocal(false); } /// /// v7.0 : Execute l'agrandissement sur l'instance LOCALE (pas de reseau). /// Appele par SynchronisationAgrandissement.Rpc ou par Agrandir() en mode solo. /// /// instantane=true : pas d'animation, pas d'explosion, construction immediate. /// Utilise pour le rejeu cote client qui rejoint une partie en cours. /// public void AgrandirLocal(bool instantane) { if (estAgrandi || enAnimation) return; _salleRef = FindObjectOfType(); if (_salleRef == null) { Debug.LogError("SalleDatacenter introuvable !"); return; } // Créer un runner persistant pour toute l'animation // (le gameObject du mur peut être détruit/désactivé par d'autres scripts) GameObject runnerObj = new GameObject("AgrandissementRunner_" + direction); CoroutineRunner runner = runnerObj.AddComponent(); runner.StartCoroutine(AnimationAgrandissement(runner, instantane)); } IEnumerator AnimationAgrandissement(CoroutineRunner runner, bool instantane) { enAnimation = true; Renderer murRend = GetComponent(); Collider murCol = GetComponent(); if (murRend != null) murRend.enabled = false; if (murCol != null) murCol.enabled = false; if (_panneau != null) _panneau.SetActive(false); // === PHASE 1 : EXPLOSION === // Skip cote serveur (pas de shader) et en mode instantane bool skipVisuel = instantane || DedicatedServerMode.IsDedicatedServer; if (!skipVisuel) { Material matDebris = new Material(Shader.Find("Standard")); matDebris.color = _salleRef.couleurMur; Vector3 dirVec = GetDirectionVector(); List debris = new List(); for (int i = 0; i < nombreDebris; i++) { GameObject d = GameObject.CreatePrimitive(PrimitiveType.Cube); d.name = "Debris_" + i; float rx = Random.Range(-transform.localScale.x / 2f, transform.localScale.x / 2f); float ry = Random.Range(0f, _salleRef.hauteurMurs); d.transform.position = transform.position + transform.right * rx + Vector3.up * ry + Random.insideUnitSphere * 0.1f; float sz = Random.Range(0.08f, 0.25f); 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 = matDebris; Rigidbody rb = d.AddComponent(); rb.mass = Random.Range(0.5f, 2f); rb.AddForce((dirVec + Vector3.up * Random.Range(0.2f, 0.6f) + Random.insideUnitSphere * 0.3f) * forceExplosion, ForceMode.Impulse); rb.AddTorque(Random.insideUnitSphere * 3f, ForceMode.Impulse); debris.Add(d); } yield return new WaitForSeconds(2f); foreach (var d in debris) if (d != null) Destroy(d); } // === PHASE 1.5 : NETTOYER LA SALLE EXISTANTE === NettoyerSalleExistante(); // === PHASE 2 : CALCUL POSITION === Vector3 salleOrigineMonde = _salleRef.transform.position; Vector3 origineNouvelle = CalculerOrigineNouvelleZone(salleOrigineMonde); float zoneL, zoneW; if (direction == DirectionAgrandissement.Nord || direction == DirectionAgrandissement.Sud) { zoneL = nouvelleLargeur; zoneW = nouvelleProfondeur; } else { zoneL = nouvelleProfondeur; zoneW = nouvelleLargeur; } GameObject ext = new GameObject("Salle_Extension_" + direction); ext.transform.position = origineNouvelle; // === PHASE 3 : SOL === Debug.Log("AgrandissementSalle: PHASE 3 - Début construction sol"); yield return runner.StartCoroutine(ConstruireSol(ext.transform, zoneL, zoneW, origineNouvelle, salleOrigineMonde, skipVisuel)); Debug.Log("AgrandissementSalle: PHASE 3 - Sol terminé"); if (!skipVisuel) yield return new WaitForSeconds(0.2f); // === PHASE 4 : MURS === Debug.Log("AgrandissementSalle: PHASE 4 - Début construction murs"); yield return runner.StartCoroutine(ConstruireMurs(ext.transform, zoneL, zoneW, skipVisuel)); Debug.Log("AgrandissementSalle: PHASE 4 - Murs terminés"); if (!skipVisuel) yield return new WaitForSeconds(0.2f); // === PHASE 5 : PLAFOND === Debug.Log("AgrandissementSalle: PHASE 5 - Début construction plafond"); yield return runner.StartCoroutine(ConstruirePlafond(ext.transform, zoneL, zoneW, origineNouvelle, salleOrigineMonde, skipVisuel)); Debug.Log("AgrandissementSalle: PHASE 5 - Plafond terminé"); if (!skipVisuel) yield return new WaitForSeconds(0.1f); // === PHASE 6 : ÉCLAIRAGE === // Skip cote serveur (lights inutiles) if (!DedicatedServerMode.IsDedicatedServer) { Debug.Log("AgrandissementSalle: PHASE 6 - Début éclairage"); yield return runner.StartCoroutine(ConstruireEclairage(ext.transform, zoneL, zoneW, origineNouvelle, salleOrigineMonde, skipVisuel)); Debug.Log("AgrandissementSalle: PHASE 6 - Éclairage terminé"); } // === PHASE 7 : EMPLACEMENTS DE BAIES === Debug.Log("AgrandissementSalle: PHASE 7 - Génération emplacements baies"); GenererEmplacementsExtension(ext.transform, zoneL, zoneW); Debug.Log("AgrandissementSalle: PHASE 7 - Construction complète !"); // Nettoyage try { if (this != null && gameObject != null) { estAgrandi = true; enAnimation = false; Destroy(gameObject); } } catch (System.Exception) { /* mur déjà détruit, pas grave */ } Destroy(runner.gameObject); } // ==================== POSITION ==================== Vector3 CalculerOrigineNouvelleZone(Vector3 salleOrigine) { float sL = _salleRef.longueur; float sW = _salleRef.largeur; return CalculerOrigineNouvelleZoneEditor(salleOrigine, sL, sW); } public Vector3 CalculerOrigineNouvelleZoneEditor(Vector3 salleOrigine, float sL, float sW) { switch (direction) { case DirectionAgrandissement.Ouest: return new Vector3(salleOrigine.x - nouvelleProfondeur, salleOrigine.y, salleOrigine.z + sW / 2f - nouvelleLargeur / 2f); case DirectionAgrandissement.Est: return new Vector3(salleOrigine.x + sL, salleOrigine.y, salleOrigine.z + sW / 2f - nouvelleLargeur / 2f); case DirectionAgrandissement.Sud: return new Vector3(salleOrigine.x + sL / 2f - nouvelleLargeur / 2f, salleOrigine.y, salleOrigine.z - nouvelleProfondeur); case DirectionAgrandissement.Nord: return new Vector3(salleOrigine.x + sL / 2f - nouvelleLargeur / 2f, salleOrigine.y, salleOrigine.z + sW); default: return salleOrigine; } } // ==================== GRILLE CONTINUE ==================== struct GrilleInfo { public float startLocalX, startLocalZ; public int gridOffsetX, gridOffsetZ; public int nbX, nbZ; } GrilleInfo CalculerGrilleContinue(Vector3 origineExt, Vector3 origineSalle, float pas, float tailleDalle, float zoneL, float zoneW) { GrilleInfo g = new GrilleInfo(); float deltaX = origineExt.x - origineSalle.x - offsetGrille.x; float deltaZ = origineExt.z - origineSalle.z - offsetGrille.y; float demiDalle = tailleDalle / 2f; // Premier indice global dont le BORD DROIT (centre + demiDalle) est > 0 // => la dalle est au moins partiellement visible dans la zone g.gridOffsetX = Mathf.CeilToInt((deltaX - demiDalle) / pas); g.gridOffsetZ = Mathf.CeilToInt((deltaZ - demiDalle) / pas); g.startLocalX = g.gridOffsetX * pas + demiDalle - deltaX; g.startLocalZ = g.gridOffsetZ * pas + demiDalle - deltaZ; // Compter les dalles dont le BORD GAUCHE (centre - demiDalle) est < zoneL // => la dalle est au moins partiellement dans la zone g.nbX = 0; float px = g.startLocalX; while ((px - demiDalle) < zoneL - 0.001f) { g.nbX++; px += pas; } g.nbZ = 0; float pz = g.startLocalZ; while ((pz - demiDalle) < zoneW - 0.001f) { g.nbZ++; pz += pas; } return g; } // ==================== NETTOYAGE SALLE EXISTANTE ==================== void NettoyerSalleExistante() { if (_salleRef == null) return; Transform salleT = _salleRef.transform; // 1) Supprimer le mur correspondant à la direction string nomMur = ""; switch (direction) { case DirectionAgrandissement.Nord: nomMur = "DC_MurNord"; break; case DirectionAgrandissement.Sud: nomMur = "DC_MurSud"; break; case DirectionAgrandissement.Est: nomMur = "DC_MurEst"; break; case DirectionAgrandissement.Ouest: nomMur = "DC_MurOuest"; break; } // Chercher tous les objets à modifier/supprimer Transform murT = null; Transform plafondSolideT = null; Transform solColliderT = null; Transform sousSolT = null; foreach (Transform child in salleT.GetComponentsInChildren()) { if (child.name == nomMur) murT = child; else if (child.name == "DC_PlafondSolide") plafondSolideT = child; else if (child.name == "DC_SolCollider") solColliderT = child; else if (child.name == "DC_SousSol") sousSolT = child; } // Supprimer le mur if (murT != null) { Debug.Log("AgrandissementSalle: Suppression " + nomMur); Destroy(murT.gameObject); } // 2) Redimensionner plafond solide, sol collider et sous-sol // pour qu'ils ne dépassent pas côté extension // (on les réduit à la taille intérieure de la salle, sans débordement) float sL = _salleRef.longueur; float sW = _salleRef.largeur; if (plafondSolideT != null) { // Réduire à longueur x largeur (sans + epaisseurMur * 2) Vector3 sc = plafondSolideT.localScale; plafondSolideT.localScale = new Vector3(sL, sc.y, sW); plafondSolideT.localPosition = new Vector3(sL / 2f, plafondSolideT.localPosition.y, sW / 2f); Debug.Log("AgrandissementSalle: PlafondSolide redimensionné à " + sL + "x" + sW); } if (solColliderT != null) { BoxCollider bc = solColliderT.GetComponent(); if (bc != null) { float solSommetN = _salleRef.hauteurSol - 0.01f; float solBaseN = -0.1f; float solEpN = solSommetN - solBaseN; bc.size = new Vector3(sL, solEpN, sW); } float sCentreN = (_salleRef.hauteurSol - 0.01f + (-0.1f)) / 2f; solColliderT.localPosition = new Vector3(sL / 2f, sCentreN, sW / 2f); Debug.Log("AgrandissementSalle: SolCollider redimensionné"); } if (sousSolT != null) { sousSolT.localScale = new Vector3(sL, sousSolT.localScale.y, sW); sousSolT.localPosition = new Vector3(sL / 2f, sousSolT.localPosition.y, sW / 2f); } } // ==================== SOL ==================== IEnumerator ConstruireSol(Transform parent, float longueur, float largeur, Vector3 origineExt, Vector3 origineSalle, bool skipVisuel) { float hauteurSol = _salleRef.hauteurSol; float tailleDalle = _salleRef.tailleDalle; float epaisseurDalle = _salleRef.epaisseurDalle; float espacement = _salleRef.espacementDalles; float pas = tailleDalle + espacement; Material matDalle = CreerMat(_salleRef.couleurDalleSol, 0.1f, 0.3f); Material matVent = CreerMat(_salleRef.couleurDalleVentilee, 0.2f, 0.5f); Material matDalleAlt = CreerMat( new Color(_salleRef.couleurDalleSol.r - 0.03f, _salleRef.couleurDalleSol.g - 0.03f, _salleRef.couleurDalleSol.b - 0.02f, 1f), 0.1f, 0.3f); Material matPied = CreerMat(_salleRef.couleurPiedestal, 0.3f, 0.4f); Material matSousSol = CreerMat(_salleRef.couleurSousSol, 0f, 0.1f); // Sous-sol visuel GameObject ss = GameObject.CreatePrimitive(PrimitiveType.Cube); ss.name = "Ext_SousSol"; ss.transform.SetParent(parent); ss.transform.localPosition = new Vector3(longueur / 2f, -0.05f, largeur / 2f); ss.transform.localScale = new Vector3(longueur, 0.1f, largeur); if (ss.GetComponent() != null && matSousSol != null) ss.GetComponent().sharedMaterial = matSousSol; ss.isStatic = true; // Supprimer le collider du sous-sol visuel (le sol collider gère tout) Collider ssCol = ss.GetComponent(); if (ssCol != null) Destroy(ssCol); // Collider sol - TOUJOURS cree (meme cote serveur pour le CharacterController) GameObject solC = new GameObject("Ext_SolCollider"); solC.transform.SetParent(parent); float solSommet = hauteurSol - 0.01f; float solBase = -0.1f; float solEp = solSommet - solBase; float solCentre = (solSommet + solBase) / 2f; solC.transform.localPosition = new Vector3(longueur / 2f, solCentre, largeur / 2f); BoxCollider col = solC.AddComponent(); col.size = new Vector3(longueur, solEp, largeur); int layerSol = LayerMask.NameToLayer("Sol"); if (layerSol >= 0) solC.layer = layerSol; // Dalle de fond continue GameObject fondSol = GameObject.CreatePrimitive(PrimitiveType.Cube); fondSol.name = "Ext_FondSol"; fondSol.transform.SetParent(parent); fondSol.transform.localPosition = new Vector3(longueur / 2f, hauteurSol - 0.001f, largeur / 2f); fondSol.transform.localScale = new Vector3(longueur, epaisseurDalle, largeur); if (fondSol.GetComponent() != null && matDalle != null) fondSol.GetComponent().sharedMaterial = matDalle; fondSol.isStatic = true; Collider fc = fondSol.GetComponent(); if (fc != null) Destroy(fc); // === Dalles alignées grille globale === GrilleInfo g = CalculerGrilleContinue(origineExt, origineSalle, pas, tailleDalle, longueur, largeur); Debug.Log($"Extension Sol: {g.nbX}x{g.nbZ} dalles, start=({g.startLocalX:F3},{g.startLocalZ:F3})"); // Calculer les allées froides de l'extension List alleesFroides = CalculerZonesAlleesFroidesExtension(longueur, largeur); int count = 0; int total = g.nbX * g.nbZ; int dallesParFrame = Mathf.Max(1, total / Mathf.Max(1, (int)(dureeConstruction * 20))); for (int ix = 0; ix < g.nbX; ix++) { for (int iz = 0; iz < g.nbZ; iz++) { float px = g.startLocalX + ix * pas; float pz = g.startLocalZ + iz * pas; int globalX = g.gridOffsetX + ix; int globalZ = g.gridOffsetZ + iz; // Vérifier si dans une allée froide bool dansAlleeFroide = false; foreach (Vector2 zone in alleesFroides) { if (pz >= zone.x && pz <= zone.y) { dansAlleeFroide = true; break; } } Material matChoisi; if (dansAlleeFroide) matChoisi = matVent; else matChoisi = ((Mathf.Abs(globalX + globalZ)) % 2 == 0) ? matDalle : matDalleAlt; GameObject dalle = GameObject.CreatePrimitive(PrimitiveType.Cube); dalle.name = $"Ext_Dalle_{ix}_{iz}"; dalle.transform.SetParent(parent); dalle.transform.localPosition = new Vector3(px, hauteurSol, pz); dalle.transform.localScale = new Vector3(tailleDalle, epaisseurDalle, tailleDalle); if (dalle.GetComponent() != null && matChoisi != null) dalle.GetComponent().sharedMaterial = matChoisi; dalle.isStatic = true; Collider dc = dalle.GetComponent(); if (dc != null) Destroy(dc); if (Mathf.Abs(globalX) % 2 == 0 && Mathf.Abs(globalZ) % 2 == 0) { float piedH = hauteurSol - epaisseurDalle / 2f; GameObject pied = GameObject.CreatePrimitive(PrimitiveType.Cube); pied.name = $"Ext_Pied_{ix}_{iz}"; pied.transform.SetParent(parent); pied.transform.localPosition = new Vector3(px, piedH / 2f, pz); pied.transform.localScale = new Vector3(0.04f, piedH, 0.04f); if (pied.GetComponent() != null && matPied != null) pied.GetComponent().sharedMaterial = matPied; pied.isStatic = true; Collider pc = pied.GetComponent(); if (pc != null) Destroy(pc); } count++; // Mode instantane : pas de yield, tout dans la meme frame if (!skipVisuel && count % dallesParFrame == 0) yield return null; } } } // ==================== MURS ==================== IEnumerator ConstruireMurs(Transform parent, float longueur, float largeur, bool skipVisuel) { Material matMur = CreerMat(_salleRef.couleurMur, 0f, 0.2f); float hauteur = _salleRef.hauteurMurs; float ep = _salleRef.epaisseurMur; float mY = hauteur / 2f; // Recul automatique : les murs latéraux de l'extension doivent s'arrêter // avant les murs existants de la salle (épaisseur du mur existant) float recul = margeReculMurs + ep; List positions = new List(); List tailles = new List(); switch (direction) { case DirectionAgrandissement.Ouest: { positions.Add(new Vector3(-ep / 2f, mY, largeur / 2f)); tailles.Add(new Vector3(ep, hauteur, largeur + ep * 2)); float murLen = longueur - recul; positions.Add(new Vector3(murLen / 2f, mY, largeur + ep / 2f)); tailles.Add(new Vector3(murLen, hauteur, ep)); positions.Add(new Vector3(murLen / 2f, mY, -ep / 2f)); tailles.Add(new Vector3(murLen, hauteur, ep)); break; } case DirectionAgrandissement.Est: { positions.Add(new Vector3(longueur + ep / 2f, mY, largeur / 2f)); tailles.Add(new Vector3(ep, hauteur, largeur + ep * 2)); float murLen = longueur - recul; float offX = recul + murLen / 2f; positions.Add(new Vector3(offX, mY, largeur + ep / 2f)); tailles.Add(new Vector3(murLen, hauteur, ep)); positions.Add(new Vector3(offX, mY, -ep / 2f)); tailles.Add(new Vector3(murLen, hauteur, ep)); break; } case DirectionAgrandissement.Nord: { positions.Add(new Vector3(longueur / 2f, mY, largeur + ep / 2f)); tailles.Add(new Vector3(longueur + ep * 2, hauteur, ep)); float murLen = largeur - recul; float offZ = recul + murLen / 2f; positions.Add(new Vector3(longueur + ep / 2f, mY, offZ)); tailles.Add(new Vector3(ep, hauteur, murLen)); positions.Add(new Vector3(-ep / 2f, mY, offZ)); tailles.Add(new Vector3(ep, hauteur, murLen)); break; } case DirectionAgrandissement.Sud: { positions.Add(new Vector3(longueur / 2f, mY, -ep / 2f)); tailles.Add(new Vector3(longueur + ep * 2, hauteur, ep)); float murLen = largeur - recul; positions.Add(new Vector3(longueur + ep / 2f, mY, murLen / 2f)); tailles.Add(new Vector3(ep, hauteur, murLen)); positions.Add(new Vector3(-ep / 2f, mY, murLen / 2f)); tailles.Add(new Vector3(ep, hauteur, murLen)); break; } } for (int i = 0; i < positions.Count; i++) { GameObject mur = GameObject.CreatePrimitive(PrimitiveType.Cube); mur.name = "Ext_Mur_" + i; mur.transform.SetParent(parent); if (skipVisuel) { // Construction immediate (rejeu ou serveur) : taille finale direct mur.transform.localScale = tailles[i]; mur.transform.localPosition = positions[i]; } else { // Animation : le mur monte progressivement mur.transform.localScale = new Vector3(tailles[i].x, 0.01f, tailles[i].z); mur.transform.localPosition = new Vector3(positions[i].x, 0.005f, positions[i].z); } if (mur.GetComponent() != null && matMur != null) mur.GetComponent().sharedMaterial = matMur; mur.isStatic = true; mur.AddComponent(); if (!skipVisuel) { float elapsed = 0f; while (elapsed < 0.6f) { elapsed += Time.deltaTime; float t = 1f - Mathf.Pow(1f - elapsed / 0.6f, 3f); float h = Mathf.Lerp(0.01f, tailles[i].y, t); mur.transform.localScale = new Vector3(tailles[i].x, h, tailles[i].z); mur.transform.localPosition = new Vector3(positions[i].x, h / 2f, positions[i].z); yield return null; } mur.transform.localScale = tailles[i]; mur.transform.localPosition = positions[i]; yield return new WaitForSeconds(0.1f); } } } // ==================== PLAFOND ==================== IEnumerator ConstruirePlafond(Transform parent, float longueur, float largeur, Vector3 origineExt, Vector3 origineSalle, bool skipVisuel) { float hauteurPlafond = _salleRef.hauteurPlafond; float hauteurMurs = _salleRef.hauteurMurs; float tailleDalle = _salleRef.tailleDallePlafond; float epaisseurPlafond = _salleRef.epaisseurPlafond; float espacement = _salleRef.espacementDalles; float pas = tailleDalle + espacement; Material matDalle = CreerMat(_salleRef.couleurPlafondDalle, 0f, 0.2f); Material matSolide = CreerMat(_salleRef.couleurSousSol, 0f, 0.1f); Material matRail = CreerMat(_salleRef.couleurPiedestal, 0.3f, 0.4f); // Plafond solide — animation descente (ou direct en mode skip) GameObject ps = GameObject.CreatePrimitive(PrimitiveType.Cube); ps.name = "Ext_PlafondSolide"; ps.transform.SetParent(parent); Vector3 posFinale = new Vector3(longueur / 2f, hauteurMurs, largeur / 2f); if (skipVisuel) { ps.transform.localPosition = posFinale; } else { ps.transform.localPosition = new Vector3(longueur / 2f, hauteurMurs + 1f, largeur / 2f); } ps.transform.localScale = new Vector3(longueur, 0.1f, largeur); if (ps.GetComponent() != null && matSolide != null) ps.GetComponent().sharedMaterial = matSolide; ps.isStatic = true; if (!skipVisuel) { float elapsed = 0f; while (elapsed < 0.8f) { elapsed += Time.deltaTime; float t = elapsed / 0.8f; t = t * t * (3f - 2f * t); ps.transform.localPosition = Vector3.Lerp(new Vector3(longueur / 2f, hauteurMurs + 1f, largeur / 2f), posFinale, t); yield return null; } ps.transform.localPosition = posFinale; } // Dalle de fond plafond GameObject fondPlaf = GameObject.CreatePrimitive(PrimitiveType.Cube); fondPlaf.name = "Ext_FondPlafond"; fondPlaf.transform.SetParent(parent); fondPlaf.transform.localPosition = new Vector3(longueur / 2f, hauteurPlafond + 0.001f, largeur / 2f); fondPlaf.transform.localScale = new Vector3(longueur, epaisseurPlafond, largeur); if (fondPlaf.GetComponent() != null && matDalle != null) fondPlaf.GetComponent().sharedMaterial = matDalle; fondPlaf.isStatic = true; Collider fpc = fondPlaf.GetComponent(); if (fpc != null) Destroy(fpc); // === Dalles plafond alignées grille globale === GrilleInfo g = CalculerGrilleContinue(origineExt, origineSalle, pas, tailleDalle, longueur, largeur); Debug.Log($"Extension Plafond: {g.nbX}x{g.nbZ} dalles"); for (int ix = 0; ix < g.nbX; ix++) { for (int iz = 0; iz < g.nbZ; iz++) { float px = g.startLocalX + ix * pas; float pz = g.startLocalZ + iz * pas; GameObject d = GameObject.CreatePrimitive(PrimitiveType.Cube); d.name = $"Ext_PlafDalle_{ix}_{iz}"; d.transform.SetParent(parent); d.transform.localPosition = new Vector3(px, hauteurPlafond, pz); d.transform.localScale = new Vector3(tailleDalle - 0.02f, epaisseurPlafond, tailleDalle - 0.02f); if (d.GetComponent() != null && matDalle != null) d.GetComponent().sharedMaterial = matDalle; d.isStatic = true; Collider c = d.GetComponent(); if (c != null) Destroy(c); } } // Rails float railStartX = g.startLocalX - tailleDalle / 2f; float railStartZ = g.startLocalZ - tailleDalle / 2f; for (int ix = 0; ix <= g.nbX; ix++) { float px = railStartX + ix * pas; GameObject r = GameObject.CreatePrimitive(PrimitiveType.Cube); r.name = "Ext_RailX_" + ix; r.transform.SetParent(parent); r.transform.localPosition = new Vector3(px, hauteurPlafond + epaisseurPlafond / 2f, largeur / 2f); r.transform.localScale = new Vector3(0.02f, 0.03f, largeur); if (r.GetComponent() != null && matRail != null) r.GetComponent().sharedMaterial = matRail; r.isStatic = true; Collider c = r.GetComponent(); if (c != null) Destroy(c); } for (int iz = 0; iz <= g.nbZ; iz++) { float pz = railStartZ + iz * pas; GameObject r = GameObject.CreatePrimitive(PrimitiveType.Cube); r.name = "Ext_RailZ_" + iz; r.transform.SetParent(parent); r.transform.localPosition = new Vector3(longueur / 2f, hauteurPlafond + epaisseurPlafond / 2f, pz); r.transform.localScale = new Vector3(longueur, 0.03f, 0.02f); if (r.GetComponent() != null && matRail != null) r.GetComponent().sharedMaterial = matRail; r.isStatic = true; Collider c = r.GetComponent(); if (c != null) Destroy(c); } } // ==================== ÉCLAIRAGE ==================== IEnumerator ConstruireEclairage(Transform parent, float longueur, float largeur, Vector3 origineExt, Vector3 origineSalle, bool skipVisuel) { float hauteurPlafond = _salleRef.hauteurPlafond; float tailleDalle = _salleRef.tailleDallePlafond; float espacement = _salleRef.espacementDalles; int freqNeon = _salleRef.frequenceNeon; float intensite = _salleRef.intensiteLumiere; Color couleurLum = _salleRef.couleurLumiere; Color couleurNeon = _salleRef.couleurNeon; float pas = tailleDalle + espacement; GrilleInfo g = CalculerGrilleContinue(origineExt, origineSalle, pas, tailleDalle, longueur, largeur); // Calculer nbPlafondZ de la salle pour reproduire le même compteur linéaire float salleL = _salleRef.longueur; float salleW = _salleRef.largeur; int nbPlafondZSalle = 0; while ((nbPlafondZSalle * pas + tailleDalle / 2f) <= salleW + 0.001f) nbPlafondZSalle++; List mats = new List(); List lts = new List(); for (int ix = 0; ix < g.nbX; ix++) { for (int iz = 0; iz < g.nbZ; iz++) { int globalX = g.gridOffsetX + ix; int globalZ = g.gridOffsetZ + iz; // Reproduire le compteur linéaire de la salle : globalX * nbZ + globalZ // Pour les indices négatifs, on utilise Abs pour garder la cohérence int globalCount = Mathf.Abs(globalX) * nbPlafondZSalle + Mathf.Abs(globalZ); bool estNeon = (globalCount % freqNeon == 0); if (estNeon) { float px = g.startLocalX + ix * pas; float pz = g.startLocalZ + iz * pas; GameObject neon = GameObject.CreatePrimitive(PrimitiveType.Cube); neon.name = $"Ext_Neon_{ix}_{iz}"; neon.transform.SetParent(parent); neon.transform.localPosition = new Vector3(px, hauteurPlafond, pz); neon.transform.localScale = new Vector3(tailleDalle - 0.05f, 0.03f, tailleDalle - 0.05f); Material m = new Material(Shader.Find("Standard")); m.color = couleurNeon; m.EnableKeyword("_EMISSION"); if (skipVisuel) m.SetColor("_EmissionColor", couleurNeon * 2f); // tout de suite allume else m.SetColor("_EmissionColor", Color.black); m.SetFloat("_Glossiness", 0.8f); if (neon.GetComponent() != null) neon.GetComponent().material = m; neon.isStatic = true; Collider c = neon.GetComponent(); if (c != null) Destroy(c); GameObject lo = new GameObject($"Ext_Light_{ix}_{iz}"); lo.transform.SetParent(parent); lo.transform.localPosition = new Vector3(px, hauteurPlafond - 0.1f, pz); Light l = lo.AddComponent(); l.type = LightType.Point; l.color = couleurLum; l.range = 4f; l.shadows = LightShadows.Hard; l.intensity = skipVisuel ? intensite : 0f; lo.isStatic = true; mats.Add(m); lts.Add(l); } } } Debug.Log($"Extension Eclairage: {mats.Count} néons créés"); // Animation d'allumage progressif uniquement en mode non-skip if (!skipVisuel) { Color emMax = couleurNeon * 2f; for (int i = 0; i < mats.Count; i++) { float el = 0f; while (el < 0.3f) { el += Time.deltaTime; float t = el / 0.3f; if (mats[i] != null) mats[i].SetColor("_EmissionColor", emMax * t); if (lts[i] != null) lts[i].intensity = Mathf.Lerp(0f, intensite, t); yield return null; } if (mats[i] != null) mats[i].SetColor("_EmissionColor", emMax); if (lts[i] != null) lts[i].intensity = intensite; yield return new WaitForSeconds(0.06f); } } } // ==================== GIZMOS ==================== #if UNITY_EDITOR void OnDrawGizmosSelected() { if (!afficherGizmos) return; SalleDatacenter salle = FindObjectOfType(); if (salle == null) return; Vector3 salleOrigine = salle.transform.position; float sL = salle.longueur; float sW = salle.largeur; Vector3 origineExt = CalculerOrigineNouvelleZoneEditor(salleOrigine, sL, sW); float zoneL, zoneW; if (direction == DirectionAgrandissement.Nord || direction == DirectionAgrandissement.Sud) { zoneL = nouvelleLargeur; zoneW = nouvelleProfondeur; } else { zoneL = nouvelleProfondeur; zoneW = nouvelleLargeur; } float hauteurSol = salle.hauteurSol; float hMur = salle.hauteurMurs; float ep = salle.epaisseurMur; // Zone d'extension Gizmos.color = couleurGizmoZone; Vector3 centre = origineExt + new Vector3(zoneL / 2f, hauteurSol + 0.05f, zoneW / 2f); Vector3 size = new Vector3(zoneL, 0.02f, zoneW); Gizmos.DrawCube(centre, size); Gizmos.color = new Color(couleurGizmoZone.r, couleurGizmoZone.g, couleurGizmoZone.b, 1f); Gizmos.DrawWireCube(centre, size); // Grille extension (damier) float tailleDalle = salle.tailleDalle; float espacement = salle.espacementDalles; float pas = tailleDalle + espacement; GrilleInfo g = CalculerGrilleContinue(origineExt, salleOrigine, pas, tailleDalle, zoneL, zoneW); for (int ix = 0; ix < g.nbX; ix++) { for (int iz = 0; iz < g.nbZ; iz++) { float px = g.startLocalX + ix * pas; float pz = g.startLocalZ + iz * pas; Vector3 pos = origineExt + new Vector3(px, hauteurSol + 0.06f, pz); int globalX = g.gridOffsetX + ix; int globalZ = g.gridOffsetZ + iz; bool foncee = (globalX + globalZ) % 2 == 1; Gizmos.color = foncee ? new Color(0.3f, 0.3f, 0.3f, 0.6f) : new Color(1f, 1f, 0.5f, 0.6f); Gizmos.DrawCube(pos, new Vector3(tailleDalle * 0.9f, 0.01f, tailleDalle * 0.9f)); } } // Grille salle existante (wireframe) int nbXSalle = Mathf.CeilToInt(sL / pas); int nbZSalle = Mathf.CeilToInt(sW / pas); Gizmos.color = new Color(0.2f, 0.6f, 1f, 0.15f); for (int x = 0; x < nbXSalle; x++) { for (int z = 0; z < nbZSalle; z++) { float px = x * pas + tailleDalle / 2f; float pz = z * pas + tailleDalle / 2f; Vector3 pos = salleOrigine + new Vector3(px, hauteurSol + 0.07f, pz); Gizmos.DrawWireCube(pos, new Vector3(tailleDalle * 0.85f, 0.005f, tailleDalle * 0.85f)); } } // Murs latéraux (preview) Gizmos.color = new Color(1f, 0.4f, 0.2f, 0.25f); switch (direction) { case DirectionAgrandissement.Ouest: case DirectionAgrandissement.Est: { float murLen = zoneL - margeReculMurs; float startX = (direction == DirectionAgrandissement.Ouest) ? 0 : margeReculMurs; Gizmos.DrawCube(origineExt + new Vector3(startX + murLen / 2f, hMur / 2f, zoneW + ep / 2f), new Vector3(murLen, hMur, ep)); Gizmos.DrawCube(origineExt + new Vector3(startX + murLen / 2f, hMur / 2f, -ep / 2f), new Vector3(murLen, hMur, ep)); break; } case DirectionAgrandissement.Nord: case DirectionAgrandissement.Sud: { float murLen = zoneW - margeReculMurs; float startZ = (direction == DirectionAgrandissement.Sud) ? 0 : margeReculMurs; Gizmos.DrawCube(origineExt + new Vector3(zoneL + ep / 2f, hMur / 2f, startZ + murLen / 2f), new Vector3(ep, hMur, murLen)); Gizmos.DrawCube(origineExt + new Vector3(-ep / 2f, hMur / 2f, startZ + murLen / 2f), new Vector3(ep, hMur, murLen)); break; } } // Label Vector3 labelPos = origineExt + new Vector3(zoneL / 2f, hMur + 0.5f, zoneW / 2f); Handles.Label(labelPos, $"Extension {direction}\n{zoneL}m x {zoneW}m\n" + $"Grille: {g.nbX}x{g.nbZ} dalles\n" + $"Start: ({g.startLocalX:F3}, {g.startLocalZ:F3})\n" + $"Offset: ({offsetGrille.x:F3}, {offsetGrille.y:F3})\n" + $"Recul: {margeReculMurs}m"); } #endif // ==================== EMPLACEMENTS EXTENSION ==================== /// /// Calcule les zones d'allées froides pour l'extension (en local de l'extension). /// Pour Est/Ouest : réutilise les mêmes positions Z que la salle d'origine. /// Pour Nord/Sud : recalcule pour les nouvelles dimensions. /// List CalculerZonesAlleesFroidesExtension(float extLongueur, float extLargeur) { if (_salleRef == null) return new List(); // Pour Est/Ouest : si même largeur que la salle, réutiliser DIRECTEMENT // les zones d'allées froides de la salle (même positions Z) if (direction == DirectionAgrandissement.Est || direction == DirectionAgrandissement.Ouest) { if (Mathf.Abs(extLargeur - _salleRef.largeur) < 0.1f) return _salleRef.CalculerZonesAlleesFroides(); } // Sinon : recalculer pour les dimensions de l'extension float profondeur = _salleRef.profondeurEmplacement; float alleeFroide = _salleRef.espaceAlleeFroide; float alleeChaude = _salleRef.espaceAlleeChaude; float marge = _salleRef.margeDebutRangee; float pasDalle = _salleRef.tailleDalle + _salleRef.espacementDalles; float largeurPaire = profondeur * 2 + alleeFroide; int nbPairesMax = Mathf.FloorToInt((extLargeur - marge * 2 + alleeChaude) / (largeurPaire + alleeChaude)); if (nbPairesMax < 1) nbPairesMax = 1; int nbRangeesExt = nbPairesMax * 2; float totalZ = nbPairesMax * largeurPaire + (nbPairesMax - 1) * alleeChaude; float startZ = (extLargeur - totalZ) / 2f; if (startZ < marge) startZ = marge; startZ = Mathf.Round(startZ / pasDalle) * pasDalle; List zones = new List(); float currentZ = startZ; int rangeeNum = 1; for (int paire = 0; paire < nbPairesMax; paire++) { if (rangeeNum <= nbRangeesExt) { rangeeNum++; currentZ += profondeur; } zones.Add(new Vector2(currentZ, currentZ + alleeFroide)); currentZ += alleeFroide; if (rangeeNum <= nbRangeesExt) { rangeeNum++; currentZ += profondeur; } if (paire < nbPairesMax - 1) currentZ += alleeChaude; } return zones; } /// /// Génère les emplacements de baies dans l'extension. /// Reprend la même logique que SalleDatacenter.GenererEmplacementsBaies() /// mais adaptée aux dimensions de l'extension. /// void GenererEmplacementsExtension(Transform parent, float extLongueur, float extLargeur) { if (_salleRef == null) return; float profondeur = _salleRef.profondeurEmplacement; float alleeFroide = _salleRef.espaceAlleeFroide; float alleeChaude = _salleRef.espaceAlleeChaude; float largeurEmpl = _salleRef.largeurEmplacement; float marge = _salleRef.margeDebutRangee; float espBaies = _salleRef.espaceBaiesDansRangee; float hauteurSol = _salleRef.hauteurSol; float epaisseurDalle = _salleRef.epaisseurDalle; float pasDalle = _salleRef.tailleDalle + _salleRef.espacementDalles; GameObject parentEmpl = new GameObject("Ext_Emplacements"); parentEmpl.transform.SetParent(parent); parentEmpl.transform.localPosition = Vector3.zero; float largeurPaire = profondeur * 2 + alleeFroide; // Pour Est/Ouest avec même largeur : réutiliser EXACTEMENT les mêmes paramètres int nbPairesMax; int nbRangeesExt; float startZ; bool memeAxeQueSalle = (direction == DirectionAgrandissement.Est || direction == DirectionAgrandissement.Ouest) && Mathf.Abs(extLargeur - _salleRef.largeur) < 0.1f; if (memeAxeQueSalle) { // Même nombre de paires et même startZ que la salle nbPairesMax = Mathf.CeilToInt(_salleRef.nbRangees / 2f); nbRangeesExt = _salleRef.nbRangees; float totalZSalle = nbPairesMax * largeurPaire + (nbPairesMax - 1) * alleeChaude; bool rangeeImpaire = (_salleRef.nbRangees % 2 != 0); if (rangeeImpaire) totalZSalle -= profondeur + alleeFroide; startZ = (_salleRef.largeur - totalZSalle) / 2f; if (startZ < marge) startZ = marge; startZ = Mathf.Round(startZ / pasDalle) * pasDalle; } else { nbPairesMax = Mathf.FloorToInt((extLargeur - marge * 2 + alleeChaude) / (largeurPaire + alleeChaude)); if (nbPairesMax < 1) nbPairesMax = 1; nbRangeesExt = nbPairesMax * 2; float totalZ = nbPairesMax * largeurPaire + (nbPairesMax - 1) * alleeChaude; startZ = (extLargeur - totalZ) / 2f; if (startZ < marge) startZ = marge; startZ = Mathf.Round(startZ / pasDalle) * pasDalle; } // Combien d'emplacements en X int emplParRangee = Mathf.FloorToInt((extLongueur - marge * 2 + espBaies) / (largeurEmpl + espBaies)); if (emplParRangee < 1) emplParRangee = 1; float totalXExt = emplParRangee * (largeurEmpl + espBaies) - espBaies; float startX = (extLongueur - totalXExt) / 2f; startX = Mathf.Round(startX / pasDalle) * pasDalle; float currentZ = startZ; int emplacementNum = 1; int rangeeNum = 1; float yEmplacement = hauteurSol + epaisseurDalle / 2f + 0.01f; for (int paire = 0; paire < nbPairesMax; paire++) { if (rangeeNum <= nbRangeesExt) { GenererRangeeExtension(parentEmpl, startX, currentZ, 0f, rangeeNum, emplParRangee, largeurEmpl, profondeur, espBaies, yEmplacement, ref emplacementNum); rangeeNum++; currentZ += profondeur; } currentZ += alleeFroide; if (rangeeNum <= nbRangeesExt) { GenererRangeeExtension(parentEmpl, startX, currentZ, 180f, rangeeNum, emplParRangee, largeurEmpl, profondeur, espBaies, yEmplacement, ref emplacementNum); rangeeNum++; currentZ += profondeur; } if (paire < nbPairesMax - 1) currentZ += alleeChaude; } Debug.Log($"Extension: {emplacementNum - 1} emplacements en {rangeeNum - 1} rangées ({nbPairesMax} paires)"); } void GenererRangeeExtension(GameObject parent, float startX, float posZ, float orientation, int rangeeNum, int emplParRangee, float largeurEmpl, float profondeur, float espBaies, float yEmplacement, ref int emplacementNum) { for (int i = 0; i < emplParRangee; i++) { float posX = startX + i * (largeurEmpl + espBaies) + largeurEmpl / 2f; GameObject emplacement = GameObject.CreatePrimitive(PrimitiveType.Cube); emplacement.name = "Ext_Empl_R" + rangeeNum + "_" + (i + 1); emplacement.transform.SetParent(parent.transform); emplacement.transform.localPosition = new Vector3(posX, yEmplacement, posZ + profondeur / 2f); emplacement.transform.localScale = new Vector3(largeurEmpl, 0.01f, profondeur); emplacement.transform.localRotation = Quaternion.identity; // Layer Emplacement (comme dans SalleDatacenter) int layerEmplacement = LayerMask.NameToLayer("Emplacement"); if (layerEmplacement >= 0) emplacement.layer = layerEmplacement; EmplacementBaie empl = emplacement.AddComponent(); BoxCollider emplCol = emplacement.GetComponent(); if (emplCol != null) emplCol.isTrigger = true; empl.numeroEmplacement = i + 1; empl.rangee = rangeeNum; empl.orientationFaceAvant = orientation; empl.largeurEmplacement = largeurEmpl; empl.profondeurEmplacement = profondeur; // Label visuel : skip cote dedicated server (pas besoin du TextMesh) if (!DedicatedServerMode.IsDedicatedServer) { GameObject numLabel = new GameObject("Ext_Num_" + emplacementNum); numLabel.transform.SetParent(emplacement.transform); numLabel.transform.localPosition = new Vector3(0, 0.01f, 0); numLabel.transform.localRotation = Quaternion.Euler(90, 0, 0); TextMesh tm = numLabel.AddComponent(); tm.text = "E-R" + rangeeNum + "-" + (i + 1); tm.fontSize = 24; tm.characterSize = 0.04f; tm.anchor = TextAnchor.MiddleCenter; tm.alignment = TextAlignment.Center; tm.color = new Color(0.3f, 0.6f, 0.9f, 0.5f); } emplacementNum++; } } // ==================== HELPERS ==================== Vector3 GetDirectionVector() { switch (direction) { case DirectionAgrandissement.Nord: return Vector3.forward; case DirectionAgrandissement.Sud: return Vector3.back; case DirectionAgrandissement.Est: return Vector3.right; case DirectionAgrandissement.Ouest: return Vector3.left; default: return Vector3.forward; } } Material CreerMat(Color c, float metallic = 0f, float gloss = 0.2f) { if (DedicatedServerMode.IsDedicatedServer) return null; Material m = new Material(Shader.Find("Standard")); m.color = c; m.SetFloat("_Metallic", metallic); m.SetFloat("_Glossiness", gloss); return m; } void CreerTexte(Transform parent, string texte, Vector3 pos, int fontSize, float charSize, Color couleur) { if (DedicatedServerMode.IsDedicatedServer) return; GameObject o = new GameObject("Txt_" + texte); o.transform.SetParent(parent); o.transform.localPosition = pos; o.transform.localRotation = Quaternion.identity; TextMesh tm = o.AddComponent(); tm.text = texte; tm.fontSize = fontSize; tm.characterSize = charSize; tm.anchor = TextAnchor.MiddleCenter; tm.alignment = TextAlignment.Center; tm.color = couleur; tm.fontStyle = FontStyle.Bold; // Rendre le texte visible des deux côtés MeshRenderer mr = o.GetComponent(); if (mr != null && mr.sharedMaterial != null) { mr.sharedMaterial.shader = Shader.Find("GUI/Text Shader"); } } } #if UNITY_EDITOR [CustomEditor(typeof(AgrandissementSalle))] public class AgrandissementSalleEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); AgrandissementSalle agr = (AgrandissementSalle)target; GUILayout.Space(10); SalleDatacenter salle = FindObjectOfType(); if (salle != null) { float tailleDalle = salle.tailleDalle; float pas = tailleDalle + salle.espacementDalles; Vector3 salleOrigine = salle.transform.position; float sL = salle.longueur; float sW = salle.largeur; float zoneL, zoneW; if (agr.direction == AgrandissementSalle.DirectionAgrandissement.Nord || agr.direction == AgrandissementSalle.DirectionAgrandissement.Sud) { zoneL = agr.nouvelleLargeur; zoneW = agr.nouvelleProfondeur; } else { zoneL = agr.nouvelleProfondeur; zoneW = agr.nouvelleLargeur; } Vector3 origineExt = agr.CalculerOrigineNouvelleZoneEditor(salleOrigine, sL, sW); float deltaX = origineExt.x - salleOrigine.x; float deltaZ = origineExt.z - salleOrigine.z; EditorGUILayout.HelpBox( $"Salle: {sL}x{sW}m @ ({salleOrigine.x:F2}, {salleOrigine.z:F2})\n" + $"Extension: {zoneL}x{zoneW}m @ ({origineExt.x:F2}, {origineExt.z:F2})\n" + $"Delta: ({deltaX:F3}, {deltaZ:F3})\n" + $"Pas sol: {pas:F4}m | Pas plafond: {(salle.tailleDallePlafond + salle.espacementDalles):F4}m", MessageType.Info); } GUILayout.Space(5); GUI.backgroundColor = new Color(0.8f, 0.4f, 0.1f); if (GUILayout.Button("Créer le panneau sur ce mur", GUILayout.Height(35))) { agr.CreerPanneau(); EditorUtility.SetDirty(agr); } GUI.backgroundColor = new Color(0.2f, 0.7f, 0.3f); if (GUILayout.Button("Reset offset grille", GUILayout.Height(25))) { Undo.RecordObject(agr, "Reset offset grille"); agr.offsetGrille = Vector2.zero; EditorUtility.SetDirty(agr); } GUI.backgroundColor = Color.white; if (agr.estAgrandi) EditorGUILayout.HelpBox("Déjà agrandie.", MessageType.Info); } } #endif // Helper simple pour exécuter des coroutines sur un GameObject stable public class CoroutineRunner : MonoBehaviour { }