diff --git a/Assets/Audio.meta b/Assets/Audio.meta
new file mode 100644
index 0000000..46411c7
--- /dev/null
+++ b/Assets/Audio.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f0fad760a5c49f94ea668f15bad0b624
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX.meta b/Assets/Audio/SFX.meta
new file mode 100644
index 0000000..c255fb7
--- /dev/null
+++ b/Assets/Audio/SFX.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6255e3e53840c534a9b742ff3b0e50cf
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/capture.wav b/Assets/Audio/SFX/capture.wav
new file mode 100644
index 0000000..d3f1a86
Binary files /dev/null and b/Assets/Audio/SFX/capture.wav differ
diff --git a/Assets/Audio/SFX/capture.wav.meta b/Assets/Audio/SFX/capture.wav.meta
new file mode 100644
index 0000000..ddd7f80
--- /dev/null
+++ b/Assets/Audio/SFX/capture.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: d1116462f5aa2ed4faa8c787e3daecda
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/check.wav b/Assets/Audio/SFX/check.wav
new file mode 100644
index 0000000..57b444d
Binary files /dev/null and b/Assets/Audio/SFX/check.wav differ
diff --git a/Assets/Audio/SFX/check.wav.meta b/Assets/Audio/SFX/check.wav.meta
new file mode 100644
index 0000000..0e70cd9
--- /dev/null
+++ b/Assets/Audio/SFX/check.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: e23a5aff8454f13418bf5946c74c1b95
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/checkmate.wav b/Assets/Audio/SFX/checkmate.wav
new file mode 100644
index 0000000..9c9969e
Binary files /dev/null and b/Assets/Audio/SFX/checkmate.wav differ
diff --git a/Assets/Audio/SFX/checkmate.wav.meta b/Assets/Audio/SFX/checkmate.wav.meta
new file mode 100644
index 0000000..dcc1948
--- /dev/null
+++ b/Assets/Audio/SFX/checkmate.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 82487001f2a9b4443a0d2a2ca1c05c41
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/invalid_move.wav b/Assets/Audio/SFX/invalid_move.wav
new file mode 100644
index 0000000..3bd9e75
Binary files /dev/null and b/Assets/Audio/SFX/invalid_move.wav differ
diff --git a/Assets/Audio/SFX/invalid_move.wav.meta b/Assets/Audio/SFX/invalid_move.wav.meta
new file mode 100644
index 0000000..fa5e51c
--- /dev/null
+++ b/Assets/Audio/SFX/invalid_move.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 52b4cb0754a08f844ab08975a263ef77
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/move.wav b/Assets/Audio/SFX/move.wav
new file mode 100644
index 0000000..e7773a5
Binary files /dev/null and b/Assets/Audio/SFX/move.wav differ
diff --git a/Assets/Audio/SFX/move.wav.meta b/Assets/Audio/SFX/move.wav.meta
new file mode 100644
index 0000000..b30c42b
--- /dev/null
+++ b/Assets/Audio/SFX/move.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 294db75531d2c664da0b7cd6bc38c9de
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/promotion.wav b/Assets/Audio/SFX/promotion.wav
new file mode 100644
index 0000000..db08ca8
Binary files /dev/null and b/Assets/Audio/SFX/promotion.wav differ
diff --git a/Assets/Audio/SFX/promotion.wav.meta b/Assets/Audio/SFX/promotion.wav.meta
new file mode 100644
index 0000000..e69b363
--- /dev/null
+++ b/Assets/Audio/SFX/promotion.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: a5ac5d35758bfc741b5602b80736c4e8
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Editor/AudioClipAssigner.cs b/Assets/Editor/AudioClipAssigner.cs
new file mode 100644
index 0000000..0cf2676
--- /dev/null
+++ b/Assets/Editor/AudioClipAssigner.cs
@@ -0,0 +1,87 @@
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using AjedrezPurgatorio.Audio;
+
+namespace AjedrezPurgatorio.EditorTools
+{
+ ///
+ /// Asigna los SFX placeholder (Assets/Audio/SFX/*.wav) a todos los
+ /// AudioManager de las escenas y guarda las escenas modificadas.
+ /// Ejecutar con:
+ /// Unity -batchmode -projectPath . -executeMethod
+ /// AjedrezPurgatorio.EditorTools.AudioClipAssigner.AssignPlaceholderClipsToAllScenes
+ ///
+ public static class AudioClipAssigner
+ {
+ private static readonly (string field, string file)[] Bindings =
+ {
+ ("_moveSFX", "move"),
+ ("_captureSFX", "capture"),
+ ("_checkSFX", "check"),
+ ("_checkmateSFX", "checkmate"),
+ ("_promotionSFX", "promotion"),
+ ("_invalidMoveSFX","invalid_move"),
+ };
+
+ private static readonly string[] SceneNames =
+ {
+ "MainMenu", "Chapter1", "Chapter2", "Chapter3",
+ };
+
+ public static void AssignPlaceholderClipsToAllScenes()
+ {
+ int assignedTotal = 0;
+
+ try
+ {
+ foreach (string sceneName in SceneNames)
+ {
+ Scene scene = EditorSceneManager.OpenScene(
+ $"Assets/Scenes/{sceneName}.unity", OpenSceneMode.Single);
+
+ foreach (AudioManager am in Object.FindObjectsByType(
+ FindObjectsInactive.Include, FindObjectsSortMode.None))
+ {
+ var so = new SerializedObject(am);
+ int assigned = 0;
+
+ foreach (var (field, file) in Bindings)
+ {
+ SerializedProperty prop = so.FindProperty(field);
+ var clip = AssetDatabase.LoadAssetAtPath(
+ $"Assets/Audio/SFX/{file}.wav");
+
+ if (prop == null || clip == null || prop.objectReferenceValue != null)
+ continue;
+
+ prop.objectReferenceValue = clip;
+ assigned++;
+ }
+
+ if (assigned > 0)
+ {
+ so.ApplyModifiedPropertiesWithoutUndo();
+ EditorSceneManager.MarkSceneDirty(scene);
+ Debug.Log($"[AudioClipAssigner] '{sceneName}': {assigned} clips asignados a AudioManager.");
+ }
+
+ assignedTotal += assigned;
+ }
+
+ EditorSceneManager.SaveScene(scene);
+ }
+
+ Debug.Log($"[AudioClipAssigner] COMPLETO: {assignedTotal} referencias asignadas.");
+ EditorApplication.Exit(0);
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[AudioClipAssigner] FALLÓ: {ex}");
+ EditorApplication.Exit(1);
+ }
+ }
+ }
+}
diff --git a/Assets/Editor/AudioClipAssigner.cs.meta b/Assets/Editor/AudioClipAssigner.cs.meta
new file mode 100644
index 0000000..3334f5a
--- /dev/null
+++ b/Assets/Editor/AudioClipAssigner.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8cfc3f0fcd46dc94c86a1fb4f5bca113
\ No newline at end of file
diff --git a/Assets/Editor/DiagAssign.cs b/Assets/Editor/DiagAssign.cs
new file mode 100644
index 0000000..c9be381
--- /dev/null
+++ b/Assets/Editor/DiagAssign.cs
@@ -0,0 +1,29 @@
+using System.Reflection;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+public static class DiagAssign2
+{
+ public static void Run()
+ {
+ var db = AssetDatabase.LoadAssetAtPath("Assets/Game/Data/Resources/SpeakerDatabase.asset");
+
+ var scene = EditorSceneManager.OpenScene("Assets/Scenes/Chapter1.unity", OpenSceneMode.Single);
+ var ui = Object.FindFirstObjectByType(FindObjectsInactive.Include);
+
+ var field = typeof(DialogueUI).GetField("_speakerDatabase", BindingFlags.NonPublic | BindingFlags.Instance);
+ field.SetValue(ui, db);
+ Debug.Log($"[Diag] tras reflexion={field.GetValue(ui) != null}");
+ EditorUtility.SetDirty(ui);
+
+ EditorSceneManager.MarkSceneDirty(scene);
+ bool ok = EditorSceneManager.SaveScene(scene);
+ Debug.Log($"[Diag] SaveScene ok={ok}");
+
+ var scene2 = EditorSceneManager.OpenScene("Assets/Scenes/Chapter1.unity", OpenSceneMode.Single);
+ var ui2 = Object.FindFirstObjectByType(FindObjectsInactive.Include);
+ Debug.Log($"[Diag] tras recargar={field.GetValue(ui2) != null}");
+ EditorApplication.Exit(0);
+ }
+}
diff --git a/Assets/Editor/DiagAssign.cs.meta b/Assets/Editor/DiagAssign.cs.meta
new file mode 100644
index 0000000..c29d5ee
--- /dev/null
+++ b/Assets/Editor/DiagAssign.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6b27ce28b927f4e4d91a89cd4ef5deda
\ No newline at end of file
diff --git a/Assets/Editor/PurgatoryCanvasMaintenance.cs b/Assets/Editor/PurgatoryCanvasMaintenance.cs
new file mode 100644
index 0000000..f0280f9
--- /dev/null
+++ b/Assets/Editor/PurgatoryCanvasMaintenance.cs
@@ -0,0 +1,469 @@
+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
+{
+ ///
+ /// 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.
+ ///
+ 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 protectedAncestorIds = CollectProtectedAncestorIds(scene);
+ List 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 toDelete = new List();
+ StringBuilder summary = new StringBuilder();
+ int keptTotal = 0;
+
+ foreach (DuplicateGroup group in groups.OrderByDescending(g => g.Depth))
+ {
+ List alive = group.Members.Where(t => t != null).ToList();
+ if (alive.Count < 2)
+ continue;
+
+ List 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 Members = new List();
+ }
+
+ ///
+ /// 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.
+ ///
+ private static HashSet CollectProtectedAncestorIds(Scene scene)
+ {
+ HashSet protectedIds = new HashSet();
+
+ foreach (GameObject root in scene.GetRootGameObjects())
+ {
+ foreach (Component comp in root.GetComponentsInChildren(true))
+ {
+ if (comp == null)
+ continue; // script faltante
+
+ TryCollectReferences(comp, protectedIds);
+ }
+ }
+
+ HashSet ancestors = new HashSet();
+ 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 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}");
+ }
+ }
+
+ ///
+ /// Agrupa RectTransform hermanos con nombre idéntico. Solo considera objetos
+ /// con padre (no raíces) para acotar el riesgo al UI generado por setup.
+ ///
+ private static List CollectDuplicateGroups(Scene scene)
+ {
+ Dictionary byKey = new Dictionary();
+
+ foreach (GameObject root in scene.GetRootGameObjects())
+ {
+ foreach (RectTransform rt in root.GetComponentsInChildren(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 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();
+ 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 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)
+ // ------------------------------------------------------------------
+
+ ///
+ /// 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
+ ///
+ 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 protectedAncestorIds = CollectProtectedAncestorIds(scene);
+ List groups = CollectDuplicateGroups(scene);
+
+ int removed = 0;
+ foreach (DuplicateGroup group in groups.OrderByDescending(g => g.Depth))
+ {
+ List alive = group.Members.Where(t => t != null).ToList();
+ if (alive.Count < 2)
+ continue;
+
+ List 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;
+ }
+ }
+}
diff --git a/Assets/Editor/PurgatoryCanvasMaintenance.cs.meta b/Assets/Editor/PurgatoryCanvasMaintenance.cs.meta
new file mode 100644
index 0000000..77c1ffa
--- /dev/null
+++ b/Assets/Editor/PurgatoryCanvasMaintenance.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c50f0212759220b4990b34b8fe3c7ff9
\ No newline at end of file
diff --git a/Assets/Editor/SpeakerDatabaseSetup.cs b/Assets/Editor/SpeakerDatabaseSetup.cs
new file mode 100644
index 0000000..27f627e
--- /dev/null
+++ b/Assets/Editor/SpeakerDatabaseSetup.cs
@@ -0,0 +1,166 @@
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace AjedrezPurgatorio.EditorTools
+{
+ ///
+ /// Puebla el SpeakerDatabase.asset con los retratos existentes y asigna el
+ /// asset a todos los DialogueUI de las escenas. Guarda los cambios.
+ /// Ejecutar con:
+ /// Unity -batchmode -projectPath . -executeMethod
+ /// AjedrezPurgatorio.EditorTools.SpeakerDatabaseSetup.PopulateAndAssign
+ ///
+ /// Nota: 'muerte' y 'ricardo' no tienen PNG de retrato todavía; se registran
+ /// con portrait nulo para evitar warnings y facilitar añadir arte después.
+ ///
+ public static class SpeakerDatabaseSetup
+ {
+ private const string DatabasePath = "Assets/Game/Data/Resources/SpeakerDatabase.asset";
+ private const string PortraitsFolder = "Assets/Game/Data/Resources/Portraits";
+
+ private static readonly (string speaker, string portrait)[] Entries =
+ {
+ ("narrator", "narrator"),
+ ("muerte", null),
+ ("ricardo", null),
+ ("alfil", "alfil"),
+ ("caballo", "caballo"),
+ ("peon", "peon"),
+ ("reina", "reina"),
+ ("torre", "torre"),
+ ("espejo", "espejo"),
+ ("coleccionista", "coleccionista"),
+ ("enemigo", "enemigo"),
+ ("voz_nino", "voz_nino"),
+ };
+
+ private static readonly string[] SceneNames =
+ {
+ "MainMenu", "Chapter1", "Chapter2", "Chapter3",
+ };
+
+ public static void PopulateAndAssign()
+ {
+ try
+ {
+ int populated = PopulateDatabase();
+ int assigned = AssignToScenes();
+
+ Debug.Log($"[SpeakerDatabaseSetup] COMPLETO: {populated} speakers poblados, {assigned} DialogueUI actualizados.");
+ EditorApplication.Exit(0);
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[SpeakerDatabaseSetup] FALLÓ: {ex}");
+ EditorApplication.Exit(1);
+ }
+ }
+
+ private static int PopulateDatabase()
+ {
+ var db = AssetDatabase.LoadAssetAtPath(DatabasePath);
+ if (db == null)
+ throw new System.IO.FileNotFoundException($"No se encontró {DatabasePath}");
+
+ // Los PNG de Portraits pueden no estar importados en la AssetDatabase
+ // (p.ej. Library regenerada). Importar explícitamente sin Refresh global.
+ int imported = 0;
+ foreach (var (_, portraitFile) in Entries)
+ {
+ if (string.IsNullOrEmpty(portraitFile))
+ continue;
+
+ string path = $"{PortraitsFolder}/{portraitFile}.png";
+ if (!System.IO.File.Exists(path))
+ {
+ Debug.LogError($"[SpeakerDatabaseSetup] No existe en disco: {path}");
+ continue;
+ }
+
+ try
+ {
+ AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
+ imported++;
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[SpeakerDatabaseSetup] Fallo importando {path}: {ex.Message}");
+ }
+ }
+ Debug.Log($"[SpeakerDatabaseSetup] {imported} retratos re-importados.");
+
+ var so = new SerializedObject(db);
+ SerializedProperty list = so.FindProperty("_speakers");
+ if (list == null)
+ throw new System.MissingMemberException("_speakers no encontrado en SpeakerDatabase.");
+
+ list.ClearArray();
+
+ int count = 0;
+ foreach (var (speaker, portraitFile) in Entries)
+ {
+ list.InsertArrayElementAtIndex(list.arraySize);
+ SerializedProperty element = list.GetArrayElementAtIndex(list.arraySize - 1);
+
+ element.FindPropertyRelative("speakerName").stringValue = speaker;
+
+ Sprite sprite = null;
+ if (!string.IsNullOrEmpty(portraitFile))
+ sprite = AssetDatabase.LoadAssetAtPath($"{PortraitsFolder}/{portraitFile}.png");
+
+ if (!string.IsNullOrEmpty(portraitFile) && sprite == null)
+ Debug.LogWarning($"[SpeakerDatabaseSetup] Portrait no encontrado para '{speaker}': {portraitFile}.png");
+
+ element.FindPropertyRelative("portrait").objectReferenceValue = sprite;
+ count++;
+ }
+
+ so.ApplyModifiedPropertiesWithoutUndo();
+ EditorUtility.SetDirty(db);
+ AssetDatabase.SaveAssets();
+ return count;
+ }
+
+ private static int AssignToScenes()
+ {
+ var db = AssetDatabase.LoadAssetAtPath(DatabasePath);
+ int assignedTotal = 0;
+
+ // Nota: SerializedObject.ApplyModifiedProperties* no persiste este
+ // cambio en este proyecto (probado: el valor se pierde incluso en
+ // memoria tras Apply). Reflexión directa + SetDirty sí funciona.
+ var field = typeof(DialogueUI).GetField("_speakerDatabase",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ foreach (string sceneName in SceneNames)
+ {
+ Scene scene = EditorSceneManager.OpenScene(
+ $"Assets/Scenes/{sceneName}.unity", OpenSceneMode.Single);
+
+ bool dirty = false;
+
+ foreach (DialogueUI ui in Object.FindObjectsByType(
+ FindObjectsInactive.Include, FindObjectsSortMode.None))
+ {
+ if (ui == null || field.GetValue(ui) as SpeakerDatabase != null)
+ continue;
+
+ field.SetValue(ui, db);
+ EditorUtility.SetDirty(ui);
+ assignedTotal++;
+ dirty = true;
+ }
+
+ if (dirty)
+ EditorSceneManager.MarkSceneDirty(scene);
+
+ EditorSceneManager.SaveScene(scene);
+ }
+
+ return assignedTotal;
+ }
+ }
+}
diff --git a/Assets/Editor/SpeakerDatabaseSetup.cs.meta b/Assets/Editor/SpeakerDatabaseSetup.cs.meta
new file mode 100644
index 0000000..81d97c1
--- /dev/null
+++ b/Assets/Editor/SpeakerDatabaseSetup.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bdc614fe81279584a9cc6267514ff99c
\ No newline at end of file
diff --git a/Assets/Game/Data/Resources/Portraits/alfil.png.meta b/Assets/Game/Data/Resources/Portraits/alfil.png.meta
index 54cdb9b..426e60c 100644
--- a/Assets/Game/Data/Resources/Portraits/alfil.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/alfil.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: 337871385a0b47dca76233089ebc113a
+guid: 4c1965114e4cdad458cc690285ff2074
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 1676898138316344188
+ second: alfil_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: alfil_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: c7b0cd2e8f9854710800000000000000
+ internalID: 1676898138316344188
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ alfil_0: 1676898138316344188
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/caballo.png.meta b/Assets/Game/Data/Resources/Portraits/caballo.png.meta
index c899b10..a40f6f6 100644
--- a/Assets/Game/Data/Resources/Portraits/caballo.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/caballo.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: e993fcc745b44d74bdff78ca54ecae2f
+guid: a579bb1b0b7e3d642a72c423e82aa57e
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: -8012410325477036581
+ second: caballo_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: caballo_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: bdde443b2433ec090800000000000000
+ internalID: -8012410325477036581
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ caballo_0: -8012410325477036581
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/coleccionista.png.meta b/Assets/Game/Data/Resources/Portraits/coleccionista.png.meta
index 79d436f..37d1493 100644
--- a/Assets/Game/Data/Resources/Portraits/coleccionista.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/coleccionista.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: 0aa3b637028c46aea491d5f19c5bf328
+guid: bd525630b7cffb8478281a6102df7005
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 434492148875499951
+ second: coleccionista_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: coleccionista_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: fa99b6fae40a70600800000000000000
+ internalID: 434492148875499951
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ coleccionista_0: 434492148875499951
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/enemigo.png.meta b/Assets/Game/Data/Resources/Portraits/enemigo.png.meta
index 0aebfc2..b5cf643 100644
--- a/Assets/Game/Data/Resources/Portraits/enemigo.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/enemigo.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: 66d4d365262a43c48947a40f1ad7dc14
+guid: bbbab8e1671274e4ba56866bfc33c2d6
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: -1092110371768519499
+ second: enemigo_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: enemigo_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5b4b733376b08d0f0800000000000000
+ internalID: -1092110371768519499
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ enemigo_0: -1092110371768519499
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/espejo.png.meta b/Assets/Game/Data/Resources/Portraits/espejo.png.meta
index bbbd2c4..52568eb 100644
--- a/Assets/Game/Data/Resources/Portraits/espejo.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/espejo.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: f7197410ae7f45c4b45da28c2c8b4905
+guid: 1ca7029a0492e9c40b6c8a99db8f0f1c
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: -6550897566918066470
+ second: espejo_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: espejo_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: adeb0ebfe598615a0800000000000000
+ internalID: -6550897566918066470
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ espejo_0: -6550897566918066470
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/narrator.png.meta b/Assets/Game/Data/Resources/Portraits/narrator.png.meta
index 06c8f51..8e7f174 100644
--- a/Assets/Game/Data/Resources/Portraits/narrator.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/narrator.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: 148fa96098ae472f9bbf4cde9fab00d6
+guid: 737c4034b70db6d47978d8d288f51f59
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 5364273658523971450
+ second: narrator_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: narrator_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: a7f6e46a87ab17a40800000000000000
+ internalID: 5364273658523971450
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ narrator_0: 5364273658523971450
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/peon.png.meta b/Assets/Game/Data/Resources/Portraits/peon.png.meta
index 7f05aae..eff3334 100644
--- a/Assets/Game/Data/Resources/Portraits/peon.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/peon.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: 77143a27a96a42c281bb2d9d26385e71
+guid: e1b4509d5eb75d54a8706bd0e7c1682f
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 7304130777000316518
+ second: peon_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: peon_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 66e0a142e3c7d5560800000000000000
+ internalID: 7304130777000316518
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ peon_0: 7304130777000316518
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/reina.png.meta b/Assets/Game/Data/Resources/Portraits/reina.png.meta
index 1083a2d..2348133 100644
--- a/Assets/Game/Data/Resources/Portraits/reina.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/reina.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: d628de6a3bbf438ea574cef6994edc02
+guid: efef67743c203fa418f587d7794ad737
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 9131763069496480379
+ second: reina_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: reina_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: b725e5a212a8abe70800000000000000
+ internalID: 9131763069496480379
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ reina_0: 9131763069496480379
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/torre.png.meta b/Assets/Game/Data/Resources/Portraits/torre.png.meta
index 3889f54..7269c09 100644
--- a/Assets/Game/Data/Resources/Portraits/torre.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/torre.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: e26e45f73d204da189b4248ba0a84d7c
+guid: c6c9116fbc38e4643ab2beb0d17759cc
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: -5933413741351382349
+ second: torre_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: torre_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 3b2545983a748ada0800000000000000
+ internalID: -5933413741351382349
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ torre_0: -5933413741351382349
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/Portraits/voz_nino.png.meta b/Assets/Game/Data/Resources/Portraits/voz_nino.png.meta
index 21f1efa..4341f51 100644
--- a/Assets/Game/Data/Resources/Portraits/voz_nino.png.meta
+++ b/Assets/Game/Data/Resources/Portraits/voz_nino.png.meta
@@ -1,14 +1,17 @@
fileFormatVersion: 2
-guid: f2b9a076c55b47c992bc2c7cbefa9532
+guid: 4358198be608bd747bce86a521d1d173
TextureImporter:
- internalIDToNameTable: []
+ internalIDToNameTable:
+ - first:
+ 213: 1191133803369584460
+ second: voz_nino_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
+ sRGBTexture: 1
linearTexture: 0
- correctGamma: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
@@ -25,7 +28,7 @@ TextureImporter:
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
- ignoreMasterTextureLimit: 0
+ ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
@@ -43,7 +46,7 @@ TextureImporter:
nPOTScale: 0
lightmap: 0
compressionQuality: 50
- spriteMode: 1
+ spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -63,11 +66,11 @@ TextureImporter:
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
- applyGammaDecay: 0
+ applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- - serializedVersion: 3
+ - serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@@ -80,22 +83,74 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
- sprites: []
+ sprites:
+ - serializedVersion: 2
+ name: voz_nino_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 256
+ height: 256
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: c47ddcd12e1c78010800000000000000
+ internalID: 1191133803369584460
+ vertices: []
+ indices:
+ edges: []
+ weights: []
outline: []
+ customData:
physicsShape: []
bones: []
- spriteID:
+ spriteID:
internalID: 0
vertices: []
- indices:
+ indices:
edges: []
weights: []
secondaryTextures: []
- nameFileIdTable: {}
- spritePackingTag:
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ voz_nino_0: 1191133803369584460
+ mipmapLimitGroupName:
pSDRemoveMatte: 0
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Data/Resources/SpeakerDatabase.asset b/Assets/Game/Data/Resources/SpeakerDatabase.asset
index 03d1374..8f8de32 100644
--- a/Assets/Game/Data/Resources/SpeakerDatabase.asset
+++ b/Assets/Game/Data/Resources/SpeakerDatabase.asset
@@ -11,5 +11,29 @@ MonoBehaviour:
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 281f13885be2fde46bc468d72ea1291a, type: 3}
m_Name: SpeakerDatabase
- m_EditorClassIdentifier:
- _speakers: []
+ m_EditorClassIdentifier:
+ _speakers:
+ - speakerName: narrator
+ portrait: {fileID: 5364273658523971450, guid: 737c4034b70db6d47978d8d288f51f59, type: 3}
+ - speakerName: muerte
+ portrait: {fileID: 0}
+ - speakerName: ricardo
+ portrait: {fileID: 0}
+ - speakerName: alfil
+ portrait: {fileID: 1676898138316344188, guid: 4c1965114e4cdad458cc690285ff2074, type: 3}
+ - speakerName: caballo
+ portrait: {fileID: -8012410325477036581, guid: a579bb1b0b7e3d642a72c423e82aa57e, type: 3}
+ - speakerName: peon
+ portrait: {fileID: 7304130777000316518, guid: e1b4509d5eb75d54a8706bd0e7c1682f, type: 3}
+ - speakerName: reina
+ portrait: {fileID: 9131763069496480379, guid: efef67743c203fa418f587d7794ad737, type: 3}
+ - speakerName: torre
+ portrait: {fileID: -5933413741351382349, guid: c6c9116fbc38e4643ab2beb0d17759cc, type: 3}
+ - speakerName: espejo
+ portrait: {fileID: -6550897566918066470, guid: 1ca7029a0492e9c40b6c8a99db8f0f1c, type: 3}
+ - speakerName: coleccionista
+ portrait: {fileID: 434492148875499951, guid: bd525630b7cffb8478281a6102df7005, type: 3}
+ - speakerName: enemigo
+ portrait: {fileID: -1092110371768519499, guid: bbbab8e1671274e4ba56866bfc33c2d6, type: 3}
+ - speakerName: voz_nino
+ portrait: {fileID: 1191133803369584460, guid: 4358198be608bd747bce86a521d1d173, type: 3}
diff --git a/Assets/Game/Scripts/Data/CampaignState.cs b/Assets/Game/Scripts/Data/CampaignState.cs
index fbc7a7f..d0c63c9 100644
--- a/Assets/Game/Scripts/Data/CampaignState.cs
+++ b/Assets/Game/Scripts/Data/CampaignState.cs
@@ -229,6 +229,29 @@ public class CampaignState : ScriptableObject
return new List(_alivePieces);
}
+ ///
+ /// Obtiene las PieceIdentity muertas (no presentes en _alivePieces).
+ /// Usado por el Duelo del Purgatorio para elegir al candidato de rescate.
+ ///
+ public List GetDeadIdentities()
+ {
+ var result = new List();
+
+ if (_allIdentities == null || _allIdentities.Count == 0)
+ return result;
+
+ foreach (var identity in _allIdentities)
+ {
+ if (identity != null && !string.IsNullOrEmpty(identity.characterName)
+ && !_alivePieces.Contains(identity.characterName.ToLower()))
+ {
+ result.Add(identity);
+ }
+ }
+
+ return result;
+ }
+
///
/// Obtiene la lista de PieceIdentity disponibles para el próximo tablero.
/// Compatibilidad con BoardManager y PieceIdentityManager.
diff --git a/Assets/Game/Scripts/Mono/Core/CampaignManager.cs b/Assets/Game/Scripts/Mono/Core/CampaignManager.cs
index 808d544..2c0a5d7 100644
--- a/Assets/Game/Scripts/Mono/Core/CampaignManager.cs
+++ b/Assets/Game/Scripts/Mono/Core/CampaignManager.cs
@@ -44,6 +44,12 @@ public class CampaignManager : MonoBehaviour
// Propiedades
public ChapterData CurrentChapter { get; private set; }
public int CurrentChapterIndex => _currentChapterIndex;
+
+ ///
+ /// Estado de campaña activo (ScriptableObject). Expuesto para sistemas runtime
+ /// como el Duelo del Purgatorio que necesitan registrar piezas muertas/recuperadas.
+ ///
+ public CampaignState State => _campaignState;
public bool IsCampaignActive => _campaignActive;
public int TotalChapters => _activeConfig?.ChapterCount ?? 0;
public DeadKingData CurrentDeadKing => _currentDeadKing;
@@ -436,13 +442,61 @@ public class CampaignManager : MonoBehaviour
private void OnGameCheckmate(bool whiteWins)
{
if (!_campaignActive) return;
-
+
bool playerWins = whiteWins; // Player controls white
+ if (playerWins && PurgatoryDuelManager.TryStartFromCampaign(this))
+ {
+ return; // El duelo del Purgatorio decide el desenlace
+ }
+
if (playerWins)
OnChapterVictory();
else
OnChapterDefeat();
}
+
+ ///
+ /// El jugador capturó a La Muerte en el duelo: continuar como victoria de capítulo.
+ ///
+ public void ResumeAfterDuelVictory()
+ {
+ OnChapterVictory();
+ }
+
+ ///
+ /// El rescate por dados tuvo éxito: el capítulo continúa con el tablero restaurado.
+ ///
+ public void ResumeAfterDuelRescue()
+ {
+ Debug.Log("[CampaignManager] Rescate del duelo exitoso. El capítulo continúa.");
+ TransitionToPlaying();
+ }
+
+ ///
+ /// Detiene los sistemas de capítulo (niebla, malditas, reloj, jefes, triggers)
+ /// mientras el Duelo del Purgatorio está activo.
+ ///
+ public void SuspendChapterSystems()
+ {
+ EndChapterSystems();
+ }
+
+ ///
+ /// Derrota final en el duelo: reiniciar la campaña desde el primer tablero.
+ ///
+ public void RestartAtFirstChapter()
+ {
+ Debug.Log("[CampaignManager] El Purgatorio reclama la partida. Reinicio al Capítulo 1.");
+
+ if (DialogueSystem.Instance != null)
+ DialogueSystem.Instance.ForceEndDialogue();
+
+ if (GameStateManager.Instance != null)
+ GameStateManager.Instance.TransitionTo(GameState.Menu);
+
+ _campaignActive = true;
+ LoadChapterScene(0);
+ }
private void OnGameDraw(string reason)
{
diff --git a/Assets/Game/Scripts/Mono/Core/GameStateManager.cs b/Assets/Game/Scripts/Mono/Core/GameStateManager.cs
index 2be76e8..30e8e6a 100644
--- a/Assets/Game/Scripts/Mono/Core/GameStateManager.cs
+++ b/Assets/Game/Scripts/Mono/Core/GameStateManager.cs
@@ -58,9 +58,10 @@ public class GameStateManager : MonoBehaviour
public enum GameState
{
- Menu, // Menú principal
- Playing, // Partida de ajedrez en curso
- Dialogue, // Diálogo (intro/outro de capítulo) — overlay en la escena del capítulo
- Purgatory, // Mini-juego de dados — overlay en la escena del capítulo
- GameOver // Dead King input / resultado — overlay en la escena del capítulo
+ Menu, // Menú principal
+ Playing, // Partida de ajedrez en curso
+ Dialogue, // Diálogo (intro/outro de capítulo) — overlay en la escena del capítulo
+ Purgatory, // Mini-juego de dados — overlay en la escena del capítulo
+ PurgatoryDuel, // Duelo final contra La Muerte tras dar jaque mate
+ GameOver // Dead King input / resultado — overlay en la escena del capítulo
}
diff --git a/Assets/Game/Scripts/Mono/Meta/DeadKingPool.cs b/Assets/Game/Scripts/Mono/Meta/DeadKingPool.cs
index a9e84ad..4fbdac7 100644
--- a/Assets/Game/Scripts/Mono/Meta/DeadKingPool.cs
+++ b/Assets/Game/Scripts/Mono/Meta/DeadKingPool.cs
@@ -99,7 +99,11 @@ namespace AjedrezPurgatorio.Meta
if (_isInitialized)
return;
- _persistentFilePath = Path.Combine(Application.persistentDataPath, "dead_kings.json");
+ // Preservar una ruta ya asignada (inyección para tests); solo
+ // asignar la persistente por defecto si el campo está vacío.
+ if (string.IsNullOrEmpty(_persistentFilePath))
+ _persistentFilePath = Path.Combine(Application.persistentDataPath, "dead_kings.json");
+
LoadPool();
_isInitialized = true;
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel.meta b/Assets/Game/Scripts/Mono/Purgatory/Duel.meta
new file mode 100644
index 0000000..9b7855d
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ecc57bd50bd471047882bb2df0ed6097
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs b/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs
new file mode 100644
index 0000000..500cc08
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+///
+/// Frases de La Muerte durante el Duelo del Purgatorio.
+/// Voz solemne según purgatorio-cosmology.md ("la Muerte", máscara formal).
+///
+public static class MuerteQuotes
+{
+ private static readonly string[] Intro =
+ {
+ "Mate perfecto... para el tablero. Aquí abajo, la partida sigue.",
+ "Llegaste al final del tablero. Bienvenido al principio del mío.",
+ "Todos los caminos del ajedrez terminan frente a mí. Tú llegaste caminando.",
+ "Tu reina no me mateó a mí. Solo despejó la mesa."
+ };
+
+ private static readonly string[] Milestone =
+ {
+ "El Purgatorio tiene hambre. Cada turno, una casilla menos.",
+ "El suelo que pisas ya fue. Apúrate.",
+ "No te apresuro. El vacío lo hace por mí.",
+ "Cada casilla que cae fue una partida más. Como la tuya."
+ };
+
+ private static readonly string[] PlayerWin =
+ {
+ "...Bien jugado. El tablero es tuyo. Por ahora.",
+ "Me capturas hoy. Mañana seré tu siguiente jaque.",
+ "Toma tu victoria. Yo me quedo con el tiempo.",
+ "Nadie me había alcanzado sin perder algo primero. Vuelve pronto."
+ };
+
+ private static readonly string[] PlayerLost =
+ {
+ "Te dije que nadie gana aquí. Solo algunos posponen.",
+ "Tu pieza descansa. ¿Quieres negociar por ella? Los dados no mienten.",
+ "El vacío también juega. Y siempre va conmigo.",
+ "Otra partida termina. Otra alma aprende."
+ };
+
+ public static string GetIntro() => Pick(Intro);
+ public static string GetMilestone() => Pick(Milestone);
+ public static string GetPlayerWin() => Pick(PlayerWin);
+ public static string GetPlayerLost() => Pick(PlayerLost);
+
+ private static string Pick(string[] pool)
+ {
+ if (pool == null || pool.Length == 0)
+ return string.Empty;
+ return pool[Random.Range(0, pool.Length)];
+ }
+}
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs.meta b/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs.meta
new file mode 100644
index 0000000..e12f986
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/MuerteQuotes.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c4d50f6d2066ae74fa6450df51918194
\ No newline at end of file
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs
new file mode 100644
index 0000000..b14a14d
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs
@@ -0,0 +1,613 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using AjedrezPurgatorio.Audio;
+using UnityEngine;
+
+///
+/// Orquesta el Duelo del Purgatorio: tras el jaque mate del jugador, su pieza
+/// queda sola contra La Muerte mientras el tablero se devora a sí mismo.
+/// Todo el flujo es runtime (sin edición de escenas) según purgatorio-final.md.
+///
+public class PurgatoryDuelManager : MonoBehaviour
+{
+ public static PurgatoryDuelManager Instance { get; private set; }
+
+ private enum Phase
+ {
+ Idle,
+ PlayerTurn,
+ Resolving,
+ Ended
+ }
+
+ private Phase _phase = Phase.Idle;
+ private int _turn;
+ private bool _rescueUsed;
+
+ private Piece _playerPiece;
+ private Piece _boss;
+ private readonly HashSet _devoured = new HashSet();
+ private readonly List<(Piece piece, Vector2Int pos)> _hiddenPieces = new List<(Piece, Vector2Int)>();
+ private readonly Dictionary _originalTileColors = new Dictionary();
+ private readonly System.Random _rng = new System.Random();
+
+ private CampaignManager _campaign;
+ private PurgatoryDuelUI _ui;
+
+ public bool IsActive => _phase != Phase.Idle && _phase != Phase.Ended;
+
+ // ══════════════════════════════════════════════════════
+ // INICIO
+ // ══════════════════════════════════════════════════════
+
+ ///
+ /// Punto de entrada llamado por CampaignManager al recibir OnCheckmate(playerWins).
+ /// Retorna false si el duelo no puede iniciarse y la campaña debe resolver victoria normal.
+ ///
+ public static bool TryStartFromCampaign(CampaignManager campaign)
+ {
+ if (Instance != null && Instance.IsActive)
+ return false;
+
+ if (campaign == null || GameManager.Instance == null || BoardManager.Instance == null)
+ return false;
+
+ Piece[,] board = GameManager.Instance.board;
+ if (board == null)
+ return false;
+
+ Piece matingPiece = FindMatingPiece(board);
+ if (matingPiece == null)
+ return false;
+
+ GameObject go = new GameObject(typeof(PurgatoryDuelManager).Name);
+ go.transform.SetParent(campaign.transform, worldPositionStays: false);
+ Instance = go.AddComponent();
+ Instance.Begin(campaign, matingPiece);
+ return true;
+ }
+
+ ///
+ /// La pieza de mate es la pieza blanca de mayor valor que ataca al rey negro.
+ ///
+ private static Piece FindMatingPiece(Piece[,] board)
+ {
+ Vector2Int blackKingPos = default;
+ bool kingFound = false;
+ foreach (Piece piece in board)
+ {
+ if (piece is King && !piece.isWhite)
+ {
+ blackKingPos = piece.currentPos;
+ kingFound = true;
+ break;
+ }
+ }
+
+ if (!kingFound)
+ return null;
+
+ Piece best = null;
+ foreach (Piece piece in board)
+ {
+ if (piece == null || !piece.isWhite)
+ continue;
+
+ if (!piece.GetAttackSquares(board).Contains(blackKingPos))
+ continue;
+
+ if (best == null || piece.pieceValue > best.pieceValue)
+ best = piece;
+ }
+
+ return best;
+ }
+
+ private void Begin(CampaignManager campaign, Piece matingPiece)
+ {
+ _campaign = campaign;
+ _playerPiece = matingPiece;
+ _turn = 0;
+ _rescueUsed = false;
+ _devoured.Clear();
+ _hiddenPieces.Clear();
+ _originalTileColors.Clear();
+ _phase = Phase.PlayerTurn;
+
+ if (GameStateManager.Instance != null)
+ GameStateManager.Instance.TransitionTo(GameState.PurgatoryDuel);
+
+ var ai = FindObjectOfType();
+ if (ai != null)
+ ai.SetEnabled(false);
+
+ if (_campaign != null)
+ _campaign.SuspendChapterSystems();
+
+ BuildArena();
+ BuildUI();
+
+ Debug.Log($"[PurgatoryDuel] Iniciado. Pieza del jugador: {_playerPiece.GetType().Name} en {_playerPiece.currentPos}.");
+ }
+
+ private void BuildArena()
+ {
+ Piece[,] board = GameManager.Instance.board;
+
+ for (int x = 0; x < 8; x++)
+ {
+ for (int y = 0; y < 8; y++)
+ {
+ Piece piece = board[x, y];
+ if (piece == null || piece == _playerPiece)
+ continue;
+
+ _hiddenPieces.Add((piece, new Vector2Int(x, y)));
+ board[x, y] = null;
+ piece.gameObject.SetActive(false);
+ }
+ }
+
+ _boss = SpawnBoss(board);
+ }
+
+ private Piece SpawnBoss(Piece[,] board)
+ {
+ GameObject prefab = BoardManager.Instance.blackKingPrefab != null
+ ? BoardManager.Instance.blackKingPrefab
+ : BoardManager.Instance.kingPrefab;
+
+ if (prefab == null)
+ {
+ Debug.LogError("[PurgatoryDuel] Sin prefab de Rey para La Muerte.");
+ return null;
+ }
+
+ float tileSize = BoardManager.Instance.tileSize;
+ Vector2Int spawnPos = PickBossSpawn(board);
+ GameObject bossObj = Instantiate(prefab, new Vector3(spawnPos.x * tileSize, spawnPos.y * tileSize, -1f), Quaternion.identity);
+ bossObj.name = "LaMuerte";
+
+ Piece boss = bossObj.GetComponent();
+ boss.isWhite = false;
+ boss.currentPos = spawnPos;
+ boss.hasMoved = true;
+ board[spawnPos.x, spawnPos.y] = boss;
+
+ foreach (var spriteRenderer in bossObj.GetComponentsInChildren(true))
+ spriteRenderer.color = new Color(0.55f, 0.07f, 0.07f);
+
+ return boss;
+ }
+
+ private static Vector2Int PickBossSpawn(Piece[,] board)
+ {
+ foreach (Vector2Int candidate in new[]
+ {
+ new Vector2Int(4, 4),
+ new Vector2Int(3, 3),
+ new Vector2Int(4, 3),
+ new Vector2Int(3, 4),
+ new Vector2Int(0, 7),
+ new Vector2Int(7, 7)
+ })
+ {
+ if (board[candidate.x, candidate.y] == null)
+ return candidate;
+ }
+
+ return new Vector2Int(0, 7);
+ }
+
+ private void BuildUI()
+ {
+ _ui = gameObject.AddComponent();
+ _ui.Build();
+ _ui.ShowQuote(MuerteQuotes.GetIntro());
+ _ui.SetTurn(1);
+ }
+
+ // ══════════════════════════════════════════════════════
+ // INPUT DEL JUGADOR
+ // ══════════════════════════════════════════════════════
+
+ public void HandleTileClick(Vector2Int position)
+ {
+ if (_phase != Phase.PlayerTurn || _playerPiece == null)
+ return;
+
+ if (position == _playerPiece.currentPos)
+ {
+ ToggleHighlights();
+ return;
+ }
+
+ if (!GetLegalMoves().Contains(position))
+ return;
+
+ StartCoroutine(ExecutePlayerMove(position));
+ }
+
+ private void ToggleHighlights()
+ {
+ BoardManager.Instance.HideAllIndicators();
+ if (_highlightVisible)
+ {
+ _highlightVisible = false;
+ return;
+ }
+
+ foreach (Vector2Int move in GetLegalMoves())
+ BoardManager.Instance.ShowIndicator(move);
+
+ _highlightVisible = true;
+ }
+
+ private bool _highlightVisible;
+
+ ///
+ /// Movimientos legales de la pieza del jugador: reglas de ajedrez de la pieza,
+ /// sin casillas devoradas y solo hacia casillas vacías o con La Muerte.
+ /// No usa MoveValidator porque no hay rey blanco que dejar en jaque.
+ ///
+ private List GetLegalMoves()
+ {
+ Piece[,] board = GameManager.Instance.board;
+ var moves = new List();
+
+ foreach (Vector2Int move in _playerPiece.GetAvailableMoves(board))
+ {
+ if (_devoured.Contains(move))
+ continue;
+
+ Piece target = board[move.x, move.y];
+ if (target != null && target.isWhite)
+ continue;
+
+ moves.Add(move);
+ }
+
+ return moves;
+ }
+
+ private IEnumerator ExecutePlayerMove(Vector2Int targetPos)
+ {
+ _phase = Phase.Resolving;
+ BoardManager.Instance.HideAllIndicators();
+ _highlightVisible = false;
+
+ if (_boss != null && targetPos == _boss.currentPos)
+ {
+ CaptureBoss(targetPos);
+ yield break;
+ }
+
+ MovePieceOnBoard(_playerPiece, targetPos);
+
+ yield return ResolveTurn();
+ }
+
+ private void MovePieceOnBoard(Piece piece, Vector2Int newPos)
+ {
+ float tileSize = BoardManager.Instance.tileSize;
+ Vector2Int oldPos = piece.currentPos;
+
+ GameManager.Instance.board[oldPos.x, oldPos.y] = null;
+ GameManager.Instance.board[newPos.x, newPos.y] = piece;
+
+ piece.transform.position = new Vector3(newPos.x * tileSize, newPos.y * tileSize, -1f);
+ piece.currentPos = newPos;
+ piece.hasMoved = true;
+ }
+
+ private void CaptureBoss(Vector2Int bossPos)
+ {
+ Debug.Log("[PurgatoryDuel] ¡La Muerte capturada!");
+ _phase = Phase.Ended;
+
+ GameManager.Instance.board[bossPos.x, bossPos.y] = null;
+ Destroy(_boss.gameObject);
+ _boss = null;
+
+ MovePieceOnBoard(_playerPiece, bossPos);
+
+ if (AudioManager.Instance != null)
+ AudioManager.Instance.PlayCapture();
+
+ _ui.ShowQuote(MuerteQuotes.GetPlayerWin());
+ _ui.ShowVictory("EL TABLERO ES TUYO");
+
+ CleanupDuel(restorePieces: true);
+
+ StartCoroutine(FinishAfterDelay(() =>
+ {
+ if (_campaign != null)
+ _campaign.ResumeAfterDuelVictory();
+ TearDown();
+ }));
+ }
+
+ // ══════════════════════════════════════════════════════
+ // RESOLUCIÓN DE TURNO: COLAPSO + JEFE
+ // ══════════════════════════════════════════════════════
+
+ private IEnumerator ResolveTurn()
+ {
+ _turn++;
+ _ui.SetTurn(Mathf.Max(1, _turn));
+ _ui.ShowQuote(MuerteQuotes.GetMilestone());
+
+ yield return CollapseStep();
+
+ if (_phase == Phase.Ended)
+ yield break;
+
+ yield return BossStep();
+
+ if (_phase == Phase.Ended)
+ yield break;
+
+ if (GetLegalMoves().Count == 0)
+ {
+ Debug.Log("[PurgatoryDuel] Sin movimientos legales: turno pasado.");
+ StartCoroutine(ResolveTurn());
+ yield break;
+ }
+
+ _phase = Phase.PlayerTurn;
+ }
+
+ private IEnumerator CollapseStep()
+ {
+ int tiles = PurgatoryDuelRules.TilesForTurn(_turn);
+ if (tiles <= 0)
+ yield break;
+
+ var excluded = new HashSet(_devoured);
+ if (_boss != null)
+ excluded.Add(_boss.currentPos);
+
+ List victims = PurgatoryDuelRules.PickTilesToDevour(tiles, excluded, _rng);
+ foreach (Vector2Int tile in victims)
+ {
+ DevourTile(tile);
+
+ if (_playerPiece != null && tile == _playerPiece.currentPos)
+ {
+ Defeat("El vacío te tragó");
+ yield break;
+ }
+
+ yield return new WaitForSeconds(0.15f);
+ }
+ }
+
+ private void DevourTile(Vector2Int tile)
+ {
+ _devoured.Add(tile);
+
+ GameObject square = BoardManager.Instance.GetSquare(tile.x, tile.y);
+ if (square != null)
+ {
+ foreach (Renderer renderer in square.GetComponentsInChildren())
+ {
+ _originalTileColors[renderer] = renderer.material.color;
+ renderer.material.color = new Color(0.05f, 0.03f, 0.06f);
+ }
+ }
+
+ Debug.Log($"[PurgatoryDuel] Casilla devorada: ({tile.x}, {tile.y}).");
+ }
+
+ private IEnumerator BossStep()
+ {
+ if (_boss == null)
+ yield break;
+
+ for (int step = 0; step < PurgatoryDuelRules.BossStepsPerTurn; step++)
+ {
+ var blocked = new HashSet(_devoured);
+ Vector2Int next = PurgatoryDuelRules.NextChaseStep(_boss.currentPos, _playerPiece.currentPos, blocked);
+
+ if (next == _boss.currentPos)
+ yield break;
+
+ MovePieceOnBoard(_boss, next);
+
+ if (next == _playerPiece.currentPos)
+ {
+ Defeat("La Muerte te alcanzó");
+ yield break;
+ }
+
+ yield return new WaitForSeconds(0.25f);
+ }
+ }
+
+ // ══════════════════════════════════════════════════════
+ // DERROTA Y RESCATE
+ // ══════════════════════════════════════════════════════
+
+ private void Defeat(string reason)
+ {
+ Debug.LogWarning($"[PurgatoryDuel] Derrota: {reason}");
+ _phase = Phase.Ended;
+ BoardManager.Instance.HideAllIndicators();
+
+ PieceIdentity rescueCandidate = GetRescueCandidate();
+ if (!_rescueUsed && rescueCandidate != null)
+ {
+ _ui.ShowDefeatPanel(
+ reason,
+ () => StartRescue(rescueCandidate),
+ DeclineRescue);
+ }
+ else
+ {
+ FinalDefeat();
+ }
+ }
+
+ ///
+ /// Identidad muerta más valiosa disponible para el rescate por dados.
+ ///
+ private PieceIdentity GetRescueCandidate()
+ {
+ CampaignState state = _campaign != null ? _campaign.State : null;
+ if (state == null)
+ return null;
+
+ return state.GetDeadIdentities()
+ .OrderByDescending(IdentityValue)
+ .FirstOrDefault();
+ }
+
+ private static int IdentityValue(PieceIdentity identity)
+ {
+ return identity.pieceType switch
+ {
+ PieceType.Queen => 9,
+ PieceType.Rook => 5,
+ PieceType.Bishop => 3,
+ PieceType.Knight => 3,
+ PieceType.Pawn => 1,
+ _ => 0
+ };
+ }
+
+ private void StartRescue(PieceIdentity candidate)
+ {
+ _rescueUsed = true;
+ _ui.HideDefeatPanel();
+ StartCoroutine(ExecuteRescue(candidate));
+ }
+
+ private IEnumerator ExecuteRescue(PieceIdentity candidate)
+ {
+ DiceSystem diceSystem = FindObjectOfType();
+ if (diceSystem == null)
+ {
+ Debug.LogError("[PurgatoryDuel] DiceSystem no encontrado. Rescate imposible.");
+ FinalDefeat();
+ yield break;
+ }
+
+ DiceRollResult playerRoll = diceSystem.RollForPlayer(candidate, 0);
+ DiceRollResult deathRoll = diceSystem.RollForDeath();
+ PurgatoryWinner winner = diceSystem.DetermineWinner(playerRoll.finalResult, deathRoll.finalResult);
+
+ var rollUI = FindObjectOfType(true);
+ var resultUI = FindObjectOfType(true);
+
+ if (rollUI != null && resultUI != null)
+ {
+ rollUI.Show();
+ yield return rollUI.AnimatePlayerRoll(playerRoll);
+ yield return rollUI.AnimateDeathRoll(deathRoll);
+ rollUI.Hide();
+ resultUI.Show(winner, candidate, playerRoll.finalResult, deathRoll.finalResult);
+ yield return new WaitUntil(() => !resultUI.IsShowing);
+ }
+ else
+ {
+ Debug.Log($"[PurgatoryDuel] Rescate sin UI: jugador {playerRoll.finalResult} vs Muerte {deathRoll.finalResult}.");
+ }
+
+ if (winner == PurgatoryWinner.Player)
+ {
+ Debug.Log($"[PurgatoryDuel] Rescate exitoso: {candidate.characterName} vuelve al tablero.");
+
+ if (_campaign.State != null)
+ _campaign.State.MarkPieceRecovered(candidate);
+
+ _ui.ShowQuote("...Tómalas. El tablero te espera.");
+ CleanupDuel(restorePieces: true);
+
+ yield return FinishAfterDelay(() =>
+ {
+ if (_campaign != null)
+ _campaign.ResumeAfterDuelRescue();
+ TearDown();
+ });
+ }
+ else
+ {
+ Debug.Log("[PurgatoryDuel] Rescate fallido.");
+ FinalDefeat();
+ }
+ }
+
+ private void DeclineRescue()
+ {
+ _rescueUsed = true;
+ _ui.HideDefeatPanel();
+ FinalDefeat();
+ }
+
+ private void FinalDefeat()
+ {
+ _ui.ShowQuote(MuerteQuotes.GetPlayerLost());
+
+ CleanupDuel(restorePieces: false);
+
+ StartCoroutine(FinishAfterDelay(() =>
+ {
+ if (_campaign != null)
+ _campaign.RestartAtFirstChapter();
+ TearDown();
+ }));
+ }
+
+ private IEnumerator FinishAfterDelay(System.Action action)
+ {
+ yield return new WaitForSeconds(1.6f);
+ action?.Invoke();
+ }
+
+ // ══════════════════════════════════════════════════════
+ // LIMPIEZA
+ // ══════════════════════════════════════════════════════
+
+ ///
+ /// Devuelve las piezas ocultas a sus casillas originales y restaura el color
+ /// de las casillas devoradas. Con restorePieces=false (derrota final) las
+ /// piezas permanecen apartadas: la escena se recarga de inmediato.
+ ///
+ private void CleanupDuel(bool restorePieces)
+ {
+ Piece[,] board = GameManager.Instance.board;
+ if (board != null && restorePieces)
+ {
+ foreach (var entry in _hiddenPieces)
+ {
+ if (entry.piece == null)
+ continue;
+
+ board[entry.pos.x, entry.pos.y] = entry.piece;
+ entry.piece.gameObject.SetActive(true);
+ }
+ }
+
+ foreach (var pair in _originalTileColors)
+ {
+ if (pair.Key != null)
+ pair.Key.material.color = pair.Value;
+ }
+
+ if (BoardManager.Instance != null)
+ BoardManager.Instance.HideAllIndicators();
+
+ _hiddenPieces.Clear();
+ _originalTileColors.Clear();
+ }
+
+ private void TearDown()
+ {
+ _phase = Phase.Idle;
+ if (Instance == this)
+ Instance = null;
+
+ Destroy(gameObject);
+ }
+}
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs.meta b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs.meta
new file mode 100644
index 0000000..89a6e11
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d51ec1b9d6be556448a34c159b5f2c6e
\ No newline at end of file
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs
new file mode 100644
index 0000000..93ea5b0
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+///
+/// Reglas puras del Duelo del Purgatorio: curva de colapso y persecución del Jefe.
+/// Sin dependencias de escena para poder testear en EditMode.
+///
+public static class PurgatoryDuelRules
+{
+ public const int BoardSize = 8;
+ public const int CollapseGraceTurns = 2;
+ public const int DoubleCollapseFromTurn = 8;
+ public const int BossStepsPerTurn = 2;
+
+ ///
+ /// Casillas devoradas en el turno dado (1-indexed).
+ /// Turnos 1-2: 0 · Turnos 3-7: 1 · Turno 8+: 2.
+ ///
+ public static int TilesForTurn(int turn)
+ {
+ if (turn <= CollapseGraceTurns)
+ return 0;
+ if (turn < DoubleCollapseFromTurn)
+ return 1;
+ return 2;
+ }
+
+ ///
+ /// Elige casillas aleatorias a devorar, excluyendo vacíos previos y la casilla
+ /// del Jefe. La casilla del jugador SÍ es candidata (derrota por colapso).
+ ///
+ public static List PickTilesToDevour(int count, HashSet excluded, System.Random rng)
+ {
+ var candidates = new List(BoardSize * BoardSize);
+ for (int x = 0; x < BoardSize; x++)
+ {
+ for (int y = 0; y < BoardSize; y++)
+ {
+ var p = new Vector2Int(x, y);
+ if (!excluded.Contains(p))
+ candidates.Add(p);
+ }
+ }
+
+ for (int i = candidates.Count - 1; i > 0; i--)
+ {
+ int j = rng.Next(i + 1);
+ (candidates[i], candidates[j]) = (candidates[j], candidates[i]);
+ }
+
+ int n = Mathf.Min(count, candidates.Count);
+ return candidates.GetRange(0, n);
+ }
+
+ ///
+ /// Un paso greedy del Jefe hacia el jugador (movimiento de rey, 1 casilla),
+ /// evitando casillas bloqueadas (vacíos). Prefiere la diagonal; si está
+ /// bloqueada acepta un paso axial que no aumente la distancia Chebyshev
+ /// (permite rodear huecos). Devuelve 'from' si no hay paso válido.
+ ///
+ public static Vector2Int NextChaseStep(Vector2Int from, Vector2Int target, HashSet blocked)
+ {
+ int dx = Mathf.Clamp(target.x - from.x, -1, 1);
+ int dy = Mathf.Clamp(target.y - from.y, -1, 1);
+
+ if (dx == 0 && dy == 0)
+ return from;
+
+ Vector2Int diag = from + new Vector2Int(dx, dy);
+ if (dx != 0 && dy != 0 && !blocked.Contains(diag))
+ return diag;
+
+ Vector2Int stepX = from + new Vector2Int(dx, 0);
+ Vector2Int stepY = from + new Vector2Int(0, dy);
+ bool xOk = dx != 0 && !blocked.Contains(stepX) && Chebyshev(stepX, target) <= Chebyshev(from, target);
+ bool yOk = dy != 0 && !blocked.Contains(stepY) && Chebyshev(stepY, target) <= Chebyshev(from, target);
+
+ if (xOk && yOk)
+ {
+ return Mathf.Abs(target.x - from.x) >= Mathf.Abs(target.y - from.y) ? stepX : stepY;
+ }
+ if (xOk) return stepX;
+ if (yOk) return stepY;
+
+ return from;
+ }
+
+ public static int Chebyshev(Vector2Int a, Vector2Int b)
+ {
+ return Mathf.Max(Mathf.Abs(a.x - b.x), Mathf.Abs(a.y - b.y));
+ }
+
+ public static bool IsInBounds(Vector2Int p)
+ {
+ return p.x >= 0 && p.x < BoardSize && p.y >= 0 && p.y < BoardSize;
+ }
+}
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs.meta b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs.meta
new file mode 100644
index 0000000..c8fd82a
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelRules.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e27f0d42be82acf45a36399e09bb0102
\ No newline at end of file
diff --git a/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelUI.cs b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelUI.cs
new file mode 100644
index 0000000..90c78ba
--- /dev/null
+++ b/Assets/Game/Scripts/Mono/Purgatory/Duel/PurgatoryDuelUI.cs
@@ -0,0 +1,161 @@
+using TMPro;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using UnityEngine.UI;
+
+///
+/// Overlay de UI del Duelo del Purgatorio. Se construye íntegramente en runtime
+/// (sin edición de escenas) sobre su propio Canvas con sorting order alto.
+///
+public class PurgatoryDuelUI : MonoBehaviour
+{
+ private Canvas _canvas;
+ private TextMeshProUGUI _turnText;
+ private TextMeshProUGUI _quoteText;
+ private GameObject _defeatPanel;
+ private TextMeshProUGUI _defeatReasonText;
+ private Button _rescueButton;
+ private Button _surrenderButton;
+ private GameObject _victoryBanner;
+
+ public void Build()
+ {
+ EnsureEventSystem();
+
+ var canvasGO = new GameObject("PurgatoryDuelCanvas");
+ _canvas = canvasGO.AddComponent