Files
Ajedrez_Purgatorio/Assets/Editor/PurgatoryCanvasMaintenance.cs
T
jimmyabv f7d56066c0 feat: Purgatory Final duel vs La Muerte + SFX wiring + runtime options menu
- Duel flow: mating piece vs boss while board collapses (PurgatoryDuelManager/Rules/UI/MuerteQuotes)
- GameState.PurgatoryDuel + CampaignManager rescue/restart hooks + SquareClick routing
- Dice-based rescue of most valuable dead identity; final defeat restarts Chapter 1
- 6 SFX clips wired in Ch1/2/3 via AudioClipAssigner
- OptionsMenuController runtime audio/graphics panel, wired to MainMenu
- Tests migrated to Assets/Tests/EditMode subfolders + PurgatoryDuelRulesTests (200 total)
- Editor tools: DiagAssign, SpeakerDatabaseSetup, PurgatoryCanvasMaintenance
2026-08-24 19:39:52 -03:00

470 lines
18 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using AjedrezPurgatorio.UI;
namespace AjedrezPurgatorio.EditorTools
{
/// <summary>
/// Herramientas de mantenimiento para las escenas de capítulo:
/// 1. Elimina jerarquías de UI duplicadas (sets completos creados por corridas
/// repetidas de los scripts de setup bajo PurgatoryCanvas).
/// 2. Copia los paneles finales (Victory/Defeat/Memorial) desde Chapter3 a la escena activa.
///
/// Uso: abrir la escena objetivo -> ejecutar menú -> revisar en jerarquía -> guardar (Ctrl+S).
/// Ninguna operación guarda la escena automáticamente.
/// </summary>
public static class PurgatoryCanvasMaintenance
{
private const string Chapter3ScenePath = "Assets/Scenes/Chapter3.unity";
private const string MenuRoot = "Tools/Purgatory UI/";
// ------------------------------------------------------------------
// 1) Deduplicación de paneles
// ------------------------------------------------------------------
[MenuItem(MenuRoot + "Deduplicate Panels (Active Scene)")]
public static void DeduplicateActiveScene()
{
Scene scene = EditorSceneManager.GetActiveScene();
if (!scene.IsValid() || string.IsNullOrEmpty(scene.path))
{
EditorUtility.DisplayDialog("Deduplicate Panels",
"Abre una escena guardada antes de ejecutar la deduplicación.", "OK");
return;
}
HashSet<int> protectedAncestorIds = CollectProtectedAncestorIds(scene);
List<DuplicateGroup> groups = CollectDuplicateGroups(scene);
if (groups.Count == 0)
{
EditorUtility.DisplayDialog("Deduplicate Panels",
$"No se encontraron grupos duplicados en '{scene.name}'.", "OK");
return;
}
// Planificar sin destruir nada todavía.
List<GameObject> toDelete = new List<GameObject>();
StringBuilder summary = new StringBuilder();
int keptTotal = 0;
foreach (DuplicateGroup group in groups.OrderByDescending(g => g.Depth))
{
List<Transform> alive = group.Members.Where(t => t != null).ToList();
if (alive.Count < 2)
continue;
List<Transform> survivors =
alive.Where(t => IsProtected(t.gameObject, protectedAncestorIds)).ToList();
if (survivors.Count == 0)
{
survivors.Add(alive.OrderByDescending(t => t.GetSiblingIndex()).First());
}
foreach (Transform t in alive.Where(t => !survivors.Contains(t)))
toDelete.Add(t.gameObject);
keptTotal += survivors.Count;
summary.AppendLine(
$"{group.DisplayName}: conservar {survivors.Count}, eliminar {alive.Count - survivors.Count}");
}
if (toDelete.Count == 0)
{
EditorUtility.DisplayDialog("Deduplicate Panels",
"Hay grupos duplicados pero todos sus miembros están protegidos por referencias serializadas. " +
"No se eliminará nada.", "OK");
return;
}
string preview = summary.ToString();
if (preview.Length > 1200)
preview = preview.Substring(0, 1200) + "\n...";
bool confirmed = EditorUtility.DisplayDialog(
"Deduplicate Panels",
$"Escena '{scene.name}': se eliminarán {toDelete.Count} objetos duplicados " +
$"(quedan {keptTotal} copias funcionales).\n\n{preview}\n\n" +
"La operación es reversible con Ctrl+Z y NO guarda la escena.",
"Eliminar duplicados", "Cancelar");
if (!confirmed)
return;
int removed = 0;
foreach (GameObject go in toDelete)
{
if (go == null)
continue; // ya murió junto a un ancestro eliminado
Undo.DestroyObjectImmediate(go);
removed++;
}
EditorSceneManager.MarkSceneDirty(scene);
Debug.Log($"[PurgatoryCanvasMaintenance] Deduplicación completa en '{scene.name}': " +
$"{removed} objetos eliminados. Revisa la jerarquía y guarda la escena.");
EditorUtility.DisplayDialog("Deduplicate Panels",
$"{removed} objetos eliminados en '{scene.name}'.\n\n" +
"Revisa la jerarquía y guarda la escena (Ctrl+S).", "OK");
}
private sealed class DuplicateGroup
{
public string DisplayName;
public int Depth;
public List<Transform> Members = new List<Transform>();
}
/// <summary>
/// Recolecta los instance IDs de todos los objetos referenciados por campos
/// serializados de la escena, incluyendo todos sus ancestros. Así se conserva
/// la copia exacta a la que apuntan campos como PurgatoryManager._offerUI,
/// aunque la referencia apunte a un hijo profundo del panel.
/// </summary>
private static HashSet<int> CollectProtectedAncestorIds(Scene scene)
{
HashSet<int> protectedIds = new HashSet<int>();
foreach (GameObject root in scene.GetRootGameObjects())
{
foreach (Component comp in root.GetComponentsInChildren<Component>(true))
{
if (comp == null)
continue; // script faltante
TryCollectReferences(comp, protectedIds);
}
}
HashSet<int> ancestors = new HashSet<int>();
foreach (int id in protectedIds)
{
UnityEngine.Object obj = EditorUtility.InstanceIDToObject(id);
Transform t = null;
if (obj is Component component && component != null)
t = component.transform;
else if (obj is GameObject go && go != null)
t = go.transform;
while (t != null)
{
ancestors.Add(t.gameObject.GetInstanceID());
t = t.parent;
}
}
return ancestors;
}
private static void TryCollectReferences(Component comp, HashSet<int> into)
{
try
{
SerializedObject so = new SerializedObject(comp);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.NextVisible(enterChildren))
{
enterChildren = true;
if (prop.propertyType == SerializedPropertyType.ObjectReference &&
prop.objectReferenceValue != null)
{
into.Add(prop.objectReferenceValue.GetInstanceID());
}
}
}
catch (System.Exception e)
{
Debug.LogWarning($"[PurgatoryCanvasMaintenance] No se pudo leer {comp.GetType().Name}: {e.Message}");
}
}
/// <summary>
/// Agrupa RectTransform hermanos con nombre idéntico. Solo considera objetos
/// con padre (no raíces) para acotar el riesgo al UI generado por setup.
/// </summary>
private static List<DuplicateGroup> CollectDuplicateGroups(Scene scene)
{
Dictionary<string, DuplicateGroup> byKey = new Dictionary<string, DuplicateGroup>();
foreach (GameObject root in scene.GetRootGameObjects())
{
foreach (RectTransform rt in root.GetComponentsInChildren<RectTransform>(true))
{
if (rt == null || rt.parent == null)
continue;
Transform parent = rt.parent;
string key = parent.GetInstanceID() + "|" + rt.name;
if (!byKey.TryGetValue(key, out DuplicateGroup group))
{
group = new DuplicateGroup
{
DisplayName = GetPath(rt),
Depth = CountDepth(rt)
};
byKey[key] = group;
}
group.Members.Add(rt);
}
}
return byKey.Values.Where(g => g.Members.Count > 1).ToList();
}
private static bool IsProtected(GameObject go, HashSet<int> protectedAncestorIds)
{
return protectedAncestorIds.Contains(go.GetInstanceID());
}
private static int CountDepth(Transform t)
{
int depth = 0;
while (t.parent != null)
{
depth++;
t = t.parent;
}
return depth;
}
private static string GetPath(Transform t)
{
var parts = new List<string>();
while (t != null)
{
parts.Insert(0, t.name);
t = t.parent;
}
return string.Join("/", parts);
}
// ------------------------------------------------------------------
// 2) Copia de paneles finales desde Chapter3
// ------------------------------------------------------------------
[MenuItem(MenuRoot + "Copy End Screens from Chapter3 (Active Scene)")]
public static void CopyEndScreensFromChapter3()
{
Scene active = EditorSceneManager.GetActiveScene();
if (!active.IsValid() || string.IsNullOrEmpty(active.path))
{
EditorUtility.DisplayDialog("Copy End Screens",
"Abre una escena guardada (Chapter1 o Chapter2) primero.", "OK");
return;
}
if (active.path == Chapter3ScenePath)
{
EditorUtility.DisplayDialog("Copy End Screens",
"La escena activa ES Chapter3. Abre Chapter1 o Chapter2 como destino.", "OK");
return;
}
GameObject targetCanvas = active.GetRootGameObjects()
.FirstOrDefault(go => go.name == "PurgatoryCanvas");
if (targetCanvas == null)
{
EditorUtility.DisplayDialog("Copy End Screens",
$"No se encontró 'PurgatoryCanvas' como raíz de '{active.name}'.", "OK");
return;
}
Scene chapter3 = default;
int copied = 0;
StringBuilder report = new StringBuilder();
try
{
chapter3 = EditorSceneManager.OpenScene(Chapter3ScenePath, OpenSceneMode.Additive);
foreach (GameObject src in EnumerateSourcePanels(chapter3))
{
if (targetCanvas.transform.Find(src.name) != null)
{
report.AppendLine($"{src.name}: ya existe, omitido.");
continue;
}
GameObject copy = Object.Instantiate(src);
SceneManager.MoveGameObjectToScene(copy, active);
copy.transform.SetParent(targetCanvas.transform, false);
copy.name = src.name;
Undo.RegisterCreatedObjectUndo(copy, "Copy End Screen Panel");
copied++;
report.AppendLine($"{src.name}: copiado.");
}
}
finally
{
if (chapter3.IsValid())
EditorSceneManager.CloseScene(chapter3, true);
}
EditorSceneManager.MarkSceneDirty(active);
Debug.Log($"[PurgatoryCanvasMaintenance] Copy End Screens en '{active.name}': " +
$"{copied} paneles copiados desde Chapter3. Revisa y guarda la escena.");
EditorUtility.DisplayDialog("Copy End Screens",
$"'{active.name}': {copied} paneles copiados.\n\n{report}\n" +
"Revisa la jerarquía y guarda la escena (Ctrl+S).", "OK");
}
private static IEnumerable<GameObject> EnumerateSourcePanels(Scene chapter3)
{
foreach (var type in new[] { typeof(VictoryUI), typeof(DefeatUI), typeof(MemorialUI) })
{
foreach (Component comp in Object.FindObjectsByType(type, FindObjectsInactive.Include, FindObjectsSortMode.None))
{
if (comp != null && comp.gameObject.scene == chapter3)
yield return comp.gameObject;
}
}
}
// ------------------------------------------------------------------
// 3) Automatización headless (batchmode -executeMethod)
// ------------------------------------------------------------------
/// <summary>
/// Procesa Chapter1/2/3 sin interacción: deduplica paneles y copia las
/// pantallas finales de Chapter3 a Chapter1/2. Guarda cada escena.
/// Ejecutar con:
/// Unity -batchmode -projectPath . -executeMethod
/// AjedrezPurgatorio.EditorTools.PurgatoryCanvasMaintenance.BatchProcessChapterScenes
/// </summary>
public static void BatchProcessChapterScenes()
{
var chapterPaths = new[]
{
"Assets/Scenes/Chapter1.unity",
"Assets/Scenes/Chapter2.unity",
"Assets/Scenes/Chapter3.unity",
};
int totalRemoved = 0;
int totalCopied = 0;
try
{
foreach (string path in chapterPaths)
{
Scene scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
int removed = RemoveDuplicatesHeadless(scene);
totalRemoved += removed;
int copied = 0;
if (scene.path != Chapter3ScenePath)
copied = CopyEndScreensHeadless(scene);
totalCopied += copied;
EditorSceneManager.SaveScene(scene);
Debug.Log($"[PurgatoryCanvasMaintenance][Batch] '{scene.name}': " +
$"{removed} duplicados eliminados, {copied} pantallas copiadas. Escena guardada.");
}
Debug.Log($"[PurgatoryCanvasMaintenance][Batch] COMPLETO: {totalRemoved} duplicados, {totalCopied} copias.");
EditorApplication.Exit(0);
}
catch (System.Exception ex)
{
Debug.LogError($"[PurgatoryCanvasMaintenance][Batch] FALLÓ: {ex}");
EditorApplication.Exit(1);
}
}
private static int RemoveDuplicatesHeadless(Scene scene)
{
HashSet<int> protectedAncestorIds = CollectProtectedAncestorIds(scene);
List<DuplicateGroup> groups = CollectDuplicateGroups(scene);
int removed = 0;
foreach (DuplicateGroup group in groups.OrderByDescending(g => g.Depth))
{
List<Transform> alive = group.Members.Where(t => t != null).ToList();
if (alive.Count < 2)
continue;
List<Transform> survivors =
alive.Where(t => IsProtected(t.gameObject, protectedAncestorIds)).ToList();
if (survivors.Count == 0)
survivors.Add(alive.OrderByDescending(t => t.GetSiblingIndex()).First());
foreach (Transform t in alive.Where(t => !survivors.Contains(t)))
{
GameObject go = t.gameObject;
if (go == null)
continue; // ya murió junto a un ancestro eliminado
Object.DestroyImmediate(go);
removed++;
}
}
if (removed > 0)
EditorSceneManager.MarkSceneDirty(scene);
return removed;
}
private static int CopyEndScreensHeadless(Scene active)
{
GameObject targetCanvas = active.GetRootGameObjects()
.FirstOrDefault(go => go.name == "PurgatoryCanvas");
if (targetCanvas == null)
{
Debug.LogWarning($"[PurgatoryCanvasMaintenance][Batch] '{active.name}' sin PurgatoryCanvas; se omiten las copias.");
return 0;
}
Scene chapter3 = default;
int copied = 0;
try
{
chapter3 = EditorSceneManager.OpenScene(Chapter3ScenePath, OpenSceneMode.Additive);
foreach (GameObject src in EnumerateSourcePanels(chapter3))
{
if (targetCanvas.transform.Find(src.name) != null)
continue;
GameObject copy = Object.Instantiate(src);
SceneManager.MoveGameObjectToScene(copy, active);
copy.transform.SetParent(targetCanvas.transform, false);
copy.name = src.name;
copied++;
}
}
finally
{
if (chapter3.IsValid())
EditorSceneManager.CloseScene(chapter3, true);
}
if (copied > 0)
EditorSceneManager.MarkSceneDirty(active);
return copied;
}
}
}