docs: ADR-0010..0014 aceptados (Piece Identity, Dice, Dialogue, Dead Kings, Audio) y registry

This commit is contained in:
2026-08-16 17:21:24 -03:00
parent d273e55dde
commit b8676dcaa7
7 changed files with 1153 additions and 1 deletions
@@ -0,0 +1,215 @@
# ADR-0010: Piece Identity System — datos de identidad vs. estado de vida
## Status
Accepted
## Date
2026-08-16
## Last Verified
2026-08-16
## Decision Makers
Diseñador/director (usuario) + agente de arquitectura (opencode)
## Summary
Formaliza el sistema de identidad de piezas (16 piezas blancas con nombre, rol, relación y
quotes). Separa los **datos estáticos** de identidad (`PieceIdentity` SO, ADR-0005) del
**estado de vida por campaña** (Alive/Captured/In Purgatory/Recovered/Dead), que pasa a ser
responsabilidad exclusiva de `CampaignState` (único writer). `PieceIdentityManager` asigna
identidades por tipo de pieza al inicio de cada tablero.
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Unity 6.3 LTS (6000.3.13f1) |
| **Domain** | Core / Scripting |
| **Knowledge Risk** | HIGH — post-cutoff, verificada contra engine-reference |
| **References Consulted** | `VERSION.md`, `modules/core-scripting.md` (si existe), ADR-0005 |
| **Post-Cutoff APIs Used** | None (`ScriptableObject`, `Object.Equals`, LINQ) |
| **Verification Required** | None |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0005 (Accepted) — `PieceIdentity` como SO data container; ADR-0001 (Accepted) — lógica en servicios POCO; ADR-0003 (Accepted) — señales `OnPieceCaptured` para notificar capturas |
| **Enables** | ADR-0011 (dados), ADR-0012 (branching de diálogos por estado de pieza) |
| **Blocks** | Epics de Purgatorio y Campaña |
| **Ordering Note** | `CampaignState` debe ser el único writer del estado de vida antes de que Purgatorio (ADR-0011) y diálogos (ADR-0012) lo consuman |
## Context
### Problem Statement
El GDD (piece-identity.md) define 5 estados de vida por campaña que varios sistemas consumen
(Board tooltip, Dice, Dialogue, HUD, Dead Kings). Sin un dueño único del estado, cada sistema
lee de una fuente distinta y los estados derivados (recovered vs dead_permanent) pueden
desincronizarse.
### Current State
- `PieceIdentity` SO (data container, ADR-0005): `characterName`, `role`, `relationship`,
`quotes[]`, `pieceType`, más un campo `isAlive` (estado runtime dentro del asset) y
`ResetState()`.
- `CampaignState` SO: mantiene `_alivePieces`, `_totalPiecesLost`, `_currentChapterPiecesLost`,
`GetAvailablePiecesForNextBoard()`, `IsPieceAlive()`, `MarkPieceDead()`/`MarkPieceRecovered()`.
- `PieceIdentityManager` (MonoBehaviour): asigna identidades por tipo de pieza al inicio de cada
tablero leyendo `CampaignState` y filtrando `i.isAlive`.
**Duplicación detectada**: el estado de vida vive a la vez en `PieceIdentity.isAlive` (SO) y en
`CampaignState._alivePieces` (SO). Dos fuentes de verdad para lo mismo.
### Constraints
- SO = datos, sin lógica de negocio (ADR-0005); la lógica vive en servicios POCO (ADR-0001).
- Un solo dueño por responsabilidad (ADR-0000, estancia 5).
- El estado de vida es por campaña, no global.
### Requirements
- Datos de identidad editables en el inspector (name, role, relationship, quotes).
- Estado de vida con un único writer accesible desde Purgatorio, Diálogos y HUD.
- Asignación por tipo de pieza consistente entre tableros.
## Decision
### Architecture
```
PieceIdentity (SO, datos estáticos) CampaignState (SO, estado de vida)
├── characterName, role, relationship ──► ├── _alivePieces (nombres vivos)
├── quotes[3] ├── _totalPiecesLost
├── pieceType ├── _currentChapterPiecesLost
└── (sin isAlive — estado movido a Campaign) └── IsPieceAlive(name) / MarkPieceDead / MarkPieceRecovered
│ único writer
PieceIdentityManager (asigna identidad por tipo al iniciar tablero)
```
### Key Interfaces
```
// Datos (SO, ADR-0005)
[CreateAssetMenu(menuName = "Game/Piece Identity")]
public class PieceIdentity : ScriptableObject
{
public string characterName;
public string role;
public string relationship;
public string[] quotes;
public PieceType pieceType;
}
// Estado de vida — único writer (CampaignState)
public bool IsPieceAlive(string pieceName);
public void MarkPieceDead(PieceIdentity identity); // ← usado por PurgatoryManager
public void MarkPieceRecovered(PieceIdentity identity); // ← usado por PurgatoryManager
public List<PieceIdentity> GetAvailablePiecesForNextBoard();
// Asignación al iniciar tablero
public void AssignIdentities(); // PieceIdentityManager → piece.identity
```
### Implementation Guidelines
- **Eliminar** `PieceIdentity.isAlive` y `ResetState()` como fuente de verdad; las consultas de
vida usan `CampaignState.IsPieceAlive(characterName)`. El campo en el SO puede quedarse solo
como cache de editor, nunca como estado de campaña.
- El Rey nunca entra en el flujo de Purgatorio (decisión en ADR-0011); su identidad (Ricardo
Valdés) alimenta Dead Kings al terminar la campaña (ADR-0013).
- Los 5 estados del GDD se representan: `Alive` y `Recovered` ⇒ presente en `_alivePieces`;
`Dead (Permanent)` ⇒ removido vía `MarkPieceDead`; `Captured (Pendiente)` y `In Purgatory` son
transitorios en manos de `PurgatoryManager` durante el overlay (ADR-0006).
## Alternatives Considered
### Alternative 1: Estado de vida dentro del SO de identidad
- **Description**: cada `PieceIdentity` guarda su propio `isAlive` y se persiste en el asset.
- **Pros**: lectura directa por pieza.
- **Cons**: muta assets en runtime (ensucia el proyecto); dos fuentes de verdad con
`CampaignState`; no serializable por campaña.
- **Estimated Effort**: menor.
- **Rejection Reason**: contradice la fuente de verdad única de estado runtime (ADR-0000 estancia 3).
### Alternative 2: Pool de identidades por partida (instancias en runtime)
- **Description**: clonar identidades por partida y guardar estado en la instancia.
- **Pros**: aislamiento total.
- **Cons**: más infraestructura de clonado sin beneficio para el tamaño del proyecto.
- **Estimated Effort**: mayor.
- **Rejection Reason**: `CampaignState` ya es el dueño de la campaña; duplicar en instancias añade
complejidad sin necesidad.
## Consequences
### Positive
- Fuente de verdad única del estado de vida (CampaignState) para Board, Dice, Dialogue y HUD.
- Datos de identidad limpios y editables (sin estado de campaña dentro del asset).
- Asignación por tipo de pieza consistente entre tableros.
### Negative
- Refactor de consumidores que lean `PieceIdentity.isAlive` (grep a `isAlive`).
- `PieceIdentityManager` accede a `GameManager.Instance.board` (dependencia a manager de
tablero, acoplada a MonoBehaviour) — deuda a resolver si se migra la asignación a un servicio POCO.
### Neutral
- El estado de piezas de negras se mantiene fuera del sistema (piezas genéricas, salvo Dead Kings).
## Risks
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| Consumidores siguen usando `isAlive` del SO tras la migración | Media | Media | Regla de revisión: consultar vida solo vía `CampaignState`; validación en code review |
| `PieceIdentityManager` acoplado a `GameManager` | Media | Baja | Documentado; candidato a POCO cuando se estabilice el board service |
## Performance Implications
SOs referenciados por el inspector; consultas a `CampaignState` (List.Contains) O(n) sobre 16
piezas. Sin impacto en frame time.
## Migration Plan
1. Mover consumidores de `PieceIdentity.isAlive``CampaignState.IsPieceAlive` (Board tooltip,
PurgatoryManager, DialogueSystem ya usan CampaignState).
2. Deprecar `isAlive`/`ResetState` en el SO; mantener por compatibilidad temporal con nota.
3. Verificar `AssignIdentities()` filtra solo por tipo + disponibilidad en `CampaignState`.
**Rollback plan**: revertir por git; el ADR queda documentado como guía si se revierte.
## Validation Criteria
- [ ] Grep de `PieceIdentity.isAlive` fuera del SO devuelve 0 (salvo cache de editor).
- [ ] `MarkPieceDead`/`MarkPieceRecovered` de PurgatoryManager reflejan correctamente en
`IsPieceAlive`.
- [ ] Asignación por tipo: Reina = Elena Valdés; 8 peones = empleados de la fábrica.
- [ ] Pieza muerta permanente no aparece en tableros siguientes (`GetAvailablePiecesForNextBoard`).
## GDD Requirements Addressed
| GDD Document | System | Requirement | How This ADR Satisfies It |
|-------------|--------|-------------|--------------------------|
| `design/gdd/piece-identity.md` | Piece Identity | 16 piezas blancas con identidad narrativa (name, role, relationship, quotes) | `PieceIdentity` SO como data container (ADR-0005) |
| `design/gdd/piece-identity.md` | Piece Identity | Ciclo de vida: Alive, Captured, In Purgatory, Recovered, Dead | Estados en `CampaignState` como único writer |
| `design/gdd/piece-identity.md` | Piece Identity | Asignación por tipo de pieza | `PieceIdentityManager.AssignIdentities()` |
| `design/gdd/piece-identity.md` | Piece Identity | Muertos permanentes no se reponen | `GetAvailablePiecesForNextBoard()` filtra por vida |
| `design/gdd/systems-index.md` | 9 — Piece Identity System | Identidades de 16-20 piezas | Cubre TR-piece-identity-001..016 (datos + estados + asignación) |
## Related
- ADR-0005 — `PieceIdentity` como SO data container.
- ADR-0011 — Purgatorio consume el estado de vida (MarkPieceDead/Recovered).
- ADR-0012 — branching `piece_alive` lee `CampaignState`.
- Código: `Assets/Game/Scripts/ScriptableObjects/PieceIdentity.cs`,
`Assets/Game/Scripts/Mono/Core/PieceIdentityManager.cs`,
`Assets/Game/Scripts/Data/CampaignState.cs`.
@@ -0,0 +1,222 @@
# ADR-0011: Dice System (Purgatorio) — lógica pura de dados + orquestador de overlay
## Status
Accepted
## Date
2026-08-16
## Last Verified
2026-08-16
## Decision Makers
Diseñador/director (usuario) + agente de arquitectura (opencode)
## Summary
Formaliza el mini-juego de dados del Purgatorio. **`DiceSystem`** queda como lógica pura y
testeable de tiradas (2d6, modificadores de pieza/desesperación/penalización, determinación de
ganador, clamp a mínimo 2), **`PurgatoryManager`** como orquestador singleton Awake-explicit que
cablea Oferta→Dados→Resultado usando el overlay UI (ADR-0006) con pausa por `Time.timeScale`
(ADR-0007) y aplica el resultado en `CampaignState` (ADR-0010).
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Unity 6.3 LTS (6000.3.13f1) |
| **Domain** | Gameplay Logic / Core |
| **Knowledge Risk** | HIGH — post-cutoff, verificada contra engine-reference |
| **References Consulted** | `VERSION.md` |
| **Post-Cutoff APIs Used** | None (`Random.Range`, `Time.timeScale`, coroutines) |
| **Verification Required** | None |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0003 (Accepted) — señal `OnPieceCaptured` dispara el flujo; ADR-0005 (Accepted) — estado en SOs; ADR-0006 (Accepted) — overlay UI para oferta/dados/resultado; ADR-0007 (Accepted) — pausa por `Time.timeScale`; ADR-0010 (Accepted) — estado de vida de la pieza en `CampaignState` |
| **Enables** | Dice UI (TR-dice-ui-001) renderiza el flujo; lógica de dados testeable en Edit Mode |
| **Blocks** | Epics de Purgatorio y Campaña |
| **Ordering Note** | Requiere ADR-0006 (overlay) y ADR-0007 (pausa) ya aceptados; consume `MarkPieceRecovered/Dead` de ADR-0010 |
## Context
### Problem Statement
El Purgatorio es la mecánica de riesgo/recompensa central (Pilar P2). Las reglas del
enfrentamiento (fórmulas, modificadores, límites, anti-exploit) y la orquestación del flujo
(qué UI se muestra, cuándo se pausa el juego, cómo se persiste el resultado) no tenían dueño de
arquitectura. Esto bloquea la implementación testeable de las probabilidades (~42% base) y del
flujo sin perder contexto del tablero.
### Current State
- `DiceSystem` (MonoBehaviour): `Roll2d6()`, `RollForPlayer(piece, visits)` con bonuses
(Reina +2, mayores +1, peón 0), `_desperationThreshold=4`, `_penaltyPerVisit=1`, clamp `Max(2, ...)`,
`RollForDeath()`, `DetermineWinner(> = player, else death)`. Lee `GameManager.Instance.board`
para el conteo de piezas blancas (desesperación).
- `PurgatoryManager` (MonoBehaviour singleton Awake-explicit con `DontDestroyOnLoad`):
`OnPieceCaptured(PieceIdentity)`, guard de Rey, cuota `_maxVisitsPerBoard=3`,
oferta→`ExecuteDiceRollSequence()` coroutine con UI de dados→resultado→aplicar en
`_campaignState.MarkPieceRecovered/MarkPieceDead``ExitPurgatory()`.
- Overlays `PurgatoryOfferUI`/`DiceRollUI`/`DiceResultUI` cableados en `Chapter1.unity`
(ADR-0006). Pausa/resume con `Time.timeScale` (ADR-0007).
- Cierre del overlay = "No" (anti-exploit, TR-dice-014).
### Constraints
- El tablero y su estado deben permanecer intactos durante el flujo (ADR-0006).
- El gameplay se pausa durante oferta/dados/resultado (ADR-0007).
- La lógica debe ser testeable sin escena Unity (ADR-0001: servicios POCO).
### Requirements
- Fórmulas del GDD: `player_roll = 2d6 + piece_bonus + desperation_bonus - purgatory_penalty`;
`death_roll = 2d6`; `winner = player_roll > death_roll ? PLAYER : DEATH`.
- Resultado: victoria → `recovered` (regresa en el siguiente tablero); derrota → `dead_permanent`.
- Límite 3 visitas por tablero; Rey nunca al Purgatorio; clamp mínimo 2; una captura por
secuencia; cerrar = "No".
- Resultado notificado a Campaign System para persistencia (TR-dice-021).
## Decision
### Architecture
```
GameManager.OnPieceCaptured (señal, ADR-0003)
PurgatoryManager (singleton Awake-explicit)
├── guard: King → skip · cuota 3 → MarkPieceDead (muerte directa)
├── ShowPurgatoryOffer() → Time.timeScale = 0 → PurgatoryOfferUI (overlay, ADR-0006)
├── ExecuteDiceRollSequence() [coroutine]
│ ├── DiceSystem.RollForPlayer(piece, visits) (lógica pura)
│ ├── DiceSystem.RollForDeath() (lógica pura)
│ ├── DiceSystem.DetermineWinner() (lógica pura)
│ └── DiceRollUI / DiceResultUI animan el flujo (ADR-0006, WaitForSecondsRealtime)
├── Resultado: CampaignState.MarkPieceRecovered | MarkPieceDead (ADR-0010)
└── ExitPurgatory() → Time.timeScale = 1 → vuelve al tablero
```
### Key Interfaces
```
// Lógica pura de dados — sin dependencia de escena (candidata a POCO, ADR-0001)
public int Roll2d6();
public DiceRollResult RollForPlayer(PieceIdentity capturedPiece, int purgatoryVisitsThisBoard);
public DiceRollResult RollForDeath();
public PurgatoryWinner DetermineWinner(int playerTotal, int deathTotal);
// Orquestación (singleton Awake-explicit)
public void OnPieceCaptured(PieceIdentity capturedPiece);
public void ResetVisitsForNewBoard();
```
### Implementation Guidelines
- **Lógica de dados en `DiceSystem` sin acoplar a managers**: la desesperación debe recibir el
conteo de piezas como parámetro (hoy `GameManager.Instance.board`) para poder testear el POCO
sin escena. Migration-friendly.
- El flujo de UI queda en `PurgatoryManager` (orquestador), no en la lógica.
- Cierre del overlay o del juego durante el Purgatorio = "No" (anti-exploit, TR-dice-014):
el resultado por defecto ante cierre inesperado es perder la pieza para el tablero (no
permanente salvo cuota agotada).
- Solo la última captura de una secuencia ofrece Purgatorio (TR-dice-013).
- Animación de dados con `WaitForSecondsRealtime` (ADR-0007) para no quedar congelada con
`Time.timeScale = 0`.
- Probabilidades objetivo: base ~42%, Reina ~58%, peón 3ª visita ~28% (TR-dice-018) — validar
con tests de distribución.
## Alternatives Considered
### Alternative 1: Dados resueltos como escena separada
- **Description**: cargar una escena de dados al capturar pieza.
- **Pros**: aislamiento visual.
- **Cons**: destruye/requiere serializar el tablero; rompe el overlay (ADR-0006) y la pausa global.
- **Estimated Effort**: mayor.
- **Rejection Reason**: contradice ADR-0006 (preservar contexto del tablero) y degrada la UX.
### Alternative 2: Lógica de dados inline en PurgatoryManager
- **Description**: toda la matemática dentro del orquestador.
- **Pros**: menos archivos.
- **Cons**: no testeable; mezcla orquestación con reglas; viola ADR-0001.
- **Estimated Effort**: menor.
- **Rejection Reason**: la lógica pura en `DiceSystem` permite tests Edit Mode de las
probabilidades y de los modificadores.
## Consequences
### Positive
- Fórmulas y límites con un único dueño y testeables (Edit Mode).
- Flujo overlay sin perder el tablero; pausa/reanuda consistente con ADR-0007.
- Resultado persistido en `CampaignState` (único writer, ADR-0010).
### Negative
- `DiceSystem` hoy depende de `GameManager.Instance.board` para la desesperación (deuda:
migrar a parámetro).
- El anti-exploit "cerrar = No" requiere manejar el cierre de overlay como negación explícita.
- La escena de juego crece con paneles overlay (ADR-0006).
### Neutral
- Config de balance (bonuses, cuota, umbrales) como `[SerializeField]` en `DiceSystem` /
`PurgatoryManager`; candidata a moverse a `BalanceConfig` (ADR-0005) cuando exista editor.
## Risks
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| Animación de dados congelada por `Time.timeScale = 0` | Media | Media | Usar `WaitForSecondsRealtime` en UI (ADR-0007) |
| Cierre durante el flujo = retry (exploit) | Baja | Alta | Cierre = "No"; resultado procesado antes de reanudar |
| `DiceSystem` acoplado a `GameManager` para desesperación | Media | Baja | Migrar conteo de piezas a parámetro |
## Performance Implications
Lógica O(1) por tirada; overlay en memoria (ADR-0006); sin impacto en frame time.
## Migration Plan
1. Migrar `GetDesperationBonus()` para recibir el conteo de piezas blancas como parámetro
(desacoplar de `GameManager.Instance`).
2. Validar anti-exploit de cierre en `DiceResultUI`/`PurgatoryOfferUI` (el cierre equivale a "No").
3. Mover la configuración de balance a `BalanceConfig` (ADR-0005) si se crea el editor de balance.
**Rollback plan**: revertir por git; ADR documenta el contrato.
## Validation Criteria
- [ ] Tests Edit Mode: `DetermineWinner` (incl. empate → Death), modificadores (Reina/Torre/Peón),
penalty acumulativo, clamp mínimo 2.
- [ ] Al capturar pieza, partida pausada + overlay visible sin descargar el tablero.
- [ ] El Rey nunca activa el flujo; cuota de 3 → muerte directa en la 4ª captura.
- [ ] Victoria → pieza recuperada en el siguiente tablero; derrota → nunca regresa.
- [ ] Cerrar el juego/overlay durante el flujo = "No" (no retry).
## GDD Requirements Addressed
| GDD Document | System | Requirement | How This ADR Satisfies It |
|-------------|--------|-------------|--------------------------|
| `design/gdd/dice-system.md` | Dice System | 2d6 vs 2d6, empate = Muerte, sin re-rolls | `Roll2d6`/`DetermineWinner` |
| `design/gdd/dice-system.md` | Dice System | Modificadores: pieza, desesperación, penalización | `RollForPlayer` con bonuses y penalty |
| `design/gdd/dice-system.md` | Dice System | Victoria → recovered; derrota → dead_permanent | `MarkPieceRecovered`/`MarkPieceDead` |
| `design/gdd/dice-system.md` | Dice System | Máximo 3 visitas; Rey excluido; clamp 2 | Guardas en `PurgatoryManager`/`DiceSystem` |
| `design/gdd/dice-system.md` | Dice System | Cerrar = 'No' (anti-exploit) | Cierre del overlay como negación |
| `design/gdd/systems-index.md` | 10 — Dice System | Mini-juego de dados contra la Muerte | Cubre TR-dice-001..021 (fórmulas, estado, límites, anti-exploit, notificación) |
## Related
- ADR-0003 — señal `OnPieceCaptured` dispara el flujo.
- ADR-0006 — overlay UI para oferta/dados/resultado.
- ADR-0007 — pausa por `Time.timeScale`.
- ADR-0010 — estado de vida de la pieza.
- Código: `Assets/Game/Scripts/Mono/Purgatory/DiceSystem.cs`,
`Assets/Game/Scripts/Mono/Purgatory/PurgatoryManager.cs`.
@@ -0,0 +1,230 @@
# ADR-0012: Dialogue System — nodos JSON con branching condicional y typewriter
## Status
Accepted
## Date
2026-08-16
## Last Verified
2026-08-16
## Decision Makers
Diseñador/director (usuario) + agente de arquitectura (opencode)
## Summary
Formaliza el sistema de diálogos data-driven. Los diálogos se cargan desde **TextAssets JSON**
(`DialogueData` → lista de `DialogueNode` con `speaker`, `portrait`, `text`, `next`,
`conditions[]`) vía `JsonUtility`. `DialogueSystem` es un singleton Awake-explicit que presenta
los nodos con typewriter y avanzado anti-skip (primer click completa, segundo avanza), evalúa
branching contra el estado de campaña (`piece_alive`, `chapter_complete`, `pieces_lost_count`,
`default`), y notifica el fin de diálogo a Game State (ADR-0003).
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Unity 6.3 LTS (6000.3.13f1) |
| **Domain** | Core / Scripting |
| **Knowledge Risk** | HIGH — post-cutoff, verificada contra engine-reference |
| **References Consulted** | `VERSION.md` |
| **Post-Cutoff APIs Used** | None (`JsonUtility`, `TextAsset`, coroutines) |
| **Verification Required** | None |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0003 (Accepted) — eventos `OnDialogueComplete`/`OnNodeDisplayed`/…; ADR-0005 (Accepted) — datos en assets/JSON; ADR-0010 (Accepted) — estado de piezas para branching `piece_alive`; ADR-0006 (Accepted) — quotes in-game como overlay breve sin pausar |
| **Enables** | Narrativa de campaña y líneas de la Muerte en el Purgatorio |
| **Blocks** | Epics de Narrativa/Campaña |
| **Ordering Note** | Branching `piece_alive` requiere que `CampaignState` sea el único writer de vida (ADR-0010) |
## Context
### Problem Statement
El GDD (dialogue-system.md) exige diálogos desde datos externos (no hardcodeados), con nodos,
branching condicional por estado de piezas/capítulo, typewriter, avanzado anti-skip y quotes
in-game que no pausen la partida. Sin un ADR, cada trigger (capítulo intro/outro, captura,
Purgatorio, derrota) podría implementar su propia presentación y romper la narrativa.
### Current State
- `DialogueSystem` (MonoBehaviour singleton Awake-explicit con `DontDestroyOnLoad`): carga
`List<TextAsset>` JSON en `_dialogues` (Dictionary), `StartDialogue(id)`,
`DisplayNode` → typewriter coroutine (`_charsPerSecond=30`), `AdvanceDialogue()`
(anti-skip: completa→avanza), `EvaluateBranching` con tipos `piece_alive`, `chapter_complete`,
`pieces_lost_count`, `default`, eventos `OnNodeDisplayed/OnNodeComplete/OnDialogueComplete/
OnSpeakerChanged/OnTextUpdated`, `SkipDialogue()`, `SetTextSpeed()`.
- Fail-safes implementados: pieza inexistente → false + nodo default; sin portrait → sin error;
sin default y todas false → sigue el campo `next` normal.
- Branching `piece_alive`/`chapter_complete` leen `CampaignState` (ADR-0010).
### Constraints
- Los diálogos se cargan de datos externos (JSON), no hardcodeados.
- El avanzado no debe permitir salto accidental (anti-skip).
- Las quotes in-game de 1 nodo se muestran como overlay 1-2 s sin pausar la partida (ADR-0006).
- La derrota muestra diálogo de la Muerte antes del flujo Dead Kings.
### Requirements
- `DialogueNode`: `id`, `speaker`, `portrait`, `text`, `next`, `conditions[]` (opcional).
- Avance con click/tecla; sin selección de respuestas en el VS.
- Branching por `piece_alive`, `chapter_complete`, `pieces_lost_count`, `default`.
- Notificar a Game State al terminar el diálogo.
- Dice System dispara líneas contextuales de la Muerte (ADR-0011).
## Decision
### Architecture
```
TextAssets (JSON) ──► DialogueSystem (singleton Awake-explicit)
│ carga (JsonUtility) → _dialogues[id]
├── StartDialogue(id)
├── DisplayNode(node) → typewriter (chars/sec)
├── AdvanceDialogue() [anti-skip: 1º completa, 2º avanza]
│ └── EvaluateBranching(conditions) → next_node
│ ├── piece_alive / chapter_complete / pieces_lost_count
│ └── default / fail-safe → campo next
└── EndDialogue() → OnDialogueComplete → Game State (ADR-0003)
DialogueNode schema (JSON):
{ "id", "speaker", "portrait", "text", "next",
"conditions": [ { "type": "piece_alive|chapter_complete|pieces_lost_count|default",
"param": "…", "nextNode": "…" } ] }
```
### Key Interfaces
```
// Datos (JSON → POCO, JsonUtility)
[Serializable] class DialogueData { public string id; public List<DialogueNode> nodes; }
[Serializable] class DialogueNode { public string id; public string speaker;
public string portrait; public string text;
public string next; public List<DialogueCondition> conditions; }
[Serializable] class DialogueCondition { public string type; public string param; public string nextNode; }
// Sistema
public void StartDialogue(string dialogueId);
public void CompleteCurrentNode(); // primer click
public void AdvanceDialogue(); // segundo click / completado
public void SkipDialogue();
public void SetTextSpeed(float charsPerSecond);
// Eventos (ADR-0003)
event Action<DialogueNode> OnNodeDisplayed;
event Action<DialogueNode> OnNodeComplete;
event Action OnDialogueComplete;
event Action<string> OnSpeakerChanged;
event Action<string> OnTextUpdated;
```
### Implementation Guidelines
- **JSON via `JsonUtility`** (ADR-0000) con TextAssets asignados en el inspector; cada fichero
es un `DialogueData` con su `id`.
- **Avance**: si está escribiendo → completa el nodo (anti-skip); si está completo → avanza.
- **Branching**: evaluar condiciones en orden; `default` como fallback explícito; sin default y
todas false → usar `next` normal del nodo (fail-safe).
- **Quotes in-game** (captura): 1 nodo, overlay 1-2 s, NO pausar la partida (ADR-0006).
- **Purgatorio**: la Muerte dispara líneas contextuales por tipo de pieza (ADR-0011 → diálogo).
- **Derrota de campaña**: diálogo de la Muerte antes del flujo Dead Kings (ADR-0013).
- Desuscripción de eventos en `OnDestroy` (ADR-0000/0003).
## Alternatives Considered
### Alternative 1: Diálogos hardcodeados en C#
- **Description**: nodos definidos en código.
- **Pros**: sin assets.
- **Cons**: requiere recompilar por cada línea; viola el requisito data-driven del GDD.
- **Estimated Effort**: menor.
- **Rejection Reason**: iteración narrativa lenta; contradice TR-dialogue-002.
### Alternative 2: Motor de diálogos externo (Yarn Spinner / Ink)
- **Description**: librería de terceros con su propio lenguaje de scripting.
- **Pros**: herramientas de autoría potentes.
- **Cons**: dependencia externa; sobre-ingeniería para branching básico por estado (el GDD
especifica condiciones simples).
- **Estimated Effort**: mayor.
- **Rejection Reason**: el branching del VS es condicional simple sobre `CampaignState`; la
infraestructura propia en JSON cubre el alcance sin dependencias.
## Consequences
### Positive
- Data-driven: narrativa editable sin recompilar.
- Branching centralizado contra `CampaignState` (único writer de vida, ADR-0010).
- Fail-safes robustos (pieza inexistente, sin portrait, sin default).
- Eventos desacoplan el sistema de la UI (ADR-0003).
### Negative
- `JsonUtility` no serializa `[SerializeField]` privados ni diccionarios: el schema JSON debe
ser plano (listas de nodos), sin diccionarios anidados.
- Sin selección de respuestas por el jugador en el VS (alcance GDD).
### Neutral
- Los ficheros de diálogo viven como TextAssets en el inspector (`List<TextAsset>`).
## Risks
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| Click rápido salta diálogo entero | Media | Media | Anti-skip: primer click completa, segundo avanza |
| Condición refiere pieza inexistente | Baja | Media | Tratar como `false` + nodo default |
| JSON inválido rompe carga | Baja | Media | `try/catch` por fichero + log de error por fichero |
| Coroutine typewriter congelada (overlay quote no pausa) | Baja | Media | Quotes no pausan partida (ADR-0006); typewriter usa `WaitForSecondsRealtime` |
## Performance Implications
Carga de JSON solo en `Awake` (dictionary); typewriter es una coroutine por nodo. Sin impacto
en frame time del tablero.
## Migration Plan
Implementado (Sprint 4). Pendiente: autoría de contenido para Capítulo 2 y 3; mapear
`OnDialogueComplete` → transición de estado de Game State por diálogo (intro → board,
outro → siguiente capítulo/memorial).
**Rollback plan**: revertir por git; ADR documenta el contrato JSON.
## Validation Criteria
- [ ] Diálogos de intro/outro por capítulo se cargan desde JSON y se muestran con typewriter.
- [ ] Avance anti-skip: primer click completa, segundo avanza.
- [ ] Branching por `piece_alive`, `chapter_complete`, `pieces_lost_count` y `default` según estado.
- [ ] Pieza inexistente → `false` + nodo default; sin portrait → texto sin portrait.
- [ ] Quotes de captura en overlay 1-2 s sin pausar la partida.
- [ ] `OnDialogueComplete` dispara la transición de estado correspondiente.
## GDD Requirements Addressed
| GDD Document | System | Requirement | How This ADR Satisfies It |
|-------------|--------|-------------|--------------------------|
| `design/gdd/dialogue-system.md` | Dialogue System | Diálogos desde JSON, no hardcodeados | TextAssets JSON + `JsonUtility` |
| `design/gdd/dialogue-system.md` | Dialogue System | Nodos: speaker, portrait, text, next, conditions | Schema `DialogueNode` |
| `design/gdd/dialogue-system.md` | Dialogue System | Branching por estado de piezas/capítulo | `EvaluateBranching` contra `CampaignState` |
| `design/gdd/dialogue-system.md` | Dialogue System | Anti-skip (1º completa, 2º avanza) | `CompleteCurrentNode`/`AdvanceDialogue` |
| `design/gdd/dialogue-system.md` | Dialogue System | Quotes in-game sin pausar partida | Overlay breve (ADR-0006) |
| `design/gdd/dialogue-system.md` | Dialogue System | Notifica a Game State al terminar | `OnDialogueComplete` (ADR-0003) |
| `design/gdd/systems-index.md` | 20 — Dialogue System | Narrativa entre partidas y eventos | Cubre TR-dialogue-001..018 (nodos, branching, typewriter, fail-safes, estados) |
## Related
- ADR-0003 — eventos del sistema.
- ADR-0006 — overlay para quotes in-game.
- ADR-0010 — estado de piezas para branching.
- ADR-0011 — la Muerte dispara líneas contextuales.
- Código: `Assets/Game/Scripts/Mono/Core/DialogueSystem.cs`.
@@ -0,0 +1,222 @@
# ADR-0013: Dead Kings System — pool local JSON con spawn probabilístico
## Status
Accepted
## Date
2026-08-16
## Last Verified
2026-08-16
## Decision Makers
Diseñador/director (usuario) + agente de arquitectura (opencode)
## Summary
Formaliza el sistema de Reyes Muertos (meta social asíncrono, Pilar P3). `DeadKingPool` es un
**ScriptableObject** de acceso singleton (ADR-0004) que persiste el pool local en JSON dentro de
`Application.persistentDataPath` (ADR-0000), con rotación FIFO (máx 100), filtrado de contenido
("***"), spawn probabilístico (`base + chapter×multiplier`) solo si el pool tiene reyes, y
fallbacks (rey estándar predefinido, "Anónimo", pool corrupto → pool nuevo vacío).
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Unity 6.3 LTS (6000.3.13f1) |
| **Domain** | Core / Persistence |
| **Knowledge Risk** | HIGH — post-cutoff, verificada contra engine-reference |
| **References Consulted** | `VERSION.md` |
| **Post-Cutoff APIs Used** | None (`JsonUtility`, `File`, `Resources.Load`, `Random`) |
| **Verification Required** | None (Resources.Load deprecado — aceptado para el VS, ADR-0000) |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0000 (Accepted) — JsonUtility + `persistentDataPath`; ADR-0004 (Accepted) — singleton dual para SO en `Resources/`; ADR-0001 (Accepted) — lógica POCO testeable |
| **Enables** | Spawn de Dead King como Rey enemigo en el tablero (Board System) |
| **Blocks** | Epics de Meta/Campaña (derrota → input → pool → spawn) |
| **Ordering Note** | El trigger de derrota (jaque mate al Rey) y el flujo de input ocurren tras el diálogo de la Muerte (ADR-0012) |
## Context
### Problem Statement
El GDD (dead-kings.md) define el pool de Reyes Muertos (input de nombre/mensaje al perder,
persistencia local, spawn probabilístico en campañas posteriores, FIFO 100, filtrado de
contenido) sin decisión de arquitectura. Sin un ADR no hay contrato de persistencia ni de
spawn, bloqueando el flujo de derrota y el contenido meta.
### Current State
- `DeadKingPool` (ScriptableObject, namespace `AjedrezPurgatorio.Meta`): singleton vía
`Instance` getter con `Resources.Load<DeadKingPool>("DeadKingPool")` (ADR-0004).
- Persistencia: `dead_kings.json` en `Application.persistentDataPath`; wrapper `[Serializable]
DeadKingPoolData { List<DeadKingData> deadKings }` + `JsonUtility` (pretty print).
- API: `AddDeadKing(data)` (filtra contenido + FIFO), `GetRandomDeadKing()`, `ShouldSpawnDeadKing
(chapterIndex)` (fórmula `base + chapter×multiplier`, requiere pool > 0), `GetAllDeadKings()`,
`ClearPool()`, `LoadPool()`/`SavePool()`.
- Filtrado: `_profanityList` + `Regex.Replace(…, "***")`, case-insensitive.
- `DeadKingData` con `playerName`, `message`, `chapterReached`, `piecesLost`, `date`,
`campaignStats`.
- Fallbacks: pool vacío → rey estándar predefinido; pool corrupto → pool nuevo vacío.
### Constraints
- Persistencia en JSON local para el VS; futuro cloud (pool global) sin reescribir el contrato.
- Máximo 100 reyes; FIFO (remover el más antiguo antes de insertar).
- Filtrado de contenido: reemplazar, no bloquear el envío.
- El pool debe ser accesible desde Board y Campaña sin duplicar instancias.
### Requirements
- Input: nombre máx 20, mensaje máx 100, filtrado.
- Datos por rey: player_name, message, chapter_reached, pieces_lost, date, campaign_stats.
- Spawn: probabilidad por capítulo (20/40/60%) = `base + chapter_index × multiplier`, solo si
pool > 0; pool vacío → rey estándar predefinido.
- Al capturar un Rey Muerto, mostrar nombre + mensaje original.
- Debounce 500ms en el botón de enviar; nombre vacío → "Anónimo".
## Decision
### Architecture
```
Derrota de campaña (jaque mate al Rey)
│ [diálogo de la Muerte, ADR-0012]
InputScreen (nombre ≤20, mensaje ≤100) → Debounce 500ms
│ (sanitizar "***", nombre vacío → "Anónimo")
DeadKingPool.AddDeadKing(DeadKingData) ← SO singleton (ADR-0004)
│ FIFO (máx 100) → SavePool()
persistentDataPath/dead_kings.json (JsonUtility, ADR-0000)
Spawn en campañas:
Board ← ShouldSpawnDeadKing(chapterIndex) = Random.value < base + idx*multiplier
&& pool.Count > 0
pool vacío → Rey estándar predefinido ("Un rey olvidado" / "Nadie recuerda mi nombre")
```
### Key Interfaces
```
// Datos (JSON, JsonUtility)
[Serializable] class DeadKingPoolData { public List<DeadKingData> deadKings; }
// Pool (SO singleton, ADR-0004)
public static DeadKingPool Instance;
public void AddDeadKing(DeadKingData data); // filtra + FIFO + SavePool
public DeadKingData GetRandomDeadKing(); // null si vacío
public bool ShouldSpawnDeadKing(int chapterIndex); // base + idx*multiplier, pool>0
public List<DeadKingData> GetAllDeadKings();
public void ClearPool();
public void LoadPool(); // corrupto → pool nuevo vacío
public void SavePool();
```
### Implementation Guidelines
- **Contrato de persistencia**: `DeadKingPoolData` (wrapper serializable) es el formato JSON;
no romper el formato si se añade cloud.
- **FIFO**: si `Count >= _maxPoolSize`, remover el más antiguo por `date` antes de insertar.
- **Sanitización**: reemplazo case-insensitive de `_profanityList` por "***"; NO bloquear envío.
- **Fallbacks**: pool vacío → rey estándar predefinido; archivo corrupto → pool nuevo vacío
(graceful, log warning).
- **Debounce 500 ms** en el botón de enviar (anti-accidental) y nombre vacío → "Anónimo".
- El jugador puede enfrentar su propio Rey Muerto (feature narrativa, no bug).
## Alternatives Considered
### Alternative 1: Pool en memoria sin persistencia
- **Description**: pool solo durante la sesión.
- **Pros**: trivial.
- **Cons**: pierde la mecánica social asíncrona entre campañas/ejecuciones.
- **Estimated Effort**: menor.
- **Rejection Reason**: contradice el GDD (persistencia local para el VS, futuro cloud).
### Alternative 2: Base de datos / cloud desde el VS
- **Description**: servicio remoto para el pool global desde el inicio.
- **Pros**: pool global inmediato.
- **Cons**: infraestructura, cuentas, moderación y coste para el VS.
- **Estimated Effort**: mayor.
- **Rejection Reason**: el GDD define pool local JSON para el VS; cloud es fase completa.
## Consequences
### Positive
- Contrato de persistencia claro (JSON local, portable, depurable).
- Spawn probabilístico con fallbacks robustos (pool vacío, corrupto).
- Sanitización sin bloquear el envío.
- Singleton via `Resources.Load` (ADR-0004) accesible desde Board y Campaña.
### Negative
- `Resources.Load` deprecado (deuda aceptada, ADR-0000/0005); migrar a Addressables si escala.
- El filtrado con lista corta no es moderación completa (VS mínimo).
- La sanción de "***" usa regex por palabra — O(longitud×lista), irrelevante para inputs cortos.
### Neutral
- Dos tipos de acceso: `Resources.Load` (pool) frente a SOs referenciados en inspector.
## Risks
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| Pool corrupto rompe la carga | Baja | Media | `try/catch` + pool nuevo vacío |
| Contenido ofensivo no capturado por la lista | Media | Baja | Filtro básico documentado como mínimo; futuro reporte/cloud |
| `Resources.Load` deprecado escala mal | Media | Media | API única `Instance` para migrar a Addressables sin tocar consumidores |
| FIFO desordena por `date` igual | Baja | Baja | Fallback al primer elemento; aceptable |
## Performance Implications
Persistencia solo en `AddDeadKing` (escritura JSON); `ShouldSpawnDeadKing` O(pool.Count) por
capítulo. Sin impacto en frame time.
## Migration Plan
Implementado (Sprint 5). Pendiente: cablear el trigger de derrota (tras diálogo de la Muerte) y
el reemplazo del Rey negro estándar por un Dead King en el Board System (spawn visual distinto,
revelación al capturar).
**Rollback plan**: revertir por git; ADR documenta el contrato JSON y de spawn.
## Validation Criteria
- [ ] Al perder campaña, pantalla de input (nombre ≤20, mensaje ≤100) con debounce 500 ms.
- [ ] Datos guardados en `persistentDataPath/dead_kings.json` con el formato `DeadKingPoolData`.
- [ ] FIFO: el pool nunca supera 100; al insertar en lleno, se remueve el más antiguo.
- [ ] `ShouldSpawnDeadKing` = `Random.value < base + idx*multiplier` y pool > 0.
- [ ] Pool vacío → rey estándar predefinido; corrupto → pool nuevo vacío.
- [ ] Nombre vacío → "Anónimo"; contenido ofensivo → "***" sin bloquear envío.
- [ ] Al capturar un Rey Muerto, se muestra nombre + mensaje original.
## GDD Requirements Addressed
| GDD Document | System | Requirement | How This ADR Satisfies It |
|-------------|--------|-------------|--------------------------|
| `design/gdd/dead-kings.md` | Dead Kings | Pool local JSON en persistentDataPath | `DeadKingPool` con `LoadPool`/`SavePool` |
| `design/gdd/dead-kings.md` | Dead Kings | Máximo 100; FIFO | Rotación por `date` en `AddDeadKing` |
| `design/gdd/dead-kings.md` | Dead Kings | spawn = base + chapter×multiplier; solo si pool>0 | `ShouldSpawnDeadKing` |
| `design/gdd/dead-kings.md` | Dead Kings | Pool vacío → rey estándar; corrupto → pool nuevo | Fallbacks en carga/spawn |
| `design/gdd/dead-kings.md` | Dead Kings | Filtro de contenido → "***"; no bloquear | `FilterContent` con regex |
| `design/gdd/dead-kings.md` | Dead Kings | Debounce 500 ms; nombre vacío → "Anónimo" | Regla de input screen |
| `design/gdd/systems-index.md` | 14 — Dead Kings System | Mecánica social asíncrona | Cubre TR-dead-kings-001..018 (input, pool, spawn, sanitización, fallbacks, estados) |
## Related
- ADR-0000 — JsonUtility + `persistentDataPath`.
- ADR-0004 — singleton dual (SO en `Resources/`).
- ADR-0012 — diálogo de la Muerte al recibir/capturar Dead King.
- Código: `Assets/Game/Scripts/Mono/Meta/DeadKingPool.cs`.
+228
View File
@@ -0,0 +1,228 @@
# ADR-0014: Audio System — manager único con buses SFX/Música y estilo Awake-explicit
## Status
Accepted
## Date
2026-08-16
## Last Verified
2026-08-16
## Decision Makers
Diseñador/director (usuario) + agente de arquitectura (opencode)
## Summary
Formaliza el audio del juego. Un único **`AudioManager`** (MonoBehaviour singleton) expone dos
buses internos: **SFX** (one-shot) y **Música** (loop), con API `PlayMove/PlayCapture/…/
PlaySFX(clip)/PlayMusic(clip)/StopMusic/SetVolume`. La música por escena se gobierna desde el
flujo de Scene Management (ADR-0009). **Resuelve la deuda de patrón detectada**: el singleton
debe migrarse de lazy-getter con auto-creación al estilo canónico **Awake-explicit** (ADR-0000).
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Unity 6.3 LTS (6000.3.13f1) |
| **Domain** | Audio |
| **Knowledge Risk** | HIGH — post-cutoff, verificada contra engine-reference |
| **References Consulted** | `VERSION.md`, `modules/audio.md` (AudioSource, PlayOneShot, loop, AudioMixer) |
| **Post-Cutoff APIs Used** | None (`AudioSource.PlayOneShot`, `clip`, `loop`, volúmenes; AudioMixer opcional para buses) |
| **Verification Required** | Validar AudioMixer (ducking de música durante diálogo) cuando exista contenido de audio real |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0000 (Accepted) — estilo Awake-explicit, un manager por responsabilidad; ADR-0003 (Accepted) — SFX de eventos (captura, jaque, etc.) |
| **Enables** | `TR-sfx-001` (bus de efectos) y `TR-music-001` (música por escena, depende de Scene Management) |
| **Blocks** | Epics de Polish/Audio |
| **Ordering Note** | La migración a Awake-explicit es deuda de ADR-0000 (ítems 2-4); no bloquea el VS pero se documenta aquí |
## Context
### Problem Statement
El GDD (systems-index.md) define un bus de audio para efectos (TR-sfx-001) y música por escena
gobernada por Scene Management (TR-music-001). No existía un ADR que fijara el dueño del audio,
la API de reproducción o el estilo de singleton. Además, el patrón actual viola una stance
registrada: `AudioManager` usa **lazy-getter con auto-creación de GameObject**, prohibido por
`forbidden_patterns.lazy_getter_singleton` y contrario a `api_decisions.cross_scene_manager_access`.
### Current State
- `AudioManager` (namespace `AjedrezPurgatorio.Audio`, MonoBehaviour singleton):
- **Lazy-getter con auto-creación**: `Instance` getter crea `new GameObject("AudioManager")` +
`AddComponent` + `DontDestroyOnLoad` si `_instance == null`**viola** la stance Awake-explicit.
- Dos `AudioSource` creados en `Initialize()`: `_sfxSource` (one-shot, volumen 0.7) y
`_musicSource` (loop, volumen 0.5).
- API SFX: `PlayMove/PlayCapture/PlayCheck/PlayCheckmate/PlayPromotion/PlayInvalidMove/PlaySFX(clip)`.
- API Música: `PlayMusic(clip, loop)/StopMusic/SetSFXVolume/SetMusicVolume`.
- Clips configurados como `[SerializeField]` (move/capture/check/checkmate/promotion/invalidMove).
### Constraints
- Un solo manager por responsabilidad (ADR-0000 estancia 5); sin duplicados.
- Singleton cross-escena con estilo Awake-explicit canónico (stance registrada).
- Un solo `AudioListener` activo a la vez (regla del módulo de audio) — normalmente en la cámara.
- Música por escena gobernada por el flujo de escenas (Scene Management, ADR-0009).
### Requirements
- Bus de efectos (SFX) y música por separado, con volúmenes independientes.
- API simple y explícita para cada evento de juego (movimiento, captura, jaque, etc.).
- La música cambia con la escena (capítulo/menú) vía Scene Management.
- Volúmenes configurables (SFX y música) ajustables en runtime.
## Decision
### Architecture
```
GameManager / Board events (ADR-0003) Scene Management (ADR-0009)
│ OnPieceCaptured / OnCheck / … │ SceneTransition
▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ AudioManager (singleton Awake-explicit, DontDestroyOnLoad) │
│ ├── _sfxSource (one-shot, volumen SFX) ──► SFX events │
│ └── _musicSource (loop, volumen Música) ──► música por │
│ escena │
└───────────────────────────────────────────────────────────────┘
```
### Key Interfaces
```
// Singleton canónico (ADR-0000) — sustituye al lazy-getter actual
public static AudioManager Instance { get; private set; } // asignado en Awake + guard de duplicado
// SFX (one-shot)
public void PlayMove(); public void PlayCapture(); public void PlayCheck();
public void PlayCheckmate(); public void PlayPromotion(); public void PlayInvalidMove();
public void PlaySFX(AudioClip clip, float volumeMultiplier = 1f);
// Música (loop por escena)
public void PlayMusic(AudioClip musicClip, bool loop = true);
public void StopMusic();
public void SetSFXVolume(float volume); public void SetMusicVolume(float volume);
```
### Implementation Guidelines
- **Migrar el getter a Awake-explicit**: `Instance { get; private set; }` asignado en `Awake`
con guard de duplicado + `DontDestroyOnLoad` (esqueleto canónico de ADR-0000). Eliminar la
auto-creación de GameObject en el getter. El `AudioManager` debe estar presente en la escena
inicial (GameStateManager primero por orden de ejecución) o crearse explícitamente al boot.
- **SFX**: `PlayOneShot` sobre `_sfxSource` (no interrumpe el sonido actual). Si se quiere
variación de tono (±10%), usar `source.pitch` antes de `PlayOneShot` (patrón del módulo).
- **Música**: `_musicSource.clip` + `loop` + `Play()`. La música por escena se decide en el
flujo de Scene Management (ADR-0009) tras cada transición.
- **AudioListener único**: mantenerlo en la cámara principal; deshabilitar listeners extra.
- **Opcional (AudioMixer)**: agrupar SFX/Música en un mixer (Master → SFX, Music) para volúmenes
y ducking durante diálogos (ADR-0012) cuando exista contenido real. No es bloqueante del VS.
- Config de clips como `[SerializeField]` en el inspector.
## Alternatives Considered
### Alternative 1: Mantener lazy-getter con auto-creación
- **Description**: dejar el getter actual (crea el GameObject si falta).
- **Pros**: auto-cura ante referencias tempranas.
- **Cons**: viola la stance `cross_scene_manager_access` y el patrón prohibido
`lazy_getter_singleton`; oculta el ciclo de vida; testing difícil.
- **Estimated Effort**: menor.
- **Rejection Reason**: el ADR-0000 ya prohibe el lazy-getter y exige el estilo canónico; este
ADR resuelve la deuda en vez de perpetuarla.
### Alternative 2: AudioSource por evento (múltiples fuentes efímeras)
- **Description**: crear/destruir fuentes por cada SFX.
- **Pros**: sonidos superpuestos sin límite.
- **Cons**: overhead de instanciación; más código; innecesario para el VS.
- **Estimated Effort**: mayor.
- **Rejection Reason**: dos buses (one-shot + loop) cubren el alcance; `PlayOneShot` ya permite
superposición de SFX.
## Consequences
### Positive
- Dueño único del audio con API explícita por evento de juego.
- Buses SFX/Música con volúmenes independientes (TR-sfx-001).
- Música por escena gobernada por Scene Management (TR-music-001).
- Resuelve la deuda de patrón lazy-getter detectada en la review de arquitectura.
### Negative
- La migración a Awake-explicit requiere que el `AudioManager` exista en la escena inicial
(si se accede antes del Awake, error claro en vez de auto-creación).
- Sin AudioMixer por ahora: el ducking de música durante diálogos queda pendiente de contenido.
- El coste de dos `AudioSource` en un mismo GameObject es mínimo pero implica configurar
`playOnAwake=false` y volúmenes por separado (ya hecho).
### Neutral
- Los clips se asignan en el inspector (`[SerializeField]`), no se cargan por código.
## Risks
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|-----------|
| Acceso a `Instance` antes del `Awake` rompe tras la migración | Media | Media | Orden de ejecución estable (GameStateManager primero); log de error claro |
| Varios `AudioListener` activos (ecos) | Baja | Media | Deshabilitar listeners extra; uno por cámara |
| Volumen de SFX con `PlayOneShot(clip, multiplier)` acumulado | Baja | Baja | Usar multiplier ≤ 1; validar en mezcla |
| Música por escena desalineada con transiciones | Media | Baja | Decidir clip de música en el flujo de Scene Management (ADR-0009) |
## Performance Implications
| Metric | Before | Expected After | Budget |
|--------|--------|---------------|--------|
| CPU (frame time) | — | +despreciable (2 AudioSources) | — |
| Memory | — | 2 AudioSources + clips referenciados | — |
| Load Time | — | sin cambio (clips referenciados en inspector) | — |
## Migration Plan
1. Cambiar `AudioManager` a singleton Awake-explicit (getter → propiedad + guard en `Awake`;
eliminar auto-creación). Verificar que el GameObject existe en la escena inicial.
2. Crear script de *Boot/Scene Root* que asegure la presencia del manager antes de que los
sistemas lo usen (orden: GameStateManager → AudioManager).
3. Confirmar que `GameManager`/`Board`/`Dialogue` llaman a la API pública (sin cambio de firma).
4. (Opcional, con contenido) Añadir AudioMixer con grupos SFX/Música y ducking de música durante
diálogos (ADR-0012).
**Rollback plan**: revertir por git; la migración es local y el ADR documenta el estilo canónico.
## Validation Criteria
- [ ] `AudioManager` implementa el esqueleto Awake-explicit (sin auto-creación en el getter).
- [ ] SFX se reproducen con `PlayOneShot` sin interrumpir la música.
- [ ] `PlayMusic`/`StopMusic` cambian la música; la música por escena se decide en
Scene Management (ADR-0009).
- [ ] Volúmenes SFX y música independientes y ajustables en runtime.
- [ ] Un solo `AudioListener` activo.
- [ ] Grep de `if (_instance == null)` en `AudioManager` devuelve solo el guard de Awake.
## GDD Requirements Addressed
| GDD Document | System | Requirement | How This ADR Satisfies It |
|-------------|--------|-------------|--------------------------|
| `design/gdd/systems-index.md` | 23 — Music Manager | Música por escena; depende de Scene Management | `PlayMusic`/`StopMusic` gobernados por ADR-0009 |
| `design/gdd/systems-index.md` | 24 — SFX System | Bus de audio para efectos | `_sfxSource` one-shot + API por evento |
| `design/gdd/systems-index.md` | Audio | Volúmenes configurables | `SetSFXVolume`/`SetMusicVolume` |
| `design/gdd/systems-index.md` | Audio | Un solo manager por responsabilidad | Singleton único (ADR-0000) |
> Cubre TR-sfx-001 (bus de efectos) y TR-music-001 (música por escena). Los eventos de audio
> específicos (captura, jaque) se reproducen al escuchar las señales de ADR-0003.
## Related
- ADR-0000 — estilo Awake-explicit, un manager por responsabilidad, prohibición de lazy-getter.
- ADR-0009 — Scene Management decide la música por escena.
- ADR-0003 — eventos de juego que disparan SFX.
- Código: `Assets/Game/Scripts/Mono/Audio/AudioManager.cs`.
+26 -1
View File
@@ -43,7 +43,19 @@ last_updated: "2026-08-16"
# interface: how other systems access this state (read-only property, method call,
# signal, shared resource, etc.) — this is the contract other ADRs depend on.
state_ownership: []
state_ownership:
- state: piece_lifecycle_state
status: active
owner_system: campaign-system # CampaignState (SO)
adr: docs/architecture/adr-0010-piece-identity-system.md
interface: "CampaignState.IsPieceAlive(name), MarkPieceDead(identity), MarkPieceRecovered(identity), GetAvailablePiecesForNextBoard()"
write_access: campaign-system-only # único writer; otros leen
referenced_by:
- docs/architecture/adr-0010-piece-identity-system.md
- docs/architecture/adr-0011-dice-system-purgatorio.md # escribe (MarkPieceDead/Recovered vía PurgatoryManager)
- docs/architecture/adr-0012-dialogue-system.md # lee (branching piece_alive)
added: 2026-08-16
revised: ""
# Example:
#
@@ -202,6 +214,17 @@ api_decisions:
added: 2026-08-16
revised: ""
- purpose: audio_playback
status: active
api: "Único AudioManager (singleton Awake-explicit) con 2 buses: _sfxSource (PlayOneShot, SFX) y _musicSource (loop, música por escena vía Scene Management)"
not: "instancias AudioSource por consumidor ni lazy-getter"
adr: docs/architecture/adr-0014-audio-system.md
reason: "Un solo dueño del audio (ADR-0000), API por evento de juego; la música por escena la decide ADR-0009."
referenced_by:
- docs/architecture/adr-0014-audio-system.md
added: 2026-08-16
revised: ""
# Example:
#
# api_decisions:
@@ -242,6 +265,8 @@ forbidden_patterns:
description: "Los singleton cross-escena deben seguir el estilo Awake-explicit canónico; prohibido el getter con auto-creación de GameObject."
why: "El lazy-getter oculta el ciclo de vida del manager, fomenta ámbito global implícito y dificulta el testing."
adr: docs/architecture/adr-0000-patrones-infraestructura-global.md
referenced_by:
- docs/architecture/adr-0014-audio-system.md # manda migrar AudioManager a Awake-explicit (deuda ADR-0000)
added: 2026-08-16
# Example:
+10
View File
@@ -1,5 +1,15 @@
# Session State
## Session Extract — /architecture-decision ADR-0010..0014 2026-08-16
- ADR-0010 Piece Identity — datos de identidad vs. estado de vida en CampaignState (Accepted)
- ADR-0011 Dice System (Purgatorio) — lógica pura + PurgatoryManager orquestador overlay (Accepted)
- ADR-0012 Dialogue System — nodos JSON con branching condicional y typewriter (Accepted)
- ADR-0013 Dead Kings System — pool local JSON con spawn probabilístico (Accepted)
- ADR-0014 Audio System — manager único SFX/Música, migración a Awake-explicit (Accepted)
- Registry actualizado: state_ownership.piece_lifecycle_state, api_decisions.audio_playback, lazy_getter_singleton→ADR-0014
- Deuda documentada: AudioManager lazy-getter → Awake-explicit (ADR-0014); DiceSystem → desacople GameManager
- NEXT: /architecture-review en sesión fresca (prohibido en esta misma sesión) + /gate-check pre-production
## Session Extract — /architecture-review 2026-08-16
- Verdict: CONCERNS
- Requirements: 145 total — 51 covered, 27 partial, 67 gaps