mirror of
https://github.com/Earth-Genesis-Games/Ajedrez_Purgatorio.git
synced 2026-09-11 09:47:35 +00:00
feat: Purgatory Final duel vs La Muerte + SFX wiring + runtime options menu
- Duel flow: mating piece vs boss while board collapses (PurgatoryDuelManager/Rules/UI/MuerteQuotes) - GameState.PurgatoryDuel + CampaignManager rescue/restart hooks + SquareClick routing - Dice-based rescue of most valuable dead identity; final defeat restarts Chapter 1 - 6 SFX clips wired in Ch1/2/3 via AudioClipAssigner - OptionsMenuController runtime audio/graphics panel, wired to MainMenu - Tests migrated to Assets/Tests/EditMode subfolders + PurgatoryDuelRulesTests (200 total) - Editor tools: DiagAssign, SpeakerDatabaseSetup, PurgatoryCanvasMaintenance
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0fad760a5c49f94ea668f15bad0b624
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6255e3e53840c534a9b742ff3b0e50cf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using AjedrezPurgatorio.Audio;
|
||||
|
||||
namespace AjedrezPurgatorio.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<AudioManager>(
|
||||
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<AudioClip>(
|
||||
$"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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cfc3f0fcd46dc94c86a1fb4f5bca113
|
||||
@@ -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<SpeakerDatabase>("Assets/Game/Data/Resources/SpeakerDatabase.asset");
|
||||
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Chapter1.unity", OpenSceneMode.Single);
|
||||
var ui = Object.FindFirstObjectByType<DialogueUI>(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<DialogueUI>(FindObjectsInactive.Include);
|
||||
Debug.Log($"[Diag] tras recargar={field.GetValue(ui2) != null}");
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b27ce28b927f4e4d91a89cd4ef5deda
|
||||
@@ -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
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c50f0212759220b4990b34b8fe3c7ff9
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace AjedrezPurgatorio.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<SpeakerDatabase>(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<Sprite>($"{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<SpeakerDatabase>(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<DialogueUI>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdc614fe81279584a9cc6267514ff99c
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -229,6 +229,29 @@ public class CampaignState : ScriptableObject
|
||||
return new List<string>(_alivePieces);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene las PieceIdentity muertas (no presentes en _alivePieces).
|
||||
/// Usado por el Duelo del Purgatorio para elegir al candidato de rescate.
|
||||
/// </summary>
|
||||
public List<PieceIdentity> GetDeadIdentities()
|
||||
{
|
||||
var result = new List<PieceIdentity>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene la lista de PieceIdentity disponibles para el próximo tablero.
|
||||
/// Compatibilidad con BoardManager y PieceIdentityManager.
|
||||
|
||||
@@ -44,6 +44,12 @@ public class CampaignManager : MonoBehaviour
|
||||
// Propiedades
|
||||
public ChapterData CurrentChapter { get; private set; }
|
||||
public int CurrentChapterIndex => _currentChapterIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Estado de campaña activo (ScriptableObject). Expuesto para sistemas runtime
|
||||
/// como el Duelo del Purgatorio que necesitan registrar piezas muertas/recuperadas.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// El jugador capturó a La Muerte en el duelo: continuar como victoria de capítulo.
|
||||
/// </summary>
|
||||
public void ResumeAfterDuelVictory()
|
||||
{
|
||||
OnChapterVictory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// El rescate por dados tuvo éxito: el capítulo continúa con el tablero restaurado.
|
||||
/// </summary>
|
||||
public void ResumeAfterDuelRescue()
|
||||
{
|
||||
Debug.Log("[CampaignManager] Rescate del duelo exitoso. El capítulo continúa.");
|
||||
TransitionToPlaying();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detiene los sistemas de capítulo (niebla, malditas, reloj, jefes, triggers)
|
||||
/// mientras el Duelo del Purgatorio está activo.
|
||||
/// </summary>
|
||||
public void SuspendChapterSystems()
|
||||
{
|
||||
EndChapterSystems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derrota final en el duelo: reiniciar la campaña desde el primer tablero.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecc57bd50bd471047882bb2df0ed6097
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Frases de La Muerte durante el Duelo del Purgatorio.
|
||||
/// Voz solemne según purgatorio-cosmology.md ("la Muerte", máscara formal).
|
||||
/// </summary>
|
||||
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)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4d50f6d2066ae74fa6450df51918194
|
||||
@@ -0,0 +1,613 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AjedrezPurgatorio.Audio;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Vector2Int> _devoured = new HashSet<Vector2Int>();
|
||||
private readonly List<(Piece piece, Vector2Int pos)> _hiddenPieces = new List<(Piece, Vector2Int)>();
|
||||
private readonly Dictionary<Renderer, Color> _originalTileColors = new Dictionary<Renderer, Color>();
|
||||
private readonly System.Random _rng = new System.Random();
|
||||
|
||||
private CampaignManager _campaign;
|
||||
private PurgatoryDuelUI _ui;
|
||||
|
||||
public bool IsActive => _phase != Phase.Idle && _phase != Phase.Ended;
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// INICIO
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<PurgatoryDuelManager>();
|
||||
Instance.Begin(campaign, matingPiece);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La pieza de mate es la pieza blanca de mayor valor que ataca al rey negro.
|
||||
/// </summary>
|
||||
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<AIController>();
|
||||
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<Piece>();
|
||||
boss.isWhite = false;
|
||||
boss.currentPos = spawnPos;
|
||||
boss.hasMoved = true;
|
||||
board[spawnPos.x, spawnPos.y] = boss;
|
||||
|
||||
foreach (var spriteRenderer in bossObj.GetComponentsInChildren<SpriteRenderer>(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<PurgatoryDuelUI>();
|
||||
_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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private List<Vector2Int> GetLegalMoves()
|
||||
{
|
||||
Piece[,] board = GameManager.Instance.board;
|
||||
var moves = new List<Vector2Int>();
|
||||
|
||||
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<Vector2Int>(_devoured);
|
||||
if (_boss != null)
|
||||
excluded.Add(_boss.currentPos);
|
||||
|
||||
List<Vector2Int> 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<Renderer>())
|
||||
{
|
||||
_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<Vector2Int>(_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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identidad muerta más valiosa disponible para el rescate por dados.
|
||||
/// </summary>
|
||||
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<DiceSystem>();
|
||||
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<DiceRollUI>(true);
|
||||
var resultUI = FindObjectOfType<DiceResultUI>(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
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d51ec1b9d6be556448a34c159b5f2c6e
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Reglas puras del Duelo del Purgatorio: curva de colapso y persecución del Jefe.
|
||||
/// Sin dependencias de escena para poder testear en EditMode.
|
||||
/// </summary>
|
||||
public static class PurgatoryDuelRules
|
||||
{
|
||||
public const int BoardSize = 8;
|
||||
public const int CollapseGraceTurns = 2;
|
||||
public const int DoubleCollapseFromTurn = 8;
|
||||
public const int BossStepsPerTurn = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Casillas devoradas en el turno dado (1-indexed).
|
||||
/// Turnos 1-2: 0 · Turnos 3-7: 1 · Turno 8+: 2.
|
||||
/// </summary>
|
||||
public static int TilesForTurn(int turn)
|
||||
{
|
||||
if (turn <= CollapseGraceTurns)
|
||||
return 0;
|
||||
if (turn < DoubleCollapseFromTurn)
|
||||
return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Elige casillas aleatorias a devorar, excluyendo vacíos previos y la casilla
|
||||
/// del Jefe. La casilla del jugador SÍ es candidata (derrota por colapso).
|
||||
/// </summary>
|
||||
public static List<Vector2Int> PickTilesToDevour(int count, HashSet<Vector2Int> excluded, System.Random rng)
|
||||
{
|
||||
var candidates = new List<Vector2Int>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static Vector2Int NextChaseStep(Vector2Int from, Vector2Int target, HashSet<Vector2Int> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e27f0d42be82acf45a36399e09bb0102
|
||||
@@ -0,0 +1,161 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Canvas>();
|
||||
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_canvas.sortingOrder = 500;
|
||||
canvasGO.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
canvasGO.AddComponent<GraphicRaycaster>();
|
||||
canvasGO.transform.SetParent(transform, false);
|
||||
|
||||
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
|
||||
|
||||
var title = CreateText("Title", "EL PURGATORIO", 42, new Color(0.85f, 0.12f, 0.12f), font);
|
||||
Anchor(title.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -70), new Vector2(900, 60));
|
||||
|
||||
_turnText = CreateText("Turn", "Turno 1", 26, new Color(0.9f, 0.88f, 0.8f), font);
|
||||
Anchor(_turnText.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -115), new Vector2(600, 40));
|
||||
|
||||
_quoteText = CreateText("Quote", string.Empty, 24, new Color(0.75f, 0.72f, 0.65f), font);
|
||||
_quoteText.fontStyle = FontStyles.Italic;
|
||||
Anchor(_quoteText.rectTransform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0, -165), new Vector2(1100, 70));
|
||||
|
||||
BuildDefeatPanel(font);
|
||||
BuildVictoryBanner(font);
|
||||
HideDefeatPanel();
|
||||
HideVictory();
|
||||
}
|
||||
|
||||
private void BuildDefeatPanel(TMP_FontAsset font)
|
||||
{
|
||||
_defeatPanel = new GameObject("DefeatPanel");
|
||||
_defeatPanel.transform.SetParent(_canvas.transform, false);
|
||||
|
||||
var bg = _defeatPanel.AddComponent<Image>();
|
||||
bg.color = new Color(0f, 0f, 0f, 0.82f);
|
||||
bg.raycastTarget = true;
|
||||
Stretch(bg.rectTransform);
|
||||
|
||||
_defeatReasonText = CreateText("Reason", string.Empty, 34, new Color(0.9f, 0.88f, 0.8f), font);
|
||||
_defeatReasonText.transform.SetParent(_defeatPanel.transform, false);
|
||||
Anchor(_defeatReasonText.rectTransform, new Vector2(0.5f, 0.55f), new Vector2(0.5f, 0.55f), Vector2.zero, new Vector2(1000, 90));
|
||||
|
||||
_rescueButton = CreateButton("RescueButton", "Desafiar por rescate", font, new Color(0.45f, 0.08f, 0.08f));
|
||||
_rescueButton.transform.SetParent(_defeatPanel.transform, false);
|
||||
Anchor((RectTransform)_rescueButton.transform, new Vector2(0.5f, 0.32f), new Vector2(0.5f, 0.32f), new Vector2(-160, 0), new Vector2(300, 64));
|
||||
|
||||
_surrenderButton = CreateButton("SurrenderButton", "Rendirse", font, new Color(0.15f, 0.15f, 0.17f));
|
||||
_surrenderButton.transform.SetParent(_defeatPanel.transform, false);
|
||||
Anchor((RectTransform)_surrenderButton.transform, new Vector2(0.5f, 0.32f), new Vector2(0.5f, 0.32f), new Vector2(160, 0), new Vector2(240, 64));
|
||||
}
|
||||
|
||||
private void BuildVictoryBanner(TMP_FontAsset font)
|
||||
{
|
||||
_victoryBanner = new GameObject("VictoryBanner");
|
||||
_victoryBanner.transform.SetParent(_canvas.transform, false);
|
||||
|
||||
var text = CreateText("Text", string.Empty, 52, new Color(0.95f, 0.9f, 0.7f), font);
|
||||
text.transform.SetParent(_victoryBanner.transform, false);
|
||||
Stretch(text.rectTransform);
|
||||
}
|
||||
|
||||
public void SetTurn(int turn) => _turnText.text = $"Turno {turn}";
|
||||
|
||||
public void ShowQuote(string quote) => _quoteText.text = quote;
|
||||
|
||||
public void ShowDefeatPanel(string reason, UnityEngine.Events.UnityAction onRescue, UnityEngine.Events.UnityAction onSurrender)
|
||||
{
|
||||
_defeatPanel.SetActive(true);
|
||||
_defeatReasonText.text = reason;
|
||||
_rescueButton.onClick.RemoveAllListeners();
|
||||
_surrenderButton.onClick.RemoveAllListeners();
|
||||
if (onRescue != null) _rescueButton.onClick.AddListener(onRescue);
|
||||
if (onSurrender != null) _surrenderButton.onClick.AddListener(onSurrender);
|
||||
}
|
||||
|
||||
public void HideDefeatPanel() => _defeatPanel.SetActive(false);
|
||||
|
||||
public void ShowVictory(string message)
|
||||
{
|
||||
_victoryBanner.SetActive(true);
|
||||
_victoryBanner.GetComponentInChildren<TextMeshProUGUI>(true).text = message;
|
||||
}
|
||||
|
||||
public void HideVictory() => _victoryBanner.SetActive(false);
|
||||
|
||||
private static TextMeshProUGUI CreateText(string name, string content, int size, Color color, TMP_FontAsset font)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = content;
|
||||
tmp.fontSize = size;
|
||||
tmp.color = color;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
tmp.raycastTarget = false;
|
||||
if (font != null)
|
||||
tmp.font = font;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private static Button CreateButton(string name, string label, TMP_FontAsset font, Color color)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var image = go.AddComponent<Image>();
|
||||
image.color = color;
|
||||
var button = go.AddComponent<Button>();
|
||||
|
||||
var text = CreateText("Label", label, 26, Color.white, font);
|
||||
text.transform.SetParent(go.transform, false);
|
||||
Stretch(text.rectTransform);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static void Anchor(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offset, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = anchorMin;
|
||||
rt.anchorMax = anchorMax;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt.anchoredPosition = offset;
|
||||
rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void EnsureEventSystem()
|
||||
{
|
||||
if (Object.FindObjectOfType<EventSystem>() != null)
|
||||
return;
|
||||
|
||||
var es = new GameObject("PurgatoryDuelEventSystem");
|
||||
es.AddComponent<EventSystem>();
|
||||
es.AddComponent<StandaloneInputModule>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f756a4d9093cc840a6474f22d9c483a
|
||||
@@ -8,6 +8,14 @@ public class SquareClick : MonoBehaviour
|
||||
{
|
||||
if (GameManager.Instance == null) return;
|
||||
|
||||
// Duelo del Purgatorio: el manager del duelo maneja el input.
|
||||
if (GameStateManager.Instance != null && GameStateManager.Instance.CurrentState == GameState.PurgatoryDuel)
|
||||
{
|
||||
if (PurgatoryDuelManager.Instance != null)
|
||||
PurgatoryDuelManager.Instance.HandleTileClick(position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't process clicks during dialogue, purgatory, or game over
|
||||
if (GameStateManager.Instance != null && GameStateManager.Instance.CurrentState != GameState.Playing)
|
||||
return;
|
||||
|
||||
@@ -154,16 +154,14 @@ namespace AjedrezPurgatorio.UI
|
||||
|
||||
/// <summary>
|
||||
/// Called when "Options" button is clicked.
|
||||
/// Opens options menu (placeholder for now).
|
||||
/// Opens the runtime-built options menu (audio + graphics).
|
||||
/// </summary>
|
||||
private void OnOptionsClick()
|
||||
{
|
||||
if (_enableDebugLogging)
|
||||
Debug.Log("[MainMenu] Options clicked.");
|
||||
|
||||
// TODO: Implement options menu (audio, graphics, controls)
|
||||
// For now, just log
|
||||
Debug.Log("[MainMenu] Options menu not yet implemented.");
|
||||
OptionsMenuController.Toggle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using AjedrezPurgatorio.Audio;
|
||||
|
||||
namespace AjedrezPurgatorio.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Menú de opciones construido en runtime (mismo enfoque que el botón de
|
||||
/// campañas alternativas del MainMenuController: no modifica escenas).
|
||||
/// Ajustes: volumen master/música/SFX y pantalla completa.
|
||||
/// Persistencia vía PlayerPrefs; los valores se aplican al abrir el menú
|
||||
/// y al arrancar la aplicación.
|
||||
/// Controles re-mapeables: fuera de alcance del slice actual.
|
||||
/// </summary>
|
||||
public class OptionsMenuController : MonoBehaviour
|
||||
{
|
||||
public static OptionsMenuController Instance { get; private set; }
|
||||
|
||||
private const string KeyMaster = "Options_MasterVolume";
|
||||
private const string KeyMusic = "Options_MusicVolume";
|
||||
private const string KeySFX = "Options_SFXVolume";
|
||||
private const string KeyFullscreen = "Options_Fullscreen";
|
||||
|
||||
private const float DefaultVolume = 1f;
|
||||
|
||||
private GameObject _panel;
|
||||
private Slider _masterSlider;
|
||||
private Slider _musicSlider;
|
||||
private Slider _sfxSlider;
|
||||
private Toggle _fullscreenToggle;
|
||||
private bool _suppressEvents;
|
||||
private Font _font;
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aplica las preferencias guardadas al arrancar el juego.
|
||||
/// </summary>
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void ApplySavedPreferencesAtBoot()
|
||||
{
|
||||
AudioListener.volume = PlayerPrefs.GetFloat(KeyMaster, DefaultVolume);
|
||||
|
||||
var audio = FindObjectOfType<AudioManager>();
|
||||
if (audio != null)
|
||||
{
|
||||
audio.SetMusicVolume(PlayerPrefs.GetFloat(KeyMusic, DefaultVolume));
|
||||
audio.SetSFXVolume(PlayerPrefs.GetFloat(KeySFX, DefaultVolume));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Abre/cierra el menú de opciones, creándolo la primera vez.
|
||||
/// Llamar desde MainMenuController.OnOptionsClick.
|
||||
/// </summary>
|
||||
public static void Toggle()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
var go = new GameObject("OptionsMenu");
|
||||
go.AddComponent<OptionsMenuController>();
|
||||
}
|
||||
|
||||
if (Instance._panel != null && Instance._panel.activeSelf)
|
||||
Instance.Close();
|
||||
else
|
||||
Instance.Open();
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_panel == null)
|
||||
BuildPanel();
|
||||
|
||||
SyncControlsFromSettings();
|
||||
_panel.SetActive(true);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (_panel != null)
|
||||
_panel.SetActive(false);
|
||||
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Settings Application
|
||||
|
||||
private void ApplyMasterVolume(float value)
|
||||
{
|
||||
AudioListener.volume = value;
|
||||
PlayerPrefs.SetFloat(KeyMaster, value);
|
||||
}
|
||||
|
||||
private void ApplyMusicVolume(float value)
|
||||
{
|
||||
if (AudioManager.Instance != null)
|
||||
AudioManager.Instance.SetMusicVolume(value);
|
||||
PlayerPrefs.SetFloat(KeyMusic, value);
|
||||
}
|
||||
|
||||
private void ApplySFXVolume(float value)
|
||||
{
|
||||
if (AudioManager.Instance != null)
|
||||
AudioManager.Instance.SetSFXVolume(value);
|
||||
PlayerPrefs.SetFloat(KeySFX, value);
|
||||
}
|
||||
|
||||
private void ApplyFullscreen(bool value)
|
||||
{
|
||||
Screen.fullScreen = value;
|
||||
PlayerPrefs.SetInt(KeyFullscreen, value ? 1 : 0);
|
||||
}
|
||||
|
||||
private void SyncControlsFromSettings()
|
||||
{
|
||||
_suppressEvents = true;
|
||||
|
||||
_masterSlider.value = PlayerPrefs.GetFloat(KeyMaster, DefaultVolume);
|
||||
_musicSlider.value = PlayerPrefs.GetFloat(KeyMusic, DefaultVolume);
|
||||
_sfxSlider.value = PlayerPrefs.GetFloat(KeySFX, DefaultVolume);
|
||||
_fullscreenToggle.isOn = PlayerPrefs.GetInt(
|
||||
KeyFullscreen, Screen.fullScreen ? 1 : 0) == 1;
|
||||
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI Construction
|
||||
|
||||
private Font GetFont()
|
||||
{
|
||||
if (_font != null)
|
||||
return _font;
|
||||
|
||||
// Unity 6: LegacyRuntime.ttf es el builtin; Arial.ttf era el nombre antiguo.
|
||||
_font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (_font == null)
|
||||
_font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
|
||||
return _font;
|
||||
}
|
||||
|
||||
private void BuildPanel()
|
||||
{
|
||||
Font font = GetFont();
|
||||
|
||||
// Canvas overlay propio para vivir sobre cualquier escena de menú.
|
||||
var canvasGo = new GameObject("OptionsMenuCanvas");
|
||||
canvasGo.transform.SetParent(transform, false);
|
||||
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 500;
|
||||
canvasGo.AddComponent<CanvasScaler>();
|
||||
canvasGo.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// Fondo oscurecido que bloquea clicks hacia el menú principal.
|
||||
var backdrop = DefaultControls.CreatePanel(
|
||||
new DefaultControls.Resources());
|
||||
backdrop.name = "Backdrop";
|
||||
backdrop.transform.SetParent(canvasGo.transform, false);
|
||||
Stretch(backdrop.GetComponent<RectTransform>());
|
||||
var backdropImage = backdrop.GetComponent<Image>();
|
||||
backdropImage.color = new Color(0f, 0f, 0f, 0.75f);
|
||||
|
||||
// Panel central.
|
||||
var window = new GameObject("Window", typeof(RectTransform), typeof(Image));
|
||||
window.transform.SetParent(backdrop.transform, false);
|
||||
var windowRect = window.GetComponent<RectTransform>();
|
||||
windowRect.sizeDelta = new Vector2(460f, 400f);
|
||||
window.GetComponent<Image>().color = new Color(0.09f, 0.07f, 0.11f, 0.97f);
|
||||
|
||||
CreateLabel(window.transform, "Purgatorio — Opciones", 26,
|
||||
new Vector2(0f, -34f), font, TextAnchor.UpperCenter);
|
||||
|
||||
// Sliders: Master / Música / SFX.
|
||||
_masterSlider = CreateVolumeRow(window.transform, "Master", 60f,
|
||||
ApplyMasterVolume, font);
|
||||
_musicSlider = CreateVolumeRow(window.transform, "Música", 120f,
|
||||
ApplyMusicVolume, font);
|
||||
_sfxSlider = CreateVolumeRow(window.transform, "SFX", 180f,
|
||||
ApplySFXVolume, font);
|
||||
|
||||
// Pantalla completa.
|
||||
_fullscreenToggle = CreateToggleRow(window.transform, "Pantalla completa", 240f,
|
||||
value => ApplyFullscreen(value), font);
|
||||
|
||||
// Botón Volver.
|
||||
var back = DefaultControls.CreateButton(new DefaultControls.Resources());
|
||||
back.name = "BackButton";
|
||||
back.transform.SetParent(window.transform, false);
|
||||
var backRect = back.GetComponent<RectTransform>();
|
||||
backRect.sizeDelta = new Vector2(160f, 42f);
|
||||
backRect.anchoredPosition = new Vector2(0f, -158f);
|
||||
SetText(back.transform.Find("Text")?.GetComponent<Text>(), "Volver", 18, font);
|
||||
back.GetComponent<Button>().onClick.AddListener(Close);
|
||||
|
||||
_panel = canvasGo;
|
||||
_panel.SetActive(false);
|
||||
}
|
||||
|
||||
private Slider CreateVolumeRow(Transform parent, string label, float y,
|
||||
UnityEngine.Events.UnityAction<float> onChanged, Font font)
|
||||
{
|
||||
CreateLabel(parent, label, 18, new Vector2(-150f, -y), font, TextAnchor.MiddleRight);
|
||||
|
||||
var slider = DefaultControls.CreateSlider(new DefaultControls.Resources());
|
||||
slider.name = $"{label}Slider";
|
||||
slider.transform.SetParent(parent, false);
|
||||
var rect = slider.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(220f, 20f);
|
||||
rect.anchoredPosition = new Vector2(40f, -y);
|
||||
|
||||
var s = slider.GetComponent<Slider>();
|
||||
s.minValue = 0f;
|
||||
s.maxValue = 1f;
|
||||
s.onValueChanged.AddListener(v =>
|
||||
{
|
||||
if (!_suppressEvents)
|
||||
onChanged(v);
|
||||
});
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private Toggle CreateToggleRow(Transform parent, string label, float y,
|
||||
UnityEngine.Events.UnityAction<bool> onChanged, Font font)
|
||||
{
|
||||
CreateLabel(parent, label, 18, new Vector2(-150f, -y), font, TextAnchor.MiddleRight);
|
||||
|
||||
var toggleGo = DefaultControls.CreateToggle(new DefaultControls.Resources());
|
||||
toggleGo.name = $"{label}Toggle";
|
||||
toggleGo.transform.SetParent(parent, false);
|
||||
var rect = toggleGo.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(160f, 20f);
|
||||
rect.anchoredPosition = new Vector2(40f, -y);
|
||||
|
||||
var t = toggleGo.GetComponent<Toggle>();
|
||||
t.onValueChanged.AddListener(v =>
|
||||
{
|
||||
if (!_suppressEvents)
|
||||
onChanged(v);
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
private void CreateLabel(Transform parent, string content, int size,
|
||||
Vector2 anchoredPosition, Font font, TextAnchor alignment)
|
||||
{
|
||||
var go = new GameObject($"Label_{content}", typeof(RectTransform), typeof(Text));
|
||||
go.transform.SetParent(parent, false);
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(200f, 28f);
|
||||
rect.anchoredPosition = anchoredPosition;
|
||||
|
||||
var text = go.GetComponent<Text>();
|
||||
text.text = content;
|
||||
text.font = font;
|
||||
text.fontSize = size;
|
||||
text.alignment = alignment;
|
||||
text.color = Color.white;
|
||||
}
|
||||
|
||||
private void SetText(Text target, string content, int size, Font font)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
target.text = content;
|
||||
target.fontSize = size;
|
||||
target.font = font;
|
||||
target.color = Color.black;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rect)
|
||||
{
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8dfbb2b4249216d4889a8418cd6d28a2
|
||||
+4392
-3576
File diff suppressed because it is too large
Load Diff
+6677
-5940
File diff suppressed because it is too large
Load Diff
+6244
-10123
File diff suppressed because it is too large
Load Diff
@@ -17603,12 +17603,12 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 4cce55b9d3e1ed740b1d680e1ee76597, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::AjedrezPurgatorio.Audio.AudioManager
|
||||
_moveSFX: {fileID: 2102800, guid: bd7eca68231b48f198532c2a8f8b8ed3, type: 3}
|
||||
_captureSFX: {fileID: 2102800, guid: bbee09e29e03488a87056057de3333d3, type: 3}
|
||||
_checkSFX: {fileID: 2102800, guid: 39c30d32bdb442ee844d9c2204244d96, type: 3}
|
||||
_checkmateSFX: {fileID: 2102800, guid: 4d305733468c4b7199722466abeeace9, type: 3}
|
||||
_promotionSFX: {fileID: 2102800, guid: f7347cab1bf24c4a93f0594fab7b84d8, type: 3}
|
||||
_invalidMoveSFX: {fileID: 0}
|
||||
_moveSFX: {fileID: 8300000, guid: 294db75531d2c664da0b7cd6bc38c9de, type: 3}
|
||||
_captureSFX: {fileID: 8300000, guid: d1116462f5aa2ed4faa8c787e3daecda, type: 3}
|
||||
_checkSFX: {fileID: 8300000, guid: e23a5aff8454f13418bf5946c74c1b95, type: 3}
|
||||
_checkmateSFX: {fileID: 8300000, guid: 82487001f2a9b4443a0d2a2ca1c05c41, type: 3}
|
||||
_promotionSFX: {fileID: 8300000, guid: a5ac5d35758bfc741b5602b80736c4e8, type: 3}
|
||||
_invalidMoveSFX: {fileID: 8300000, guid: 52b4cb0754a08f844ab08975a263ef77, type: 3}
|
||||
_sfxVolume: 0.7
|
||||
_musicVolume: 0.5
|
||||
_enableDebugLogging: 0
|
||||
@@ -32374,7 +32374,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.28, y: 0.04}
|
||||
m_AnchorMax: {x: 0.72, y: 0.18}
|
||||
m_AnchoredPosition: {x: 0, y: -0.000015258789}
|
||||
m_AnchoredPosition: {x: 0, y: -0.000030517578}
|
||||
m_SizeDelta: {x: 0, y: -0.000030517578}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1969906157
|
||||
@@ -34160,7 +34160,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.28, y: 0.04}
|
||||
m_AnchorMax: {x: 0.72, y: 0.18}
|
||||
m_AnchoredPosition: {x: 0, y: -0.000015258789}
|
||||
m_AnchoredPosition: {x: 0, y: -0.000030517578}
|
||||
m_SizeDelta: {x: 0, y: -0.000030517578}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2083124479
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ee8ffb7fec27f545b11d379314dcc6b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Campaign
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests de integración de CampaignManager — Suite 3 del plan de automatización.
|
||||
///
|
||||
/// Alcance EditMode (sin cargas de escena): registro de campañas desde Resources,
|
||||
/// selección de campaña activa, propiedades derivadas y guard clauses de
|
||||
/// LoadChapter que retornan antes de tocar SceneManager.
|
||||
///
|
||||
/// Nota: AddComponent en EditMode NO invoca Awake, así que invocamos
|
||||
/// BuildCampaignRegistry por reflexión sin pasar por el singleton ni
|
||||
/// DontDestroyOnLoad.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Campaign")]
|
||||
[Category("CampaignManager")]
|
||||
[Category("FastTests")]
|
||||
public class CampaignManagerConfigTests
|
||||
{
|
||||
private GameObject _root;
|
||||
private CampaignManager _manager;
|
||||
|
||||
private MethodInfo _buildRegistryMethod;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_root = new GameObject("CampaignManagerTestRoot");
|
||||
_manager = _root.AddComponent<CampaignManager>();
|
||||
|
||||
_buildRegistryMethod = typeof(CampaignManager).GetMethod(
|
||||
"BuildCampaignRegistry", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.IsNotNull(_buildRegistryMethod, "BuildCampaignRegistry no encontrado.");
|
||||
|
||||
// Carga el registry desde Resources/Campaigns igual que Awake,
|
||||
// pero sin singleton ni DontDestroyOnLoad (incompatibles con EditMode).
|
||||
// Sin _campaignConfig asignado, LoadFromJson registra un error esperado.
|
||||
LogAssert.Expect(LogType.Error, "[CampaignConfig] No se asignó archivo JSON de campaña.");
|
||||
_buildRegistryMethod.Invoke(_manager, null);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Registro de campañas
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Registry_LoadsAtLeastOneCampaignFromResources()
|
||||
{
|
||||
Assert.GreaterOrEqual(_manager.CampaignCount, 1,
|
||||
"Resources/Campaigns debe contener al menos la campaña Gambito.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Registry_IgnoresDuplicateCampaignIds_OnRebuild()
|
||||
{
|
||||
int countAfterFirstBuild = _manager.CampaignCount;
|
||||
|
||||
// BuildCampaignRegistry limpia y reconstruye; los duplicados se descartan.
|
||||
// El rebuild repite el log de config sin JSON (el Expect del SetUp ya se consumió).
|
||||
LogAssert.Expect(LogType.Error, "[CampaignConfig] No se asignó archivo JSON de campaña.");
|
||||
_buildRegistryMethod.Invoke(_manager, null);
|
||||
|
||||
Assert.AreEqual(countAfterFirstBuild, _manager.CampaignCount,
|
||||
"Reconstruir el registry no debe duplicar campañas.");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Selección de campaña activa
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void SetDefaultCampaign_SelectsFirstRegistered()
|
||||
{
|
||||
Assert.GreaterOrEqual(_manager.CampaignCount, 1);
|
||||
Assert.IsTrue(_manager.SetDefaultCampaign());
|
||||
Assert.IsNotNull(_manager.ActiveCampaignId);
|
||||
Assert.IsNotEmpty(_manager.ActiveCampaignId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetDefaultCampaign_WithoutRegistry_Fails()
|
||||
{
|
||||
// Manager recién creado SIN invocar BuildCampaignRegistry:
|
||||
// el registro interno está vacío.
|
||||
var emptyRoot = new GameObject("EmptyCampaignManagerRoot");
|
||||
var empty = emptyRoot.AddComponent<CampaignManager>();
|
||||
try
|
||||
{
|
||||
Assert.AreEqual(0, empty.CampaignCount);
|
||||
Assert.IsFalse(empty.SetDefaultCampaign(),
|
||||
"Sin campañas registradas debe retornar false.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(emptyRoot);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCampaign_WithGambitoId_Succeeds()
|
||||
{
|
||||
Assert.IsTrue(_manager.SetCampaign("gambito"));
|
||||
Assert.AreEqual("gambito", _manager.ActiveCampaignId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCampaign_WithInvalidId_Fails_AndKeepsPreviousActive()
|
||||
{
|
||||
Assert.IsTrue(_manager.SetDefaultCampaign());
|
||||
string previousId = _manager.ActiveCampaignId;
|
||||
|
||||
LogAssert.Expect(LogType.Error,
|
||||
"[CampaignManager] Campaña no encontrada: campana-inexistente-xyz. Disponibles: gambito");
|
||||
Assert.IsFalse(_manager.SetCampaign("campana-inexistente-xyz"));
|
||||
|
||||
Assert.AreEqual(previousId, _manager.ActiveCampaignId,
|
||||
"Un fallo de selección no debe alterar la campaña activa.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TotalChapters_IsPositive_AfterValidSelection()
|
||||
{
|
||||
Assert.IsTrue(_manager.SetCampaign("gambito"));
|
||||
Assert.Greater(_manager.TotalChapters, 0,
|
||||
"La campaña Gambito debe declarar al menos un capítulo.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ActiveCampaignId_DefaultsToFirstRegistered_BeforeExplicitSelection()
|
||||
{
|
||||
// ValidateConfiguration/BuildCampaignRegistry deja _activeConfig = campaigns[0].
|
||||
Assert.IsNotNull(_manager.ActiveCampaignId,
|
||||
"Tras construir el registry hay una campaña activa por defecto.");
|
||||
Assert.IsNotEmpty(_manager.ActiveCampaignId);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Guard clauses de LoadChapter (retornan antes de cargar escena)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void LoadChapter_NegativeIndex_Rejected_WithoutActivatingCampaign()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[CampaignManager] Índice fuera de rango: -1");
|
||||
|
||||
_manager.LoadChapter(-1);
|
||||
|
||||
Assert.IsFalse(_manager.IsCampaignActive,
|
||||
"Índice inválido no debe activar la campaña.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoadChapter_IndexOutOfRange_Rejected_WithoutActivatingCampaign()
|
||||
{
|
||||
Assert.IsTrue(_manager.SetCampaign("gambito"));
|
||||
int outOfRange = _manager.TotalChapters; // índices válidos: 0..TotalChapters-1
|
||||
|
||||
LogAssert.Expect(LogType.Error, $"[CampaignManager] Índice fuera de rango: {outOfRange}");
|
||||
|
||||
_manager.LoadChapter(outOfRange);
|
||||
|
||||
Assert.IsFalse(_manager.IsCampaignActive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StartCampaign_WithValidConfig_ActivatesCampaignState()
|
||||
{
|
||||
Assert.IsTrue(_manager.SetCampaign("gambito"));
|
||||
|
||||
// StartCampaign llama a LoadChapterScene → SceneTransitionManager o
|
||||
// SceneManager.LoadScene. En EditMode eso es inviable, así que solo
|
||||
// verificamos las guardas previas mediante TotalChapters > 0.
|
||||
Assert.Greater(_manager.TotalChapters, 0,
|
||||
"Con config válida StartCampaign superaría sus guardas iniciales.");
|
||||
Assert.IsFalse(_manager.IsCampaignActive,
|
||||
"En EditMode nunca llegamos a ejecutar la carga de escena.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45b54ed0265849a4bb4696b8c3c01058
|
||||
@@ -0,0 +1,462 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Campaign
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests para CampaignState - Sistema de persistencia de campaña.
|
||||
/// Cubre: persistencia de piezas, capítulos completados, contadores de pérdidas, reset.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("CampaignState")]
|
||||
[Category("FastTests")]
|
||||
public class CampaignStateTests
|
||||
{
|
||||
private CampaignState _campaignState;
|
||||
private GameObject _mockRoot;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Crear instancia fresca de CampaignState para cada test
|
||||
_campaignState = ScriptableObject.CreateInstance<CampaignState>();
|
||||
_mockRoot = new GameObject("CampaignStateMockRoot");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
// Cleanup
|
||||
if (_campaignState != null)
|
||||
{
|
||||
Object.DestroyImmediate(_campaignState);
|
||||
}
|
||||
if (_mockRoot != null)
|
||||
{
|
||||
Object.DestroyImmediate(_mockRoot);
|
||||
}
|
||||
}
|
||||
|
||||
#region IsPieceAlive Tests
|
||||
|
||||
[Test]
|
||||
public void IsPieceAlive_WhenPieceRegistered_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
|
||||
// Act
|
||||
bool isAlive = _campaignState.IsPieceAlive("elena");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(isAlive, "Elena debería estar viva después de registrarla");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsPieceAlive_WhenPieceNotRegistered_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool isAlive = _campaignState.IsPieceAlive("elena");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(isAlive, "Elena no debería estar viva si nunca fue registrada");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Elena")]
|
||||
[TestCase("ELENA")]
|
||||
[TestCase("elena")]
|
||||
[TestCase("ElEnA")]
|
||||
public void IsPieceAlive_CaseInsensitive(string pieceName)
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
|
||||
// Act
|
||||
bool isAlive = _campaignState.IsPieceAlive(pieceName);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(isAlive, $"IsPieceAlive debería ser case-insensitive para '{pieceName}'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceLost_RemovesFromAlivePieces()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("elena"), "Precondición: Elena debe estar viva");
|
||||
|
||||
// Act
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("elena"), "Elena no debería estar viva después de RegisterPieceLost");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceLost_IncrementsCounters()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
int initialTotal = _campaignState.TotalPiecesLost;
|
||||
int initialCurrent = _campaignState.CurrentChapterPiecesLost;
|
||||
|
||||
// Act
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(initialTotal + 1, _campaignState.TotalPiecesLost, "TotalPiecesLost debería incrementar");
|
||||
Assert.AreEqual(initialCurrent + 1, _campaignState.CurrentChapterPiecesLost, "CurrentChapterPiecesLost debería incrementar");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceAlive_AddsToAlivePieces()
|
||||
{
|
||||
// Act
|
||||
_campaignState.RegisterPieceAlive("ricardo");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("ricardo"), "Ricardo debería estar vivo después de RegisterPieceAlive");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceAlive_NoDuplicates()
|
||||
{
|
||||
// Act
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
_campaignState.RegisterPieceAlive("elena"); // Doble registro
|
||||
|
||||
// Assert
|
||||
var availableNames = _campaignState.GetAvailablePieceNames();
|
||||
int elenaCount = 0;
|
||||
foreach (var name in availableNames)
|
||||
{
|
||||
if (name.ToLower() == "elena")
|
||||
elenaCount++;
|
||||
}
|
||||
Assert.AreEqual(1, elenaCount, "Elena no debería duplicarse en _alivePieces");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsChapterComplete Tests
|
||||
|
||||
[Test]
|
||||
public void IsChapterComplete_InitiallyFalse()
|
||||
{
|
||||
// Act
|
||||
bool isComplete = _campaignState.IsChapterComplete("ch1_factory");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(isComplete, "Los capítulos deberían empezar sin completar");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompleteChapter_MarksAsComplete()
|
||||
{
|
||||
// Act
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_campaignState.IsChapterComplete("ch1_factory"), "ch1_factory debería estar completado");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompleteChapter_StoresVictoryStatus()
|
||||
{
|
||||
// Act
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_campaignState.LastChapterWasVictory, "LastChapterWasVictory debería ser true");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompleteChapter_StoresDefeatStatus()
|
||||
{
|
||||
// Act
|
||||
_campaignState.CompleteChapter("ch3_court", wasVictory: false);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.LastChapterWasVictory, "LastChapterWasVictory debería ser false después de derrota");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LastChapterWasVictory_UpdatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
Assert.IsTrue(_campaignState.LastChapterWasVictory, "Precondición: primera victoria");
|
||||
|
||||
// Act
|
||||
_campaignState.CompleteChapter("ch2_hospital", wasVictory: false);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.LastChapterWasVictory, "LastChapterWasVictory debería actualizarse con el último resultado");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Counter Tests
|
||||
|
||||
[Test]
|
||||
public void StartChapter_ResetsPiecesLostThisChapter()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
Assert.AreEqual(1, _campaignState.CurrentChapterPiecesLost, "Precondición: 1 pieza perdida en ch1");
|
||||
|
||||
// Act
|
||||
_campaignState.StartChapter("ch2_hospital");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _campaignState.CurrentChapterPiecesLost, "CurrentChapterPiecesLost debería resetearse al iniciar nuevo capítulo");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceLost_IncrementsTotal()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
int initialTotal = _campaignState.TotalPiecesLost;
|
||||
|
||||
// Act
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
_campaignState.RegisterPieceLost("ricardo");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(initialTotal + 2, _campaignState.TotalPiecesLost, "TotalPiecesLost debería incrementar con cada pérdida");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterPieceLost_IncrementsCurrentChapter()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
|
||||
// Act
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
_campaignState.RegisterPieceLost("ricardo");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(2, _campaignState.CurrentChapterPiecesLost, "CurrentChapterPiecesLost debería ser 2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TotalPiecesLost_AccumulatesAcrossChapters()
|
||||
{
|
||||
// Arrange & Act
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
_campaignState.RegisterPieceLost("ricardo");
|
||||
|
||||
_campaignState.StartChapter("ch2_hospital");
|
||||
_campaignState.RegisterPieceLost("carlos");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(3, _campaignState.TotalPiecesLost, "TotalPiecesLost debería acumular pérdidas de todos los capítulos");
|
||||
Assert.AreEqual(1, _campaignState.CurrentChapterPiecesLost, "CurrentChapterPiecesLost debería ser solo del capítulo actual");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Test]
|
||||
public void Reset_ClearsAllData()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
_campaignState.RegisterPieceAlive("ricardo");
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
|
||||
// Act
|
||||
_campaignState.Reset();
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("elena"), "Elena no debería estar en _alivePieces después de Reset");
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("ricardo"), "Ricardo no debería estar en _alivePieces después de Reset");
|
||||
Assert.IsFalse(_campaignState.IsChapterComplete("ch1_factory"), "Capítulos completados deberían limpiarse");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reset_ResetsCounters()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.StartChapter("ch1_factory");
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
|
||||
// Act
|
||||
_campaignState.Reset();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _campaignState.TotalPiecesLost, "TotalPiecesLost debería resetearse a 0");
|
||||
Assert.AreEqual(0, _campaignState.CurrentChapterPiecesLost, "CurrentChapterPiecesLost debería resetearse a 0");
|
||||
Assert.IsFalse(_campaignState.LastChapterWasVictory, "LastChapterWasVictory debería resetearse a false");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SyncFromBoard Tests
|
||||
|
||||
[Test]
|
||||
public void SyncFromBoard_ReadsAllWhitePieces()
|
||||
{
|
||||
// Arrange
|
||||
var board = CreateMockBoard();
|
||||
AddMockPiece(board, 0, 0, isWhite: true, identity: CreateMockIdentity("elena"));
|
||||
AddMockPiece(board, 1, 0, isWhite: true, identity: CreateMockIdentity("ricardo"));
|
||||
|
||||
// Act
|
||||
_campaignState.SyncFromBoard(board);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("elena"), "Elena debería sincronizarse del tablero");
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("ricardo"), "Ricardo debería sincronizarse del tablero");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyncFromBoard_IgnoresBlackPieces()
|
||||
{
|
||||
// Arrange
|
||||
var board = CreateMockBoard();
|
||||
AddMockPiece(board, 0, 0, isWhite: false, identity: CreateMockIdentity("black_piece"));
|
||||
|
||||
// Act
|
||||
_campaignState.SyncFromBoard(board);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("black_piece"), "Piezas negras no deberían sincronizarse");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyncFromBoard_IgnoresPiecesWithoutIdentity()
|
||||
{
|
||||
// Arrange
|
||||
var board = CreateMockBoard();
|
||||
AddMockPiece(board, 0, 0, isWhite: true, identity: null);
|
||||
|
||||
// Act
|
||||
_campaignState.SyncFromBoard(board);
|
||||
|
||||
// Assert
|
||||
var availableNames = _campaignState.GetAvailablePieceNames();
|
||||
Assert.AreEqual(0, availableNames.Count, "Piezas sin identidad no deberían sincronizarse");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyncFromBoard_ClearsOldDataBeforeSync()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("old_piece");
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("old_piece"), "Precondición: old_piece existe");
|
||||
|
||||
var board = CreateMockBoard();
|
||||
AddMockPiece(board, 0, 0, isWhite: true, identity: CreateMockIdentity("new_piece"));
|
||||
|
||||
// Act
|
||||
_campaignState.SyncFromBoard(board);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("old_piece"), "Datos antiguos deberían limpiarse antes de sync");
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("new_piece"), "Nuevos datos deberían estar presentes");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Compatibility Methods Tests
|
||||
|
||||
[Test]
|
||||
public void MarkPieceDead_WithPieceIdentity_CallsRegisterPieceLost()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
var identity = CreateMockIdentity("elena");
|
||||
|
||||
// Act
|
||||
_campaignState.MarkPieceDead(identity);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(_campaignState.IsPieceAlive("elena"), "Elena debería estar muerta después de MarkPieceDead");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MarkPieceRecovered_WithPieceIdentity_CallsRegisterPieceAlive()
|
||||
{
|
||||
// Arrange
|
||||
var identity = CreateMockIdentity("elena");
|
||||
|
||||
// Act
|
||||
_campaignState.MarkPieceRecovered(identity);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(_campaignState.IsPieceAlive("elena"), "Elena debería estar viva después de MarkPieceRecovered");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAvailablePieceNames_ReturnsCorrectList()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
_campaignState.RegisterPieceAlive("ricardo");
|
||||
_campaignState.RegisterPieceAlive("carlos");
|
||||
|
||||
// Act
|
||||
var names = _campaignState.GetAvailablePieceNames();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(3, names.Count, "Deberían haber 3 piezas vivas");
|
||||
Assert.Contains("elena", names, "Elena debería estar en la lista");
|
||||
Assert.Contains("ricardo", names, "Ricardo debería estar en la lista");
|
||||
Assert.Contains("carlos", names, "Carlos debería estar en la lista");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private Piece[,] CreateMockBoard()
|
||||
{
|
||||
return new Piece[8, 8];
|
||||
}
|
||||
|
||||
private void AddMockPiece(Piece[,] board, int x, int y, bool isWhite, PieceIdentity identity)
|
||||
{
|
||||
// Piece es MonoBehaviour: hay que crearlo con AddComponent (un
|
||||
// 'new MockPiece()' produce un objeto Unity inválido cuyos campos
|
||||
// no persisten y SyncFromBoard lo vería vacío).
|
||||
var go = new GameObject($"MockPiece_{x}_{y}");
|
||||
go.transform.SetParent(_mockRoot.transform);
|
||||
var piece = go.AddComponent<MockPiece>();
|
||||
piece.isWhite = isWhite;
|
||||
piece.identity = identity;
|
||||
board[x, y] = piece;
|
||||
}
|
||||
|
||||
private PieceIdentity CreateMockIdentity(string characterName)
|
||||
{
|
||||
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
identity.characterName = characterName;
|
||||
return identity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock simple de Piece para tests.
|
||||
/// </summary>
|
||||
private class MockPiece : Piece
|
||||
{
|
||||
public override List<Vector2Int> GetAvailableMoves(Piece[,] board)
|
||||
{
|
||||
return new List<Vector2Int>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 687358a6e3df4a044b620112c7a8d77e
|
||||
@@ -2,185 +2,203 @@ using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for CheckDetector — null safety, FindKing, basic check/checkmate detection.
|
||||
/// </summary>
|
||||
public class CheckDetectorTests
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Chess
|
||||
{
|
||||
private CheckDetector _detector;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Chess")]
|
||||
[Category("CheckDetector")]
|
||||
[Category("FastTests")]
|
||||
public class CheckDetectorTests
|
||||
{
|
||||
_detector = new CheckDetector();
|
||||
}
|
||||
private CheckDetector _detector;
|
||||
private Piece[,] _board;
|
||||
private GameObject _root;
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_detector = null;
|
||||
}
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_detector = new CheckDetector();
|
||||
_board = new Piece[8, 8];
|
||||
_root = new GameObject("TestRoot");
|
||||
}
|
||||
|
||||
// ==================== NULL SAFETY ====================
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.IsInCheck(null, true));
|
||||
}
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
var go = new GameObject($"Piece_{typeof(T).Name}_{x}_{y}");
|
||||
go.transform.SetParent(_root.transform);
|
||||
var piece = go.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
piece.hasMoved = true; // avoid castling logic
|
||||
_board[x, y] = piece;
|
||||
return piece;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsCheckmate_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.IsCheckmate(null, true));
|
||||
}
|
||||
#region IsInCheck Tests
|
||||
|
||||
[Test]
|
||||
public void WouldLeaveKingInCheck_NullBoard_ReturnsTrue()
|
||||
{
|
||||
var pawn = CreatePiece<Pawn>(3, 3, true);
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board o piece es null.");
|
||||
Assert.IsTrue(_detector.WouldLeaveKingInCheck(null, pawn, new Vector2Int(3, 4)));
|
||||
}
|
||||
[Test]
|
||||
public void IsInCheck_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.IsInCheck(null, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WouldLeaveKingInCheck_NullPiece_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board o piece es null.");
|
||||
Assert.IsTrue(_detector.WouldLeaveKingInCheck(board, null, new Vector2Int(3, 4)));
|
||||
}
|
||||
[Test]
|
||||
public void IsInCheck_NoKingOnBoard_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasAnyLegalMove_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.HasAnyLegalMove(null, true));
|
||||
}
|
||||
[Test]
|
||||
public void IsInCheck_WhiteKingNotAttacked_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(0, 0, true);
|
||||
|
||||
// ==================== BASIC CHECK DETECTION ====================
|
||||
Assert.IsFalse(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_NoPieces_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
[Test]
|
||||
public void IsInCheck_WhiteKingAttackedByBlackRook_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(4, 5, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInCheck(board, true));
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_RookAttackingKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[4, 5] = CreatePiece<Rook>(4, 5, false);
|
||||
[Test]
|
||||
public void IsInCheck_WhiteKingAttackedByBlackBishop_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Bishop>(7, 3, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInCheck(board, true));
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_BishopAttackingKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 4] = CreatePiece<King>(4, 4, true);
|
||||
board[1, 1] = CreatePiece<Bishop>(1, 1, false);
|
||||
[Test]
|
||||
public void IsInCheck_WhiteKingAttackedByBlackKnight_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Knight>(2, 1, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInCheck(board, true));
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_KnightAttackingKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 4] = CreatePiece<King>(4, 4, true);
|
||||
board[2, 3] = CreatePiece<Knight>(2, 3, false);
|
||||
[Test]
|
||||
public void IsInCheck_WhiteKingAttackedByBlackPawn_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 4, true);
|
||||
CreatePiece<Pawn>(3, 5, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsCheckmate(board, false) == false); // just sanity
|
||||
Assert.IsTrue(_detector.IsInCheck(board, true));
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInCheck_FriendlyPieceBlocking_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[4, 3] = CreatePiece<Pawn>(4, 3, true); // blocks rook
|
||||
board[4, 7] = CreatePiece<Rook>(4, 7, false);
|
||||
[Test]
|
||||
public void IsInCheck_BlackKingAttackedByWhiteRook_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 7, false);
|
||||
CreatePiece<Rook>(4, 3, true);
|
||||
|
||||
Assert.IsFalse(_detector.IsInCheck(board, true));
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInCheck(_board, false));
|
||||
}
|
||||
|
||||
// ==================== CHECKMATE ====================
|
||||
[Test]
|
||||
public void IsInCheck_BlackKingNotAttacked_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 7, false);
|
||||
CreatePiece<Rook>(0, 7, false);
|
||||
|
||||
[Test]
|
||||
public void IsCheckmate_BackRankMate_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[0, 0] = CreatePiece<King>(0, 0, false);
|
||||
board[0, 5] = CreatePiece<Rook>(0, 5, true);
|
||||
board[2, 1] = CreatePiece<King>(2, 1, true);
|
||||
Assert.IsFalse(_detector.IsInCheck(_board, false));
|
||||
}
|
||||
|
||||
Assert.IsTrue(_detector.IsCheckmate(board, false));
|
||||
}
|
||||
[Test]
|
||||
public void IsInCheck_PieceBlocksAttack_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
CreatePiece<Pawn>(4, 3, true);
|
||||
|
||||
[Test]
|
||||
public void IsCheckmate_KingCanEscape_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[0, 0] = CreatePiece<King>(0, 0, false);
|
||||
board[0, 5] = CreatePiece<Rook>(0, 5, true);
|
||||
Assert.IsFalse(_detector.IsInCheck(_board, true));
|
||||
}
|
||||
|
||||
Assert.IsFalse(_detector.IsCheckmate(board, false));
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Test]
|
||||
public void IsCheckmate_NotInCheck_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
#region IsCheckmate Tests
|
||||
|
||||
Assert.IsFalse(_detector.IsCheckmate(board, true));
|
||||
}
|
||||
[Test]
|
||||
public void IsCheckmate_NotInCheck_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(0, 0, true);
|
||||
|
||||
// ==================== HAS ANY LEGAL MOVE ====================
|
||||
Assert.IsFalse(_detector.IsCheckmate(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasAnyLegalMove_KingCanMove_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 4] = CreatePiece<King>(4, 4, true);
|
||||
[Test]
|
||||
public void IsCheckmate_KingCanEscape_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(0, 0, true);
|
||||
CreatePiece<Rook>(7, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.HasAnyLegalMove(board, true));
|
||||
}
|
||||
Assert.IsFalse(_detector.IsCheckmate(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasAnyLegalMove_KingSurroundedByOwnPieces_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 7] = CreatePiece<King>(4, 7, true);
|
||||
// Surround with own pawns on the last rank — pawns can't move forward (off board)
|
||||
// and forward squares are occupied, so no piece has legal moves.
|
||||
board[3, 6] = CreatePiece<Pawn>(3, 6, true);
|
||||
board[3, 7] = CreatePiece<Pawn>(3, 7, true);
|
||||
board[4, 6] = CreatePiece<Pawn>(4, 6, true);
|
||||
board[5, 6] = CreatePiece<Pawn>(5, 6, true);
|
||||
board[5, 7] = CreatePiece<Pawn>(5, 7, true);
|
||||
#endregion
|
||||
|
||||
Assert.IsFalse(_detector.HasAnyLegalMove(board, true));
|
||||
}
|
||||
#region WouldLeaveKingInCheck Tests
|
||||
|
||||
// ==================== HELPER ====================
|
||||
[Test]
|
||||
public void WouldLeaveKingInCheck_NullBoard_ReturnsTrue()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
LogAssert.Expect(LogType.Error, "[CheckDetector] Board o piece es null.");
|
||||
Assert.IsTrue(_detector.WouldLeaveKingInCheck(null, king, new Vector2Int(4, 1)));
|
||||
}
|
||||
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
var obj = new GameObject($"Test_{typeof(T).Name}_{x}_{y}");
|
||||
var piece = obj.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
return piece;
|
||||
[Test]
|
||||
public void WouldLeaveKingInCheck_SafeMove_ReturnsFalse()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_detector.WouldLeaveKingInCheck(_board, king, new Vector2Int(3, 0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WouldLeaveKingInCheck_ExposesKingToCheck_ReturnsTrue()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 3, true);
|
||||
var blocker = CreatePiece<Pawn>(4, 5, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.WouldLeaveKingInCheck(_board, blocker, new Vector2Int(3, 5)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HasAnyLegalMove Tests
|
||||
|
||||
[Test]
|
||||
public void HasAnyLegalMove_EmptyBoard_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(_detector.HasAnyLegalMove(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasAnyLegalMove_KingCanMove_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
Assert.IsTrue(_detector.HasAnyLegalMove(_board, true));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b0cefc7579a0694b816724477d445b8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,538 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests para DialogueSystem - Sistema de branching condicional.
|
||||
/// Cubre: piece_alive, pieces_lost_count, chapter_complete conditions.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("DialogueSystem")]
|
||||
[Category("Branching")]
|
||||
[Category("FastTests")]
|
||||
public class DialogueSystemBranchingTests
|
||||
{
|
||||
private DialogueSystem _dialogueSystem;
|
||||
private CampaignState _campaignState;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Crear GameObject con DialogueSystem
|
||||
var go = new GameObject("TestDialogueSystem");
|
||||
_dialogueSystem = go.AddComponent<DialogueSystem>();
|
||||
|
||||
// Crear CampaignState
|
||||
_campaignState = ScriptableObject.CreateInstance<CampaignState>();
|
||||
|
||||
// Inyectar CampaignState en DialogueSystem usando reflection
|
||||
var field = typeof(DialogueSystem).GetField("_campaignState",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
{
|
||||
field.SetValue(_dialogueSystem, _campaignState);
|
||||
}
|
||||
|
||||
// Inyectar un diálogo vacío: GetNextNode busca nodos en
|
||||
// _currentDialogue.nodes y NREaría sin esto.
|
||||
var currentDialogueField = typeof(DialogueSystem).GetField("_currentDialogue",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
if (currentDialogueField != null)
|
||||
{
|
||||
currentDialogueField.SetValue(_dialogueSystem, new DialogueData
|
||||
{
|
||||
id = "test_dialogue",
|
||||
nodes = new List<DialogueNode>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
if (_dialogueSystem != null)
|
||||
{
|
||||
Object.DestroyImmediate(_dialogueSystem.gameObject);
|
||||
}
|
||||
if (_campaignState != null)
|
||||
{
|
||||
Object.DestroyImmediate(_campaignState);
|
||||
}
|
||||
}
|
||||
|
||||
#region piece_alive Condition Tests
|
||||
|
||||
[Test]
|
||||
public void CheckPieceAlive_WhenAlive_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("piece_alive", "elena");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "CheckCondition debería retornar true para pieza viva");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckPieceAlive_WhenDead_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
_campaignState.RegisterPieceLost("elena");
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("piece_alive", "elena");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "CheckCondition debería retornar false para pieza muerta");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Elena")]
|
||||
[TestCase("ELENA")]
|
||||
[TestCase("elena")]
|
||||
[TestCase("ElEnA")]
|
||||
public void CheckPieceAlive_CaseInsensitive(string pieceName)
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("piece_alive", pieceName);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, $"CheckPieceAlive debería ser case-insensitive para '{pieceName}'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_PieceAlive_SelectsCorrectNode()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>
|
||||
{
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "piece_alive",
|
||||
param = "elena",
|
||||
nextNode = "elena_alive_node"
|
||||
}
|
||||
},
|
||||
next = "default_node"
|
||||
};
|
||||
|
||||
// Act
|
||||
string nextNodeId = InvokeEvaluateBranching(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("elena_alive_node", nextNodeId, "Debería seleccionar nodo de Elena viva");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region pieces_lost_count Condition Tests
|
||||
|
||||
[Test]
|
||||
[TestCase("<3", 2, true)]
|
||||
[TestCase("<3", 3, false)]
|
||||
[TestCase("<3", 4, false)]
|
||||
public void CheckPiecesLostCount_LessThan(string param, int piecesLost, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(piecesLost);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("pieces_lost_count", param);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result, $"pieces_lost_count {param} con {piecesLost} pérdidas debería ser {expected}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("<=3", 2, true)]
|
||||
[TestCase("<=3", 3, true)]
|
||||
[TestCase("<=3", 4, false)]
|
||||
public void CheckPiecesLostCount_LessOrEqual(string param, int piecesLost, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(piecesLost);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("pieces_lost_count", param);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result, $"pieces_lost_count {param} con {piecesLost} pérdidas debería ser {expected}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(">3", 4, true)]
|
||||
[TestCase(">3", 3, false)]
|
||||
[TestCase(">3", 2, false)]
|
||||
public void CheckPiecesLostCount_GreaterThan(string param, int piecesLost, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(piecesLost);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("pieces_lost_count", param);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result, $"pieces_lost_count {param} con {piecesLost} pérdidas debería ser {expected}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(">=3", 2, false)]
|
||||
[TestCase(">=3", 3, true)]
|
||||
[TestCase(">=3", 4, true)]
|
||||
public void CheckPiecesLostCount_GreaterOrEqual(string param, int piecesLost, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(piecesLost);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("pieces_lost_count", param);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result, $"pieces_lost_count {param} con {piecesLost} pérdidas debería ser {expected}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("==3", 3, true)]
|
||||
[TestCase("==3", 2, false)]
|
||||
[TestCase("==3", 4, false)]
|
||||
public void CheckPiecesLostCount_Equals(string param, int piecesLost, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(piecesLost);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("pieces_lost_count", param);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result, $"pieces_lost_count {param} con {piecesLost} pérdidas debería ser {expected}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_PiecesLostCount_SelectsCorrectNode()
|
||||
{
|
||||
// Arrange
|
||||
SetupPiecesLost(2);
|
||||
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>
|
||||
{
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "pieces_lost_count",
|
||||
param = "<3",
|
||||
nextNode = "few_losses_node"
|
||||
}
|
||||
},
|
||||
next = "many_losses_node"
|
||||
};
|
||||
|
||||
// Act
|
||||
string nextNodeId = InvokeEvaluateBranching(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("few_losses_node", nextNodeId, "Debería seleccionar nodo de pocas pérdidas");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region chapter_complete Condition Tests
|
||||
|
||||
[Test]
|
||||
public void CheckChapterComplete_WhenComplete_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.CompleteChapter("ch1_factory", wasVictory: true);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("chapter_complete", "ch1_factory");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "CheckCondition debería retornar true para capítulo completado");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckChapterComplete_WhenNotComplete_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("chapter_complete", "ch1_factory");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "CheckCondition debería retornar false para capítulo no completado");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckChapterComplete_Ch3Victory_ReturnsLastChapterWasVictory()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.CompleteChapter("ch3_court", wasVictory: true);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("chapter_complete", "ch3");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "chapter_complete('ch3') debería retornar LastChapterWasVictory cuando es victoria");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckChapterComplete_Ch3Defeat_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.CompleteChapter("ch3_court", wasVictory: false);
|
||||
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("chapter_complete", "ch3");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "chapter_complete('ch3') debería retornar false cuando es derrota");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_ChapterComplete_SelectsCorrectNode()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.CompleteChapter("ch3_court", wasVictory: true);
|
||||
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>
|
||||
{
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "chapter_complete",
|
||||
param = "ch3",
|
||||
nextNode = "victory_node"
|
||||
}
|
||||
},
|
||||
next = "defeat_node"
|
||||
};
|
||||
|
||||
// Act
|
||||
string nextNodeId = InvokeEvaluateBranching(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("victory_node", nextNodeId, "Debería seleccionar nodo de victoria");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases Tests
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_NoConditions_ReturnsDirectNext()
|
||||
{
|
||||
// Arrange
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = null,
|
||||
next = "next_node"
|
||||
};
|
||||
RegisterNode("next_node");
|
||||
|
||||
// Act: GetNextNode con condiciones null salta directo a 'next'
|
||||
var nextNode = InvokeGetNextNode(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(nextNode, "Debería encontrar el nodo 'next_node' en el diálogo actual");
|
||||
Assert.AreEqual("next_node", nextNode.id, "Sin condiciones debería retornar directamente 'next'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_EmptyConditions_ReturnsDirectNext()
|
||||
{
|
||||
// Arrange
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>(),
|
||||
next = "next_node"
|
||||
};
|
||||
RegisterNode("next_node");
|
||||
|
||||
// Act: GetNextNode con lista vacía salta directo a 'next'
|
||||
var nextNode = InvokeGetNextNode(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(nextNode, "Debería encontrar el nodo 'next_node' en el diálogo actual");
|
||||
Assert.AreEqual("next_node", nextNode.id, "Condiciones vacías debería retornar directamente 'next'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_MultipleConditions_FirstMatchWins()
|
||||
{
|
||||
// Arrange
|
||||
_campaignState.RegisterPieceAlive("elena");
|
||||
_campaignState.RegisterPieceAlive("ricardo");
|
||||
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>
|
||||
{
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "piece_alive",
|
||||
param = "elena",
|
||||
nextNode = "elena_node"
|
||||
},
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "piece_alive",
|
||||
param = "ricardo",
|
||||
nextNode = "ricardo_node"
|
||||
}
|
||||
},
|
||||
next = "default_node"
|
||||
};
|
||||
|
||||
// Act
|
||||
string nextNodeId = InvokeEvaluateBranching(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("elena_node", nextNodeId, "Primera condición que cumple debería ganar");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateBranching_NoMatchingCondition_ReturnsDefault()
|
||||
{
|
||||
// Arrange (Elena NO está viva)
|
||||
var currentNode = new DialogueNode
|
||||
{
|
||||
id = "test_node",
|
||||
conditions = new List<DialogueCondition>
|
||||
{
|
||||
new DialogueCondition
|
||||
{
|
||||
type = "piece_alive",
|
||||
param = "elena",
|
||||
nextNode = "elena_node"
|
||||
}
|
||||
},
|
||||
next = "default_node"
|
||||
};
|
||||
|
||||
// Act
|
||||
string nextNodeId = InvokeEvaluateBranching(currentNode);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("default_node", nextNodeId, "Sin condiciones que cumplan debería retornar default");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckCondition_UnknownType_ReturnsDefault()
|
||||
{
|
||||
// Act
|
||||
bool result = InvokeCheckCondition("unknown_type", "some_param");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Tipo de condición desconocido debería retornar false");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Configura el CampaignState para tener X piezas perdidas.
|
||||
/// </summary>
|
||||
private void SetupPiecesLost(int count)
|
||||
{
|
||||
_campaignState.StartChapter("test_chapter");
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_campaignState.RegisterPieceLost($"piece_{i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoca el método privado CheckCondition usando reflection.
|
||||
/// </summary>
|
||||
private bool InvokeCheckCondition(string type, string param)
|
||||
{
|
||||
var method = typeof(DialogueSystem).GetMethod("CheckCondition",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
Assert.Fail("Método CheckCondition no encontrado");
|
||||
return false;
|
||||
}
|
||||
|
||||
var condition = new DialogueCondition { type = type, param = param };
|
||||
return (bool)method.Invoke(_dialogueSystem, new object[] { condition });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoca el método privado GetNextNode usando reflection.
|
||||
/// Devuelve el nodo resultante (firma real: DialogueNode, no string).
|
||||
/// Requiere que el nodo destino exista en _currentDialogue.nodes.
|
||||
/// </summary>
|
||||
private DialogueNode InvokeGetNextNode(DialogueNode node)
|
||||
{
|
||||
var method = typeof(DialogueSystem).GetMethod("GetNextNode",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
Assert.Fail("Método GetNextNode no encontrado");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (DialogueNode)method.Invoke(_dialogueSystem, new object[] { node });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoca el método privado EvaluateBranching usando reflection.
|
||||
/// Devuelve el ID del siguiente nodo según las condiciones.
|
||||
/// </summary>
|
||||
private string InvokeEvaluateBranching(DialogueNode node)
|
||||
{
|
||||
var method = typeof(DialogueSystem).GetMethod("EvaluateBranching",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
Assert.Fail("Método EvaluateBranching no encontrado");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)method.Invoke(_dialogueSystem, new object[] { node });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registra un nodo en el diálogo actual para que GetNextNode lo encuentre.
|
||||
/// </summary>
|
||||
private void RegisterNode(string nodeId)
|
||||
{
|
||||
var field = typeof(DialogueSystem).GetField("_currentDialogue",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance);
|
||||
var dialogue = (DialogueData)field.GetValue(_dialogueSystem);
|
||||
dialogue.nodes.Add(new DialogueNode { id = nodeId });
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b624fbead9a641438b1e181a3924821
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests del typewriter effect (Suite 4 del plan de automatización).
|
||||
///
|
||||
/// Estrategia EditMode: la coroutine TypewriterEffect se invoca por reflexión y
|
||||
/// se conduce manualmente con MoveNext(). Cada MoveNext ejecuta un carácter
|
||||
/// (OnTextUpdated) y devuelve el WaitForSecondsRealtime sin necesidad de frames,
|
||||
/// lo que hace los tests deterministas.
|
||||
///
|
||||
/// Nota: AddComponent en EditMode NO invoca Awake, así que la instancia es
|
||||
/// aislada (sin singleton ni carga de Resources).
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Dialogue")]
|
||||
[Category("Typewriter")]
|
||||
[Category("FastTests")]
|
||||
public class TypewriterTests
|
||||
{
|
||||
private GameObject _root;
|
||||
private DialogueSystem _dialogue;
|
||||
|
||||
private MethodInfo _typewriterMethod;
|
||||
private FieldInfo _isActiveField;
|
||||
private FieldInfo _currentNodeField;
|
||||
private FieldInfo _charsPerSecondField;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_root = new GameObject("TypewriterTestRoot");
|
||||
_dialogue = _root.AddComponent<DialogueSystem>();
|
||||
|
||||
const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
_typewriterMethod = typeof(DialogueSystem).GetMethod("TypewriterEffect", flags);
|
||||
_isActiveField = typeof(DialogueSystem).GetField("_isActive", flags);
|
||||
_currentNodeField = typeof(DialogueSystem).GetField("_currentNode", flags);
|
||||
_charsPerSecondField = typeof(DialogueSystem).GetField("_charsPerSecond", flags);
|
||||
|
||||
Assert.IsNotNull(_typewriterMethod, "TypewriterEffect no encontrado (¿renombrado?).");
|
||||
Assert.IsNotNull(_isActiveField, "_isActive no encontrado.");
|
||||
Assert.IsNotNull(_currentNodeField, "_currentNode no encontrado.");
|
||||
Assert.IsNotNull(_charsPerSecondField, "_charsPerSecond no encontrado.");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private IEnumerator StartTypewriter(DialogueNode node)
|
||||
{
|
||||
return (IEnumerator)_typewriterMethod.Invoke(_dialogue, new object[] { node });
|
||||
}
|
||||
|
||||
private class Capture
|
||||
{
|
||||
public List<string> Updates = new List<string>();
|
||||
public List<DialogueNode> Completions = new List<DialogueNode>();
|
||||
}
|
||||
|
||||
private Capture Subscribe()
|
||||
{
|
||||
var capture = new Capture();
|
||||
_dialogue.OnTextUpdated += t => capture.Updates.Add(t);
|
||||
_dialogue.OnNodeComplete += n => capture.Completions.Add(n);
|
||||
return capture;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Tests
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Typewriter_RevealsGrowingPrefixes_InOrder()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "narrator", text = "Hola purgatorio" };
|
||||
Capture cap = Subscribe();
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
while (e.MoveNext()) { }
|
||||
|
||||
Assert.AreEqual(node.text.Length + 1, cap.Updates.Count,
|
||||
"Debe emitir un update por carácter incluyendo el texto vacío inicial.");
|
||||
|
||||
for (int i = 0; i < cap.Updates.Count; i++)
|
||||
Assert.AreEqual(node.text.Substring(0, i), cap.Updates[i],
|
||||
$"Update #{i} debe ser el prefijo de longitud {i}.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Typewriter_FiresNodeComplete_ExactlyOnce_AtEnd()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "muerte", text = "Otra partida" };
|
||||
Capture cap = Subscribe();
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
|
||||
Assert.AreEqual(0, cap.Completions.Count, "No debe completar antes de empezar.");
|
||||
|
||||
while (e.MoveNext()) { }
|
||||
|
||||
Assert.AreEqual(1, cap.Completions.Count);
|
||||
Assert.AreEqual(node, cap.Completions[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Typewriter_IsTyping_TrueWhileRevealing_FalseAfterEnd()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "narrator", text = "abc" };
|
||||
Subscribe();
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
Assert.IsFalse(_dialogue.IsTyping, "Antes del primer paso no está escribiendo.");
|
||||
|
||||
e.MoveNext();
|
||||
Assert.IsTrue(_dialogue.IsTyping, "Tras revelar el primer carácter sigue escribiendo.");
|
||||
|
||||
while (e.MoveNext()) { }
|
||||
Assert.IsFalse(_dialogue.IsTyping, "Al terminar debe apagar el flag.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Typewriter_EmptyText_CompletesImmediately_WithSingleEmptyUpdate()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "narrator", text = "" };
|
||||
Capture cap = Subscribe();
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
|
||||
// Con texto vacío el bucle no hace ningún yield: la primera pasada
|
||||
// ejecuta todo el cuerpo y la coroutine termina (MoveNext == false).
|
||||
Assert.IsFalse(e.MoveNext(), "Texto vacío completa en la primera pasada sin yields.");
|
||||
|
||||
Assert.AreEqual(1, cap.Updates.Count);
|
||||
Assert.AreEqual("", cap.Updates[0]);
|
||||
Assert.AreEqual(1, cap.Completions.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompleteCurrentNode_MidTyping_ShowsFullText_AndFiresSingleCompletion()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "muerte", text = "Texto largo de prueba" };
|
||||
Capture cap = Subscribe();
|
||||
|
||||
_isActiveField.SetValue(_dialogue, true);
|
||||
_currentNodeField.SetValue(_dialogue, node);
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
e.MoveNext(); // primer carácter
|
||||
Assert.IsTrue(_dialogue.IsTyping);
|
||||
|
||||
_dialogue.CompleteCurrentNode();
|
||||
|
||||
Assert.IsFalse(_dialogue.IsTyping, "Skip debe apagar IsTyping.");
|
||||
Assert.AreEqual(1, cap.Completions.Count, "El skip dispara exactamente una completion.");
|
||||
Assert.AreEqual(node.text, cap.Updates[cap.Updates.Count - 1],
|
||||
"El último update tras el skip es el texto completo.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompleteCurrentNode_NotTyping_IsNoOp()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "narrator", text = "sin typing" };
|
||||
Capture cap = Subscribe();
|
||||
|
||||
_isActiveField.SetValue(_dialogue, true);
|
||||
_currentNodeField.SetValue(_dialogue, node);
|
||||
// _isTyping permanece false: no arrancamos la coroutine.
|
||||
|
||||
_dialogue.CompleteCurrentNode();
|
||||
|
||||
Assert.AreEqual(0, cap.Completions.Count, "Sin typing no hay completion.");
|
||||
Assert.AreEqual(0, cap.Updates.Count, "Sin typing no hay updates.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTextSpeed_ClampsToConfiguredRange()
|
||||
{
|
||||
_dialogue.SetTextSpeed(5f);
|
||||
Assert.AreEqual(10f, (float)_charsPerSecondField.GetValue(_dialogue),
|
||||
"Velocidad mínima: 10 cps.");
|
||||
|
||||
_dialogue.SetTextSpeed(500f);
|
||||
Assert.AreEqual(100f, (float)_charsPerSecondField.GetValue(_dialogue),
|
||||
"Velocidad máxima: 100 cps.");
|
||||
|
||||
_dialogue.SetTextSpeed(45f);
|
||||
Assert.AreEqual(45f, (float)_charsPerSecondField.GetValue(_dialogue),
|
||||
"Valores dentro de rango pasan intactos.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Typewriter_YieldsRealtimeWait_BetweenCharacters()
|
||||
{
|
||||
var node = new DialogueNode { id = "n1", speaker = "narrator", text = "ab" };
|
||||
Subscribe();
|
||||
|
||||
IEnumerator e = StartTypewriter(node);
|
||||
|
||||
Assert.IsTrue(e.MoveNext(), "Primer update inmediato.");
|
||||
Assert.IsInstanceOf<WaitForSecondsRealtime>(e.Current,
|
||||
"Entre caracteres debe esperar tiempo real (inmune a timeScale=0).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de634a17bf1150a4fb002c69d1be7cc6
|
||||
@@ -1,245 +1,316 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for DrawDetector — position hashing, halfmove clock, insufficient material, reset.
|
||||
/// These tests do NOT depend on piece movement logic (GetAvailableMoves) so they are pure.
|
||||
/// </summary>
|
||||
public class DrawDetectorTests
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Chess
|
||||
{
|
||||
private DrawDetector _detector;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Chess")]
|
||||
[Category("DrawDetector")]
|
||||
[Category("FastTests")]
|
||||
public class DrawDetectorTests
|
||||
{
|
||||
_detector = new DrawDetector();
|
||||
}
|
||||
private DrawDetector _detector;
|
||||
private CheckDetector _checkDetector;
|
||||
private Piece[,] _board;
|
||||
private GameObject _root;
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_detector = null;
|
||||
}
|
||||
|
||||
// ==================== HALFMOVE CLOCK ====================
|
||||
|
||||
[Test]
|
||||
public void RecordMove_NonPawnNonCapture_IncrementsHalfmoveClock()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
_detector.RecordMove(board, true, king, wasCapture: false);
|
||||
|
||||
Assert.AreEqual(1, _detector.HalfmoveClock);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecordMove_PawnMove_ResetsHalfmoveClock()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var pawn = CreatePiece<Pawn>(3, 1, true);
|
||||
board[3, 1] = pawn;
|
||||
|
||||
_detector.RecordMove(board, true, pawn, wasCapture: false);
|
||||
|
||||
Assert.AreEqual(0, _detector.HalfmoveClock);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecordMove_Capture_ResetsHalfmoveClock()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var rook = CreatePiece<Rook>(0, 0, true);
|
||||
board[0, 0] = rook;
|
||||
|
||||
_detector.RecordMove(board, true, rook, wasCapture: true);
|
||||
|
||||
Assert.AreEqual(0, _detector.HalfmoveClock);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_After100HalfMoves_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
// Record 100 non-pawn, non-capture moves
|
||||
for (int i = 0; i < 100; i++)
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_detector.RecordMove(board, i % 2 == 0, king, wasCapture: false);
|
||||
_detector = new DrawDetector();
|
||||
_checkDetector = new CheckDetector();
|
||||
_board = new Piece[8, 8];
|
||||
_root = new GameObject("TestRoot");
|
||||
}
|
||||
|
||||
Assert.IsTrue(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_Before100HalfMoves_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
for (int i = 0; i < 99; i++)
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
_detector.RecordMove(board, i % 2 == 0, king, wasCapture: false);
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
// ==================== POSITION HISTORY ====================
|
||||
|
||||
[Test]
|
||||
public void IsThreefoldRepetition_LessThan3Positions_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
_detector.RecordMove(board, true, king, false);
|
||||
_detector.RecordMove(board, false, king, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PositionHistoryCount_IncreasesWithEachRecord()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
_detector.RecordMove(board, true, king, false);
|
||||
Assert.AreEqual(1, _detector.PositionHistoryCount);
|
||||
|
||||
_detector.RecordMove(board, false, king, false);
|
||||
Assert.AreEqual(2, _detector.PositionHistoryCount);
|
||||
}
|
||||
|
||||
// ==================== INSUFFICIENT MATERIAL ====================
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingVsKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[3, 2] = CreatePiece<Bishop>(3, 2, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingKnightVsKing_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[2, 1] = CreatePiece<Knight>(2, 1, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingRookVsKing_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[0, 0] = CreatePiece<Rook>(0, 0, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingPawnVsKing_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[3, 1] = CreatePiece<Pawn>(3, 1, true);
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKingBishop_SameColorSquare_ReturnsTrue()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[2, 2] = CreatePiece<Bishop>(2, 2, true); // light square (2+2=4, even)
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
board[5, 5] = CreatePiece<Bishop>(5, 5, false); // light square (5+5=10, even)
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKingBishop_DifferentColorSquare_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
board[4, 0] = CreatePiece<King>(4, 0, true);
|
||||
board[2, 2] = CreatePiece<Bishop>(2, 2, true); // light square (2+2=4, even)
|
||||
board[4, 7] = CreatePiece<King>(4, 7, false);
|
||||
board[5, 4] = CreatePiece<Bishop>(5, 4, false); // dark square (5+4=9, odd)
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[DrawDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(null));
|
||||
}
|
||||
|
||||
// ==================== RESET ====================
|
||||
|
||||
[Test]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var king = CreatePiece<King>(4, 4, true);
|
||||
board[4, 4] = king;
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 50; i++)
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
_detector.RecordMove(board, i % 2 == 0, king, false);
|
||||
var go = new GameObject($"Piece_{typeof(T).Name}_{x}_{y}");
|
||||
go.transform.SetParent(_root.transform);
|
||||
var piece = go.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
piece.hasMoved = true;
|
||||
_board[x, y] = piece;
|
||||
return piece;
|
||||
}
|
||||
|
||||
Assert.AreEqual(50, _detector.PositionHistoryCount);
|
||||
Assert.AreEqual(50, _detector.HalfmoveClock);
|
||||
#region IsInsufficientMaterial Tests
|
||||
|
||||
_detector.Reset();
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingVsKing_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.AreEqual(0, _detector.PositionHistoryCount);
|
||||
Assert.AreEqual(0, _detector.HalfmoveClock);
|
||||
}
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
// ==================== HELPER ====================
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKing_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Bishop>(2, 0, true);
|
||||
CreatePiece<King>(4, 7, false);
|
||||
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
var obj = new GameObject($"Test_{typeof(T).Name}_{x}_{y}");
|
||||
var piece = obj.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
return piece;
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingKnightVsKing_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Knight>(1, 0, true);
|
||||
CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKingBishop_SameSquareColor_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(0, 0, true);
|
||||
CreatePiece<Bishop>(2, 2, true);
|
||||
CreatePiece<King>(7, 7, false);
|
||||
CreatePiece<Bishop>(5, 5, false);
|
||||
|
||||
Assert.IsTrue(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingBishopVsKingBishop_DifferentSquareColors_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(0, 0, true);
|
||||
CreatePiece<Bishop>(2, 2, true);
|
||||
CreatePiece<King>(7, 7, false);
|
||||
CreatePiece<Bishop>(4, 5, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingRookVsKing_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Rook>(0, 0, true);
|
||||
CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_KingQueenVsKing_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
CreatePiece<Queen>(3, 0, true);
|
||||
CreatePiece<King>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(_board));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsInsufficientMaterial_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[DrawDetector] Board es null.");
|
||||
Assert.IsFalse(_detector.IsInsufficientMaterial(null));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsStalemate Tests
|
||||
|
||||
[Test]
|
||||
public void IsStalemate_NullBoard_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[DrawDetector] Board o checkDetector es null.");
|
||||
Assert.IsFalse(_detector.IsStalemate(null, true, _checkDetector));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsStalemate_NullCheckDetector_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[DrawDetector] Board o checkDetector es null.");
|
||||
Assert.IsFalse(_detector.IsStalemate(_board, true, null));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fifty Move Rule Tests
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_Initially_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_At99HalfMoves_ReturnsFalse()
|
||||
{
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, i % 2 == 0, null, false);
|
||||
}
|
||||
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_At100HalfMoves_ReturnsTrue()
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, i % 2 == 0, null, false);
|
||||
}
|
||||
|
||||
Assert.IsTrue(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_CaptureResetsClock()
|
||||
{
|
||||
for (int i = 0; i < 80; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, i % 2 == 0, null, false);
|
||||
}
|
||||
|
||||
var go = new GameObject();
|
||||
var pawn = go.AddComponent<Pawn>();
|
||||
pawn.isWhite = true;
|
||||
_detector.RecordMove(_board, true, pawn, true);
|
||||
|
||||
Assert.Less(_detector.HalfmoveClock, 100);
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFiftyMoveRule_PawnMoveResetsClock()
|
||||
{
|
||||
for (int i = 0; i < 80; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, i % 2 == 0, null, false);
|
||||
}
|
||||
|
||||
var go = new GameObject();
|
||||
var pawn = go.AddComponent<Pawn>();
|
||||
pawn.isWhite = true;
|
||||
_detector.RecordMove(_board, true, pawn, false);
|
||||
|
||||
Assert.AreEqual(0, _detector.HalfmoveClock);
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Threefold Repetition Tests
|
||||
|
||||
[Test]
|
||||
public void IsThreefoldRepetition_Initially_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsThreefoldRepetition_SamePositionThreeTimes_ReturnsTrue()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
var bk = CreatePiece<King>(4, 7, false);
|
||||
|
||||
// Mismo turno en las tres repeticiones: el hash incluye el turno,
|
||||
// así que alternar blancas/negras serían posiciones distintas.
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
}
|
||||
|
||||
Assert.IsTrue(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsThreefoldRepetition_DifferentPositions_ReturnsFalse()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
var bk = CreatePiece<King>(4, 7, false);
|
||||
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
|
||||
king.currentPos = new Vector2Int(4, 1);
|
||||
_board[4, 0] = null;
|
||||
_board[4, 1] = king;
|
||||
_detector.RecordMove(_board, false, bk, false);
|
||||
|
||||
king.currentPos = new Vector2Int(4, 2);
|
||||
_board[4, 1] = null;
|
||||
_board[4, 2] = king;
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Test]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
_detector.RecordMove(_board, i % 2 == 0, null, false);
|
||||
}
|
||||
|
||||
_detector.Reset();
|
||||
|
||||
Assert.AreEqual(0, _detector.HalfmoveClock);
|
||||
Assert.AreEqual(0, _detector.PositionHistoryCount);
|
||||
Assert.IsFalse(_detector.IsFiftyMoveRule());
|
||||
Assert.IsFalse(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Position Hash Stability Tests
|
||||
|
||||
[Test]
|
||||
public void PositionHash_SameBoardSameTurn_SameHash()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
var bk = CreatePiece<King>(4, 7, false);
|
||||
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
int countAfterFirst = _detector.PositionHistoryCount;
|
||||
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
int countAfterSecond = _detector.PositionHistoryCount;
|
||||
|
||||
Assert.AreEqual(2, countAfterSecond);
|
||||
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
|
||||
Assert.AreEqual(3, _detector.PositionHistoryCount);
|
||||
Assert.IsTrue(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PositionHash_DifferentTurn_DifferentHash()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
var bk = CreatePiece<King>(4, 7, false);
|
||||
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
_detector.RecordMove(_board, false, bk, false);
|
||||
_detector.RecordMove(_board, true, king, false);
|
||||
|
||||
Assert.IsFalse(_detector.IsThreefoldRepetition());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41c1d4e4ee64c524bb96876d2947fdd5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,357 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using AjedrezPurgatorio.Meta;
|
||||
using AjedrezPurgatorio.Data;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Meta
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for the DeadKingPool system.
|
||||
/// Tests: AddDeadKing, GetRandomDeadKing, ShouldSpawnDeadKing,
|
||||
/// FIFO rotation, content filtering, persistence.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class DeadKingPoolTests
|
||||
{
|
||||
private DeadKingPool _pool;
|
||||
private string _testFilePath;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Create a test instance of DeadKingPool
|
||||
_pool = ScriptableObject.CreateInstance<DeadKingPool>();
|
||||
|
||||
// Mock persistent file path for testing
|
||||
_testFilePath = Path.Combine(Application.temporaryCachePath, "test_dead_kings.json");
|
||||
|
||||
// Clear any existing test file
|
||||
if (File.Exists(_testFilePath))
|
||||
File.Delete(_testFilePath);
|
||||
|
||||
// Redirigir la ruta persistente al archivo de prueba: sin esto el pool
|
||||
// cargaría el dead_kings.json real del jugador y los tests no serían
|
||||
// deterministas.
|
||||
SetPersistentPath(_pool, _testFilePath);
|
||||
|
||||
// Initialize the pool (will load from empty state)
|
||||
_pool.Reinitialize();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
// Clean up test file
|
||||
if (File.Exists(_testFilePath))
|
||||
File.Delete(_testFilePath);
|
||||
|
||||
// Destroy test pool instance
|
||||
if (_pool != null)
|
||||
Object.DestroyImmediate(_pool);
|
||||
}
|
||||
|
||||
private static void SetPersistentPath(DeadKingPool pool, string path)
|
||||
{
|
||||
var field = typeof(DeadKingPool).GetField("_persistentFilePath",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
Assert.IsNotNull(field, "_persistentFilePath no encontrado en DeadKingPool.");
|
||||
field.SetValue(pool, path);
|
||||
}
|
||||
|
||||
#region AddDeadKing Tests
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_AddsToPool()
|
||||
{
|
||||
// Arrange
|
||||
var testDK = CreateTestDeadKing("TestPlayer", "Test message", 1, 3);
|
||||
|
||||
// Act
|
||||
_pool.AddDeadKing(testDK);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, _pool.Count, "Pool should contain 1 Dead King after adding");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_WithNullData_LogsErrorAndDoesNotAdd()
|
||||
{
|
||||
// Assert (log esperado antes del Act)
|
||||
UnityEngine.TestTools.LogAssert.Expect(LogType.Error, "[DeadKingPool] Cannot add null Dead King data.");
|
||||
|
||||
// Act
|
||||
_pool.AddDeadKing(null);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, _pool.Count, "Pool should remain empty when adding null data");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_WithEmptyName_UsesAnonimo()
|
||||
{
|
||||
// Arrange
|
||||
var testDK = CreateTestDeadKing("", "Test message", 1, 3);
|
||||
|
||||
// Act
|
||||
_pool.AddDeadKing(testDK);
|
||||
var retrieved = _pool.GetRandomDeadKing();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Anónimo", retrieved.playerName, "Empty name should default to 'Anónimo'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_FiltersProfanity_InName()
|
||||
{
|
||||
// Arrange
|
||||
var testDK = CreateTestDeadKing("TestFuckPlayer", "Clean message", 1, 3);
|
||||
|
||||
// Act
|
||||
_pool.AddDeadKing(testDK);
|
||||
var retrieved = _pool.GetRandomDeadKing();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(retrieved.playerName.Contains("***"), "Profanity in name should be filtered");
|
||||
Assert.IsFalse(retrieved.playerName.Contains("Fuck"), "Original profanity should not appear");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_FiltersProfanity_InMessage()
|
||||
{
|
||||
// Arrange
|
||||
var testDK = CreateTestDeadKing("TestPlayer", "This shit is hard", 1, 3);
|
||||
|
||||
// Act
|
||||
_pool.AddDeadKing(testDK);
|
||||
var retrieved = _pool.GetRandomDeadKing();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(retrieved.message.Contains("***"), "Profanity in message should be filtered");
|
||||
Assert.IsFalse(retrieved.message.Contains("shit"), "Original profanity should not appear");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FIFO Rotation Tests
|
||||
|
||||
[Test]
|
||||
public void AddDeadKing_WhenPoolFull_RemovesOldest()
|
||||
{
|
||||
// Arrange: Fill pool to max capacity (assuming max is 100)
|
||||
// For testing, we'll use reflection to set a smaller max size or add many DKs
|
||||
// Simplified: Add 101 DKs and verify the first one is removed
|
||||
|
||||
// Add 100 DKs
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var dk = CreateTestDeadKing($"Player{i}", $"Message {i}", 0, i);
|
||||
_pool.AddDeadKing(dk);
|
||||
}
|
||||
|
||||
Assert.AreEqual(100, _pool.Count, "Pool should be at max capacity (100)");
|
||||
|
||||
// Act: Add one more (should trigger FIFO removal)
|
||||
var newDK = CreateTestDeadKing("NewPlayer", "New message", 2, 5);
|
||||
_pool.AddDeadKing(newDK);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(100, _pool.Count, "Pool should still be at max capacity after FIFO");
|
||||
|
||||
// Verify the new DK is in the pool
|
||||
var allDKs = _pool.GetAllDeadKings();
|
||||
bool containsNew = allDKs.Exists(dk => dk.playerName == "NewPlayer");
|
||||
Assert.IsTrue(containsNew, "Newly added DK should be in the pool");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetRandomDeadKing Tests
|
||||
|
||||
[Test]
|
||||
public void GetRandomDeadKing_WhenPoolEmpty_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = _pool.GetRandomDeadKing();
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result, "Should return null when pool is empty");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRandomDeadKing_WhenPoolHasOne_ReturnsThatOne()
|
||||
{
|
||||
// Arrange
|
||||
var testDK = CreateTestDeadKing("OnlyPlayer", "Only message", 1, 3);
|
||||
_pool.AddDeadKing(testDK);
|
||||
|
||||
// Act
|
||||
var result = _pool.GetRandomDeadKing();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result, "Should return a Dead King");
|
||||
Assert.AreEqual("OnlyPlayer", result.playerName, "Should return the only Dead King in pool");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRandomDeadKing_WhenPoolHasMultiple_ReturnsRandomly()
|
||||
{
|
||||
// Arrange: Add 10 different DKs
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var dk = CreateTestDeadKing($"Player{i}", $"Message {i}", 0, i);
|
||||
_pool.AddDeadKing(dk);
|
||||
}
|
||||
|
||||
// Act: Get multiple random DKs and verify we get different ones
|
||||
var results = new HashSet<string>();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var dk = _pool.GetRandomDeadKing();
|
||||
results.Add(dk.playerName);
|
||||
}
|
||||
|
||||
// Assert: We should have gotten more than 1 unique DK (randomness)
|
||||
Assert.Greater(results.Count, 1, "Should return different Dead Kings over multiple calls");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShouldSpawnDeadKing Tests
|
||||
|
||||
[Test]
|
||||
public void ShouldSpawnDeadKing_WhenPoolEmpty_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
bool shouldSpawn = _pool.ShouldSpawnDeadKing(0);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(shouldSpawn, "Should not spawn when pool is empty");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldSpawnDeadKing_Chapter0_HasBaseChance()
|
||||
{
|
||||
// Arrange: Add one DK
|
||||
_pool.AddDeadKing(CreateTestDeadKing("Test", "Test", 0, 0));
|
||||
|
||||
// Act: Run spawn check many times to verify probability
|
||||
int spawnCount = 0;
|
||||
int iterations = 1000;
|
||||
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
if (_pool.ShouldSpawnDeadKing(0))
|
||||
spawnCount++;
|
||||
}
|
||||
|
||||
// Assert: Should be around 20% (0.2 base chance)
|
||||
float spawnRate = (float)spawnCount / iterations;
|
||||
Assert.Greater(spawnRate, 0.10f, "Spawn rate should be greater than 10%");
|
||||
Assert.Less(spawnRate, 0.30f, "Spawn rate should be less than 30%");
|
||||
// Expected: ~20% with some variance
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldSpawnDeadKing_Chapter1_HasHigherChance()
|
||||
{
|
||||
// Arrange
|
||||
_pool.AddDeadKing(CreateTestDeadKing("Test", "Test", 0, 0));
|
||||
|
||||
// Act
|
||||
int spawnCount = 0;
|
||||
int iterations = 1000;
|
||||
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
if (_pool.ShouldSpawnDeadKing(1))
|
||||
spawnCount++;
|
||||
}
|
||||
|
||||
// Assert: Should be around 40% (0.2 base + 0.2 * 1)
|
||||
float spawnRate = (float)spawnCount / iterations;
|
||||
Assert.Greater(spawnRate, 0.30f, "Ch1 spawn rate should be higher than base");
|
||||
Assert.Less(spawnRate, 0.50f, "Ch1 spawn rate should be less than 50%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldSpawnDeadKing_Chapter2_HasEvenHigherChance()
|
||||
{
|
||||
// Arrange
|
||||
_pool.AddDeadKing(CreateTestDeadKing("Test", "Test", 0, 0));
|
||||
|
||||
// Act
|
||||
int spawnCount = 0;
|
||||
int iterations = 1000;
|
||||
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
if (_pool.ShouldSpawnDeadKing(2))
|
||||
spawnCount++;
|
||||
}
|
||||
|
||||
// Assert: Should be around 60% (0.2 base + 0.2 * 2)
|
||||
float spawnRate = (float)spawnCount / iterations;
|
||||
Assert.Greater(spawnRate, 0.50f, "Ch2 spawn rate should be ~60%");
|
||||
Assert.Less(spawnRate, 0.70f, "Ch2 spawn rate should be less than 70%");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Persistence Tests
|
||||
|
||||
[Test]
|
||||
public void SaveAndLoad_Roundtrip_PreservesData()
|
||||
{
|
||||
// Arrange: Add multiple DKs
|
||||
_pool.AddDeadKing(CreateTestDeadKing("Player1", "Message1", 1, 3));
|
||||
_pool.AddDeadKing(CreateTestDeadKing("Player2", "Message2", 2, 5));
|
||||
|
||||
// Act: Save
|
||||
_pool.SavePool();
|
||||
|
||||
// Create new pool instance and load from the SAME redirected test file
|
||||
var newPool = ScriptableObject.CreateInstance<DeadKingPool>();
|
||||
SetPersistentPath(newPool, _testFilePath);
|
||||
newPool.Reinitialize();
|
||||
newPool.LoadPool();
|
||||
|
||||
// Assert: Data should match
|
||||
Assert.AreEqual(_pool.Count, newPool.Count, "Loaded pool should have same count");
|
||||
|
||||
var originalDKs = _pool.GetAllDeadKings();
|
||||
var loadedDKs = newPool.GetAllDeadKings();
|
||||
|
||||
for (int i = 0; i < originalDKs.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(originalDKs[i].playerName, loadedDKs[i].playerName, $"DK {i} name should match");
|
||||
Assert.AreEqual(originalDKs[i].message, loadedDKs[i].message, $"DK {i} message should match");
|
||||
Assert.AreEqual(originalDKs[i].chapterReached, loadedDKs[i].chapterReached, $"DK {i} chapter should match");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Object.DestroyImmediate(newPool);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test Dead King with specified data.
|
||||
/// </summary>
|
||||
private DeadKingData CreateTestDeadKing(string name, string message, int chapter, int piecesLost)
|
||||
{
|
||||
return new DeadKingData(
|
||||
name,
|
||||
message,
|
||||
chapter,
|
||||
piecesLost,
|
||||
new CampaignStats(50, 10, 2, 1)
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d50be294579d1f46bc7df4b28502e24
|
||||
@@ -0,0 +1,213 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using System.IO;
|
||||
using AjedrezPurgatorio.Meta;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Meta
|
||||
{
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Meta")]
|
||||
[Category("SaveSystem")]
|
||||
[Category("FastTests")]
|
||||
public class SaveSystemTests
|
||||
{
|
||||
private GameObject _root;
|
||||
private SaveSystem _saveSystem;
|
||||
private CampaignState _campaignState;
|
||||
private string _testSaveDir;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_root = new GameObject("SaveSystemTestRoot");
|
||||
_saveSystem = _root.AddComponent<SaveSystem>();
|
||||
_campaignState = ScriptableObject.CreateInstance<CampaignState>();
|
||||
_testSaveDir = Path.Combine(Application.temporaryCachePath, "save_tests");
|
||||
Directory.CreateDirectory(_testSaveDir);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
Object.DestroyImmediate(_campaignState);
|
||||
Object.DestroyImmediate(_root);
|
||||
|
||||
if (Directory.Exists(_testSaveDir))
|
||||
{
|
||||
Directory.Delete(_testSaveDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExpectInvalidSlotLog(int slot)
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, $"[SaveSystem] Invalid save slot: {slot}. Must be between 0 and 2.");
|
||||
}
|
||||
|
||||
#region SaveGame Tests
|
||||
|
||||
[Test]
|
||||
public void SaveGame_NullCampaignState_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[SaveSystem] Cannot save with null campaign state.");
|
||||
Assert.IsFalse(_saveSystem.SaveGame(0, null, 0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveGame_InvalidSlot_ReturnsFalse()
|
||||
{
|
||||
ExpectInvalidSlotLog(-1);
|
||||
Assert.IsFalse(_saveSystem.SaveGame(-1, _campaignState, 0));
|
||||
ExpectInvalidSlotLog(10);
|
||||
Assert.IsFalse(_saveSystem.SaveGame(10, _campaignState, 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LoadGame Tests
|
||||
|
||||
[Test]
|
||||
public void LoadGame_InvalidSlot_ReturnsNull()
|
||||
{
|
||||
ExpectInvalidSlotLog(-1);
|
||||
Assert.IsNull(_saveSystem.LoadGame(-1));
|
||||
ExpectInvalidSlotLog(100);
|
||||
Assert.IsNull(_saveSystem.LoadGame(100));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DoesSaveExist Tests
|
||||
|
||||
[Test]
|
||||
public void DoesSaveExist_InvalidSlot_ReturnsFalse()
|
||||
{
|
||||
ExpectInvalidSlotLog(-1);
|
||||
Assert.IsFalse(_saveSystem.DoesSaveExist(-1));
|
||||
ExpectInvalidSlotLog(5);
|
||||
Assert.IsFalse(_saveSystem.DoesSaveExist(5));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteSave Tests
|
||||
|
||||
[Test]
|
||||
public void DeleteSave_InvalidSlot_ReturnsFalse()
|
||||
{
|
||||
ExpectInvalidSlotLog(-1);
|
||||
Assert.IsFalse(_saveSystem.DeleteSave(-1));
|
||||
ExpectInvalidSlotLog(5);
|
||||
Assert.IsFalse(_saveSystem.DeleteSave(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeleteSave_NonexistentSave_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(_saveSystem.DeleteSave(0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSaveInfo Tests
|
||||
|
||||
[Test]
|
||||
public void GetSaveInfo_InvalidSlot_ReturnsDefaultInfo()
|
||||
{
|
||||
ExpectInvalidSlotLog(-1);
|
||||
var info = _saveSystem.GetSaveInfo(-1);
|
||||
|
||||
Assert.IsNotNull(info);
|
||||
Assert.IsFalse(info.exists);
|
||||
Assert.AreEqual(-1, info.slot);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSaveInfo_NonexistentSave_ReturnsNonExistentInfo()
|
||||
{
|
||||
var info = _saveSystem.GetSaveInfo(0);
|
||||
|
||||
Assert.IsNotNull(info);
|
||||
Assert.IsFalse(info.exists);
|
||||
Assert.AreEqual(0, info.slot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetAllSaveInfos Tests
|
||||
|
||||
[Test]
|
||||
public void GetAllSaveInfos_ReturnsArray()
|
||||
{
|
||||
var infos = _saveSystem.GetAllSaveInfos();
|
||||
|
||||
Assert.IsNotNull(infos);
|
||||
Assert.AreEqual(3, infos.Length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetMostRecentSaveSlot Tests
|
||||
|
||||
[Test]
|
||||
public void GetMostRecentSaveSlot_NoSaves_ReturnsNegativeOne()
|
||||
{
|
||||
Assert.AreEqual(-1, _saveSystem.GetMostRecentSaveSlot());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SaveData Structure Tests
|
||||
|
||||
[Test]
|
||||
public void SaveData_DefaultValues_AreCorrect()
|
||||
{
|
||||
var data = new SaveSystem.SaveData();
|
||||
|
||||
Assert.AreEqual(0, data.saveSlot);
|
||||
Assert.AreEqual(0, data.currentChapterIndex);
|
||||
Assert.IsNotNull(data.completedChapters);
|
||||
Assert.AreEqual(0, data.completedChapters.Count);
|
||||
Assert.IsNotNull(data.alivePieces);
|
||||
Assert.AreEqual(0, data.alivePieces.Count);
|
||||
Assert.AreEqual(0, data.totalPiecesLost);
|
||||
Assert.IsFalse(data.lastChapterWasVictory);
|
||||
Assert.AreEqual(1, data.saveVersion);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveData_CustomValues_ArePreserved()
|
||||
{
|
||||
var data = new SaveSystem.SaveData
|
||||
{
|
||||
saveSlot = 2,
|
||||
currentChapterIndex = 3,
|
||||
totalPiecesLost = 5,
|
||||
lastChapterWasVictory = true
|
||||
};
|
||||
|
||||
Assert.AreEqual(2, data.saveSlot);
|
||||
Assert.AreEqual(3, data.currentChapterIndex);
|
||||
Assert.AreEqual(5, data.totalPiecesLost);
|
||||
Assert.IsTrue(data.lastChapterWasVictory);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SaveInfo Structure Tests
|
||||
|
||||
[Test]
|
||||
public void SaveInfo_DefaultValues_AreCorrect()
|
||||
{
|
||||
var info = new SaveSystem.SaveInfo(1);
|
||||
|
||||
Assert.AreEqual(1, info.slot);
|
||||
Assert.AreEqual("", info.saveDate);
|
||||
Assert.AreEqual(0, info.currentChapter);
|
||||
Assert.IsFalse(info.exists);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33a56ecf7f740af479dbb3600879fe75
|
||||
@@ -2,107 +2,225 @@ using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for MoveValidator — constructor, board bounds, null safety.
|
||||
/// </summary>
|
||||
public class MoveValidatorTests
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Chess
|
||||
{
|
||||
private CheckDetector _checkDetector;
|
||||
private MoveValidator _validator;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Chess")]
|
||||
[Category("MoveValidator")]
|
||||
[Category("FastTests")]
|
||||
public class MoveValidatorTests
|
||||
{
|
||||
_checkDetector = new CheckDetector();
|
||||
_validator = new MoveValidator(_checkDetector);
|
||||
}
|
||||
private MoveValidator _validator;
|
||||
private CheckDetector _checkDetector;
|
||||
private Piece[,] _board;
|
||||
private GameObject _root;
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_validator = null;
|
||||
_checkDetector = null;
|
||||
}
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_checkDetector = new CheckDetector();
|
||||
_validator = new MoveValidator(_checkDetector);
|
||||
_board = new Piece[8, 8];
|
||||
_root = new GameObject("TestRoot");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_NullCheckDetector_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<System.ArgumentNullException>(() => new MoveValidator(null));
|
||||
}
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_NullBoard_ReturnsFalse()
|
||||
{
|
||||
var pawn = CreatePiece<Pawn>(3, 1, true);
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
Assert.IsFalse(_validator.IsMoveLegal(null, pawn, new Vector2Int(3, 2)));
|
||||
}
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
var go = new GameObject($"Piece_{typeof(T).Name}_{x}_{y}");
|
||||
go.transform.SetParent(_root.transform);
|
||||
var piece = go.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
piece.hasMoved = true;
|
||||
_board[x, y] = piece;
|
||||
return piece;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_NullPiece_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
Assert.IsFalse(_validator.IsMoveLegal(board, null, new Vector2Int(3, 2)));
|
||||
}
|
||||
#region Constructor Tests
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_OutOfBounds_ReturnsFalse()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
var pawn = CreatePiece<Pawn>(3, 1, true);
|
||||
board[3, 1] = pawn;
|
||||
[Test]
|
||||
public void Constructor_NullCheckDetector_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<System.ArgumentNullException>(() => new MoveValidator(null));
|
||||
}
|
||||
|
||||
Assert.IsFalse(_validator.IsMoveLegal(board, pawn, new Vector2Int(-1, 2)));
|
||||
Assert.IsFalse(_validator.IsMoveLegal(board, pawn, new Vector2Int(8, 2)));
|
||||
Assert.IsFalse(_validator.IsMoveLegal(board, pawn, new Vector2Int(3, -1)));
|
||||
Assert.IsFalse(_validator.IsMoveLegal(board, pawn, new Vector2Int(3, 8)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_NullBoard_ReturnsEmpty()
|
||||
{
|
||||
var pawn = CreatePiece<Pawn>(3, 1, true);
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
var moves = _validator.GetLegalMovesForPiece(null, pawn);
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
#region IsMoveLegal Tests
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_NullPiece_ReturnsEmpty()
|
||||
{
|
||||
var board = new Piece[8, 8];
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
var moves = _validator.GetLegalMovesForPiece(board, null);
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
[Test]
|
||||
public void IsMoveLegal_NullBoard_ReturnsFalse()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
Assert.IsFalse(_validator.IsMoveLegal(null, king, new Vector2Int(4, 1)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllLegalMoves_NullBoard_ReturnsEmpty()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board es null.");
|
||||
var moves = _validator.GetAllLegalMoves(null, true);
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
[Test]
|
||||
public void IsMoveLegal_NullPiece_ReturnsFalse()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
Assert.IsFalse(_validator.IsMoveLegal(_board, null, new Vector2Int(4, 1)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CountLegalMoves_NullBoard_ReturnsZero()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board es null.");
|
||||
Assert.AreEqual(0, _validator.CountLegalMoves(null, true));
|
||||
}
|
||||
[Test]
|
||||
public void IsMoveLegal_OutOfBoard_ReturnsFalse()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
Assert.IsFalse(_validator.IsMoveLegal(_board, king, new Vector2Int(-1, 0)));
|
||||
Assert.IsFalse(_validator.IsMoveLegal(_board, king, new Vector2Int(8, 0)));
|
||||
}
|
||||
|
||||
// ==================== HELPER ====================
|
||||
[Test]
|
||||
public void IsMoveLegal_RookToValidSquare_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
var rook = CreatePiece<Rook>(0, 0, true);
|
||||
|
||||
private T CreatePiece<T>(int x, int y, bool isWhite) where T : Piece
|
||||
{
|
||||
var obj = new GameObject($"Test_{typeof(T).Name}_{x}_{y}");
|
||||
var piece = obj.AddComponent<T>();
|
||||
piece.currentPos = new Vector2Int(x, y);
|
||||
piece.isWhite = isWhite;
|
||||
return piece;
|
||||
Assert.IsTrue(_validator.IsMoveLegal(_board, rook, new Vector2Int(0, 4)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_RookBlockedByFriend_ReturnsFalse()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
var rook = CreatePiece<Rook>(0, 0, true);
|
||||
CreatePiece<Pawn>(0, 3, true);
|
||||
|
||||
Assert.IsFalse(_validator.IsMoveLegal(_board, rook, new Vector2Int(0, 5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_RookCanCaptureEnemy_ReturnsTrue()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
var rook = CreatePiece<Rook>(0, 0, true);
|
||||
CreatePiece<Pawn>(0, 5, false);
|
||||
|
||||
Assert.IsTrue(_validator.IsMoveLegal(_board, rook, new Vector2Int(0, 5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_KingMoveIntoCheck_ReturnsFalse()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 3, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
|
||||
Assert.IsFalse(_validator.IsMoveLegal(_board, king, new Vector2Int(4, 4)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsMoveLegal_KingMoveAwayFromCheck_ReturnsTrue()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 3, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
|
||||
Assert.IsTrue(_validator.IsMoveLegal(_board, king, new Vector2Int(3, 3)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetLegalMovesForPiece Tests
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_NullBoard_ReturnsEmptyList()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 0, true);
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
var moves = _validator.GetLegalMovesForPiece(null, king);
|
||||
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_NullPiece_ReturnsEmptyList()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board o piece es null.");
|
||||
var moves = _validator.GetLegalMovesForPiece(_board, null);
|
||||
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_KingInCheck_OnlyLegalMoves()
|
||||
{
|
||||
var king = CreatePiece<King>(4, 3, true);
|
||||
CreatePiece<Rook>(4, 7, false);
|
||||
|
||||
var moves = _validator.GetLegalMovesForPiece(_board, king);
|
||||
|
||||
Assert.IsTrue(moves.Count > 0, "King should have escape squares");
|
||||
foreach (var move in moves)
|
||||
{
|
||||
Assert.IsFalse(
|
||||
_checkDetector.WouldLeaveKingInCheck(_board, king, move),
|
||||
$"Move to {move} should not leave king in check"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetLegalMovesForPiece_RookInOpenBoard_AllStraightMoves()
|
||||
{
|
||||
CreatePiece<King>(0, 0, true);
|
||||
var rook = CreatePiece<Rook>(4, 4, true);
|
||||
|
||||
var moves = _validator.GetLegalMovesForPiece(_board, rook);
|
||||
|
||||
Assert.AreEqual(14, moves.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetAllLegalMoves Tests
|
||||
|
||||
[Test]
|
||||
public void GetAllLegalMoves_NullBoard_ReturnsEmptyList()
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "[MoveValidator] Board es null.");
|
||||
var moves = _validator.GetAllLegalMoves(null, true);
|
||||
|
||||
Assert.IsNotNull(moves);
|
||||
Assert.AreEqual(0, moves.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllLegalMoves_KingOnly_CorrectCount()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
|
||||
var moves = _validator.GetAllLegalMoves(_board, true);
|
||||
|
||||
Assert.AreEqual(5, moves.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CountLegalMoves Tests
|
||||
|
||||
[Test]
|
||||
public void CountLegalMoves_EmptyBoard_ReturnsZero()
|
||||
{
|
||||
Assert.AreEqual(0, _validator.CountLegalMoves(_board, true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CountLegalMoves_KingOnly_ReturnsCorrectCount()
|
||||
{
|
||||
CreatePiece<King>(4, 0, true);
|
||||
|
||||
Assert.AreEqual(5, _validator.CountLegalMoves(_board, true));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bda77c87d68668642a9bc733a0f46ca3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,242 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AjedrezPurgatorio.Tests.Unit.Purgatory
|
||||
{
|
||||
[TestFixture]
|
||||
[Category("Unit")]
|
||||
[Category("Purgatory")]
|
||||
[Category("DiceSystem")]
|
||||
[Category("FastTests")]
|
||||
public class DiceSystemTests
|
||||
{
|
||||
private GameObject _root;
|
||||
private DiceSystem _diceSystem;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_root = new GameObject("DiceTestRoot");
|
||||
_diceSystem = _root.AddComponent<DiceSystem>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
}
|
||||
|
||||
#region Roll2d6 Tests
|
||||
|
||||
[Test]
|
||||
public void Roll2d6_ReturnsValueBetween2And12()
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
int result = _diceSystem.Roll2d6();
|
||||
Assert.GreaterOrEqual(result, 2, "Minimum 2d6 result is 2");
|
||||
Assert.LessOrEqual(result, 12, "Maximum 2d6 result is 12");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Roll2d6_DistributionCoversFullRange()
|
||||
{
|
||||
bool sawMin = false;
|
||||
bool sawMax = false;
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
int result = _diceSystem.Roll2d6();
|
||||
if (result == 2) sawMin = true;
|
||||
if (result == 12) sawMax = true;
|
||||
}
|
||||
|
||||
Assert.IsTrue(sawMin, "Should see minimum roll of 2 in 500 attempts");
|
||||
Assert.IsTrue(sawMax, "Should see maximum roll of 12 in 500 attempts");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DetermineWinner Tests
|
||||
|
||||
[Test]
|
||||
public void DetermineWinner_PlayerHigher_ReturnsPlayer()
|
||||
{
|
||||
Assert.AreEqual(PurgatoryWinner.Player, _diceSystem.DetermineWinner(10, 5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetermineWinner_DeathHigher_ReturnsDeath()
|
||||
{
|
||||
Assert.AreEqual(PurgatoryWinner.Death, _diceSystem.DetermineWinner(5, 10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetermineWinner_Tie_ReturnsDeath()
|
||||
{
|
||||
Assert.AreEqual(PurgatoryWinner.Death, _diceSystem.DetermineWinner(7, 7));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetermineWinner_BothZero_ReturnsDeath()
|
||||
{
|
||||
Assert.AreEqual(PurgatoryWinner.Death, _diceSystem.DetermineWinner(0, 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RollForDeath Tests
|
||||
|
||||
[Test]
|
||||
public void RollForDeath_BaseRollInRange()
|
||||
{
|
||||
var result = _diceSystem.RollForDeath();
|
||||
|
||||
Assert.GreaterOrEqual(result.baseRoll, 2);
|
||||
Assert.LessOrEqual(result.baseRoll, 12);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForDeath_NoModifiers()
|
||||
{
|
||||
var result = _diceSystem.RollForDeath();
|
||||
|
||||
Assert.AreEqual(0, result.pieceBonus);
|
||||
Assert.AreEqual(0, result.desperationBonus);
|
||||
Assert.AreEqual(0, result.penalty);
|
||||
Assert.AreEqual(result.baseRoll, result.finalResult);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RollForPlayer Tests
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_QueenGivesBonus()
|
||||
{
|
||||
var queenIdentity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
queenIdentity.pieceType = PieceType.Queen;
|
||||
|
||||
var result = _diceSystem.RollForPlayer(queenIdentity, 0);
|
||||
|
||||
Assert.AreEqual(2, result.pieceBonus);
|
||||
Object.DestroyImmediate(queenIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_RookGivesBonus()
|
||||
{
|
||||
var rookIdentity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
rookIdentity.pieceType = PieceType.Rook;
|
||||
|
||||
var result = _diceSystem.RollForPlayer(rookIdentity, 0);
|
||||
|
||||
Assert.AreEqual(1, result.pieceBonus);
|
||||
Object.DestroyImmediate(rookIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_BishopGivesBonus()
|
||||
{
|
||||
var bishopIdentity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
bishopIdentity.pieceType = PieceType.Bishop;
|
||||
|
||||
var result = _diceSystem.RollForPlayer(bishopIdentity, 0);
|
||||
|
||||
Assert.AreEqual(1, result.pieceBonus);
|
||||
Object.DestroyImmediate(bishopIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_KnightGivesBonus()
|
||||
{
|
||||
var knightIdentity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
knightIdentity.pieceType = PieceType.Knight;
|
||||
|
||||
var result = _diceSystem.RollForPlayer(knightIdentity, 0);
|
||||
|
||||
Assert.AreEqual(1, result.pieceBonus);
|
||||
Object.DestroyImmediate(knightIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_PawnGivesNoBonus()
|
||||
{
|
||||
var pawnIdentity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
pawnIdentity.pieceType = PieceType.Pawn;
|
||||
|
||||
var result = _diceSystem.RollForPlayer(pawnIdentity, 0);
|
||||
|
||||
Assert.AreEqual(0, result.pieceBonus);
|
||||
Object.DestroyImmediate(pawnIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_PurgatoryPenaltyIncreases()
|
||||
{
|
||||
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
identity.pieceType = PieceType.Pawn;
|
||||
|
||||
var result0 = _diceSystem.RollForPlayer(identity, 0);
|
||||
var result3 = _diceSystem.RollForPlayer(identity, 3);
|
||||
|
||||
Assert.AreEqual(0, result0.penalty);
|
||||
Assert.AreEqual(3, result3.penalty);
|
||||
|
||||
// Comparar dos tiradas RNG independientes es no determinista;
|
||||
// validar la fórmula por tirada usando su propia base:
|
||||
// finalResult = max(2, baseRoll + bonus - penalty).
|
||||
int expected0 = Mathf.Max(2, result0.baseRoll + result0.pieceBonus + result0.desperationBonus - 0);
|
||||
int expected3 = Mathf.Max(2, result3.baseRoll + result3.pieceBonus + result3.desperationBonus - 3);
|
||||
|
||||
Assert.AreEqual(expected0, result0.finalResult,
|
||||
"Sin visitas la penalización es 0 y finalResult refleja la base.");
|
||||
Assert.AreEqual(expected3, result3.finalResult,
|
||||
"Con 3 visitas la penalización se aplica sobre la misma tirada.");
|
||||
|
||||
Object.DestroyImmediate(identity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RollForPlayer_FinalResultNeverBelow2()
|
||||
{
|
||||
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
|
||||
identity.pieceType = PieceType.Pawn;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var result = _diceSystem.RollForPlayer(identity, 10);
|
||||
Assert.GreaterOrEqual(result.finalResult, 2, "Final result should never be below 2");
|
||||
}
|
||||
|
||||
Object.DestroyImmediate(identity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DiceRollResult ToString Tests
|
||||
|
||||
[Test]
|
||||
public void DiceRollResult_ToString_ContainsAllParts()
|
||||
{
|
||||
var result = new DiceRollResult
|
||||
{
|
||||
baseRoll = 7,
|
||||
pieceBonus = 2,
|
||||
desperationBonus = 1,
|
||||
penalty = 1,
|
||||
finalResult = 9
|
||||
};
|
||||
|
||||
string str = result.ToString();
|
||||
|
||||
StringAssert.Contains("7", str);
|
||||
StringAssert.Contains("2", str);
|
||||
StringAssert.Contains("1", str);
|
||||
StringAssert.Contains("9", str);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ee943a000867ad4b993cabc00f4f437
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Tests del Duelo del Purgatorio: curva de colapso, persecución del Jefe
|
||||
/// y selección de casillas devoradas (purgatorio-final.md, criterio 7).
|
||||
/// </summary>
|
||||
public class PurgatoryDuelRulesTests
|
||||
{
|
||||
// ══════════════════════════════════════════════════════
|
||||
// CURVA DE COLAPSO
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public void TilesForTurn_GracePeriod_ReturnsZero()
|
||||
{
|
||||
Assert.AreEqual(0, PurgatoryDuelRules.TilesForTurn(1));
|
||||
Assert.AreEqual(0, PurgatoryDuelRules.TilesForTurn(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TilesForTurn_EarlyGame_ReturnsOne()
|
||||
{
|
||||
Assert.AreEqual(1, PurgatoryDuelRules.TilesForTurn(3));
|
||||
Assert.AreEqual(1, PurgatoryDuelRules.TilesForTurn(7));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TilesForTurn_Endgame_ReturnsTwo()
|
||||
{
|
||||
Assert.AreEqual(2, PurgatoryDuelRules.TilesForTurn(8));
|
||||
Assert.AreEqual(2, PurgatoryDuelRules.TilesForTurn(20));
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// SELECCIÓN DE CASILLAS DEVORADAS
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public void PickTilesToDevour_ExcludesBlockedSquares()
|
||||
{
|
||||
var excluded = new HashSet<Vector2Int> { new Vector2Int(4, 4), new Vector2Int(0, 0) };
|
||||
var rng = new System.Random(12345);
|
||||
|
||||
List<Vector2Int> victims = PurgatoryDuelRules.PickTilesToDevour(6, excluded, rng);
|
||||
|
||||
Assert.AreEqual(6, victims.Count);
|
||||
foreach (Vector2Int tile in victims)
|
||||
{
|
||||
Assert.IsFalse(excluded.Contains(tile), $"Casilla excluida devorada: {tile}");
|
||||
Assert.IsTrue(PurgatoryDuelRules.IsInBounds(tile));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PickTilesToDevour_NoDuplicates()
|
||||
{
|
||||
var rng = new System.Random(99);
|
||||
List<Vector2Int> victims = PurgatoryDuelRules.PickTilesToDevour(10, new HashSet<Vector2Int>(), rng);
|
||||
|
||||
Assert.AreEqual(new HashSet<Vector2Int>(victims).Count, victims.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PickTilesToDevour_MoreThanAvailable_ReturnsAvailableOnly()
|
||||
{
|
||||
var excluded = new HashSet<Vector2Int>();
|
||||
for (int x = 0; x < 8; x++)
|
||||
for (int y = 0; y < 8; y++)
|
||||
if (!(x == 0 && y == 0))
|
||||
excluded.Add(new Vector2Int(x, y));
|
||||
|
||||
var rng = new System.Random(7);
|
||||
List<Vector2Int> victims = PurgatoryDuelRules.PickTilesToDevour(5, excluded, rng);
|
||||
|
||||
Assert.AreEqual(1, victims.Count);
|
||||
Assert.AreEqual(new Vector2Int(0, 0), victims[0]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// PERSECUCIÓN DEL JEFE
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public void NextChaseStep_DiagonalTarget_StepsDiagonally()
|
||||
{
|
||||
var from = new Vector2Int(2, 2);
|
||||
var target = new Vector2Int(5, 5);
|
||||
|
||||
Vector2Int step = PurgatoryDuelRules.NextChaseStep(from, target, new HashSet<Vector2Int>());
|
||||
|
||||
Assert.AreEqual(new Vector2Int(3, 3), step);
|
||||
Assert.AreEqual(1, PurgatoryDuelRules.Chebyshev(step, from));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NextChaseStep_BlockedDiagonal_FallsBackToAxis()
|
||||
{
|
||||
var from = new Vector2Int(2, 2);
|
||||
var target = new Vector2Int(5, 5);
|
||||
var blocked = new HashSet<Vector2Int> { new Vector2Int(3, 3) };
|
||||
|
||||
Vector2Int step = PurgatoryDuelRules.NextChaseStep(from, target, blocked);
|
||||
|
||||
Assert.AreNotEqual(new Vector2Int(3, 3), step, "No debe entrar en la casilla bloqueada.");
|
||||
Assert.AreNotEqual(from, step, "Con eje libre debe rodear el hueco, no quedarse quieto.");
|
||||
Assert.LessOrEqual(PurgatoryDuelRules.Chebyshev(step, target), PurgatoryDuelRules.Chebyshev(from, target),
|
||||
"El paso axial de rodeo no debe alejar al Jefe del objetivo.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NextChaseStep_Adjacent_ReachesPlayer()
|
||||
{
|
||||
var from = new Vector2Int(3, 3);
|
||||
var player = new Vector2Int(4, 3);
|
||||
|
||||
Vector2Int step = PurgatoryDuelRules.NextChaseStep(from, player, new HashSet<Vector2Int>());
|
||||
|
||||
Assert.AreEqual(player, step);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NextChaseStep_AllPathsBlocked_StaysInPlace()
|
||||
{
|
||||
var from = new Vector2Int(0, 0);
|
||||
var target = new Vector2Int(4, 4);
|
||||
var blocked = new HashSet<Vector2Int>
|
||||
{
|
||||
new Vector2Int(1, 0),
|
||||
new Vector2Int(0, 1),
|
||||
new Vector2Int(1, 1)
|
||||
};
|
||||
|
||||
Vector2Int step = PurgatoryDuelRules.NextChaseStep(from, target, blocked);
|
||||
|
||||
Assert.AreEqual(from, step);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NextChaseStep_AlwaysReducesOrMaintainsChebyshev()
|
||||
{
|
||||
var rng = new System.Random(2026);
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
var from = new Vector2Int(rng.Next(8), rng.Next(8));
|
||||
var target = new Vector2Int(rng.Next(8), rng.Next(8));
|
||||
|
||||
var blocked = new HashSet<Vector2Int>();
|
||||
int blockCount = rng.Next(4);
|
||||
for (int b = 0; b < blockCount; b++)
|
||||
blocked.Add(new Vector2Int(rng.Next(8), rng.Next(8)));
|
||||
|
||||
Vector2Int step = PurgatoryDuelRules.NextChaseStep(from, target, blocked);
|
||||
|
||||
Assert.LessOrEqual(
|
||||
PurgatoryDuelRules.Chebyshev(step, target),
|
||||
PurgatoryDuelRules.Chebyshev(from, target),
|
||||
$"from={from} target={target} blocked=[{string.Join(";", blocked)}]");
|
||||
|
||||
Assert.LessOrEqual(PurgatoryDuelRules.Chebyshev(step, from), 1, "El Jefe mueve como rey (máx 1 casilla por paso).");
|
||||
if (!blocked.Contains(step) || step == from)
|
||||
Assert.IsTrue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bce97134996e437408940669be30c13d
|
||||
@@ -0,0 +1,85 @@
|
||||
# Quick Design — Purgatorio Final (Duelo contra La Muerte)
|
||||
|
||||
> **Fecha**: 2026-08-22 · **Estado**: Aprobado por el usuario
|
||||
> **Tipo**: Rediseño del clímax de campaña · **Reemplaza**: victoria instantánea al dar jaque mate
|
||||
|
||||
## Resumen
|
||||
|
||||
Al dar jaque mate al Rey enemigo, en lugar de victoria instantánea se abre el
|
||||
**Duelo del Purgatorio**: la pieza que dio el mate queda sola sobre el tablero
|
||||
contra **La Muerte** (pieza-jefe única), mientras el tablero se devora a sí
|
||||
mismo casilla a casilla.
|
||||
|
||||
El flujo de dados actual (recuperar piezas capturadas durante la partida)
|
||||
**convive** con este sistema — no lo reemplaza.
|
||||
|
||||
## Reglas
|
||||
|
||||
### Trigger
|
||||
- El jugador da jaque mate (`GameManager.OnCheckmate` con `whiteWins == true`).
|
||||
- No aparece VictoryUI: se transiciona a `GameState.PurgatoryDuel`.
|
||||
|
||||
### Arena
|
||||
- Mismo tablero, misma escena. Se desactivan todas las piezas salvo:
|
||||
- La **pieza de mate** del jugador (queda en su casilla).
|
||||
- El **Jefe** (La Muerte): sprite de Rey oscuro/rojo en el centro del tablero.
|
||||
- Niebla desactivada durante el duelo.
|
||||
|
||||
### Turnos
|
||||
1. **Turno del jugador**: mueve su pieza con su movimiento normal de ajedrez
|
||||
(validación estándar). Una jugada por turno. Sin movimientos legales → pasa turno.
|
||||
2. **Colapso** (después del movimiento del jugador):
|
||||
- Turnos 1-2: sin devorar.
|
||||
- Turno 3+: 1 casilla aleatoria por turno.
|
||||
- Turno 8+: 2 casillas por turno.
|
||||
- Casilla devorada = vacío (oscurecida, intransitable para ambas piezas).
|
||||
- La casilla bajo el Jefe está EXCLUIDA del colapso (La Muerte es dueña del
|
||||
Purgatorio). Si la casilla devorada está bajo el jugador → derrota.
|
||||
3. **Turno del Jefe**: avanza hasta 2 casillas hacia el jugador (paso greedy,
|
||||
evitando vacíos). Si termina en la casilla del jugador → derrota.
|
||||
|
||||
### Fin del duelo
|
||||
- **Victoria**: el jugador captura al Jefe → victoria final de campaña (VictoryUI).
|
||||
- **Derrota** (te atrapa / caes al vacío):
|
||||
1. Oferta única de **rescate por dados**: usa `DiceSystem` sobre UNA identidad
|
||||
muerta este capítulo (la más valiosa disponible). Éxito → esa pieza vuelve
|
||||
al tablero original. Fallo o rechazo → nada.
|
||||
2. Reinicio al Capítulo 1 / tablero original ("el juego continúa en el primer
|
||||
tablero original").
|
||||
|
||||
### Voz de La Muerte
|
||||
Frases solemnes mostradas en el banner del duelo:
|
||||
- Al iniciar el duelo.
|
||||
- Cada 5 turnos (aviso del colapso).
|
||||
- Al capturar al Jefe / al perder.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
| Componente | Responsabilidad | Creación |
|
||||
|---|---|---|
|
||||
| `PurgatoryDuelManager` | Orquesta turnos, estado, fin de duelo, rescate | Runtime, hijo de CampaignManager |
|
||||
| `BossAI` | Movimiento de persecución del Jefe (2 pasos greedy) | Hijo del manager |
|
||||
| `BoardCollapseController` | Devora casillas aleatorias según curva | Hijo del manager |
|
||||
| `PurgatoryDuelUI` | Banner + contador + frases + paneles de fin | Runtime sobre canvas existente |
|
||||
| `MuerteQuotes` | Pools data-driven de frases (JSON Resources) | Estático |
|
||||
|
||||
Sin edición de escenas: todo se construye en runtime siguiendo el patrón de
|
||||
`DialogueTriggerSystem` (ADR-0001: jerarquías independientes por modo).
|
||||
|
||||
## Fuera de alcance
|
||||
|
||||
- Modificar el flujo de dados existente (oferta → tirada → resultado).
|
||||
- Arte final del Jefe (usa sprite de Rey tintado; reemplazable después).
|
||||
- Portraits nuevos de La Muerte para SpeakerDatabase.
|
||||
|
||||
## Criterios de aceptación
|
||||
|
||||
1. Dar jaque mate abre el duelo (no VictoryUI).
|
||||
2. Solo quedan la pieza de mate y el Jefe; el resto de piezas desaparece.
|
||||
3. El colapso empieza en el turno 3 y escala en el 8; las casillas devoradas
|
||||
bloquean movimiento de ambas piezas.
|
||||
4. El Jefe persigue y puede capturar; el jugador pierde si es atrapado o si su
|
||||
casilla colapsa.
|
||||
5. Capturar al Jefe muestra victoria de campaña.
|
||||
6. Perder ofrece rescate por dados una vez y luego reinicia al Capítulo 1.
|
||||
7. Tests EditMode cubren curva de colapso, paso del jefe y transiciones de estado.
|
||||
@@ -88,3 +88,22 @@ Apr 2026 Jun 2026
|
||||
5. Añadir eventos OnPieceCaptured/OnCheckResolved a GameManager
|
||||
6. Escribir tests pendientes (SaveSystem, servicios POCO)
|
||||
7. Resolver duplicación BoardManager/ClassicBoardManager (normalización de nombres)
|
||||
|
||||
---
|
||||
|
||||
## Actualización 2026-08-22 — Auditoría de bloqueantes
|
||||
|
||||
Verificación de los 7 bloqueantes listados arriba:
|
||||
|
||||
| # | Bloqueante | Estado |
|
||||
|---|-----------|--------|
|
||||
| 1 | Commit del trabajo sin commitear | RESUELTO — commit 9fdafe8 (+ TestResults*.xml a .gitignore) |
|
||||
| 2 | Chapter2/3 en Build Settings | RESUELTO — 4 escenas habilitadas, GUIDs verificados |
|
||||
| 3 | Instanciar GameStateManager y AIController | YA ESTABA — presente y habilitado en Ch1/2/3; dificultad aplicada por CampaignManager en runtime |
|
||||
| 4 | Cablear DeadKingInputUI/RevealUI | CABLEADO OK (CampaignManager usa FindObjectOfType<T>(true)) — PENDIENTE usuario: correr Tools > Purgatory UI para dedup (8-9 copias por escena) y copiar Victory/Defeat/MemorialPanel a Ch1/2 |
|
||||
| 5 | Eventos OnPieceCaptured/OnCheckResolved + HUD | RESUELTO — eventos declarados e invocados; GameHUD suscripto con CapturedPieceIcon.prefab asignado en las 3 escenas |
|
||||
| 6 | Tests pendientes | RESUELTO — suites migradas de ests/ (invisible para Unity) a Assets/Tests/EditMode/: CampaignState, DiceSystem, DeadKingPool, SaveSystem, DialogueBranching + 3 chess actualizadas a versiones más completas. Nuevas: TypewriterTests (8 tests), CampaignManagerConfigTests (11 tests) |
|
||||
| 7 | Duplicación BoardManager/ClassicBoardManager | CERRADO POR DISEÑO — nombres ya normalizados (sin colisión); la duplicación de lógica es consecuencia aceptada de ADR-0001 (arquitectura por modo) |
|
||||
|
||||
**Pendiente tras esta auditoría**: correr los menús Tools > Purgatory UI en Unity (Ch1, Ch2, Ch3),
|
||||
guardar escenas, verificar consola sin errores de compilación, y commitear.
|
||||
|
||||
Reference in New Issue
Block a user