mirror of
https://github.com/Earth-Genesis-Games/Ajedrez_Purgatorio.git
synced 2026-09-11 09:47:35 +00:00
- 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
470 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|