2026-04-18 09:24:59 +00:00

889 lines
36 KiB
C#

// GestionnaireMultiSalles.cs - v2.0 Multi-compatible
using UnityEngine;
using Mirror;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// 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
/// </summary>
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<SalleInfo> salles = new List<SalleInfo>();
private int _nextId = 1;
// ==================== SYNC MULTI ====================
/// <summary>
/// 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.
/// </summary>
private readonly SyncList<string> _sallesSyncList = new SyncList<string>();
// ==================== 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 ====================
/// <summary>
/// Enregistre une salle existante (appelé par SalleDatacenter.Start() pour la salle d'origine).
/// </summary>
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<SalleInfo> GetToutesSalles() => salles;
public int GetNombreSalles() => salles.Count;
// ==================== VÉRIFICATIONS ====================
/// <summary>
/// Vérifie si une direction est disponible pour une salle donnée.
/// </summary>
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;
}
/// <summary>
/// Retourne les directions disponibles pour une salle.
/// </summary>
public List<string> GetDirectionsDisponibles(SalleDatacenter salle)
{
List<string> dispo = new List<string>();
string[] toutes = { "Nord", "Sud", "Est", "Ouest" };
foreach (string dir in toutes)
if (DirectionDisponible(salle, dir))
dispo.Add(dir);
return dispo;
}
// ==================== CRÉATION NOUVELLE SALLE ====================
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Version avec achat intégré — appelée par la boutique.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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 ====================
/// <summary>
/// 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.
/// </summary>
[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));
}
/// <summary>
/// Execute l'animation ET la construction geometrique en local.
/// Cote serveur OU cote client : chacun genere sa propre copie.
/// </summary>
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);
}
/// <summary>
/// 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)
/// </summary>
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<SalleDatacenter>();
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;
}
/// <summary>
/// Rejouer une entree de la SyncList en mode instantane (pour un client qui rejoint).
/// </summary>
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));
}
/// <summary>
/// Événement déclenché quand une salle est construite.
/// Paramètres : direction, nom de la salle.
/// </summary>
public System.Action<string, string> 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 ====================
/// <summary>
/// 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.
/// </summary>
IEnumerator PercerMur(SalleDatacenter salle, string direction)
{
string nomMur = "DC_Mur" + direction;
Transform murT = null;
foreach (Transform child in salle.GetComponentsInChildren<Transform>())
{
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<GameObject> debris = null;
if (!DedicatedServerMode.IsDedicatedServer)
{
Vector3 dirVec = GetDirectionVector(direction);
debris = new List<GameObject>();
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<Renderer>().material = matMur;
Rigidbody rb = d.AddComponent<Rigidbody>();
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);
}
/// <summary>
/// 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).
/// </summary>
public void SupprimerMur(SalleDatacenter salle, string direction)
{
string nomMur = "DC_Mur" + direction;
Transform murT = null;
foreach (Transform child in salle.GetComponentsInChildren<Transform>())
{
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");
}
/// <summary>
/// Crée deux morceaux de mur de chaque côté de l'ouverture du couloir.
/// </summary>
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<Renderer>() != null && matMur != null)
morceau.GetComponent<Renderer>().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<Renderer>() != null && matSol != null)
sol.GetComponent<Renderer>().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<BoxCollider>();
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<Renderer>() != null && matMur != null)
murG.GetComponent<Renderer>().sharedMaterial = matMur;
murG.isStatic = true;
murG.AddComponent<BoxCollider>();
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<Renderer>() != null && matMur != null)
murD.GetComponent<Renderer>().sharedMaterial = matMur;
murD.isStatic = true;
murD.AddComponent<BoxCollider>();
}
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<Renderer>() != null && matMur != null)
murAv.GetComponent<Renderer>().sharedMaterial = matMur;
murAv.isStatic = true;
murAv.AddComponent<BoxCollider>();
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<Renderer>() != null && matMur != null)
murAr.GetComponent<Renderer>().sharedMaterial = matMur;
murAr.isStatic = true;
murAr.AddComponent<BoxCollider>();
}
// 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<Renderer>() != null && matPlafond != null)
plafond.GetComponent<Renderer>().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<Renderer>() != null && matNeon != null)
neon.GetComponent<Renderer>().sharedMaterial = matNeon;
neon.isStatic = true;
Collider nc = neon.GetComponent<Collider>();
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>();
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;
}
}