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 b3a2c58..0000000 Binary files a/Patchs/dcsim_agrandissement_multi.zip and /dev/null differ