From 89079e554a00cea881c63be443953db15905abe0 Mon Sep 17 00:00:00 2001 From: Stephane MAURO Date: Sat, 18 Apr 2026 08:50:08 +0000 Subject: [PATCH] Agrandissement multi modif repertoires --- .../README_AGRANDISSEMENT_MULTI.md | 192 +++ .../Scripts_Modifies/AgrandissementSalle.cs | 1387 ++++++++++++++++ .../Scripts_Modifies/PlayerInteraction.cs | 1424 +++++++++++++++++ .../SynchronisationAgrandissement.cs | 281 ++++ Patchs/dcsim_agrandissement_multi.zip | Bin 32846 -> 0 bytes 5 files changed, 3284 insertions(+) create mode 100644 Patchs/Agrandissement_Multi/README_AGRANDISSEMENT_MULTI.md create mode 100644 Patchs/Agrandissement_Multi/Scripts_Modifies/AgrandissementSalle.cs create mode 100644 Patchs/Agrandissement_Multi/Scripts_Modifies/PlayerInteraction.cs create mode 100644 Patchs/Agrandissement_Multi/Scripts_Nouveaux/SynchronisationAgrandissement.cs delete mode 100644 Patchs/dcsim_agrandissement_multi.zip diff --git a/Patchs/Agrandissement_Multi/README_AGRANDISSEMENT_MULTI.md b/Patchs/Agrandissement_Multi/README_AGRANDISSEMENT_MULTI.md new file mode 100644 index 0000000..af77a44 --- /dev/null +++ b/Patchs/Agrandissement_Multi/README_AGRANDISSEMENT_MULTI.md @@ -0,0 +1,192 @@ +# Agrandissement de salles — Synchronisation multijoueur + +## Ce que fait ce pack + +Rend l'agrandissement de salle (clic [E] sur un panneau mural) **synchrone entre tous les joueurs** en multijoueur, tout en restant fonctionnel en solo. + +## Architecture + +``` +Client joueur clique [E] + │ + ▼ +PlayerInteraction.Ramasser() + │ + ▼ +agr.DemanderAgrandir() ←─── [nouveau] + │ + ▼ +SynchronisationAgrandissement.DemanderAgrandissement(salleId, direction) + │ + ├── Mode solo : execution directe locale + │ + └── Mode multi : + │ + ▼ + PlayerAgrandissementBridge.CmdDemanderAgrandissement (sur le joueur local) + │ + ▼ + Serveur : SynchronisationAgrandissement.TraiterDemandeServeur + │ + ├── Valide (pas deja agrandi dans cette direction) + ├── Ajoute a la SyncList des agrandissements effectues + │ + ▼ + RpcLancerAgrandissement(salleId, direction) + │ + ▼ + Chaque client + serveur : AgrandirLocal(false) + │ + ▼ + Animation identique chez tous (meme parametres → meme geometrie) +``` + +## Script 1 — `SynchronisationAgrandissement.cs` (NOUVEAU) + +Contient deux classes : + +1. **`SynchronisationAgrandissement`** — singleton NetworkBehaviour, orchestre tout +2. **`PlayerAgrandissementBridge`** — NetworkBehaviour a ajouter au prefab du joueur pour porter la `[Command]` + +## Script 2 — `AgrandissementSalle.cs` (MODIFIÉ) + +Changements : +- Nouvelle méthode `DemanderAgrandir()` : point d'entrée multi +- Nouvelle méthode `AgrandirLocal(bool instantane)` : execute localement (avec ou sans anim) +- `Agrandir()` conservée pour compat : équivaut à `AgrandirLocal(false)` +- Toutes les coroutines d'animation (`ConstruireSol`, `ConstruireMurs`, `ConstruirePlafond`, `ConstruireEclairage`) acceptent un paramètre `skipVisuel` pour construire instantanément (rejeu client qui rejoint) +- Gardes `DedicatedServerMode.IsDedicatedServer` sur la création de `Material`/`TextMesh`/debris +- Layer `Emplacement` correctement appliqué aux emplacements de l'extension (il manquait dans l'original) + +## Script 3 — `PlayerInteraction.cs` (MODIFIÉ) + +Un seul changement : ligne ~933, `agr.Agrandir()` remplacé par `agr.DemanderAgrandir()`. + +## Étapes dans Unity + +### 1. Remplacer les 3 scripts + +Copier dans `Assets/Scripts/` : +- `SynchronisationAgrandissement.cs` (nouveau fichier) +- `AgrandissementSalle.cs` (remplace l'existant) +- `PlayerInteraction.cs` (remplace l'existant) + +### 2. Ajouter `SynchronisationAgrandissement` à la scène + +Dans `Datacenter_01` : +1. Sélectionner le GameObject qui porte le `NetworkManager` +2. `Add Component` → `SynchronisationAgrandissement` +3. S'assurer que ce GameObject a un `NetworkIdentity` ; la plupart du temps le `NetworkManager` n'en a pas lui-même. Créer un enfant `SyncAgrandissement_Holder` avec un `NetworkIdentity` + le script si besoin. + +**Important** : pour que Mirror synchronise le `SyncList`, ce GameObject doit être spawné sur le réseau. Deux options : +- **Option A (simple)** : le mettre dans `NetworkManager.spawnPrefabs` et le spawner au `OnStartServer` via un petit bootstrap +- **Option B (scène)** : garder le GameObject dans la scène. Mirror va automatiquement le traiter comme un objet de scène avec `NetworkIdentity`. Dans ce cas, s'assurer que l'objet porte **un `NetworkIdentity`** et qu'il est présent **dès l'ouverture de la scène**. + +L'option B est ce que je recommande pour ton cas : Datacenter_01 est une scène fixe, tu ajoutes `SynchronisationAgrandissement` + `NetworkIdentity` sur un GameObject dédié, et Mirror le gère tout seul. + +### 3. Ajouter `PlayerAgrandissementBridge` au prefab du joueur + +1. Ouvrir le prefab du joueur (celui qui est dans `NetworkManager.playerPrefab`) +2. `Add Component` → `PlayerAgrandissementBridge` +3. Sauver le prefab + +Sans ce composant sur le joueur, les clients ne pourront pas envoyer leur `CmdDemanderAgrandissement` vers le serveur. + +### 4. Rebuild + +- Client Windows (pour tester avec deux joueurs) +- Dedicated Server Linux (pour déployer sur la Debian) + +## Test à faire + +### Test 1 — Mode solo +1. Lancer en éditeur (play mode normal, pas host) +2. Cliquer [E] sur un panneau d'agrandissement +3. **Attendu** : l'animation se lance comme avant, salle agrandie à la fin + +### Test 2 — Mode host + client +1. Lancer l'éditeur en Host +2. Lancer un build client et le connecter +3. **Host** clique [E] sur un panneau +4. **Attendu** : les deux voient la même animation en même temps, les nouveaux emplacements apparaissent des deux côtés + +### Test 3 — Client déclencheur +1. Host + client +2. **Client** clique [E] sur un panneau +3. **Attendu** : les deux voient l'animation, la SyncList côté serveur contient l'entrée + +### Test 4 — Dedicated + 2 clients +1. Lancer le dedicated sur la Debian +2. Connecter 2 clients Windows +3. Un des clients clique [E] +4. **Attendu** : les 2 clients voient l'animation synchrone, le serveur logue la création de l'extension + +### Test 5 — Rejoin +1. Dedicated + 1 client +2. Le client déclenche l'agrandissement +3. Un **2e client** se connecte APRÈS +4. **Attendu** : le 2e client voit l'extension instantanément (pas d'animation), l'état est cohérent + +## Points d'attention + +### GameEconomy +Ton `GameEconomy` est en mode sandbox, donc les `TenterAchat` passent toujours. La validation économique serveur n'est pas implémentée dans ce pack, elle le sera quand tu sortiras du sandbox. + +### Emplacements de baies +Les `EmplacementBaie` de l'extension sont créés **localement sur chaque instance** (pas via `NetworkServer.Spawn`). C'est cohérent avec le pattern actuel de `SalleDatacenter.GenererEmplacementsBaies()` qui fonctionne déjà en multi. + +Les baies qui seront placées dessus continueront d'utiliser le flux `CmdPlacerBaie` qui spawne bien via `NetworkServer.Spawn` dans `BoutiqueReseau.TraiterPlacementBaieServeur`. + +### Colliders sol + murs +Ils sont créés **partout (serveur et clients)** car ils sont nécessaires pour : +- Le CharacterController du joueur (marcher dans la nouvelle zone) +- La physique des objets posés +- Les raycasts d'interaction + +Pas de risque de "tomber dans le vide" côté serveur. + +### Néons +Sur le dedicated server, les `Light` ne sont pas créées (gain CPU + logs plus propres). Les clients les créent normalement. C'est sans impact gameplay. + +## Workflow de debug + +Côté serveur dedicated, les logs à surveiller : + +``` +[SyncAgrandissement] Agrandissement valide : 1_Nord (total : 1) +``` + +Côté client : +``` +[SyncAgrandissement] Rpc recu, execution agrandissement : salle 1 direction Nord +AgrandissementSalle: PHASE 3 - Début construction sol +AgrandissementSalle: PHASE 7 - Construction complète ! +``` + +Si tu vois : +``` +[SyncAgrandissement] Mur introuvable : salle 1 direction Nord +``` + +Ça veut dire que le `salleId` ou la direction ne correspondent à aucun mur dans la scène. Vérifier que `SalleDatacenter.salleId` est bien = 1 (ou que le fallback "n'importe quel mur dans la bonne direction" trouve un match). + +Si tu vois : +``` +[PlayerAgrandissementBridge] Instance SynchronisationAgrandissement introuvable cote serveur ! +``` + +Ça veut dire que le GameObject qui porte `SynchronisationAgrandissement` n'a pas bien été spawné côté serveur. Vérifier qu'il est présent dans la scène **au démarrage** (option B) ou dans `NetworkManager.spawnPrefabs` + spawné au `OnStartServer` (option A). + +## Bugs connus limités + +1. **Chat spam si plusieurs clients déclenchent en même temps** : si 2 clients cliquent [E] sur le même panneau dans la même frame, le serveur validera le premier et rejettera le second (log warning "deja agrandi"). Pas grave pour les joueurs. + +2. **Anim qui démarre décalée** : la latence réseau entre Cmd et Rpc peut faire que les clients voient l'animation avec ~50-200ms de retard entre eux. Acceptable en coop standard. + +3. **Animation coupée si un client disconnect pendant l'anim** : comme c'est local sur chaque client, ça ne casse rien pour les autres. Le joueur qui reconnect verra l'extension déjà construite via le rejeu. + +## À implémenter plus tard (pas dans ce pack) + +- Validation économique serveur-side (quand tu sortiras du sandbox) +- Persistence des agrandissements dans le SaveManager +- Support multi-salles pour l'agrandissement (actuellement `SynchronisationAgrandissement` trouve `FindObjectOfType()`, donc ça ciblera toujours la salle 1 ; à affiner quand tu auras plusieurs salles agrandissables) diff --git a/Patchs/Agrandissement_Multi/Scripts_Modifies/AgrandissementSalle.cs b/Patchs/Agrandissement_Multi/Scripts_Modifies/AgrandissementSalle.cs new file mode 100644 index 0000000..678e30a --- /dev/null +++ b/Patchs/Agrandissement_Multi/Scripts_Modifies/AgrandissementSalle.cs @@ -0,0 +1,1387 @@ +// 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 { } diff --git a/Patchs/Agrandissement_Multi/Scripts_Modifies/PlayerInteraction.cs b/Patchs/Agrandissement_Multi/Scripts_Modifies/PlayerInteraction.cs new file mode 100644 index 0000000..3f46a2a --- /dev/null +++ b/Patchs/Agrandissement_Multi/Scripts_Modifies/PlayerInteraction.cs @@ -0,0 +1,1424 @@ +using UnityEngine; +using System.Collections.Generic; +using Mirror; + +/// +/// PlayerInteraction.cs - v6.13 Multijoueur +/// +/// v6.13 : Fix des syncs manquantes en multijoueur +/// BUG 1 : La pile de ZoneLivraison ne se reorganise pas cote autres clients +/// quand un joueur ramasse un article (le client qui ramasse reorganise +/// en local, les autres ne voient rien bouger). +/// Fix : CmdReorganiserZoneLivraison envoie la reorganisation au serveur, +/// qui modifie les positions des articles -> Mirror sync via NetworkTransform. +/// +/// BUG 2 : Les equipements poses sur le chariot "volent" quand l'autre +/// joueur pilote. Mirror ne sync pas le SetParent, donc seul le client qui +/// a pose voit les equipements parentes au chariot. Les autres les voient +/// a leur position monde figee au moment de la pose. +/// Fix : CmdParenterAuChariot + RpcParenterAuChariot pour broadcaster le +/// SetParent a tous les clients (chacun fait SetParent local). Idem pour +/// le detachement au ramassage (CmdDetacherDuChariot + RpcDetacherDuChariot). +/// +/// v6.12 : Transfert d'autorite pour le pilotage du chariot en multijoueur +/// v6.11 : Pose/retrait des torons sur les axes lateraux du chariot +/// v6.10 : Fix critique - objets qui flottent apres avoir ete poses sur le chariot +/// v6.9 : Integration du systeme de pose plateau / etagere basse du Chariot v7.x +/// - Detection des zones de pose via RaycastAll + tri par distance +/// - Distinction automatique zonePosePlateau vs zonePoseEtagere +/// - Affichage du fantome a la bonne position via AfficherFantomePose(tailleU, surEtagereBasse) +/// - Cachage des fantomes quand on quitte le chariot du regard ou qu'on pose +/// - Pose sur etagere basse via PoserEquipementSurEtagere +/// - Ramassage depuis etagere basse (via equipementsSurEtagere.Contains) +/// +/// v6.8 : Systeme d'assise sur les chaises +/// v6.7 : Placement local client pour supports muraux +/// v6.5 : Remise de PDU sur SupportPDU +/// v6.4 : Portage toron "dans les mains" +/// v6.3 : Placement precis toron sur axe cible +/// v6.2 : Remise de toron sur SupportTorons +/// +public class PlayerInteraction : NetworkBehaviour +{ + [Header("Interaction")] + public float porteeInteraction = 4f; + public KeyCode toucheInteraction = KeyCode.E; + public KeyCode toucheConfiguration = KeyCode.F; + + [Header("Performance")] + [Range(1, 5)] + public int detectInterval = 2; + public bool detecterTriggers = false; + + [Header("Transport")] + public float distanceTransport = 1.5f; + public float hauteurTransport = 0.6f; + public float decalageHorizontal = 0f; + public float vitesseSuivi = 15f; + + [Header("Transport — Toron (v6.4)")] + public float distanceTransportToron = 0.85f; + public float hauteurTransportToron = 0.15f; + public float decalageHorizontalToron = 0.35f; + public Vector3 rotationEulerToron = new Vector3(0f, 90f, 0f); + + [Header("Remise sur supports muraux (v6.2/6.5)")] + public float distanceDetectionSupport = 3.5f; + public float angleDetectionSupport = 35f; + + private Camera _camera; + private Interactable _objetSurvole; + private Interactable _objetEnMain; + private Rigidbody _rbEnMain; + private Vector3 _cibleTransport; + private Quaternion _rotationOffsetTransport = Quaternion.identity; + + private RackSlot _slotSurvole; + private Chariot _chariotPilote; + private Chariot _chariotVise; + private bool _visePoignee = false; + // v6.9 : distinction zone plateau/etagere du chariot vise + private bool _chariotZoneEtagereVisee = false; + // v6.11 : index de l'axe toron cible sur le chariot (0=Gauche, 1=Droit, -1=aucun) + private int _chariotAxeToronCibleIndex = -1; + + private EmplacementBaie _emplacementVise; + private EmplacementPDU _emplacementPDUVise; + private PorteBaie _porteVisee; + + private PriseC13 _priseSurvolee; + private BoutonPower _boutonPowerVise; + + // v6.8 : système d'assise + private ChaiseSiege _chaiseSiegeVisee; + private ChaiseSiege _chaiseSiegeActuelle; + + private List _collisionsIgnorees = new List(); + private EmplacementBaie[] _tousEmplacements; + private bool _emplacementsVisibles = false; + private bool _emplacementsPDUVisibles = false; + + private int _detectFrame = 0; + + private MenuPause _cachedPause; + private MenuPrincipal _cachedMenu; + private bool _cacheInitialized = false; + + // v6.2 : axe cible courant sur SupportTorons + private int _axeSupportCibleLigne = -1; + private int _axeSupportCibleColonne = -1; + + // v6.5 : emplacement cible courant sur SupportPDU + private int _emplacementSupportPDUCibleLigne = -1; + private int _emplacementSupportPDUCibleColonne = -1; + + [SyncVar(hook = nameof(OnObjetPorteChange))] + private uint _objetPorteNetId = 0; + + private GameObject _objetPorteDistant; + + public Interactable ObjetEnMain => _objetEnMain; + + void Start() + { + _camera = GetComponentInChildren(); + if (_camera == null) _camera = Camera.main; + } + + private bool _popupEtaitOuvert = false; + + void Update() + { + if (!isLocalPlayer) return; + + // v6.8 : mode assis — seul [E] pour se lever est actif + if (_chaiseSiegeActuelle != null) + { + if (Input.GetKeyDown(toucheInteraction)) + { + _chaiseSiegeActuelle.SeLever(); + _chaiseSiegeActuelle = null; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement(""); + } + return; + } + + if (_camera == null) _camera = Camera.main; + + if (UIConfigurationEquipement.Instance != null && UIConfigurationEquipement.Instance.EstOuvert()) + { + _popupEtaitOuvert = true; + return; + } + + if (_popupEtaitOuvert) + { + _popupEtaitOuvert = false; + Cursor.lockState = CursorLockMode.Locked; + Cursor.visible = false; + var playerInput = GetComponent(); + if (playerInput != null) playerInput.enabled = true; + var fpc = GetComponent(); + if (fpc != null) fpc.enabled = true; + } + + if (Cursor.visible && _objetEnMain == null) + { + if (!_cacheInitialized || _cachedPause == null) { _cachedPause = FindObjectOfType(); _cachedMenu = FindObjectOfType(); if (_cachedPause != null) _cacheInitialized = true; } + bool pauseOuverte = (_cachedPause != null && _cachedPause.estEnPause); + bool menuOuvert = (_cachedMenu != null && _cachedMenu.EstAffiche()); + MonitoringDatacenter monitoring = FindObjectOfType(); + bool monitoringOuvert = (monitoring != null && monitoring.EstAffiche()); + PlanSalle plan = FindObjectOfType(); + bool planOuvert = (plan != null && plan.EstOuvert()); + UIBoutique boutique = FindObjectOfType(); + bool boutiqueOuverte = (boutique != null && boutique.panneauPrincipal != null && boutique.panneauPrincipal.activeSelf); + UIConfigurationEquipement configPopup = UIConfigurationEquipement.Instance; + bool configOuverte = (configPopup != null && configPopup.EstOuvert()); + UITickets tickets = UITickets.Instance; + bool ticketsOuverts = (tickets != null && tickets.EstAffiche()); + + if (!pauseOuverte && !menuOuvert && !monitoringOuvert && !planOuvert && !boutiqueOuverte && !configOuverte && !ticketsOuverts) + { + Cursor.lockState = CursorLockMode.Locked; + Cursor.visible = false; + var fpc2 = GetComponent(); + if (fpc2 != null) fpc2.enabled = true; + var pi2 = GetComponent(); + if (pi2 != null) pi2.enabled = true; + } + } + + if (_objetEnMain == null) + { + if (Time.frameCount % detectInterval == 0) + DetecterObjet(); + if (_emplacementsVisibles) { SetEmplacementsVisibles(false); _emplacementsVisibles = false; } + if (_emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(false); _emplacementsPDUVisibles = false; } + if (_axeSupportCibleLigne >= 0) QuitterSurvolSupport(); + if (_emplacementSupportPDUCibleLigne >= 0) QuitterSurvolSupportPDU(); + // v6.9 : cacher les fantomes du chariot quand on n'a plus rien en main + CacherFantomesChariotTous(); + } + else + { + DetecterCiblePose(); + bool portePDU = _objetEnMain.GetComponent() != null; + if (portePDU && !_emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(true); _emplacementsPDUVisibles = true; } + if (!portePDU && _emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(false); _emplacementsPDUVisibles = false; } + if (_emplacementsVisibles) { SetEmplacementsVisibles(false); _emplacementsVisibles = false; } + } + + if (Input.GetKeyDown(toucheInteraction)) + { + // v6.8 : s'asseoir si on vise une chaise et pas d'objet en main + if (_chaiseSiegeVisee != null && _objetEnMain == null) + { + if (_chaiseSiegeVisee.Asseoir(gameObject)) + { + _chaiseSiegeActuelle = _chaiseSiegeVisee; + _chaiseSiegeVisee = null; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement("[E] Se lever"); + } + return; + } + + if (_boutonPowerVise != null && _objetEnMain == null) { CmdAppuyerBoutonPower(_boutonPowerVise.GetComponentInParent().netId); return; } + if (_priseSurvolee != null && _priseSurvolee.estConnectee && _objetEnMain == null) { CmdBasculerInterrupteur(_priseSurvolee.GetComponentInParent().netId); return; } + if (_porteVisee != null && _objetEnMain == null) + { + NetworkIdentity porteNetId = _porteVisee.GetComponentInParent(); + if (porteNetId != null) CmdTogglePorte(porteNetId.netId, _porteVisee.gameObject.name); + return; + } + + if (_chariotPilote != null && _objetEnMain == null && _visePoignee) + { + LacherChariot(); + QuitterSurvol(); + return; + } + + if (_objetEnMain == null && _objetSurvole != null) Ramasser(); + else if (_objetEnMain != null) Poser(); + } + + if (Input.GetKeyDown(toucheConfiguration) && _objetEnMain == null && _objetSurvole != null) + { + RackSlot slotConfig = TrouverSlotDeEquipement(_objetSurvole); + if (slotConfig != null) + { + ConfigurationEquipement config = _objetSurvole.GetComponent(); + if (config == null) config = _objetSurvole.gameObject.AddComponent(); + if (UIConfigurationEquipement.Instance != null) + { UIConfigurationEquipement.Instance.Ouvrir(config, premiereOuverture: false); QuitterSurvol(); } + } + } + + if (Input.GetKeyDown(KeyCode.F5) && isServer && SaveManager.Instance != null) + { + if (SaveManager.Instance.SauvegardeRapide()) + Debug.Log("[QuickSave] Sauvegarde rapide effectuée !"); + } + } + + void FixedUpdate() + { + if (!isLocalPlayer) return; + + if (_objetEnMain != null && _rbEnMain != null) + { + Vector3 dir = new Vector3(_camera.transform.forward.x, 0, _camera.transform.forward.z).normalized; + + bool estToron = _objetEnMain.GetComponent() != null; + float dist = estToron ? distanceTransportToron : distanceTransport; + float hauteur = estToron ? hauteurTransportToron : hauteurTransport; + float decalage = estToron ? decalageHorizontalToron : decalageHorizontal; + + _cibleTransport = transform.position + dir * dist + + Vector3.up * hauteur + _camera.transform.right * decalage; + Vector3 direction = _cibleTransport - _rbEnMain.position; + Vector3 desiredVelocity = direction * vitesseSuivi; + if (direction.magnitude > 0.05f) + { + RaycastHit hit; + int excludeMask = ~LayerMask.GetMask("Joueur", "Equipement", "Sol", "Baie", "Default", "Emplacement"); + if (Physics.SphereCast(_rbEnMain.position, 0.15f, direction.normalized, out hit, direction.magnitude, excludeMask)) + if (!EstColliderIgnore(hit.collider) && hit.distance < 0.15f) + desiredVelocity = Vector3.ProjectOnPlane(desiredVelocity, hit.normal); + } + _rbEnMain.velocity = desiredVelocity; + + if (estToron) + _rbEnMain.rotation = Quaternion.LookRotation(dir) * Quaternion.Euler(rotationEulerToron); + else + _rbEnMain.rotation = Quaternion.LookRotation(dir) * _rotationOffsetTransport; + + CmdUpdateTransportPosition(_rbEnMain.position, _rbEnMain.rotation); + } + } + + // ══════════════════════════════════════════════════════════ + // COMMANDS + // ══════════════════════════════════════════════════════════ + + [Command] + void CmdRamasser(uint objetNetId) + { + if (!NetworkServer.spawned.ContainsKey(objetNetId)) return; + _objetPorteNetId = objetNetId; + var obj = NetworkServer.spawned[objetNetId]; + Rigidbody rb = obj.GetComponent(); + if (rb != null) { rb.isKinematic = false; rb.useGravity = false; rb.freezeRotation = true; } + RpcOnRamasser(objetNetId); + } + + [Command] + void CmdPoser(uint objetNetId, Vector3 position, Quaternion rotation, bool avecGravite, bool estRacke) + { + if (!NetworkServer.spawned.ContainsKey(objetNetId)) return; + _objetPorteNetId = 0; + var obj = NetworkServer.spawned[objetNetId]; + obj.transform.position = position; + obj.transform.rotation = rotation; + Rigidbody rb = obj.GetComponent(); + if (rb != null) + { + if (estRacke) { if (!rb.isKinematic) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } rb.isKinematic = true; } + else { rb.isKinematic = false; rb.useGravity = avecGravite; rb.velocity = Vector3.zero; rb.freezeRotation = false; rb.interpolation = RigidbodyInterpolation.None; } + } + RpcOnPoser(objetNetId, position, rotation, avecGravite, estRacke); + } + + [Command] + void CmdPoserToronSurSupport(uint toronNetId, int ligne, int colonne) + { + if (!NetworkServer.spawned.ContainsKey(toronNetId)) return; + _objetPorteNetId = 0; + var obj = NetworkServer.spawned[toronNetId]; + if (obj == null) return; + bool place = false; + if (SupportTorons.Instance != null) + { + if (ligne >= 0 && colonne >= 0) place = SupportTorons.Instance.PoserToronSurAxeSpecifique(obj.gameObject, ligne, colonne); + if (!place) place = SupportTorons.Instance.PoserToron(obj.gameObject); + } + if (place) { RpcOnPoser(toronNetId, obj.transform.position, obj.transform.rotation, false, true); } + else { Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; rb.velocity = Vector3.zero; } RpcOnPoser(toronNetId, obj.transform.position, obj.transform.rotation, true, false); } + } + + [Command] + void CmdPoserPDUSurSupport(uint pduNetId, int ligne, int colonne) + { + if (!NetworkServer.spawned.ContainsKey(pduNetId)) return; + _objetPorteNetId = 0; + var obj = NetworkServer.spawned[pduNetId]; + if (obj == null) return; + bool place = false; + if (SupportPDU.Instance != null) + { + if (ligne >= 0 && colonne >= 0) place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(obj.gameObject, ligne, colonne); + if (!place) place = SupportPDU.Instance.PoserPDU(obj.gameObject); + } + if (place) { RpcOnPoser(pduNetId, obj.transform.position, obj.transform.rotation, false, true); } + else { Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; rb.velocity = Vector3.zero; } RpcOnPoser(pduNetId, obj.transform.position, obj.transform.rotation, true, false); } + } + + [Command(channel = Channels.Unreliable)] + void CmdUpdateTransportPosition(Vector3 position, Quaternion rotation) { RpcUpdateTransportPosition(position, rotation); } + + [Command] + void CmdAppuyerBoutonPower(uint equipNetId) + { + if (!NetworkServer.spawned.ContainsKey(equipNetId)) return; + var obj = NetworkServer.spawned[equipNetId]; + BoutonPower bouton = obj.GetComponentInChildren(); + if (bouton != null) { bouton.Appuyer(); RpcSyncBoutonPower(equipNetId, bouton.estAllume); } + } + + [ClientRpc] + void RpcSyncBoutonPower(uint equipNetId, bool estAllume) + { + if (!NetworkClient.spawned.ContainsKey(equipNetId)) return; + var obj = NetworkClient.spawned[equipNetId]; + BoutonPower bouton = obj.GetComponentInChildren(); + if (bouton != null && bouton.estAllume != estAllume) bouton.Appuyer(); + } + + [Command] + void CmdBasculerInterrupteur(uint pduNetId) + { + if (!NetworkServer.spawned.ContainsKey(pduNetId)) return; + var obj = NetworkServer.spawned[pduNetId]; + PriseC13 prise = obj.GetComponentInChildren(); + if (prise != null) prise.BasculerInterrupteur(); + } + + [Command] + void CmdInstallerPDU(uint pduNetId, Vector3 position, Quaternion rotation) + { + if (!NetworkServer.spawned.ContainsKey(pduNetId)) return; + var obj = NetworkServer.spawned[pduNetId]; + PDU pdu = obj.GetComponent(); + if (pdu == null) return; + PDUNetworkState netState = obj.GetComponent(); + if (netState != null) netState.ServerSetInstalle(true, (int)pdu.coteInstallation); + else pdu.estInstalle = true; + RpcInstallerPDU(pduNetId, position, rotation); + } + + [ClientRpc] + void RpcInstallerPDU(uint pduNetId, Vector3 position, Quaternion rotation) + { + if (!NetworkClient.spawned.ContainsKey(pduNetId)) return; + var obj = NetworkClient.spawned[pduNetId]; + PDU pdu = obj.GetComponent(); + if (pdu == null) return; + pdu.estInstalle = true; + if (!isLocalPlayer) { obj.transform.position = position; obj.transform.rotation = rotation; Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } } + } + + [Command] + void CmdDesinstallerPDU(uint pduNetId) + { + if (!NetworkServer.spawned.ContainsKey(pduNetId)) return; + var obj = NetworkServer.spawned[pduNetId]; + PDU pdu = obj.GetComponent(); + if (pdu == null) return; + PDUNetworkState netState = obj.GetComponent(); + if (netState != null) netState.ServerSetInstalle(false); + else pdu.estInstalle = false; + RpcDesinstallerPDU(pduNetId); + } + + [ClientRpc] + void RpcDesinstallerPDU(uint pduNetId) + { + if (!NetworkClient.spawned.ContainsKey(pduNetId)) return; + var obj = NetworkClient.spawned[pduNetId]; + PDU pdu = obj.GetComponent(); + if (pdu != null) { pdu.estInstalle = false; Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; } } + } + + [Command] + void CmdInstallerDansSlot(uint equipNetId, uint baieNetId, int slotIndex) + { + if (!NetworkServer.spawned.ContainsKey(equipNetId) || !NetworkServer.spawned.ContainsKey(baieNetId)) return; + var equipObj = NetworkServer.spawned[equipNetId]; var baieObj = NetworkServer.spawned[baieNetId]; + RackSlot[] slots = baieObj.GetComponentsInChildren(); + if (slotIndex >= 0 && slotIndex < slots.Length) { RackSlot slot = slots[slotIndex]; if (!slot.estOccupe) { slot.estOccupe = true; slot.equipementInstalle = equipObj.GetComponent(); } } + RpcInstallerDansSlot(equipNetId, baieNetId, slotIndex); + } + + [ClientRpc] + void RpcInstallerDansSlot(uint equipNetId, uint baieNetId, int slotIndex) + { + if (isLocalPlayer) return; + if (!NetworkClient.spawned.ContainsKey(equipNetId) || !NetworkClient.spawned.ContainsKey(baieNetId)) return; + var equipObj = NetworkClient.spawned[equipNetId]; var baieObj = NetworkClient.spawned[baieNetId]; + RackSlot[] slots = baieObj.GetComponentsInChildren(); + if (slotIndex >= 0 && slotIndex < slots.Length) { RackSlot slot = slots[slotIndex]; if (!slot.estOccupe) { slot.estOccupe = true; slot.equipementInstalle = equipObj.GetComponent(); } } + } + + [Command] + void CmdTogglePorte(uint baieNetId, string nomPorte) + { + if (!NetworkServer.spawned.ContainsKey(baieNetId)) return; + var obj = NetworkServer.spawned[baieNetId]; + foreach (PorteBaie porte in obj.GetComponentsInChildren()) + if (porte.gameObject.name == nomPorte) { porte.TogglePorte(); RpcTogglePorte(baieNetId, nomPorte); break; } + } + + // ══════════════════════════════════════════════════════════ + // CLIENT RPCs + // ══════════════════════════════════════════════════════════ + + [ClientRpc] + void RpcOnRamasser(uint objetNetId) + { + if (isLocalPlayer) return; + if (!NetworkClient.spawned.ContainsKey(objetNetId)) return; + var obj = NetworkClient.spawned[objetNetId]; + _objetPorteDistant = obj.gameObject; + Rigidbody rb = obj.GetComponent(); + if (rb != null) { rb.isKinematic = true; rb.velocity = Vector3.zero; } + Interactable inter = obj.GetComponent(); + if (inter != null) inter.estRamasse = true; + } + + [ClientRpc] + void RpcOnPoser(uint objetNetId, Vector3 position, Quaternion rotation, bool avecGravite, bool estRacke) + { + if (isLocalPlayer) return; + if (!NetworkClient.spawned.ContainsKey(objetNetId)) return; + var obj = NetworkClient.spawned[objetNetId]; + obj.transform.position = position; obj.transform.rotation = rotation; + Rigidbody rb = obj.GetComponent(); + if (rb != null) + { + if (estRacke) { if (!rb.isKinematic) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } rb.isKinematic = true; } + else { rb.isKinematic = false; rb.useGravity = avecGravite; rb.velocity = Vector3.zero; } + } + Interactable inter = obj.GetComponent(); + if (inter != null) inter.estRamasse = false; + _objetPorteDistant = null; + } + + [ClientRpc(channel = Channels.Unreliable)] + void RpcUpdateTransportPosition(Vector3 position, Quaternion rotation) + { + if (isLocalPlayer) return; + if (_objetPorteDistant != null) + { + _objetPorteDistant.transform.position = Vector3.Lerp(_objetPorteDistant.transform.position, position, Time.deltaTime * 20f); + _objetPorteDistant.transform.rotation = Quaternion.Slerp(_objetPorteDistant.transform.rotation, rotation, Time.deltaTime * 20f); + } + } + + [ClientRpc] + void RpcTogglePorte(uint baieNetId, string nomPorte) + { + if (isServer) return; + if (!NetworkClient.spawned.ContainsKey(baieNetId)) return; + var obj = NetworkClient.spawned[baieNetId]; + foreach (PorteBaie porte in obj.GetComponentsInChildren()) + if (porte.gameObject.name == nomPorte) { porte.TogglePorte(); break; } + } + + // ══════════════════════════════════════════════════════════ + // HOOK SyncVar + // ══════════════════════════════════════════════════════════ + + void OnObjetPorteChange(uint ancienId, uint nouveauId) + { + if (isLocalPlayer) return; + if (nouveauId == 0) _objetPorteDistant = null; + else if (NetworkClient.spawned.ContainsKey(nouveauId)) _objetPorteDistant = NetworkClient.spawned[nouveauId].gameObject; + } + + // ══════════════════════════════════════════════════════════ + // HELPERS LOCAUX + // ══════════════════════════════════════════════════════════ + + private bool EstColliderIgnore(Collider col) { for (int i = 0; i < _collisionsIgnorees.Count; i++) if (_collisionsIgnorees[i] == col) return true; return false; } + + void SetEmplacementsVisibles(bool visible) + { + if (_tousEmplacements == null || _tousEmplacements.Length == 0) _tousEmplacements = FindObjectsOfType(); + foreach (EmplacementBaie empl in _tousEmplacements) if (empl != null && !empl.estOccupe) empl.SetModeVisible(visible); + } + + void SetEmplacementsPDUVisibles(bool visible) + { + foreach (EmplacementPDU empl in FindObjectsOfType()) if (empl != null && !empl.estOccupe) empl.SetModeVisible(visible); + } + + // v6.9 : helper pour cacher les fantomes du chariot + void CacherFantomesChariotTous() + { + if (_chariotVise != null) { _chariotVise.CacherFantomes(); } + if (_chariotPilote != null) { _chariotPilote.CacherFantomes(); } + // v6.11 : CacherFantomes() inclut deja le fantome toron axe + // (cf. Chariot.CacherFantomes v7.3) mais on le reappelle explicitement ici + // au cas ou la version du Chariot n'aurait pas encore ete mise a jour + if (_chariotVise != null) _chariotVise.CacherFantomeToronAxe(); + if (_chariotPilote != null) _chariotPilote.CacherFantomeToronAxe(); + } + + // ==================== DETECTION OBJET ==================== + + void DetecterObjet() + { + QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU(); + QuitterSurvolPrise(); QuitterSurvolBoutonPower(); + _chariotVise = null; _visePoignee = false; _porteVisee = null; + + if (_camera == null) return; + + Ray rayon = new Ray(_camera.transform.position, _camera.transform.forward); + QueryTriggerInteraction triggerMode = detecterTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore; + + int maskObjets = ~LayerMask.GetMask("Joueur", "Baie"); + RaycastHit hitSimple; + if (Physics.Raycast(rayon, out hitSimple, porteeInteraction, maskObjets, triggerMode)) + { + if (!EstColliderIgnore(hitSimple.collider)) + if (TraiterHitDetection(hitSimple)) return; + } + + int maskBaie = LayerMask.GetMask("Baie"); + if (Physics.Raycast(rayon, out hitSimple, porteeInteraction, maskBaie, triggerMode)) + { + if (!EstColliderIgnore(hitSimple.collider)) + if (TraiterHitBaie(hitSimple)) return; + } + + QuitterSurvol(); + } + + bool TraiterHitDetection(RaycastHit impact) + { + // v6.8 : chaise pour s'asseoir + ChaiseSiege chaise = impact.collider.GetComponentInParent(); + if (chaise != null && !chaise.estOccupee && _objetEnMain == null) + { + _chaiseSiegeVisee = chaise; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement("[E] S'asseoir"); + QuitterSurvol(); + return true; + } + if (_chaiseSiegeVisee != null && chaise == null) + _chaiseSiegeVisee = null; + + BoutonPower bouton = impact.collider.GetComponent(); + if (bouton != null) { _boutonPowerVise = bouton; bouton.Survoler(true); return true; } + + PriseC13 prise = impact.collider.GetComponent(); + if (prise != null) { _priseSurvolee = prise; prise.Survoler(true); return true; } + + PorteBaie porte = impact.collider.GetComponentInParent(); + if (porte != null) { _porteVisee = porte; QuitterSurvol(); return true; } + + Chariot chariotHit = impact.collider.GetComponentInParent(); + if (chariotHit != null && impact.collider.gameObject.name == "Ch_PoigneeZone") + { + _visePoignee = true; + Interactable ci = chariotHit.GetComponent(); + if (ci != null) SetSurvol(ci); + return true; + } + + MonitoringDatacenter monitoring = impact.collider.GetComponent(); + if (monitoring != null) { Interactable interMon = impact.collider.GetComponent(); if (interMon != null) SetSurvol(interMon); return true; } + + PDU pduVise = impact.collider.GetComponentInParent(); + if (pduVise != null && pduVise.estInstalle) { Interactable interPDU = pduVise.GetComponent(); if (interPDU != null) { SetSurvol(interPDU); return true; } } + + Interactable obj = impact.collider.GetComponentInParent(); + if (obj != null) + { + if (obj.GetComponent() != null && !_visePoignee) { QuitterSurvol(); return true; } + SetSurvol(obj); return true; + } + + return false; + } + + bool TraiterHitBaie(RaycastHit impact) + { + if (impact.collider.gameObject.name == "BaieZoneTrigger") return false; + PorteBaie porte = impact.collider.GetComponent(); + if (porte != null) { _porteVisee = porte; QuitterSurvol(); return true; } + RackSlot slot = impact.collider.GetComponent(); + if (slot != null && slot.estOccupe && slot.equipementInstalle != null) + { + if (!PorteAvantOuverte(slot)) return false; + BoutonPower boutonRack = slot.equipementInstalle.GetComponentInChildren(); + if (boutonRack != null && ViseBouton(boutonRack.transform.position)) { _boutonPowerVise = boutonRack; boutonRack.Survoler(true); return true; } + SetSurvol(slot.equipementInstalle); return true; + } + EmplacementPDU emplPDU = impact.collider.GetComponent(); + if (emplPDU != null) return false; + return false; + } + + // ==================== DETECTION CIBLE POSE ==================== + + void DetecterCiblePose() + { + _porteVisee = null; + QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU(); + + if (_camera == null) return; + + if (DetecterCibleSupportTorons()) return; + QuitterSurvolSupport(); + + if (DetecterCibleSupportPDU()) return; + QuitterSurvolSupportPDU(); + + Ray rayon = new Ray(_camera.transform.position, _camera.transform.forward); + bool portePDU = _objetEnMain != null && _objetEnMain.GetComponent() != null; + + List colsPortes = new List(); + if (_objetEnMain != null) + foreach (Collider c in _objetEnMain.GetComponentsInChildren()) + if (c.enabled) { c.enabled = false; colsPortes.Add(c); } + + int slotMask = LayerMask.GetMask("Baie", "Sol", "Emplacement"); + RaycastHit[] hits = Physics.RaycastAll(rayon, porteeInteraction, slotMask, QueryTriggerInteraction.Collide); + System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); + foreach (Collider c in colsPortes) c.enabled = true; + + RackSlot slotTouche = null; EmplacementBaie emplTouche = null; EmplacementPDU emplPDUTouche = null; + + foreach (var hit in hits) + { + if (hit.collider.gameObject.name == "BaieZoneTrigger") continue; + if (portePDU) { EmplacementPDU ep = hit.collider.GetComponent(); if (ep != null && !ep.estOccupe) { emplPDUTouche = ep; break; } continue; } + RackSlot s = hit.collider.GetComponent(); if (s != null) { slotTouche = s; break; } + EmplacementBaie e = hit.collider.GetComponent(); if (e != null) { emplTouche = e; break; } + } + + if (emplPDUTouche != null) + { + if (_emplacementPDUVise != emplPDUTouche) { QuitterSurvolEmplacementPDU(); _emplacementPDUVise = emplPDUTouche; _emplacementPDUVise.SurvolDebut(); } + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement("[E] Installer PDU (" + (_emplacementPDUVise.cote == EmplacementPDU.CotePDU.Gauche ? "Gauche" : "Droit") + ")"); + // v6.9 : on a trouve une cible non-chariot -> cacher les fantomes + CacherFantomesChariotTous(); + _chariotVise = null; _chariotZoneEtagereVisee = false; + return; + } + + if (slotTouche != null) + { + bool porteOuverte = PorteAvantOuverte(slotTouche); + if (_slotSurvole != slotTouche) { QuitterSurvolSlot(); _slotSurvole = slotTouche; if (!slotTouche.estOccupe && porteOuverte) _slotSurvole.SurvolDebut(); } + if (!slotTouche.estOccupe) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(porteOuverte ? "U" + slotTouche.numeroSlot.ToString("D2") + " - [E] Installer ici" : "Ouvrir la porte avant d'abord"); } + CacherFantomesChariotTous(); + _chariotVise = null; _chariotZoneEtagereVisee = false; + return; + } + + if (emplTouche != null && !emplTouche.estOccupe) + { if (_emplacementVise != emplTouche) { QuitterSurvolEmplacement(); _emplacementVise = emplTouche; _emplacementVise.SurvolDebut(); } CacherFantomesChariotTous(); _chariotVise = null; _chariotZoneEtagereVisee = false; return; } + + HUDManager hudClear = GetComponent(); if (hudClear != null && _slotSurvole == null) hudClear.SetInfoEquipement(""); + + // ───── v6.11 : Detection zones de pose du chariot (plateau, etagere, axes toron) ───── + Chariot ancienChariot = _chariotVise; + _chariotVise = null; + _chariotZoneEtagereVisee = false; + _chariotAxeToronCibleIndex = -1; + + bool porteToron = (_objetEnMain != null && _objetEnMain.GetComponent() != null); + + int maskChariot = LayerMask.GetMask("Chariot"); + RaycastHit[] hitsChariot; + if (maskChariot != 0) + hitsChariot = Physics.RaycastAll(rayon, porteeInteraction, maskChariot, QueryTriggerInteraction.Collide); + else + hitsChariot = Physics.RaycastAll(rayon, porteeInteraction, ~LayerMask.GetMask("Joueur", "Equipement", "Sol", "Baie", "Emplacement"), QueryTriggerInteraction.Collide); + + System.Array.Sort(hitsChariot, (a, b) => a.distance.CompareTo(b.distance)); + + Chariot chariotHit = null; + int axeToronIdx = -1; + bool zoneTrouvee = false; + + foreach (var h in hitsChariot) + { + Chariot c = h.collider.GetComponentInParent(); + if (c == null) continue; + + if (porteToron) + { + // Porte un toron : priorite aux axes toron, on ignore plateau/etagere + if (c.zonesToronAxes != null) + { + int iFound = -1; + for (int i = 0; i < c.zonesToronAxes.Length; i++) + { + if (c.zonesToronAxes[i] != null && h.collider.gameObject == c.zonesToronAxes[i]) + { + // Verifier que l'axe est libre + bool libre = (c.equipementsToronAxes == null + || i >= c.equipementsToronAxes.Count + || c.equipementsToronAxes[i] == null); + if (libre) { iFound = i; break; } + } + } + if (iFound >= 0) + { + chariotHit = c; + axeToronIdx = iFound; + zoneTrouvee = true; + break; + } + } + } + else + { + // Equipement normal : zones plateau/etagere + bool estZonePlateau = (c.zonePosePlateau != null && h.collider.gameObject == c.zonePosePlateau); + bool estZoneEtagere = (c.zonePoseEtagere != null && h.collider.gameObject == c.zonePoseEtagere); + + if (estZonePlateau || estZoneEtagere) + { + chariotHit = c; + _chariotZoneEtagereVisee = estZoneEtagere; + zoneTrouvee = true; + break; + } + } + + // Fallback : garder le chariot comme candidat si pas encore trouve de zone + if (chariotHit == null) chariotHit = c; + } + + if (chariotHit != null) + { + _chariotVise = chariotHit; + + if (ancienChariot != null && ancienChariot != chariotHit) + { + ancienChariot.CacherFantomes(); + ancienChariot.CacherFantomeToronAxe(); + } + + if (porteToron && zoneTrouvee && axeToronIdx >= 0) + { + _chariotAxeToronCibleIndex = axeToronIdx; + chariotHit.AfficherFantomeToronAxe(axeToronIdx); + HUDManager hud = GetComponent(); + if (hud != null) + hud.SetInfoEquipement(axeToronIdx == 0 + ? "[E] Accrocher le toron sur l'axe gauche" + : "[E] Accrocher le toron sur l'axe droit"); + } + else if (!porteToron && zoneTrouvee) + { + int tailleU = (_objetEnMain != null && _objetEnMain.tailleU > 0) ? _objetEnMain.tailleU : 1; + chariotHit.AfficherFantomePose(tailleU, _chariotZoneEtagereVisee); + HUDManager hud = GetComponent(); + if (hud != null) + hud.SetInfoEquipement(_chariotZoneEtagereVisee ? "[E] Poser sur l'etagere basse" : "[E] Poser sur le plateau"); + } + else + { + // Chariot vise mais pas de zone specifique : cacher les fantomes + chariotHit.CacherFantomes(); + chariotHit.CacherFantomeToronAxe(); + } + } + else + { + if (ancienChariot != null) + { + ancienChariot.CacherFantomes(); + ancienChariot.CacherFantomeToronAxe(); + } + } + } + + // ==================== DÉTECTION SUPPORTS MURAUX ==================== + + bool DetecterCibleSupportTorons() + { + if (_objetEnMain == null) return false; + if (_objetEnMain.GetComponent() == null) return false; + SupportTorons support = SupportTorons.Instance; + if (support == null) return false; + float dist = Vector3.Distance(transform.position, support.transform.position); + if (dist > distanceDetectionSupport) return false; + Vector3 dirSupport = (support.transform.position - _camera.transform.position); + float distCam = dirSupport.magnitude; + if (distCam < 0.01f) return false; + dirSupport /= distCam; + float angle = Vector3.Angle(_camera.transform.forward, dirSupport); + if (angle > angleDetectionSupport) return false; + int r, c; Vector3 posAxe; + if (!support.TrouverAxeLibreLePlusProche(transform.position, out r, out c, out posAxe)) return false; + support.AfficherSurvolAxe(r, c); + _axeSupportCibleLigne = r; _axeSupportCibleColonne = c; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement("[E] Remettre le toron sur le support"); + return true; + } + + void QuitterSurvolSupport() + { + if (_axeSupportCibleLigne < 0) return; + if (SupportTorons.Instance != null) SupportTorons.Instance.CacherSurvolAxe(); + _axeSupportCibleLigne = -1; _axeSupportCibleColonne = -1; + } + + bool DetecterCibleSupportPDU() + { + if (_objetEnMain == null) return false; + PDU pdu = _objetEnMain.GetComponent(); + if (pdu == null) return false; + if (pdu.estInstalle) return false; + SupportPDU support = SupportPDU.Instance; + if (support == null) return false; + float dist = Vector3.Distance(transform.position, support.transform.position); + if (dist > distanceDetectionSupport) return false; + Vector3 dirSupport = (support.transform.position - _camera.transform.position); + float distCam = dirSupport.magnitude; + if (distCam < 0.01f) return false; + dirSupport /= distCam; + float angle = Vector3.Angle(_camera.transform.forward, dirSupport); + if (angle > angleDetectionSupport) return false; + int r, c; Vector3 posEmpl; + if (!support.TrouverEmplacementLibreLePlusProche(transform.position, out r, out c, out posEmpl)) return false; + support.AfficherSurvolEmplacement(r, c); + _emplacementSupportPDUCibleLigne = r; _emplacementSupportPDUCibleColonne = c; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement("[E] Remettre le PDU sur le support"); + return true; + } + + void QuitterSurvolSupportPDU() + { + if (_emplacementSupportPDUCibleLigne < 0) return; + if (SupportPDU.Instance != null) SupportPDU.Instance.CacherSurvolEmplacement(); + _emplacementSupportPDUCibleLigne = -1; _emplacementSupportPDUCibleColonne = -1; + } + + // ==================== SURVOL HELPERS ==================== + + void SetSurvol(Interactable obj) { if (_objetSurvole != obj) { if (_objetSurvole != null) _objetSurvole.SurvoleeFin(); _objetSurvole = obj; _objetSurvole.SurvoleDebut(); } } + void QuitterSurvol() { if (_objetSurvole != null) { _objetSurvole.SurvoleeFin(); _objetSurvole = null; } } + void QuitterSurvolSlot() { if (_slotSurvole != null) { _slotSurvole.SurvolFin(); _slotSurvole = null; } } + void QuitterSurvolEmplacement() { if (_emplacementVise != null) { _emplacementVise.SurvolFin(); _emplacementVise = null; } } + void QuitterSurvolEmplacementPDU() { if (_emplacementPDUVise != null) { _emplacementPDUVise.SurvolFin(); _emplacementPDUVise = null; } } + void QuitterSurvolPrise() { if (_priseSurvolee != null) { _priseSurvolee.Survoler(false); _priseSurvolee = null; } } + void QuitterSurvolBoutonPower() { if (_boutonPowerVise != null) { _boutonPowerVise.Survoler(false); _boutonPowerVise = null; } } + + // ==================== RAMASSER ==================== + + void Ramasser() + { + TerminalCommande terminal = _objetSurvole.GetComponent(); + if (terminal != null) { terminal.Interagir(); QuitterSurvol(); return; } + MonitoringDatacenter monitoring = _objetSurvole.GetComponent(); + if (monitoring != null) { monitoring.Interagir(); QuitterSurvol(); return; } + TableauTickets tableau = _objetSurvole.GetComponent(); + if (tableau != null) { tableau.Interagir(); QuitterSurvol(); return; } + AgrandissementSalle agr = _objetSurvole.GetComponentInParent(); + if (agr != null && !agr.estAgrandi && !agr.enAnimation) { agr.DemanderAgrandir(); QuitterSurvol(); return; } + + Chariot chariot = _objetSurvole.GetComponent(); + if (chariot != null && _visePoignee) + { + if (_chariotPilote == chariot) + { + LacherChariot(); + } + else + { + if (_chariotPilote != null) LacherChariot(); + PrendreChariot(chariot); + QuitterSurvol(); + } + return; + } + + Chariot chariotParent = _objetSurvole.GetComponentInParent(); + if (chariotParent != null && chariot == null) + { + if (chariotParent.estPilote) { QuitterSurvol(); return; } + + // v6.11 : verifier en PREMIER si le toron survole est accroche sur un axe lateral + if (chariotParent.equipementsToronAxes != null) + { + for (int i = 0; i < chariotParent.equipementsToronAxes.Count; i++) + { + if (chariotParent.equipementsToronAxes[i] == _objetSurvole.gameObject) + { + GameObject t = chariotParent.RetirerToronDeAxeChariot(i); + if (t != null) + { + PrendreEnMain(t.GetComponent()); + return; + } + } + } + } + + // v6.9 : detecter si l'objet est sur le plateau ou sur l'etagere basse + bool surEtagere = chariotParent.equipementsSurEtagere != null + && chariotParent.equipementsSurEtagere.Contains(_objetSurvole.gameObject); + + GameObject top = surEtagere + ? chariotParent.RetirerEquipementSurEtagere() + : chariotParent.RetirerEquipementDuDessus(); + + if (top != null) + { + // v6.13 Bug 2 : broadcaster le detachement a tous les clients + // (sinon sur les autres clients l'objet reste parente au chariot et + // vole avec lui quand on le lache). + NetworkIdentity topNetId = top.GetComponent(); + if (topNetId != null) CmdDetacherDuChariot(topNetId.netId); + + PrendreEnMain(top.GetComponent()); + return; + } + } + + PDU pdu = _objetSurvole.GetComponent(); + if (pdu != null && pdu.estInstalle) + { + bool cables = false; + foreach (var p in pdu.prises) if (p != null && p.estConnectee) { cables = true; break; } + if (cables) { Debug.LogWarning("Impossible : des cables sont branches !"); QuitterSurvol(); return; } + pdu.Desinstaller(); + NetworkIdentity pduNetIdentity = pdu.GetComponent(); + if (pduNetIdentity != null) CmdDesinstallerPDU(pduNetIdentity.netId); + PrendreEnMain(_objetSurvole); + return; + } + + BaieRack baieRack = _objetSurvole.GetComponent() ?? _objetSurvole.GetComponentInChildren(); + if (baieRack != null) { EmplacementBaie empl = TrouverEmplacementDeBaie(_objetSurvole.gameObject); if (empl != null) empl.RetirerBaie(); } + + RackSlot slotSrc = TrouverSlotDeEquipement(_objetSurvole); + if (slotSrc != null) + { + if (!PorteAvantOuverte(slotSrc)) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("Ouvrir la porte avant d'abord"); QuitterSurvol(); return; } + RackSlot[] slotsArr = ObtenirSlotsParent(slotSrc); + if (slotsArr != null) slotSrc.Liberer(slotsArr); + } + + ZoneLivraison zone = _objetSurvole.GetComponentInParent(); + if (zone == null) zone = ZoneLivraison.Instance; + if (zone != null) + { + Interactable plusHaut = zone.TrouverPlusHautDansPile(_objetSurvole); + if (plusHaut == null) plusHaut = _objetSurvole; + Vector3 pos = plusHaut.transform.position; + if (plusHaut != _objetSurvole) _objetSurvole.SurvoleeFin(); + plusHaut.transform.SetParent(null); + IgnorerCollisionsAvecStructure(plusHaut.gameObject, zone.transform, true); + PrendreEnMain(plusHaut); + // v6.13 Bug 1 : reorganisation cote SERVEUR pour que les positions + // soient sync via NetworkTransform a tous les clients. + CmdReorganiserZoneLivraison(pos); + return; + } + + PrendreEnMain(_objetSurvole); + } + + void PrendreEnMain(Interactable obj) + { + if (obj == null) return; + CartonLivraison carton = obj.GetComponent(); + if (carton != null && carton.ContientBaie()) { QuitterSurvol(); carton.OuvrirPlanPlacement(); return; } + _objetEnMain = obj; _objetSurvole = null; + _objetEnMain.Ramasser(); + _rbEnMain = _objetEnMain.GetComponent(); + if (_rbEnMain != null) + { + _rbEnMain.isKinematic = false; _rbEnMain.useGravity = false; + _rbEnMain.freezeRotation = true; + _rbEnMain.collisionDetectionMode = CollisionDetectionMode.Continuous; + _rbEnMain.interpolation = RigidbodyInterpolation.Interpolate; + Vector3 dirJoueur = new Vector3(_camera.transform.forward.x, 0, _camera.transform.forward.z).normalized; + Quaternion rotJoueur = Quaternion.LookRotation(dirJoueur); + _rotationOffsetTransport = Quaternion.Inverse(rotJoueur) * _objetEnMain.transform.rotation; + } + NetworkIdentity objNetId = obj.GetComponent(); + if (objNetId != null) CmdRamasser(objNetId.netId); + } + + // ==================== POSER ==================== + + void Poser() + { + RestaurerCollisionsIgnorees(); + + if (_axeSupportCibleLigne >= 0 && _objetEnMain != null && _objetEnMain.GetComponent() != null) + { PoserSurSupportTorons(); return; } + + if (_emplacementSupportPDUCibleLigne >= 0 && _objetEnMain != null && _objetEnMain.GetComponent() != null) + { PoserSurSupportPDU(); return; } + + // v6.11 : pose d'un toron sur un axe lateral du chariot (prioritaire sur plateau/etagere) + if (_chariotVise != null && _chariotAxeToronCibleIndex >= 0 + && _objetEnMain != null && _objetEnMain.GetComponent() != null) + { + int indexAxe = _chariotAxeToronCibleIndex; + if (_chariotVise.PoserToronSurAxeChariot(_objetEnMain.gameObject, indexAxe)) + { + _chariotVise.CacherFantomeToronAxe(); + Vector3 posT = _objetEnMain.transform.position; + Quaternion rotT = _objetEnMain.transform.rotation; + NotifierPoserReseau(false, posT, rotT, estRacke: true); + + // v6.13 Bug 2 : broadcast parenting a tous les clients + NetworkIdentity eqNetId = _objetEnMain.GetComponent(); + NetworkIdentity chNetId = _chariotVise.GetComponent(); + if (eqNetId != null && chNetId != null) + CmdParenterAuChariot(eqNetId.netId, chNetId.netId); + + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + _chariotAxeToronCibleIndex = -1; + HUDManager hud = GetComponent(); + if (hud != null) hud.SetInfoEquipement(""); + return; + } + } + + bool estNonRackable = (_objetEnMain.tailleU <= 0 || _objetEnMain.GetComponent() != null); + bool estCarton = _objetEnMain.GetComponent() != null; + bool estBaie = _objetEnMain.GetComponent() != null; + bool estPDU = _objetEnMain.GetComponent() != null; + + Vector3 posFinal = _objetEnMain.transform.position; + Quaternion rotFinal = _objetEnMain.transform.rotation; + bool gravite = true; + + if (estPDU && _emplacementPDUVise != null && !_emplacementPDUVise.estOccupe) + { + PDU pduObj = _objetEnMain.GetComponent(); + pduObj.InstallerSurEmplacement(_emplacementPDUVise); + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true); + NetworkIdentity pduNetId = _objetEnMain.GetComponent(); + if (pduNetId != null) CmdInstallerPDU(pduNetId.netId, posFinal, rotFinal); + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + QuitterSurvolEmplacementPDU(); + HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(""); + CacherFantomesChariotTous(); + return; + } + if (estPDU) + { + PDU pduObj = _objetEnMain.GetComponent(); + GestionnaireAlimentation ga = GetComponent(); + if (ga != null && !pduObj.estInstalle && ga.TenterSnapPDU(pduObj)) + { + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal); + NetworkIdentity pduNetId = _objetEnMain.GetComponent(); + if (pduNetId != null) CmdInstallerPDU(pduNetId.netId, posFinal, rotFinal); + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + CacherFantomesChariotTous(); + return; + } + } + if (estCarton) + { + CartonLivraison carton = _objetEnMain.GetComponent(); + if (carton != null && carton.ContientEquipement()) + { + if (_rbEnMain != null) { _rbEnMain.isKinematic = true; _rbEnMain.velocity = Vector3.zero; } + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal); + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + carton.TransformerAuSol(); + CacherFantomesChariotTous(); + return; + } + } + if (!estNonRackable && !estBaie && !estCarton && !estPDU && _slotSurvole != null && !_slotSurvole.estOccupe) + { + if (!PorteAvantOuverte(_slotSurvole)) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("Ouvrir la porte avant d'abord"); return; } + RackSlot[] slotsArr = ObtenirSlotsParent(_slotSurvole); + if (slotsArr != null && _slotSurvole.TenterInstallation(_objetEnMain, slotsArr)) + { + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true); + NetworkIdentity equipNetId = _objetEnMain.GetComponent(); + NetworkIdentity baieNetId = _slotSurvole.GetComponentInParent(); + if (equipNetId != null && baieNetId != null) { RackSlot[] allSlots = baieNetId.GetComponentsInChildren(); int slotIdx = System.Array.IndexOf(allSlots, _slotSurvole); CmdInstallerDansSlot(equipNetId.netId, baieNetId.netId, slotIdx); } + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + QuitterSurvolSlot(); + HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(""); + CacherFantomesChariotTous(); + return; + } + } + + // ───── v6.10 : Pose sur chariot (plateau ou etagere basse) ───── + // IMPORTANT : estRacke=true pour que le serveur mette isKinematic=true. + // Sinon les objets poses deviennent non-kinematic sans gravite cote serveur + // et flottent en apesanteur des qu'on en retire un du dessus. + if (_chariotVise != null && !estNonRackable && !estBaie && !estCarton && !estPDU) + { + bool pose = _chariotZoneEtagereVisee + ? _chariotVise.PoserEquipementSurEtagere(_objetEnMain.gameObject) + : _chariotVise.PoserEquipement(_objetEnMain.gameObject); + if (pose) + { + _chariotVise.CacherFantomes(); + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true); + + // v6.13 Bug 2 : broadcast parenting a tous les clients + NetworkIdentity eqNetId = _objetEnMain.GetComponent(); + NetworkIdentity chNetId = _chariotVise.GetComponent(); + if (eqNetId != null && chNetId != null) + CmdParenterAuChariot(eqNetId.netId, chNetId.netId); + + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + return; + } + } + if (_chariotPilote != null && _chariotVise == null && !estNonRackable && !estBaie && !estCarton && !estPDU) + if (_chariotPilote.PoserEquipement(_objetEnMain.gameObject)) + { + _chariotPilote.CacherFantomes(); + gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true); + + // v6.13 Bug 2 : broadcast parenting a tous les clients + NetworkIdentity eqNetId = _objetEnMain.GetComponent(); + NetworkIdentity chNetId = _chariotPilote.GetComponent(); + if (eqNetId != null && chNetId != null) + CmdParenterAuChariot(eqNetId.netId, chNetId.netId); + + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + return; + } + + // Drop au sol + if (_rbEnMain != null) { _rbEnMain.useGravity = true; _rbEnMain.velocity = Vector3.zero; _rbEnMain.interpolation = RigidbodyInterpolation.None; } + posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation; + NotifierPoserReseau(true, posFinal, rotFinal); + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU(); + QuitterSurvolSupport(); + QuitterSurvolSupportPDU(); + CacherFantomesChariotTous(); + } + + void PoserSurSupportTorons() + { + if (_objetEnMain == null) return; + int ligneCible = _axeSupportCibleLigne; int colonneCible = _axeSupportCibleColonne; + NetworkIdentity toronNetId = _objetEnMain.GetComponent(); + if (toronNetId != null) CmdPoserToronSurSupport(toronNetId.netId, ligneCible, colonneCible); + if (!isServer && SupportTorons.Instance != null) { bool place = SupportTorons.Instance.PoserToronSurAxeSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportTorons.Instance.PoserToron(_objetEnMain.gameObject); } + else if (toronNetId == null && SupportTorons.Instance != null) { bool place = SupportTorons.Instance.PoserToronSurAxeSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportTorons.Instance.PoserToron(_objetEnMain.gameObject); } + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + QuitterSurvolSupport(); + HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(""); + } + + void PoserSurSupportPDU() + { + if (_objetEnMain == null) return; + int ligneCible = _emplacementSupportPDUCibleLigne; int colonneCible = _emplacementSupportPDUCibleColonne; + NetworkIdentity pduNetId = _objetEnMain.GetComponent(); + if (pduNetId != null) CmdPoserPDUSurSupport(pduNetId.netId, ligneCible, colonneCible); + if (!isServer && SupportPDU.Instance != null) { bool place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportPDU.Instance.PoserPDU(_objetEnMain.gameObject); } + else if (pduNetId == null && SupportPDU.Instance != null) { bool place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportPDU.Instance.PoserPDU(_objetEnMain.gameObject); } + _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null; + QuitterSurvolSupportPDU(); + HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(""); + } + + void NotifierPoserReseau(bool avecGravite, Vector3 position, Quaternion rotation, bool estRacke = false) + { + if (_objetEnMain == null) return; + NetworkIdentity objNetId = _objetEnMain.GetComponent(); + if (objNetId != null) CmdPoser(objNetId.netId, position, rotation, avecGravite, estRacke); + } + + // ==================== HELPERS ==================== + + bool ViseBouton(Vector3 positionMonde, float seuilPixels = 60f) + { + if (_camera == null) return false; + Vector3 screenPos = _camera.WorldToScreenPoint(positionMonde); + if (screenPos.z <= 0f) return false; + return Vector2.Distance(new Vector2(Screen.width / 2f, Screen.height / 2f), new Vector2(screenPos.x, screenPos.y)) < seuilPixels; + } + + bool PorteAvantOuverte(RackSlot slot) + { + if (slot == null) return true; + Transform baie = slot.transform.parent; if (baie == null) return true; + foreach (Transform enfant in baie) + if (enfant.name == "Pivot_PorteAvant") { PorteBaie porte = enfant.GetComponent(); if (porte != null) return porte.estOuverte; } + return true; + } + + RackSlot[] ObtenirSlotsParent(RackSlot slot) + { + if (slot == null) return null; + BaieRack baieRack = slot.GetComponentInParent(); if (baieRack != null && baieRack.slots != null) return baieRack.slots; + BaieProcedurale baieProc = slot.GetComponentInParent(); if (baieProc != null && baieProc.slots != null) return baieProc.slots; + Transform parent = slot.transform.parent; if (parent != null) return parent.GetComponentsInChildren(); + return null; + } + + private void IgnorerCollisionsAvecStructure(GameObject objet, Transform parent, bool ignorer) + { + Collider objCollider = objet.GetComponent(); if (objCollider == null) return; + _collisionsIgnorees.Clear(); + for (int i = 0; i < parent.childCount; i++) + { + Transform enfant = parent.GetChild(i); string nom = enfant.name; + if (nom.StartsWith("Planche_") || nom.StartsWith("Montant_") || nom.StartsWith("Etiquette_")) + { Collider sc = enfant.GetComponent(); if (sc != null) { Physics.IgnoreCollision(objCollider, sc, ignorer); if (ignorer) _collisionsIgnorees.Add(sc); } } + Interactable inter = enfant.GetComponent(); + if (inter != null && enfant.gameObject != objet) + { Collider ec = enfant.GetComponent(); if (ec != null) { Physics.IgnoreCollision(objCollider, ec, ignorer); if (ignorer) _collisionsIgnorees.Add(ec); } } + } + } + + private void RestaurerCollisionsIgnorees() + { + if (_objetEnMain == null || _collisionsIgnorees.Count == 0) return; + Collider objCollider = _objetEnMain.GetComponent(); + if (objCollider == null) { _collisionsIgnorees.Clear(); return; } + foreach (Collider col in _collisionsIgnorees) if (col != null) Physics.IgnoreCollision(objCollider, col, false); + _collisionsIgnorees.Clear(); + } + + // v6.12 : gestion pilotage chariot + transfert d'autorite multijoueur + void PrendreChariot(Chariot chariot) + { + if (chariot == null) return; + _chariotPilote = chariot; + + // Demander l'autorite au serveur pour pouvoir modifier transform.position + // via NetworkTransform ClientToServer. Sans ca, Mirror ignore les changements + // cote client et les autres joueurs ne voient pas le chariot bouger. + NetworkIdentity ni = chariot.GetComponent(); + if (ni != null) CmdDemanderAutoriteChariot(ni.netId); + + chariot.TogglePilotage(transform); + } + + void LacherChariot() + { + if (_chariotPilote == null) return; + + NetworkIdentity ni = _chariotPilote.GetComponent(); + + _chariotPilote.CacherFantomes(); + _chariotPilote.TogglePilotage(transform); + + // Relacher l'autorite APRES TogglePilotage pour que la position finale + // soit bien envoyee au serveur avant que le client perde son droit d'ecrire. + if (ni != null) CmdRelacherAutoriteChariot(ni.netId); + + _chariotPilote = null; + } + + [Command] + void CmdDemanderAutoriteChariot(uint chariotNetId) + { + if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return; + NetworkIdentity ni = NetworkServer.spawned[chariotNetId]; + if (ni == null) return; + + // Si un autre client a deja l'autorite, la retirer d'abord + if (ni.connectionToClient != null && ni.connectionToClient != connectionToClient) + ni.RemoveClientAuthority(); + + ni.AssignClientAuthority(connectionToClient); + } + + [Command] + void CmdRelacherAutoriteChariot(uint chariotNetId) + { + if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return; + NetworkIdentity ni = NetworkServer.spawned[chariotNetId]; + if (ni == null) return; + + // Ne retirer l'autorite que si c'est bien ce joueur qui l'a actuellement, + // pour eviter de voler l'autorite a quelqu'un d'autre par accident. + if (ni.connectionToClient == connectionToClient) + ni.RemoveClientAuthority(); + } + + // ===== v6.13 Bug 1 : reorganisation ZoneLivraison cote serveur ===== + [Command] + void CmdReorganiserZoneLivraison(Vector3 posRetireeWorld) + { + if (ZoneLivraison.Instance != null) + ZoneLivraison.Instance.ReorganiserNiveauComplet(posRetireeWorld); + } + + // ===== v6.13 Bug 2 : sync du parenting equipement <-> chariot ===== + [Command] + void CmdParenterAuChariot(uint equipNetId, uint chariotNetId) + { + if (!NetworkServer.spawned.ContainsKey(equipNetId)) return; + if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return; + RpcParenterAuChariot(equipNetId, chariotNetId); + } + + [ClientRpc] + void RpcParenterAuChariot(uint equipNetId, uint chariotNetId) + { + if (!NetworkClient.spawned.TryGetValue(equipNetId, out NetworkIdentity eqNi)) return; + if (!NetworkClient.spawned.TryGetValue(chariotNetId, out NetworkIdentity chNi)) return; + if (eqNi == null || chNi == null) return; + + // worldPositionStays=true : conserver la position monde actuelle. L'equipement + // est deja a la bonne position (via NetworkTransform), on veut juste l'attacher + // au chariot pour qu'il suive les futurs mouvements. + eqNi.transform.SetParent(chNi.transform, true); + } + + [Command] + void CmdDetacherDuChariot(uint equipNetId) + { + if (!NetworkServer.spawned.ContainsKey(equipNetId)) return; + RpcDetacherDuChariot(equipNetId); + } + + [ClientRpc] + void RpcDetacherDuChariot(uint equipNetId) + { + if (!NetworkClient.spawned.TryGetValue(equipNetId, out NetworkIdentity eqNi)) return; + if (eqNi == null) return; + eqNi.transform.SetParent(null, true); + } + + EmplacementBaie TrouverEmplacementDeBaie(GameObject baie) + { + if (_tousEmplacements == null) _tousEmplacements = FindObjectsOfType(); + foreach (EmplacementBaie empl in _tousEmplacements) if (empl.estOccupe && empl.baieInstallee == baie) return empl; + return null; + } + + RackSlot TrouverSlotDeEquipement(Interactable equipement) + { + if (equipement == null) return null; + foreach (BaieRack baie in FindObjectsOfType()) { if (baie.slots == null) continue; foreach (RackSlot s in baie.slots) if (s != null && s.equipementInstalle == equipement) return s; } + foreach (BaieProcedurale baie in FindObjectsOfType()) { if (baie.slots == null) continue; foreach (RackSlot s in baie.slots) if (s != null && s.equipementInstalle == equipement) return s; } + return null; + } +} \ No newline at end of file diff --git a/Patchs/Agrandissement_Multi/Scripts_Nouveaux/SynchronisationAgrandissement.cs b/Patchs/Agrandissement_Multi/Scripts_Nouveaux/SynchronisationAgrandissement.cs new file mode 100644 index 0000000..1a94086 --- /dev/null +++ b/Patchs/Agrandissement_Multi/Scripts_Nouveaux/SynchronisationAgrandissement.cs @@ -0,0 +1,281 @@ +// SynchronisationAgrandissement.cs + +using UnityEngine; +using Mirror; +using System.Collections.Generic; + +/// +/// Singleton NetworkBehaviour qui synchronise les agrandissements de salle entre +/// le serveur dedicated et tous les clients. +/// +/// Pattern : +/// 1. Un client clique [E] sur un panneau d'agrandissement +/// -> AgrandissementSalle.DemanderAgrandir() trouve le singleton et appelle CmdAgrandir(id) +/// 2. Cote serveur, CmdAgrandir valide (pas deja agrandi) et diffuse RpcLancerAgrandissement(id) +/// 3. Chaque client (+ serveur) recoit le Rpc et execute l'animation locale d'Agrandir() +/// en parallele. Tous voient la meme animation car les parametres sont identiques. +/// 4. Un client qui rejoint en cours de partie recoit la SyncList des agrandissements deja +/// faits et les rejoue instantanement (sans animation). +/// +/// A placer : en composant sur le GameObject du NetworkManager (ou tout objet avec +/// NetworkIdentity configure pour exister des le demarrage reseau). +/// +/// L'identifiant d'un agrandissement est "salleId_direction", ex: "1_Nord". +/// +public class SynchronisationAgrandissement : NetworkBehaviour +{ + public static SynchronisationAgrandissement Instance { get; private set; } + + // Liste synchronisee des agrandissements deja effectues + // Format des entrees : "salleId_Direction" (ex: "1_Nord", "2_Est") + private readonly SyncList _agrandissementsEffectues = new SyncList(); + + private void Awake() + { + if (Instance != null && Instance != this) + { + Debug.LogWarning("[SyncAgrandissement] Instance deja existante, destruction de ce duplicata"); + Destroy(this); + return; + } + Instance = this; + } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + // ============================================================ + // CLIENT : appel par AgrandissementSalle.DemanderAgrandir() + // ============================================================ + + /// + /// Appele par un client qui veut agrandir. Envoie la demande au serveur. + /// En mode solo (pas de Mirror actif), execute directement localement. + /// + public void DemanderAgrandissement(int salleId, string direction) + { + string id = ConstruireId(salleId, direction); + + // Mode solo (pas de serveur Mirror demarre) : execution directe locale + if (!NetworkServer.active && !NetworkClient.active) + { + Debug.Log($"[SyncAgrandissement] Mode solo, execution locale directe : {id}"); + ExecuterAgrandissementLocal(salleId, direction, false); + return; + } + + // Mode multi : Cmd vers serveur + // Il faut que le client ait authority. On passe par le player local qui + // porte ce role, ou directement si on est serveur nous-meme. + if (NetworkServer.active) + { + // Host ou dedicated : on est serveur, on peut traiter directement + TraiterDemandeServeur(salleId, direction); + } + else if (NetworkClient.active && NetworkClient.connection != null) + { + // Client pur : on doit passer par un PlayerBridge qui porte isLocalPlayer + PlayerAgrandissementBridge bridge = TrouverBridgeLocal(); + if (bridge != null) + { + bridge.CmdDemanderAgrandissement(salleId, direction); + } + else + { + Debug.LogError("[SyncAgrandissement] Aucun PlayerAgrandissementBridge trouve sur le joueur local !"); + } + } + } + + // ============================================================ + // SERVEUR : validation + broadcast + // ============================================================ + + /// + /// Appele cote serveur (soit par un host qui joue, soit par le bridge d'un client). + /// Valide et diffuse aux clients. + /// + [Server] + public void TraiterDemandeServeur(int salleId, string direction) + { + string id = ConstruireId(salleId, direction); + + // Validation + if (_agrandissementsEffectues.Contains(id)) + { + Debug.LogWarning($"[SyncAgrandissement] Demande refusee, deja agrandi : {id}"); + return; + } + + // Enregistrer dans la liste synchronisee (pour rejoin) + _agrandissementsEffectues.Add(id); + Debug.Log($"[SyncAgrandissement] Agrandissement valide : {id} (total : {_agrandissementsEffectues.Count})"); + + // Diffuser a tous les clients (le host le recoit aussi) + RpcLancerAgrandissement(salleId, direction); + } + + // ============================================================ + // CLIENTS : reception du Rpc + // ============================================================ + + [ClientRpc] + private void RpcLancerAgrandissement(int salleId, string direction) + { + Debug.Log($"[SyncAgrandissement] Rpc recu, execution agrandissement : salle {salleId} direction {direction}"); + ExecuterAgrandissementLocal(salleId, direction, false); + } + + // ============================================================ + // REJOIN : un nouveau client rejoue l'etat actuel + // ============================================================ + + 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; + + // Le SyncList n'est pas forcement deja rempli a ce moment-la. + // On se branche sur ses callbacks et on rejoue les entrees existantes. + _agrandissementsEffectues.Callback += OnAgrandissementsListChanged; + + // Rejouer immediatement les entrees deja presentes + if (_agrandissementsEffectues.Count > 0) + { + Debug.Log($"[SyncAgrandissement] Client rejoint : rejeu de {_agrandissementsEffectues.Count} agrandissement(s) passe(s)"); + foreach (string id in _agrandissementsEffectues) + { + RejouerInstantane(id); + } + } + } + + public override void OnStopClient() + { + base.OnStopClient(); + _agrandissementsEffectues.Callback -= OnAgrandissementsListChanged; + } + + private void OnAgrandissementsListChanged(SyncList.Operation op, int index, string oldItem, string newItem) + { + // On ignore les changements qui arrivent par le flux normal (deja gere par Rpc) + // Ce callback ne sert qu'au cas ou la SyncList est modifiee avant que le client recoive + // le Rpc correspondant (race condition rare). Dans ce cas on rejoue aussi. + // En pratique on s'en remet surtout au rejeu initial dans OnStartClient(). + } + + private void RejouerInstantane(string id) + { + int salleId; + string direction; + if (!DecomposerId(id, out salleId, out direction)) + { + Debug.LogWarning($"[SyncAgrandissement] Id mal forme : {id}"); + return; + } + ExecuterAgrandissementLocal(salleId, direction, true); + } + + // ============================================================ + // EXECUTION LOCALE (cote serveur ET cote client) + // ============================================================ + + /// + /// Execute l'agrandissement sur l'instance locale : trouve le bon mur et lance Agrandir(). + /// Si instantane=true, skip les animations (pour rejoin). + /// + private void ExecuterAgrandissementLocal(int salleId, string direction, bool instantane) + { + AgrandissementSalle mur = TrouverMurAgrandissement(salleId, direction); + if (mur == null) + { + Debug.LogWarning($"[SyncAgrandissement] Mur introuvable : salle {salleId} direction {direction}"); + return; + } + + if (mur.estAgrandi || mur.enAnimation) + { + Debug.Log($"[SyncAgrandissement] Agrandissement deja lance ou termine localement, skip"); + return; + } + + mur.AgrandirLocal(instantane); + } + + private AgrandissementSalle TrouverMurAgrandissement(int salleId, string direction) + { + AgrandissementSalle.DirectionAgrandissement dirEnum; + if (!System.Enum.TryParse(direction, out dirEnum)) return null; + + AgrandissementSalle[] tous = FindObjectsOfType(); + foreach (var a in tous) + { + if (a.direction != dirEnum) continue; + SalleDatacenter salle = FindObjectOfType(); + if (salle == null) continue; + if (salle.salleId == salleId) return a; + } + + // Fallback : si aucun salleId ne matche (salle mono, pas encore enregistree + // par le GestionnaireMultiSalles au moment de l'appel), on renvoie n'importe + // quel mur dans la bonne direction. + foreach (var a in tous) + { + if (a.direction == dirEnum) return a; + } + + return null; + } + + private PlayerAgrandissementBridge TrouverBridgeLocal() + { + PlayerAgrandissementBridge[] tous = FindObjectsOfType(); + foreach (var b in tous) + { + if (b.isLocalPlayer) return b; + } + return null; + } + + // ============================================================ + // HELPERS ID + // ============================================================ + + private static string ConstruireId(int salleId, string direction) => $"{salleId}_{direction}"; + + private static bool DecomposerId(string id, out int salleId, out string direction) + { + salleId = -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 salleId)) return false; + direction = id.Substring(sep + 1); + return true; + } +} + +/// +/// Bridge Mirror : composant a ajouter au prefab du joueur pour qu'il puisse +/// envoyer une Cmd d'agrandissement vers le serveur (car les [Command] ne peuvent +/// etre appelees que depuis un NetworkBehaviour porte par le joueur local). +/// +public class PlayerAgrandissementBridge : NetworkBehaviour +{ + [Command] + public void CmdDemanderAgrandissement(int salleId, string direction) + { + if (SynchronisationAgrandissement.Instance == null) + { + Debug.LogError("[PlayerAgrandissementBridge] Instance SynchronisationAgrandissement introuvable cote serveur !"); + return; + } + + SynchronisationAgrandissement.Instance.TraiterDemandeServeur(salleId, direction); + } +} diff --git a/Patchs/dcsim_agrandissement_multi.zip b/Patchs/dcsim_agrandissement_multi.zip deleted file mode 100644 index b3a2c58387302085c2fc1a2104044ab76b689ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32846 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