mirror of
https://github.com/Earth-Genesis-Games/Ajedrez_Purgatorio.git
synced 2026-09-11 09:47:35 +00:00
docs: ADR-0010..0014 aceptados (Piece Identity, Dice, Dialogue, Dead Kings, Audio) y registry
This commit is contained in:
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user